The solidity was launched in October 2014 when neither the Ethereum nor the virtual machine had real tests, the gas costs at the time were even considerably different from what they are now. In addition, some of the first design decisions were taken care of as a snake. In the past two months, the examples and models that have been initially considered as best practices have been exposed to reality and some of them have really proven to be anti-motives. For this reason, we recently updated part of the Solidity documentationBut since most people probably do not follow the flow of GitHub in this repository, I would like to highlight some of the conclusions here.
I will not talk about minor problems here, please read them in the documentation.
Shipping the ether
The sending of ether is supposed to be one of the simplest things of solidity, but it turns out to have certain subtleties that most people do not realize.
It is important that at best, the recipient of the ether triggers payment. The following is a BAD Example of 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; } }
Due to the maximum battery depth of 1024, the new tenderer can always increase the size of the battery to 1023, then call offer() which will cause the Send (Highstbid) Call to fail silently (that is to say that the previous tenderer will not receive the reimbursement), but the new tenderer will always be the most offering. One way to check if send managed to check its return value:
/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE! if (highestBidder != 0) if (!highestBidder.send(highestBid)) throw;
THE
throw
The declaration causes the current appeal to return. This is a bad idea, because the recipient, for example by implementing the aid function as
function() { throw; }
can always force the transfer of ether to fail and this would have the effect that no one can train it.
The only way to avoid the two situations is to convert the sending model into a withdrawal model by giving the recipient the control of the transfer:
/// 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 he always say “negative example” above the contract? Due to gas mechanics, the contract is actually good, but it is still not a good example. The reason is that it is impossible to prevent the execution of the code to the recipient in the context of a shipment. This means that even if the sending function is still in progress, the recipient can remind you of distancing. At this point, the amount of the reimbursement is always the same and therefore they would again obtain the amount and so on. In this specific example, it does not work, because the recipient only obtains the gas allowance (2100 gases) and it is impossible to make another shipment with this quantity of gas. The following code, however, is vulnerable to this attack: msg.sender.Call.VALUE (reimburses (msg.sender)) () ().
Having considered all this, the following code should be good (of course, it is still not a complete example of 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; } }
Note that we have not used launching on a stranded shipment, because we are able to reintegrate all the changes in the state manually and that the non-use of Throw has much fewer side effects.
Use of throwing
Throw instruction is often practical enough to reintegrate the changes made to the State as part of the call (or an entire transaction depending on how the function is called). You should be aware, however, that it also causes all gases to spend and is therefore expensive and potentially stall calls in the current function. Because of this, I would like to recommend using it only In the following situations:
1. Turn the Ether transfer to the current function
If a function is not intended to receive ether or not in the current state or with the current arguments, you must use the launch to reject the ether. The use of launching is the only way to reliably return the ether because of the problems of gas and battery depth: the recipient could have an error in the rescue function which takes too much gas and cannot therefore receive the ether or the function could have been called in a malicious context with too high a battery depth (perhaps even before the call function).
Note that the accidentally sending ether to a contract is not always a UX failure: you can never predict which order or when the transactions are added to a block. If the contract is written to accept the first transaction, the ether included in other transactions must be rejected.
2. Return the effects of the functions called
If you call functions on other contracts, you can never know how they are implemented. This means that the effects of these calls are not known either and therefore the only way to return to these effects is to use the throw. Of course, you must always write your contract so as not to call these functions in the first place, if you know that you will have to return the effects, but there are user cases where you only know it.
Loops and block gas limit
There is a limit of the quantity of gas which can be spent in a single block. This limit is flexible, but it is quite difficult to increase it. This means that each function of your contract must remain below a certain amount of gas in all situations (reasonable). The following 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); } }
The contract has in fact several problems, but the one I would like to emphasize here is the problem of the loop: suppose that the voting weights are transferable and divisable as tokens (think of Dao tokens as an example). This means that you can create an arbitrary number of clones of yourself. The creation of such clones will increase the length of the loop in the tallyvotes function until it takes more gas than what is available inside a single block.
This applies to everything that uses loops, also when the loops are not explicitly visible in the contract, for example when you copy tables or chains inside the storage. Again, it is good to have loops of arbitrary length if the length of the loop is controlled by the caller, for example if you itee on a table which has been transmitted as a function argument. But Never Create a situation where the loop length is controlled by a part which would not be the only one to suffer from its failure.
As a side note, this was one of the reasons why we now have the concept of accounts blocked in the DAO contract: the weight of the vote is counted to the point where the vote is expressed, to prevent the loop from being stuck, and if the weight of the vote would not be resolved until the end of the voting period, you could launch a second vote by simply transferring your tokens and then in vote again.
Receive ether / rescue function
If you want your contract to receive ether via the Send () call () regular, you must make its relief function cheap. It can only use 2300, gas which allows no writing storage or function calls which send along the ether. Basically, the only thing you should do inside the rescue function is to report an event so that external processes can react to the fact. Of course, any function of a contract can receive ether and is not linked to this gas restriction. The functions must actually reject the ether sent if they do not want to receive it, but we potentially think of reverse this behavior in a future version.