🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Checks Effects Interactions

Checks-effects-interactions is an order for writing a function. Checks come first, state updates second, and outside calls last.

The balance is cleared before the call goes out

Withdraw checks the balance, clears it, and only then sends ether. A second withdrawal finds nothing left.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Vault {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0, "nothing to withdraw");
        balances[msg.sender] = 0;
        (bool ok, ) = msg.sender.call{value: bal}("");
        require(ok, "payout failed");
    }
}

The same attacker now fails without any guard

The attacker is unchanged from the reentrancy lesson. Its re-entry now finds a cleared balance and fails.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Vault {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0, "nothing to withdraw");
        balances[msg.sender] = 0;
        (bool ok, ) = msg.sender.call{value: bal}("");
        require(ok, "payout failed");
    }
}

contract Attacker {
    Vault public v;
    bool public reentryFailed;

    constructor(address vault) {
        v = Vault(vault);
    }

    function attack() public {
        v.deposit{value: 1 ether}();
        v.withdraw();
    }

    receive() external payable {
        (bool ok, ) = address(v).call(abi.encodeWithSignature("withdraw()"));
        reentryFailed = !ok;
    }
}

A check runs before any outside call

A withdrawal with no balance reverts at the check. No ether moves and no outside address is called.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Vault {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0, "nothing to withdraw");
        balances[msg.sender] = 0;
        (bool ok, ) = msg.sender.call{value: bal}("");
        require(ok, "payout failed");
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Vault {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0, "nothing to withdraw");
        balances[msg.sender] = 0;
        (bool ok, ) = msg.sender.call{value: bal}("");
        require(ok, "payout failed");
    }
}

contract Attacker {
    Vault public v;
    bool public reentryFailed;

    constructor(address vault) {
        v = Vault(vault);
    }

    function attack() public {
        v.deposit{value: 1 ether}();
        v.withdraw();
    }

    receive() external payable {
        (bool ok, ) = address(v).call(abi.encodeWithSignature("withdraw()"));
        reentryFailed = !ok;
    }
}

Key points

  • Check: a require that validates the call before anything changes.
  • Effect: a state update, such as clearing a balance.
  • Interaction: an outside call, placed after all state updates.
  • Failed re-entry: a call back in finds the balance already cleared and reverts.
  • No guard needed: the order alone stops this attack, with no lock flag.

In this example, the vault checks the balance, clears it, and only then sends ether. The attacker re-enters during the payout, but its balance is already zero, so the second withdrawal reverts. The attacker keeps only its own ether and the victim funds stay in the vault, while a normal withdrawal still works.

Note

the pattern applies to every function that calls an outside address.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

In checks-effects-interactions order, what comes last?