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}