K

Security Education Hub

Attack vectors, vulnerability patterns, and contract auditing tools — for defensive purposes

Educational content for security professionals and auditors
CRITICAL

Reentrancy Attack

A malicious contract recursively calls back into the victim contract before the first execution completes, draining funds.

Occurs when external calls are made before state updates. The attacker deploys a contract with a fallback function that re-enters the victim. The DAO hack ($60M, 2016) used this vector.

Real-World Incidents
The DAO ($60M, 2016)
Cream Finance ($34M, 2021)
Lendf.me ($25M, 2020)
Mitigations
Follow Checks-Effects-Interactions (CEI) pattern
Use OpenZeppelin ReentrancyGuard
Avoid external calls in loops
+2 more...
Vulnerable Pattern
1// VULNERABLE - DO NOT USE IN PRODUCTION
2contract VulnerableBank {
3    mapping(address => uint256) public balances;
4
5    function withdraw() public {
6        uint256 amount = balances[msg.sender];
7        require(amount > 0, "No balance");
8
9        // ❌ DANGER: External call BEFORE state update
10        (bool success, ) = msg.sender.call{value: amount}("");
11        require(success, "Transfer failed");
12
13        // State update happens AFTER the external call
14        // Attacker can re-enter before this line executes!
15        balances[msg.sender] = 0; // ← Too late!
16    }
17}
18
19// ATTACKER CONTRACT
20contract Attacker {
21    VulnerableBank public target;
22    uint256 public attackCount;
23
24    receive() external payable {
25        if (address(target).balance >= 1 ether && attackCount < 10) {
26            attackCount++;
27            target.withdraw(); // ← Recursive re-entry!
28        }
29    }
30}
Secure Implementation
1// SECURE - Checks-Effects-Interactions Pattern
2contract SecureBank {
3    mapping(address => uint256) public balances;
4    bool private locked; // Reentrancy guard
5
6    modifier nonReentrant() {
7        require(!locked, "Reentrant call");
8        locked = true;
9        _;
10        locked = false;
11    }
12
13    function withdraw() public nonReentrant {
14        uint256 amount = balances[msg.sender];
15        require(amount > 0, "No balance");
16
17        // ✅ CHECKS: Validate conditions
18        // ✅ EFFECTS: Update state FIRST
19        balances[msg.sender] = 0;
20
21        // ✅ INTERACTIONS: External call LAST
22        (bool success, ) = msg.sender.call{value: amount}("");
23        require(success, "Transfer failed");
24    }
25}
26
27// Better: Use OpenZeppelin's ReentrancyGuard
28import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
29contract SecureBankV2 is ReentrancyGuard {
30    mapping(address => uint256) public balances;
31
32    function withdraw() public nonReentrant {
33        uint256 amount = balances[msg.sender];
34        require(amount > 0, "No balance");
35        balances[msg.sender] = 0;
36        (bool success, ) = msg.sender.call{value: amount}("");
37        require(success);
38    }
39}