Ethereum Smart Contract Best Practices
Ethereum Smart Contract Best Practices
This article is translated from: https://github.com/ConsenSys/smart-contract-best-practices.
Major Sections:
This document aims to provide some security baselines for Solidity developers. It also includes security philosophies, bug bounty program guides, code examples, and tools.
We invite the community to propose changes or additions to this document. Pull Requests are welcome. If you have relevant articles or blogs, please add them to the Bibliography. See our Contribution Guide for details.
Wanted Content
We welcome and look forward to community contributions in the following areas:
- Solidity code testing (including code structure, program frameworks, and common software engineering tests)
- Smart contract development experience summaries, and broader blockchain-based development tips
General Philosophy
Ethereum and other complex blockchain projects are in their early stages and are highly experimental. Therefore, as new bugs and security vulnerabilities are discovered and new features are developed, the security threats faced are constantly changing. This article is just a start for developers writing secure smart contracts.
Developing smart contracts requires a completely new engineering mindset, different from previous project development. Because the cost of mistakes is huge, and it is difficult to patch easily like traditional software. Just like programming hardware directly or developing financial services software, it presents greater challenges than web development and mobile development. Therefore, defending against known vulnerabilities is not enough; you also need to learn new development philosophies:
-
Prepare for failure. Any meaningful smart contract will contain errors. Therefore, your code must be able to correctly handle bugs and vulnerabilities when they appear. Always ensure the following rules:
- Pause the contract when an error occurs ("Circuit Breaker")
- Manage capital risk of accounts (rate limiting, maximum withdrawal limits)
- Effective ways to fix bugs and upgrade features
-
Rollout carefully. Try to find and fix possible bugs before officially releasing the smart contract.
- Test smart contracts thoroughly, and test promptly after any new attack vector is discovered (including already released contracts)
- Provide a bug bounty program starting from the alpha version release on the testnet
- Phased rollout, providing sufficient testing for each phase
-
Keep it simple. Complexity increases the risk of errors.
- Ensure smart contract logic is simple
- Ensure contract and function modularity
- Use widely used contracts or tools (e.g., don't write your own random number generator)
- If conditions permit, clarity is more important than performance
- Only use blockchain for the decentralized parts of your system
-
Stay updated. Ensure you get the latest security progress through resources listed in the next section.
- Check your smart contracts when any new vulnerability is discovered
- Update libraries or tools used to the latest version as quickly as possible
- Use the latest security techniques
-
Understand blockchain properties. Although your previous programming experience applies to Ethereum development, there are still some pitfalls you need to watch out for:
- Be especially careful with calls to external contracts, as you may verify a piece of malicious code and change the control flow
- Understand that your public functions are public, meaning they can be called maliciously. Your private data is also visible to others (on Ethereum)
- Understand gas costs and block gas limits
Fundamental Tradeoffs: Simplicity vs Complexity
There are many tradeoffs when evaluating the architecture and security of a smart contract. The suggestion for any smart contract is to find a balance among various tradeoff points.
From a traditional software engineering perspective: an ideal smart contract needs to be modular, able to reuse code instead of rewriting it, and support component upgrades. From a smart contract security architecture perspective, modularity and reusing rigorously reviewed contracts is the best strategy, especially in complex smart contract systems.
However, there are several important exceptions where the importance ranking obtained from contract security and traditional software engineering perspectives may differ. For each one, the optimal combination needs to be found based on the characteristics of the smart contract system to achieve balance.
- Rigid vs Upgradeable
- Monolithic vs Modular
- Duplication vs Reuse
Rigid vs Upgradeable
In many documents or development guides, including this one, malleability such as termination, upgradeability, or changeability is emphasized. However, for smart contracts, malleability and security are a fundamental tradeoff.
Malleability increases program complexity and potential attack surfaces. For smart contracts that only provide limited functions within a specific time frame, simplicity is vastly more efficient than complexity, such as governance-free, finite-time-frame token-sale contracts.
Monolithic vs Modular
A large independent smart contract puts all variables and modules into one contract. Although only a few well-known smart contract systems have truly achieved monolithic scale, keeping data and processes in one contract does enjoy some advantages -- for example, improving code review efficiency.
Like other tradeoffs discussed here, traditional software development strategies and contract security perspectives differ mainly for simple, short-lifecycle smart contracts; for more complex, long-lifecycle smart contracts, the strategy concepts are basically the same.
Duplication vs Reuse
From a software engineering perspective, smart contract systems hope to maximize reuse where reasonable. There are many ways to reuse contract code in Solidity. Using your own previously deployed verified smart contracts is the safest way to achieve code reuse.
Duplication is still needed when previously owned deployed smart contracts are not reusable. Now Live Libs and Zeppelin Solidity are seeking to provide secure smart contract components that can be reused without rewriting every time. A contract security analysis must mark reused code, especially code that has not previously established a trust level commensurate with the funds at risk in the target smart contract system.
Security Notifications
The following places typically announce newly discovered vulnerabilities in Ethereum or Solidity. The official source for security advisories is the Ethereum Blog, but vulnerabilities are often disclosed and discussed elsewhere first.
- Ethereum Blog: The official Ethereum blog
- Ethereum Blog - Security only: All relevant blogs have the Security tag
- Ethereum Gitter Chat Rooms
- Network Stats
It is strongly recommended that you browse these sites frequently, especially for vulnerabilities mentioned that may affect your smart contracts.
additionally, here is a list of core developers involved in Ethereum security modules. See the bibliography for more information.
- Vitalik Buterin: Twitter, Github, Reddit, Ethereum Blog
- Dr. Christian Reitwiessner: Twitter, Github, Ethereum Blog
- Dr. Gavin Wood: Twitter, Blog, Github
- Vlad Zamfir: Twitter, Github, Ethereum Blog
Besides following core developers, participating in various blockchain security communities is also important, as security vulnerability disclosures or research will be conducted through various parties.
Security Recommendations for Smart Contract Development with Solidity
External Calls
Avoid External Calls
Calling untrusted external contracts can introduce a range of unexpected risks and errors. External calls can execute malicious code within their contract and other contracts they depend on. Therefore, every external call presents a potential security threat. Ideally, remove external calls from your smart contract. When external calls cannot be completely removed, use the suggestions provided in other parts of this section to minimize risks.
Carefully weigh "send()", "transfer()", and "call.value()()"
When transferring Ether, you need to carefully weigh the differences between "someAddress.send()", "someAddress.transfer()", and "someAddress.call.value()()".
x.transfer(y)is equivalent toif (!x.send(y)) throw;. send is the low-level implementation of transfer, and it is recommended to use transfer directly whenever possible.someAddress.send()andsomeAddress.transfer()guarantee reentrancy safety. Although these external smart contract functions can be triggered, the 2,300 gas stipend mainly only allows logging an event.someAddress.call.value()()will send the specified amount of Ether and trigger the execution of the corresponding code. The called external smart contract code will enjoy all remaining gas. Transferring in this way is very prone to reentrancy vulnerabilities and is very unsafe.
Using send() or transfer() generates reentrancy prevention by specifying gas values, but doing so may cause issues when calling the fallback function of a contract, because the gas may be insufficient, and the Contract's fallback function execution requires at least 2,300 gas consumption.
A mechanism known as push and pull attempts to balance both, using send() or transfer() in the push part and call.value()() in the pull part.
Note that using send() or transfer() for transfers does not guarantee the smart contract itself is reentrancy safe; it only guarantees that this specific transfer operation is reentrancy safe.
Handle External Call Errors
Solidity provides a series of low-level methods for executing operations on raw addresses, such as: address.call(), address.callcode(), address.delegatecall(), and address.send(). These low-level methods do not throw exceptions (throw); they simply return false when encountering errors. On the other hand, contract calls (e.g., ExternalContract.doSomething()) automatically propagate exceptions (e.g., if doSomething() throws an exception, ExternalContract.doSomething() will also throw).
If you choose to use low-level methods, be sure to check the return value to handle possible errors.
// bad
someAddress.send(55);
someAddress.call.value(55)(); // this is doubly dangerous, as it will forward all remaining gas and doesn't check for result
someAddress.call.value(100)(bytes4(sha3("deposit()"))); // if deposit throws an exception, the raw call() will only return false and transaction will NOT be reverted
// good
if(!someAddress.send(55)) {
// Some failure code
}
ExternalContract(someAddress).deposit.value(100);Don't Assume You Know the Control Flow of External Calls
Whether using raw calls or contract calls, if this ExternalContract is untrusted, you should assume malicious code exists. Even if the ExternalContract does not contain malicious code, the other contract code it calls might. A concrete danger example is malicious code hijacking control flow leading to race conditions. (See Race Conditions for more discussion on this issue)
Favor pull over push for External Contracts
External calls can fail intentionally or unintentionally. To minimize the damage caused by these external call failures, it is usually good practice to isolate the external call function from the rest of the code, and ultimately have the payee initiate the call to that function. This practice is especially important for payment operations, such as letting users withdraw assets themselves rather than sending directly to them. (This method also avoids gas limit related issues.)
// bad
contract auction {
address highestBidder;
uint highestBid;
function bid() payable {
if (msg.value < highestBid) throw;
if (highestBidder != 0) {
if (!highestBidder.send(highestBid)) { // if this call consistently fails, no one else can bid
throw;
}
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
// good
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() payable external {
if (msg.value < highestBid) throw;
if (highestBidder != 0) {
refunds[highestBidder] += highestBid; // record the refund that this user can claim
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
if (!msg.sender.send(refund)) {
refunds[msg.sender] = refund; // reverting state because send failed
}
}
}Mark Untrusted Contracts
When your own functions call external contracts, your variable, method, and contract interface naming should indicate that they might be unsafe.
// bad
Bank.withdraw(100); // Unclear whether trusted or untrusted
function makeWithdrawal(uint amount) { // Isn't clear that this function is potentially unsafe
Bank.withdraw(amount);
}
// good
UntrustedBank.withdraw(100); // untrusted external call
TrustedBank.withdraw(100); // external but trusted bank contract maintained by XYZ Corp
function makeUntrustedWithdrawal(uint amount) {
UntrustedBank.withdraw(amount);
}Use assert() to Enforce Invariants
Assertion protection will be triggered when assertion conditions are not met -- for example, when an invariant property changes. For example, the issuance ratio of tokens on Ethereum can be resolved in this way in the token issuance contract. Assertion protection often needs to be combined with other techniques, such as pausing the contract and upgrading when an assertion is triggered. (Otherwise, the assertion will always be triggered, and you will be in a deadlock)
For example:
contract Token {
mapping(address => uint) public balanceOf;
uint public totalSupply;
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
totalSupply += msg.value;
assert(this.balance >= totalSupply);
}
}Note that assertion protection is not a strict balance check, because smart contracts can be forcibly sent Ether without going through the deposit() function!
Correctly Use assert() and require()
assert() and require() were added in Solidity 0.4.10. require(condition) uses to validate user inputs, and throws an exception if the condition is not met. It should be used to validate all user inputs. assert(condition) also throws an exception if not met, but should ideally only be used for invariants: internal errors or your smart contract getting into an invalid state. Following these paradigms, use analysis tools to verify that these invalid opcodes are never executed: meaning no invariants exist in the code, and the code has been formally verified.
Beware of Rounding with Integer Division
All integer division rounds down to the nearest integer. If you need higher precision, consider using a multiplier, or store both the numerator and denominator.
(In the future Solidity will have a fixed-point type to make this easier.)
// bad
uint x = 5 / 2; // Result is 2, all integer divison rounds DOWN to the nearest integer
// good
uint multiplier = 10;
uint x = (5 * multiplier) / 2;
uint numerator = 5;
uint denominator = 2;Remember Ether can be Forcibly Sent
Be careful when writing invariants that check account balances.
An attacker can forcibly send wei to any account, and this cannot be prevented (even if the fallback function throws).
An attacker can create a contract with just 1 wei, and then call selfdestruct(victimAddress). No code is executed in victimAddress, so this cannot be prevented.
Don't Assume Zero Balance on Contract Creation
Attackers can send wei to the contract address before the contract is created. The contract cannot assume its initial state contains zero balance. See issue 61 for more info.
Remember On-Chain Data is Public
Many applications require submitted data to be private until a certain point in time to work. Games (e.g., on-chain Rock-Paper-Scissors) and auctions (e.g., sealed-bid second-price auctions) are two typical examples. If your application has privacy issues, be sure to avoid releasing user information prematurely.
For example:
- In Rock-Paper-Scissors, require both players to submit the hash of their "move", then require both to reveal their move later; if the "move" doesn't match the previously submitted hash, throw an exception.
- In auctions, require players to submit the hash of their bid (along with a deposit exceeding their bid) in the initial phase, and then submit their bid funds in the second phase.
- When developing an application relying on a random number generator, the correct order should be (1) players submit moves, (2) generate random number, (3) players pay. Random number generation is a field worth researching; current best solutions include Bitcoin block headers (verified via http://btcrelay.org), hash-commit-reveal schemes (e.g., one party generates a number, submits its hash as a "commitment" to this number, and then reveals the number itself later), and RANDAO.
- If you are implementing frequent batch auctions, a hash-commit mechanism is also a good choice.
Tradeoffs between Abstract Contracts and Interfaces
Interfaces and Abstract contracts are used to make smart contracts better customizable and reusable. Interfaces were introduced in Solidity 0.4.11 and are very similar to Abstract contracts but cannot define methods, only declare them. Interfaces have limitations such as not being able to access storage or inherit from other Interfaces, which usually makes Abstract contracts more practical. Nevertheless, Interfaces are still very useful in the design phase before implementing smart contracts. Additionally, note that if a smart contract inherits from another Abstract contract, it must implement all functions declared but not implemented in the Abstract contract, otherwise it will also become an Abstract contract.
Participants May "Go Offline" and Not Return
Do not let refund and claim processes depend on a specific action performed by a participant without other ways to get funds. For example, in Rock-Paper-Scissors, a common mistake is not to pay out until both players submit their moves. However, a malicious player can cause the opponent to lose by never submitting their move -- effectively, if deeps sees the other player's revealed move and decides he will lose, he has every reason not to submit his own move. These issues also appear in channel settlement. When such situations arise causing problems: (1) provide a way to circumvent non-participants, possibly by setting time limits, and (2) consider providing additional economic incentives for participants to submit information in all situations where they should.
Keep Fallback Functions Simple
The Fallback function is called when a contract executes a message call with no arguments (or when no matching function is found), and when .send() or .transfer() is called, only 2,300 gas is available for the fallback function execution after failure. If you wish to listen for Ether received by .send() or .transfer(), use an event in the fallback function. Write fallback functions carefully to avoid running out of gas.
// bad
function() payable { balances[msg.sender] += msg.value; }
// good
function deposit() payable external { balances[msg.sender] += msg.value; }
function() payable { LogDepositReceived(msg.sender); }Explicitly Mark Function and State Variable Visibility
Explicitly mark the visibility of functions and state variables. Functions can be declared as external, public, internal, or private. Understand the differences; for example, external might be sufficient instead of public. For state variables, external is impossible. Explicitly marking visibility makes it easier to avoid incorrect assumptions about who can call the function or access the variable.
// bad
uint x; // the default is private for state variables, but it should be made explicit
function buy() { // the default is public
// public code
}
// good
uint private y;
function buy() external {
// only callable externally
}
function utility() public {
// callable externally, as well as internally: changing this code requires thinking about both cases.
}
function internalAction() internal {
// internal code
}Lock Pragmas to Specific Compiler Version
Smart contracts should be deployed with the same compiler version used most often during testing. Locking the compiler version helps ensure the contract isn't deployed with the latest compiler which might have undiscovered bugs. Smart contracts might also be deployed by others, and the pragma indicates which compiler version the author wishes to be used.
// bad
pragma solidity ^0.4.4;
// good
pragma solidity 0.4.4;Beware of Division by Zero (Solidity < 0.4)
Prior to version 0.4, when a number attempts to divide by zero, Solidity returns zero and does not throw an exception. Ensure the Solidity version you use is at least 0.4.
Differentiate Functions and Events
To prevent confusion between functions and events, name an event using visualization and add a prefix (we suggest LOG). For functions, always start with a lowercase letter, except for constructors.
// bad
event Transfer() {}
function transfer() {}
// good
event LogTransfer() {}
function transfer() external {}Use Newer Solidity Constructs
Use more appropriate constructors/aliases, like selfdestruct (old version suicide) and keccak256 (old version sha3). Patterns like require(msg.sender.send(1 ether)) can also be simplified to use transfer(), like msg.sender.transfer(1 ether).
Known Attacks
Race Conditions*
One of the major dangers of calling external contracts is that they can take over the control flow and make changes to data that the calling function did not expect. This class of bugs takes many forms, and both of the major bugs that led to the DAO collapse were of this type.
Reentrancy
This version of the bug is noticed because it can be called repeatedly multiple times before the first call to the function completes. Repeated calls to this function can cause massive destruction.
// INSECURE
mapping (address => uint) private userBalances;
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call withdrawBalance again
userBalances[msg.sender] = 0;
}It can be seen that when msg.sender.call.value()() is called, userBalances[msg.sender] is not zeroed out, so withdrawBalance() functions can be successfully recursively called many times before that. A very similar bug appeared in the DAO attack.
In the example given, the best way is to use send() instead of call.value()(). This will avoid executing extra code.
However, if you cannot completely remove external calls, another simple way to prevent this attack is to ensure you don't make external calls until you finish all your internal work:
mapping (address => uint) private userBalances;
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
userBalances[msg.sender] = 0;
if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // The user's balance is already 0, so future invocations won't withdraw anything
}Note that if you have another function that also calls withdrawBalance(), potential attacks exist there too, so you must recognize that any contract that calls untrusted contract code is also untrusted. Continue reading the discussion of potential threat solutions below.
Cross-function Race Conditions
Attackers can also use two different functions that share state variables to launch similar attacks.
// INSECURE
mapping (address => uint) private userBalances;
function transfer(address to, uint amount) {
if (userBalances[msg.sender] >= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call transfer()
userBalances[msg.sender] = 0;
}In this example, attackers call transfer() when their external withdrawBalance function is called. If withdrawBalance has not yet executed to userBalances[msg.sender] = 0; here, their balance has not been zeroed, so they can call transfer() to transfer tokens even though they have actually received the tokens. This weakness can also be used in DAO attacks.
The same solution works: zero out before executing transfer operations. Also note that in this example all functions are within the same contract. However, if these contracts share state, the same bug can occur in cross-contract calls.
Pitfalls in Race Condition Solutions
Since race conditions can occur in both cross-function calls and cross-contract calls, any solution that only avoids reentrancy is insufficient.
Alternatively, we suggest finishing all internal work first and then executing external calls. This rule avoids race conditions. However, you should avoid potential calls to external functions prematurely as well as avoiding calling eternal functions that also call external functions. For example, the following code is unsafe:
// INSECURE
mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;
function withdraw(address recipient) public {
uint amountToWithdraw = userBalances[recipient];
rewardsForA[recipient] = 0;
if (!(recipient.call.value(amountToWithdraw)())) { throw; }
}
function getFirstWithdrawalBonus(address recipient) public {
if (claimedBonus[recipient]) { throw; } // Each recipient should only be able to claim the bonus once
rewardsForA[recipient] += 100;
withdraw(recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again.
claimedBonus[recipient] = true;
}Although getFirstWithdrawalBonus() does not directly call an external contract, the withdraw() it calls leads to a race condition. Here you should not consider withdraw() trusted.
mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;
function untrustedWithdraw(address recipient) public {
uint amountToWithdraw = userBalances[recipient];
rewardsForA[recipient] = 0;
if (!(recipient.call.value(amountToWithdraw)())) { throw; }
}
function untrustedGetFirstWithdrawalBonus(address recipient) public {
if (claimedBonus[recipient]) { throw; } // Each recipient should only be able to claim the bonus once
claimedBonus[recipient] = true;
rewardsForA[recipient] += 100;
untrustedWithdraw(recipient); // claimedBonus has been set to true, so reentry is impossible
}In addition to fixing the bug to make reentrancy impossible, untrusted functions have also been marked. Same scenario: untrustedGetFirstWithdrawalBonus() calls untrustedWithdraw(), which calls an external contract, so here untrustedGetFirstWithdrawalBonus() is unsafe.
Another frequently mentioned solution is to use mutex. It allows "locking" the current state so only the current owner of the lock can change the current state. A simple example follows:
// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state
mapping (address => uint) private balances;
bool private lockBalances;
function deposit() payable public returns (bool) {
if (!lockBalances) {
lockBalances = true;
balances[msg.sender] += msg.value;
lockBalances = false;
return true;
}
throw;
}
function withdraw(uint amount) payable public returns (bool) {
if (!lockBalances && amount > 0 && balances[msg.sender] >= amount) {
lockBalances = true;
if (msg.sender.call(amount)()) { // Normally insecure, but the mutex saves it
balances[msg.sender] -= amount;
}
lockBalances = false;
return true;
}
throw;
}If the user attempts to call withdraw() a second time before the first call finishes, it will be locked. This looks effective, but the problem becomes severe when you use multiple contracts interacting with each other. Here is insecure code:
// INSECURE
contract StateHolder {
uint private n;
address private lockHolder;
function getLock() {
if (lockHolder != 0) { throw; }
lockHolder = msg.sender;
}
function releaseLock() {
lockHolder = 0;
}
function set(uint newState) {
if (msg.sender != lockHolder) { throw; }
n = newState;
}
}An attacker can just call getLock() and then never call releaseLock(). If they do this, the contract will be locked forever, and no further operations will happen. If you use mutexes to avoid race conditions, make sure there are no places where the lock process can be interrupted or the lock never released. (There are also potential threats here, such as deadlocks and livelocks. It's best to read extensive literature before deciding to use locks.)
Transaction Ordering Dependence (TOD) / Front Running
The above involves attackers executing malicious code within a single transaction to produce race conditions. Next, we demonstrate race conditions caused by the operating principle of the blockchain itself: the order of transactions (within the same block) is easily manipulated.
Since transactions are stored in the mempool for a short time, it is possible to know what actions will happen before miners pack them into a block. This is troublesome for a decentralized market because token transaction information can be viewed, and the transaction order can be changed before it is packed into a block. Avoiding this is difficult because it boils down to the specific contract itself. For example, in a market, it is best to implement batch auctions (this also prevents high-frequency trading problems). Another method uses a pre-commit scheme ("I will provide details later").
Timestamp Dependence
Note that block timestamps can be manipulated by miners, and all direct and indirect uses of timestamps should be considered. Block number and average block time can be used to estimate time, but this is not proof that block time may change in the future (e.g., changes expected by Casper).
uint someVariable = now + 1;
if (now % 2 == 0) { // the now can be manipulated by the miner
}
if ((someVariable - 100) % 2 == 0) { // someVariable can be manipulated by the miner
}Integer Overflow and Underflow
There are roughly 20 examples regarding overflow and underflow here.
Consider this simple transfer operation:
mapping (address => uint256) public balanceOf;
// INSECURE
function transfer(address _to, uint256 _value) {
/* Check if sender has balance */
if (balanceOf[msg.sender] < _value)
throw;
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
// SECURE
function transfer(address _to, uint256 _value) {
/* Check if sender has balance and for overflows */
if (balanceOf[msg.sender] < _value || balanceOf[_to] + _value < balanceOf[_to])
throw;
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}If the balance reaches the maximum value of uint (2^256), it will turn back to 0. This should be checked. Whether overflow is relevant depends on the specific implementation. Think about whether the uint value has a chance to become that large or who will change its value. If any user has the right to change the uint value, it will be more vulnerable. If only administrators can change its value, it might be safe because there is no other way to cross this limit.
The same logic applies to underflow. If a uint is changed to be less than 0, it will cause an underflow and be set to the maximum value (2^256).
Be careful with smaller number types like uint8, uint16, uint24, etc.: they reach maximum values much more easily.
DoS with (Unexpected) Throw
Consider the simple smart contract below:
// INSECURE
contract Auction {
address currentLeader;
uint highestBid;
function bid() payable {
if (msg.value <= highestBid) { throw; }
if (!currentLeader.send(highestBid)) { throw; } // Refund the old leader, and throw if it fails
currentLeader = msg.sender;
highestBid = msg.value;
}
}When there is a higher bid, it attempts to refund the previous highest bidder. If the refund fails, it throws an exception. This means a malicious bidder can become the current highest bidder while ensuring that any refund to their address always fails. This can prevent anyone else from calling the "bid()" function, keeping themselves in the lead forever. It is recommended to establish a pull-based payment system as mentioned before.
Another example is when a contract might pay users (e.g., backers in a crowdfunding contract) by iterating through an array. Usually ensure each payment succeeds. If not, throw an exception. The problem is, if one payment fails, you will revert the entire payment system, meaning the loop will never complete. Because one address failed to transfer, no one else gets paid.
address[] private refundAddresses;
mapping (address => uint) public refunds;
// bad
function refundAll() public {
for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
if(refundAddresses[x].send(refunds[refundAddresses[x]])) {
throw; // doubly bad, now a single failure on send will hold up all funds
}
}
}Once again, the same solution: Favor pull over push payments.
DoS with Block Gas Limit
You might have noticed another problem in the previous example: transferring to everyone at once is likely to hit the Ethereum block gas limit. Ethereum specifies the gas limit each block can spend; if exceeded, your transaction will fail.
Even without intentional attacks, this can cause problems. However, it is worst if gas costs are manipulated by attackers. In the previous example, if an attacker adds a portion of the receiving list and sets each receiving address to receive a small amount of refund. In this way, more gas will be spent, leading to reaching the block gas limit, and the entire transfer operation will end in failure.
Props again for Favor pull over push payments.
If you really must transfer by traversing a variable-length array, it is best to estimate how many blocks and transactions it will take to complete. Then you must also be able to track where you are currently so that you can resume from there when the operation fails, for example:
struct Payee {
address addr;
uint256 value;
}
Payee payees[];
uint256 nextPayeeIndex;
function payOut() {
uint256 i = nextPayeeIndex;
while (i < payees.length && msg.gas > 200000) {
payees[i].addr.send(payees[i].value);
i++;
}
nextPayeeIndex = i;
}As shown above, you must ensure that no other executing transactions will cause any errors before the next execution of payOut(). If necessary, use this method to handle it.
~~Call Depth Attack~~
Due to the hard fork performed by EIP 150, Call Depth attacks are no longer feasible* (Since Ethereum limited Call Depth to a maximum of 1024, ensuring gas is used correctly before reaching maximum depth)
Software Engineering Techniques
As we discussed in the General Philosophy section, avoiding known attacks is not enough. Since losses from on-chain attacks are huge, you must also change the way you write software to defend against various attacks.
We advocate "Prepare for Failure". It is impossible to know if your code is secure in advance. However, we can allow contracts to fail in predictable ways and then minimize the loss from failure. This chapter will take you through how to prepare for predictable failures.
Note: There is always risk when adding new components to your system. A poor design itself can be a vulnerability - some well-designed components can also have vulnerabilities during interaction. Carefully consider every technology you use in your contract and how to integrate them to create a stable and reliable system.
Upgrading Broken Contracts
If errors are found in the code or improvements need to be made to certain parts, the code needs to be changed. Finding an error on Ethereum but having no way to handle it makes little sense.
How to design a contract upgrade system on Ethereum is an area of active research, and we cannot cover all complex areas in this article. However, there are two common basic approaches. The simplest is to design a dedicated registry contract that holds the address of the latest version of the contract. A more seamless method for contract users is to design a contract that forwards call requests and data to the latest version of the contract.
Regardless of the technique used, components must be modular and well-separated so that code changes do not break existing functionality, create orphan data, or incur huge costs. Especially separate complex logic from data storage so you don't have to recreate all data when using changed functionality.
How appropriate parties participate in deciding to upgrade code is also crucial. Depending on your contract, upgrading code might require voting by single or multiple trusted parties. If this process takes a long time, you must consider whether to switch to a more efficient way to prevent attacks, such as Emergency Stop or Circuit Breaker.
Example 1: Use a Registry Contract to Store the Latest Version of the Contract
In this example, calls are not forwarded, so users must fetch the latest contract address before every interaction.
contract SomeRegister {
address backendContract;
address[] previousBackends;
address owner;
function SomeRegister() {
owner = msg.sender;
}
modifier onlyOwner() {
if (msg.sender != owner) {
throw;
}
_;
}
function changeBackend(address newBackend) public
onlyOwner()
returns (bool)
{
if(newBackend != backendContract) {
previousBackends.push(backendContract);
backendContract = newBackend;
return true;
}
}
}