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() or send()

  • 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.

struct RequestedWithdrawal {
    uint amount;
    uint time;
}

mapping (address => uint) private balances;
mapping (address => RequestedWithdrawal) private requestedWithdrawals;
uint constant withdrawalWaitPeriod = 28 days; // 4 weeks

function requestWithdrawal() public {
    if (balances[msg.sender] > 0) {
        uint amountToWithdraw = balances[msg.sender];
        balances[msg.sender] = 0; // for simplicity, we withdraw everything;
        // presumably, the deposit function prevents new deposits when withdrawals are in progress

        requestedWithdrawals[msg.sender] = RequestedWithdrawal({
            amount: amountToWithdraw,
            time: block.timestamp
        });
    }
}

function withdraw() public {
    if(requestedWithdrawals[msg.sender].amount > 0 && block.timestamp > requestedWithdrawals[msg.sender].time + withdrawalWaitPeriod) {
        uint amountToWithdraw = requestedWithdrawals[msg.sender].amount;
        requestedWithdrawals[msg.sender].amount = 0;

        require(msg.sender.send(amountToWithdraw));
    }
}

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