Mechanism Design Security in Smart Contracts
Inspired by 2-part series 6 Solidity Vulnerabilities and How to Stop Them, I thought I’d write a bit about some of the more nuanced vulnerabilities that are related to mechanism design. In simple words, we will go over the vulnerabilities that arise from how smart contracts can interact with each other beyond what the author intended with the initial code.
For now, I’ll cover 2 main categories: Frontrunning and Malicious Smart Contract Wrapping. In a future article, I will go expand on some more complex game theory based flaws. Let’s begin!
Frontrunning is a term from the trading world, where if another trader can see you are making a large order before it hits the exchange (let’s say, a large BUY order), they can put a smaller BUY “in front” of yours, and benefit from your order’s movement of the market. They will sell right after your order executes, therefore having made some “risk-free” profit at your expense.
In Ethereum, this is relevant as well for trading, but it has implications outside of that due to the transparent nature of the P2P network. Let’s look at this contract as an example:
The contract is fairly simple. The constructor sets a hash value and an amount of ETH, and sets that as the challenge for the prize. Anyone who can figure out what hashes to
puzzle can claim the prize. There is nothing wrong in the solidity code per se, it is just naively programmed.
The same way that an adversarial trader can frontrun your trade, so can an adversarial network user “ftontrun” your solution here. If the adversary is aware of this contract (perhaps the prize for this solution is in the millions, and hence many are watching it), they can scan the network and check when someone submits a valid solution. Once they see this tx in the mempool, they can take the solution, and resubmit it with their own address with a higher gas price, and if they manage to get their tx in before you, they’ll have stolen your prize!
Of course, another scenario is the miners themselves stealing your prize. In that case, they don’t even have to play the gas price game, they can simply choose not to include your tx in their block, and place their own in with the solution.
Solution: Blinded Commitments
The way to solve this is to have the smart contract accept a hash as a potential solution to be revealed a few blocks later, and the hash must be constructed in a special way, as can be seen below in the fixed version of the contract:
Instead of immediately providing the solution, the user hashes it with a salt and a their address as a blinding factor, as follows:
commitment = H(solution, salt, eth_address). The commitment is then stored in the contract, and the time at which it is first submitted is recorded.
If someone else tries to “steal” this commitment to frontrun you, the
msg.sender check inside
claim will fail. Since adversaries won’t know the solution until you reveal it 60 blocks later, they would have to recreate the commitment then and wait another 60 blocks until they can even attempt to claim the prize. The smart contract enforces this by making sure that any solution must map to a commitment made at least 60 blocks before to be valid.
In this way, the higher
blockWait is, the lower the chance of an adversary succeeding, due to the fact that they would need to be able to censor your txs for up to
blockWait times to succeed. For very high value prizes,
blockWait can be up to days, and if an adversary can censor you or make the blockchain unavailable for days at a time, you have bigger problems to worry about.
Onto the next topic, “malicious wrappers”. The creator of the wrappers may not always intend to be malicious, but either way this covers cases where an external smart contract enables an interaction that the original author deemed undesireable.
Let’s look at everyone’s favourite contract as an example, an ERC20 token ICO (with only the parts relevant to the security issue shown):
The token above implements a crowdsale + ERC20 (I’ve only implemented the
transfer function for brevity’s sake), which gives out tokens at a 1:1 ratio for ETH, until 100 ETH worth of tokens have been bought.
After that, the contract stops issuing tokens, and the tokens are locked up for 30 days. Here is the focus of our “vulnerability”, or rather a way to (trustlessly) sidestep this protection through clever leveraging of smart contracts.
Say we really wanted to be able to trade tokens before the 30 days. We could buy a bunch of tokens and start our own (off-chain) exchange and let users trade on that exchange, but that seems like a lot of work. Let’s use a malicious wrapper to create a derivative of the token that is immediately tradeable:
Here I’ve created a
TKN-FUT token contract that basically acts as a derivative, a futures contract on the original contract. When the owner creates the contract, they set which asset should underlie the derivative. Participants then send ETH to the
buyThroughFutures method, which is forwarded to the
buyTokens function, making this
TokenFutures contract the owner of the underlying
The participants own
TKN-FUT tokens, which when the underlying becomes tradeable, can be
settle'd through the smart contract (for a settlement fee, which goes to the owner of the derivative contract).
In this way, we have created a completely trustless derivative which allows us to trade (or speculate) on the original token before the token’s “release” time, and even allows us to settle our futures through the smart contract for the real thing when it’s time!
In this specific case, the simple solution here would be to just prevent smart contracts from being able to participate in the ICO. This can be done by ensuring that the caller of
Token.buyTokens also submits an ECDSA signature which can be checked by
ecrecover. This 100% prevents a smart contract from participating since smart contracts can never have private keys, hence they cannot sign.
NOTE: Do not use the EXTCODESIZE check to prevent smart contracts from calling a function. This is not foolproof, it can be subverted by a constructor call, due to the fact that while the constructor is running, EXTCODESIZE for that address returns 0.
Although, the above is not a catch-all by any means. Not allowing smart contracts to participate may not be an option in your case, or maybe you want some kinds of derivatives created but not others.
In that case, the technical solutions stop working and the “game theory” theory solutions have to come in. You have to make undesirable or damaging behaviour unprofitable, because otherwise in a trustless, decentralized network, if an attack is profitable someone will eventually do it to your platform, and there’s nothing you can do to stop them in most cases.
That will be the focus of the next article. Follow me on Medium or Twitter so you can be the first to read it when I get around to it.
If you’re a smart contract auditor looking for work, I am looking to expand the team at ZK Labs — please contact me at firstname.lastname@example.org
Similarly if you need something audited or a second pair of eyes, feel free to contact me at the email above as well. Additionally, if your project is not-for-profit, I am happy to give advice free of charge, as long as I have time.