How to Write Safe Smart Contracts

Dr. Christian Reitwiessner
@ethchris   github.com/chriseth   c@ethdev.com

ÐΞVCON1 - London - 2015-11-10

What does it mean to write safe code?

Code / contract does

  • what programmer intended,
  • what user is told it does,
  • not lose any money / ether.

Compile-Time Checks

Solidity is statically typed. Compiler can check that

  • all functions exist
  • objects are not null
  • operators can be applied
  • ...

contract C {
  address owner; uint ownr;
  function giveMeMyMoney() {
    if (msg.sender == ownr) // type error
      ownr.send(this.balance);
      // type error: no `send` in `uint`
  }
}

Runtime Checks and Exceptions

  • Exception for out-of-bounds access to arrays
    → reverts whole transaction if not caught
  • Downside: Current EVM consumes all gas upon exception.
  • contract C {
      uint[] data;
      function append(uint value) { data.push(value); }
      function replace(uint index, uint value) {
        data[index] = value;
      }
    }
  • Exceptions can also be used manually
  • contract C {
      address owner;
      function changeOwner(address _new) {
        if (msg.sender != owner) throw;
        owner = _new;
      }
    }

Be Nice

If someone uses your contract in a non-intended way, do not make them suffer.

  • throw in unexpected situations
  • contract C {
      uint[] data;
      function append(uint item) {
        if (msg.value > 0) throw;
        data.push(item);
      }
    }
  • do not delete contracts, only disable them
  • contract C {
      ...
      function payout() {
        if (msg.value < 1 ether) throw;
        performPayout();
        suicide(owner);
        // contract will vanish,
        // ether sent to address will be lost
      }
      ...
    }
  • provide natspec docs
  • contract C {
      ...
      /// Sells item `name`, cannot be undone.
      function sell(string name) {
        ...
      }
    }
  • publish source code

Avoid Race-Conditions

  • always either update the state or throw
  • contract C {
      bool bought;
      ...
      function buy(string name) {
        if (bought) throw;
        bought = true;
        ...
      }
    }
  • use the state-machine pattern (docs) with modifiers
  • even if only one user: UI buttons are easily clicked twice

Formally Verify Your Code

contract BinarySearch {
 ///@why3
 /// requires { forall i j: int. 0 <= i <= j < @data.length ->
 ///                                      @data[i] <= @data[j] }
 /// variant { @end - @begin }
 /// ensures { @ret < UInt256.max_uint256 ->
 ///           (@begin <= @ret < @end && @data[@ret] = @value) }
 function find(uint[] data, uint begin, uint end, uint value)
       internal returns (uint ret) {
   uint len = end - begin;
   if (len == 0 || (len == 1 && data[begin] != value)) return uint(-1);
   uint mid = begin + len / 2;
   if (value < data[mid]) return find(data, begin, mid, value);
   else if (value > data[mid]) return find(data, mid + 1, end, value);
   else return mid;
 }
}

How to Send and Receive Money

  • .send(...) only provides tiny amount of gas, but always triggers execution of fallback function
    function() { ...} )
    → make fallback function cheap
    → prepare for callbacks (can mess up your state!)
  • if receiver throws, transfer is reverted, but...
  • .send(...) does not propagate exceptions
    → check the return value of `.send()`

Avoid Large Cheap Arrays

contract C { uint[2**255] arrData; mapping(uint -> uint) mapData; }

arrData[i]: stored at sha3(0x00) + i
mapData[x]: stored at sha3(0x01, x)

  • arrData[sha3(0x01,x) - sha3(0x00)] will overwrite mapData[x] without breaking sha3.
  • Avoid large arrays or at least make arbitrary access costly, e.g. only append one element per transaction.

How to Save some Gas

  • use the optimiser!
  • use packed storage:
    small types will share single storage slot, optimiser tries to combine reads and writes
    contract C {
      struct S { uint128 a; uint64 b; uint64 c; }
      S data;
      function f(uint x, uint y) {
        data.a = x;
        data.b = data.a * y;
        data.c = data.b;
        // -> optimised to single write, no read.
      }
    }
  • use libraries
    re-usable "dynamically loadable" code
  • move computation off-chain:
    function search(uint value, uint index_hint) {
         if (data[index_hint] == value) return index_hint;
         else { ... /* search inside data */ }
    }
    make sure that it is still safe!