Smart Contract Security
Smart Contract Security: Best Practices and Common Pitfalls
As the use of smart contracts continues to grow, so does the importance of ensuring their security. With the rise of decentralized applications (dApps) and the increasing reliance on blockchain technology, the stakes are higher than ever. In this article, we'll delve into the world of smart contract security, highlighting best practices and common pitfalls to watch out for.
Sending Ether: A Subtle yet Critical Issue
Sending Ether is one of the most basic operations in smart contracts, but it's also one of the most prone to errors. When sending Ether, it's essential to consider the gas costs and the potential for silent failures. A silent failure occurs when the recipient's address is not properly initialized, causing the Ether transfer to fail without any visible indication.
Example 1: A Bad Example of an Auction Contract
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;
}
}
This contract has a silent failure vulnerability. When a new bidder calls the bid() function, it can increase the stack size to 1023, causing the send(highestBid) call to fail silently. The previous bidder will not receive the refund, but the new bidder will still be the highest bidder.
Example 2: A Better 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;
}
}
This contract uses a withdrawing pattern, where the recipient controls the transfer of Ether. The withdrawRefund() function allows the recipient to withdraw their refund, ensuring that the Ether transfer is successful.
Using Throw: A Double-Edged Sword
The throw statement is often used to revert any changes made to the state as part of the call. However, it's essential to use it judiciously, as it can cause all gas to be spent and potentially stall calls into the current function.
Example 3: Reverting Ether Transfer
contract myContract {
function receiveEther() {
if (msg.value > 0) {
// do something with the Ether
} else {
throw; // revert Ether transfer
}
}
}
In this example, the receiveEther() function checks if the Ether transfer is valid. If it's not, the throw statement is used to revert the Ether transfer.
Loops and the Block Gas Limit
There is a limit to how much gas can be spent in a single block. This limit is flexible, but it's quite hard to increase it. When using loops, it's essential to ensure that the loop length is controlled by the caller, rather than a party that would not be the only one suffering from its failure.
Example 4: A Bad Example of a Voting Contract
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);
}
}
This contract has a silent failure vulnerability. When vote weights are transferrable and splittable, an arbitrary number of clones can be created, increasing the length of the loop in the tallyVotes() function until it takes more gas than is available inside a single block.
Receiving Ether / the Fallback Function
If you want your contract to receive Ether via the regular send() call, you have to make its fallback function cheap. It can only use 2300 gas, which neither allows any storage write nor function calls that send along Ether.
Example 5: A Cheap Fallback Function
contract myContract {
function() {
// do something cheap, like log an event
}
}
In this example, the fallback function is cheap and only logs an event.
Conclusion
Smart contract security is a critical aspect of decentralized applications. By following best practices and avoiding common pitfalls, you can ensure that your smart contracts are secure and reliable. Remember to consider the handicap of gas costs, use throw judiciously, and ensure that loops are controlled by the caller. By doing so, you can create secure and trustworthy smart contracts that will benefit the entire blockchain community.
Source: https://blog.ethereum.org/en/2016/06/10/smart-contract-security




