Smart contract security | Ethereum Foundation Blog
Solidity was launched in October 2014, when there was no real-world testing of either the Ethereum network or virtual machines. Gas costs back then were much different than they are now. Additionally, some of the early design decisions were carried over from Serpent. Over the past few months, examples and patterns that were initially considered best practices have been exposed as reality, and some of them have actually turned out to be anti-patterns. Because of this, I’ve updated some items recently. Robustness DocumentationHowever, most people will not be following the github commit stream for that repository, so I’d like to highlight some findings here.
I won’t talk about minor issues here. documentation.
Send Ether
Sending Ether is considered one of the simplest things to do in Solidity, but it turns out there are some subtleties that most people don’t realize.
At best, it is important for the ether recipient to initiate the payout. next bad Example of an auction contract:
// THIS IS A NEGATIVE EXAMPLE! DO NOT USE! contract auction address highestBidder; uint highestBid; function bid() if (msg.value < highestBid) throw; if (highestBidder != 0) highestBidder.send(highestBid); // refund previous bidder highestBidder = msg.sender; highestBid = msg.value;
Since the maximum stack depth is 1024, a new bidder can always increase the stack size to 1023 and then call. put() this is Send (highest bid) Call it to fail automatically (i.e. the previous bidder will not receive a refund). However, the new bidder will still be the highest bidder. One way to check whether let go Success means checking the return value.
/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE! if (highestBidder != 0) if (!highestBidder.send(highestBid)) throw;
that much
throw
statement will revert the current call. This is a bad idea. Because the receiver implements the fallback function for example like this:
function() throw;
You can always force an Ether transfer to fail, which will prevent anyone from overbidding her.
The only way to prevent both situations is to give the receiver control over the transmission, transforming the transmission pattern into a withdrawal pattern.
/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE! contract auction address highestBidder; uint highestBid; mapping(address => uint) refunds; function bid() if (msg.value < highestBid) throw; if (highestBidder != 0) refunds(highestBidder) += highestBid; highestBidder = msg.sender; highestBid = msg.value; function withdrawRefund() if (msg.sender.send(refunds(msg.sender))) refunds(msg.sender) = 0;
Why does it still say “Negative Yes” on the contract? Because of gas dynamics, the contract is actually fine, but it’s not a good example yet. This is because it is impossible to prevent code execution on the recipient as part of the transfer. This means that the receiver can call Refund again while the send function is still in progress. At that point, your refund will still be the same, so you’ll get that money back. In this specific example, it doesn’t work because the recipient only receives the gas stipend (2100 gas) and it is impossible to perform any other transfers with this amount of gas. However, the following code is vulnerable to this attack: msg.sender.call.value(refund(msg.sender))().
Taking all of this into account, the following code should be fine (of course, it’s not yet a complete example of an auction contract):
contract auction address highestBidder; uint highestBid; mapping(address => uint) refunds; function bid() if (msg.value < highestBid) throw; if (highestBidder != 0) refunds(highestBidder) += highestBid; highestBidder = msg.sender; highestBid = msg.value; function withdrawRefund() uint refund = refunds(msg.sender); refunds(msg.sender) = 0; if (!msg.sender.send(refund)) refunds(msg.sender) = refund;
I didn’t use throws for failed sends because I can revert all state changes manually and not using throws has much fewer side effects.
use throw
The throw statement is often very convenient for reverting changes to state as part of a call (or an entire transaction, depending on how the function is called). However, you should be aware that this can be expensive as it consumes all the gas, and can potentially break the call to the current function. That is why I would like to recommend that you use it. Only In the following situations:
1. Return Ether transfers to their current functionality.
If a function is not intended to receive Ether, is not current, or takes current arguments, it must use throw to reject Ether. Due to gas and stack depth issues, using throw is the only way to reliably send Ether back. The receiver may have an error in the fallback function that uses too much gas and cannot receive Ether, or the function may have been called in a malicious manner. Context with stack depth too high (perhaps before the calling function)
Accidentally sending Ether to a contract isn’t always a UX failure. The order or time in which transactions will be added to a block cannot be predicted. If the contract is written to only accept the first transaction, any Ethereum included in other transactions should be rejected.
2. Revert the effect of the called function
If you call a function from another contract, you will never know how that function is implemented. This means that the effects of these calls are also unknown, so the only way to reverse these effects is to use throw. Of course, if you know you need to revert the effect, you should always write a contract to avoid calling these functions in the first place, but there are some use cases where you might only find out about this after the fact.
Loop and block gas limitations
There is a limit to the amount of gas that can be consumed in a single block. This limit is flexible but very difficult to increase. This means that every single feature of the contract must remain below a certain amount of gas in all (reasonable) circumstances. Here is a bad example of a voting contract:
/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE! contract Voting mapping(address => uint) voteWeight; address() yesVotes; uint requiredWeight; address beneficiary; uint amount; function voteYes() yesVotes.push(msg.sender); function tallyVotes() uint yesVotes; for (uint i = 0; i < yesVotes.length; ++i) yesVotes += voteWeight(yesVotes(i)); if (yesVotes > requiredWeight) beneficiary.send(amount);
There are actually a number of problems with contracts, but the one I want to highlight here is the problem with loops. Assume the voting weights are transferable and divisible like tokens (think DAO tokens for example). This means that you can create any number of copies of yourself. Creating these clones increases the loop length of the TallyVotes function until it requires more gas than is available within a single block.
This applies to anything that uses a loop, even if the loop is not explicitly indicated in the contract (e.g. copying an array or string inside storage). Again, if the length of the loop is controlled by the caller (e.g. iterating an array passed as a function argument), it is fine to have loops of arbitrary length. but never It creates a situation where the loop length is controlled by a party other than the only one suffering from the failure.
FYI, this is one of the reasons we have the concept of blocked accounts within DAO contracts. Vote weights are calculated at the point where the vote is being cast to prevent stuck loops, and vote weights are not fixed until the end of the voting period, so you can vote again after sending your tokens to cast a second vote.
Ether reception/replacement function
If we want our contract to receive Ether via a regular send() call, we need to create a cheap fallback function. It only allows use of 2300 gas and does not allow storage writes or function calls that send Ether. Basically, the only thing that needs to be done within the fallback function is to log an event so that external processes can react to that fact. Of course, all features of the contract can receive ETH and are not tied to any applicable gas limits. Functions should reject sent Ether if they do not actually want to receive the Ether, but we are thinking about potentially reversing this behavior in a future release.