Last weekend the B9lab team competed in the 2017 Dutch Blockchain Hackathon. One of the sponsors, ING, also created an Ethereum side challenge and during the event asked the participating hackers to solve a problem:

If you find the bug in the smart contract and you’re the first to withdraw the (majority of the) Ether you’ll get to keep the Ether as well as receive a half-day Innovation Workshop delivered by ING!

In the early hours of Saturday morning, during a short break from working on our main hackathon project, we realised that the ING challenge still hadn’t been solved by anyone yet and so we decided to give it a go. See if you can work out how to exploit ING’s faulty bank smart contract for yourself, I’ll provide the answer below:

pragma solidity ^0.4.0;



/**

* @title allows inheriting contracts to selfdestruct and send remaining funds to bank-owner.

*/

contract mortal {

/* Define variable owner of the type address*/

address owner;



/* this function is executed at initialization and sets the owner of the contract */

function mortal() { owner = msg.sender; }



/* Function to recover the funds on the contract */

function kill() {

if (msg.sender == owner) selfdestruct(owner);

}

}





/**

* @title A Transparent Decentralized Autonomous (and vulnerable) Bank.

* **** THIS CONTRACT CONTAINS A BUG - DO NOT USE ****

*/

contract VulnerableBank is mortal {



// Record of all balances (in wei).

mapping (address => uint256) public userBalances;



// Payment execution event.

event Transfer(string message, address from, address to, uint256 amount);

event Deposit(address from, uint256 amount);



/**

* @dev View the balance of the specified address.

* @param user Address of the user.

* @return balance The calculated perimeter.

*/

function getBalance(address user) constant returns(uint) {

return userBalances[user];

}



/**

* @dev Store the amount of wei sent with the message in your bank-account.

*/

function deposit() payable {

Deposit(msg.sender, msg.value);

userBalances[msg.sender] += msg.value;

}



/**;

* @dev Transfers the specified amount of wei from the sender's balance

* to the specified recipient-address.

* @param amount The amount of wei to transfer.

* @param to The recipients address.

*/

function executePayment(uint256 amount, address to) payable {



// Only allow transfers from the message sender.

address from = msg.sender;

//address from = tx.origin;



if (amount > userBalances[from]) {

Transfer("Amount greater than balance...", from, to, amount);



} else {

if (!to.call.value(amount)()) {

Transfer("Call.value went wrong...", from, to, amount);

} else {

userBalances[from] -= amount;

Transfer("Payment executed.", from, to, amount);

}

}

}

}

*

*

******* WARNING ANSWER BELOW ***********

*

*

Did you manage to work it out?

The smart contract can be exploited with a re-entry pattern attack. It contains a similar bug found in “The DAO” that led to the now infamous $150M hack and eventual hard fork of Ethereum.

The bank contract accepts ether deposits from any address, and will only payout ether when a request comes from the same address that made the deposit. Although there are measures to check the balance of the depositor, the contract can be exploited with a recursive withdrawal pattern because it updates the balance after the transfer. So, if a user deposits ether using a contract of their own and then immediately requests it back in that same contract, then the faulty bank contract will not have yet come around to updating the user’s balance. The bank contract will therefore withdraw the amount back to the depositor multiple times and in effect drain the balances of other users of the contract in the process.

So, we knew how to exploit the contract, but we wanted to not only drain the contract of the majority of its ether, but to extract all of it with the minimum amount of gas used. We therefore needed to drain the exact balance and in a single attack. We duplicated the faulty bank contract on TestRPC by using its own original deploy bytecode in order to carry out quick and free tests and make sure we had done everything correctly. We actually found out later that the Solidity code given in the challenge documentation was purposefully incorrect. The actual deployed code contained a trip-wire whereby if you re-entered too many times it would fail, however, our code was efficient enough to avoid this happening. We also checked the exact current balance of the account so we knew how much ether to send in the attack (the balance was actually slightly higher than when ING created the challenge due to several other teams sending it ether to see what would happen, thanks guys!). Once we were absolutely sure of what we were doing and how the faulty contract would react, we successfully carried out the attack and drained it of everything, leaving it with a balance of zero:

Start to finish, the whole thing took us around an hour. A few minutes to work out that it was a re-entry exploit. Around 30 minutes to write the attack code and the rest on stumbling upon Mist overlooked “features”, worries over losing ether on the main net and recording the process. We’ve put all of our code online here so that you can see exactly how we did it.

A big thank you to ING for creating this fun challenge and for the awesome prize of an Innovation Workshop!

Photo: Frank Groeliken

If you want to learn more about Ethereum, Blockchains in general and how to write bug-free smart contracts check out our online Academy. We have a LOT planned for this year, so stay tuned ;)