Skip to content
Solidity

Reentrancy & Checks-Effects-Interactions

Harden contracts against the most common smart-contract attack.

By EZ4Code Team
securityreentrancybest-practice

Code

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

contract SafeVault {
    mapping(address => uint256) public balances;
    bool private locked; // reentrancy guard

    modifier noReentrant() {
        require(!locked, "reentrant");
        locked = true;
        _;
        locked = false;
    }

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

    // VULNERABLE pattern (DO NOT USE):
    // function withdrawBad() external {
    //   uint256 bal = balances[msg.sender];
    //   (bool ok,) = msg.sender.call{value: bal}("");
    //   require(ok);
    //   balances[msg.sender] = 0;  // state update AFTER external call
    // }

    function withdraw() external noReentrant {
        uint256 bal = balances[msg.sender];
        require(bal > 0, "nothing");

        // 1) Checks (require)
        // 2) Effects (mutate state FIRST)
        balances[msg.sender] = 0;
        // 3) Interactions (external call LAST)
        (bool ok, ) = payable(msg.sender).call{value: bal}("");
        require(ok, "transfer failed");
    }
}

Explanation

Reentrancy happens when an external call re-enters your function before state is updated, letting the caller drain funds. Defense in depth: (1) follow Checks-Effects-Interactions — update state before any external call; (2) use a reentrancy guard (mutex); (3) prefer pull-over-push payments (let users withdraw their own share); (4) bound loops and gas; (5) use OpenZeppelin's ReentrancyGuard and SafeERC20 in production. Also use pragma with a caret (^0.8.24) and run slither/mythril before deploying.

More Solidity Snippets