Best Practices
smart contract best practices
External Calls
Use caution when making external calls
Mark untrusted contracts
Avoid state changes after external calls
Don't use
transfer()
orsend()
Handle errors in external calls
Favor pull over push for external calls
Don't delegatecall to untrusted code
Force-feeding Ether
Beware of coding an invariant that strictly checks the balance of a contract.
An attacker can forcibly send ether to any account using selfdestruct
and this cannot be prevented (not even with a fallback function that does a revert()
).
Also, since contract addresses can be precomputed, ether can be sent to an address before the contract is deployed.
Speed Bumps
Speed bumps slow down actions, so that if malicious actions occur, there is time to recover.
Rate Limiting
Rate limiting halts or requires approval for substantial changes. E.g. limit deposit withdrawals, tokens issued by contract.
Deployment
Contracts should have a substantial and prolonged testing period - before substantial money is put at risk. Ideally one should have:
A full test suite with 100% test coverage (or close to it)
Deploy on your own testnet
Deploy on the public testnet with substantial testing and bug bounties
Exhaustive testing should allow various players to interact with the contract at volume
Deploy on the mainnet in beta, with limits to the amount at risk
Automatic Deprecation
During testing, you can force an automatic deprecation by preventing any actions, after a certain time period. For example, an alpha contract may work for several weeks and then automatically shut down all actions, except for the final withdrawal.
Restrict amount of ether per user/contract
Reduces risk in case of insecure contracts
Assert, Require, Revert
Enforce invariants with assert()
An assert guard triggers when an assertion fails. Assert guards should often be combined with other techniques, such as pausing the contract and allowing upgrades, else you may end up stuck, with an assertion that is always failing.
**Use assert()
, require()
, revert()
properly
The assert function should only be used to test for internal errors, and to check invariants.
The **require** function should be used to ensure valid conditions, such as inputs, or contract state variables are met, or to validate return values from calls to external contracts.
Following this paradigm allows formal analysis tools to verify that the invalid opcode can never be reached: meaning no invariants in the code are violated and that the code is formally verified.
Modifiers as Guards
The code inside a modifier is usually executed before the function body, so any state changes or external calls will violate the Checks-Effects-Interactions pattern. Moreover, these statements may also remain unnoticed by the developer, as the code for modifier may be far from the function declaration. For example, an external call in modifier can lead to the reentrancy attack:
In this case, the Registry
contract can make a reentrancy attack by calling Election.vote()
inside isVoter()
.
Integer Division
All integer division rounds down to the nearest integer. If you need more precision, consider using a multiplier, or store both the numerator and denominator.
Locking Pragmas
Contracts should be deployed with the same compiler version and flags that they have been tested the most with. Locking the pragma helps ensure that contracts do not accidentally get deployed using, for example, the latest compiler which may have higher risks of undiscovered bugs.
Event Monitoring
Events can be used to monitor the contract's activity after it is deployed. Transaction history itself may be insufficient, as message calls between contracts are not recorded in the blockchain.
Events also show input parameters, and can be used to trigger functions in the user interface. Events that were emitted stay in the blockchain along with the other contract data and are available for future audit.
Complex Inheritance
When a contract is deployed, the compiler will linearize the inheritance from right to left (after the keyword is the parents are listed from the most base-like to the most derived).
The consequence of the linearization will yield a fee
value of 5, since C is the most derived contract.
EXTCODESIZE Checks
Avoid using extcodesize
to check for Externally Owned Accounts.
The following modifier (or a similar check) is often used to verify whether a call was made from an externally owned account (EOA) or a contract account:
The idea is straightforward: if an address contains code, it's not an EOA but a contract account. However, a contract does not have source code available during construction. This means that while the constructor is running, it can make calls to other contracts, but extcodesize
for its address returns zero. Below is a minimal example that shows how this check can be circumvented:
Because contract addresses can be pre-computed, this check could also fail if it checks an address which is empty at block n
, but which has a contract deployed to it at some block greater than n
.
Last updated