Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Smart contract security audit — 10 DeFi bug classes (accounting desync, access control, incomplete path, off-by-one, oracle, ERC4626, reentrancy, flash loan, signature replay, proxy), pre-dive kill signals (TVL < $500K etc), Foundry PoC template, grep patterns for each class, and real Immunefi paid examples. Use for any Solidity/Rust contract audit or when deciding whether a DeFi target is worth hunting.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 233% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 140% | 0% |
| case-12 | ✓→✗ | ▼ Worse | 196% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 140% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 312% | 0% |
10 bug classes. Pre-dive kill signals. Foundry PoC template. Real paid examples.
> ZKsync lesson: $322M TVL + OZ audit + 750K LOC + 5 sessions = 0 findings. Large well-audited bridges are extremely hard.
max_realistic_payout = min(10% × TVL, program_cap) — if < $10K, skipSoft kill: OZ/ToB/Cyfrin audit on current version + codebase > 500K LOC → expect 40+ hours for maybe 1 finding. Only proceed if bounty floor > $50K AND you have protocol-specific expertise.
Target scoring (go if >= 6/10):
> "Read ALL sibling functions. If vote() has a modifier, check poke(), reset(), harvest(). The missing modifier on the sibling IS the bug."
This single rule explains 19% of all Critical findings.
> #1 Critical bug class — 28% of all Criticals on Immunefi.
Two state variables supposed to stay in sync. One code path updates A but forgets B. Later code reads both and makes decisions based on stale B.
Real Value = A - B
If A updated but B isn't → Real Value appears larger → phantom valueVariant 1: Phantom Yield (Yeet protocol — 35 duplicate reports)
solidityfunction startUnstake(uint256 amount) external { totalSupply -= amount; // decremented BEFORE transfer // aToken.balanceOf(this) still reflects old value // yieldAmount = aToken.balanceOf - totalSupply = phantom yield }
Variant 2: Fast Path Skips State Update (Alchemix V3)
solidityfunction claimRedemption(uint256 tokenId) external { if (transmuter.balance >= amount) { transmuter.transfer(user, amount); _burn(tokenId); return; // EARLY RETURN — cumulativeEarmarked, _redemptionWeight, totalDebt never updated } // Slow path: updates all state vars correctly alchemist.redeem(...); }
Variant 3: Update Happens in Wrong Order (Alchemix)
solidityfunction deposit(uint256 amount) external { _shares = (amount * totalShares) / totalAssets; // calculated BEFORE deposit totalAssets += amount; // assets added AFTER shares calculated → wrong rate }
bash# Find all accounting variables grep -rn "totalSupply\|totalShares\|totalAssets\|totalDebt\|cumulativeReward\|rewardPerShare" contracts/ # Find all early returns in claim/redeem functions grep -rn "\breturn\b" contracts/ -B3 | grep -B3 "if\b" # For each early return: which state updates in normal path are skipped?
> #2 Critical — 19% of Criticals. $953M lost in 2024 alone.
solidityfunction vote(uint256 tokenId) external onlyNewEpoch(tokenId) { // guarded function reset(uint256 tokenId) external onlyNewEpoch(tokenId) { // guarded function poke(uint256 tokenId) external { // NO GUARD → infinite FLUX inflation }
solidityfunction split(uint256 tokenId, uint256 amount) external { _requireOwned(tokenId); // checks if token EXISTS, not if caller OWNS it _burn(tokenId); _mint(msg.sender, amount); // attacker steals tokens they don't own }
solidity// VULNERABLE — non-admin silently gets through: modifier onlyAdmin() { if (msg.sender == admin) { _; // body only executes for admin, but non-admin doesn't revert } } // CORRECT: require(msg.sender == admin, "Not admin"); _;
solidityfunction initialize(address _owner) public { // MISSING: initializer modifier owner = _owner; // anyone can call → become owner } // Fix: constructor() { _disableInitializers(); }
bash# Find sibling function families — do ALL have the same modifier set? grep -rn "function vote\|function poke\|function reset\|function update\|function claim\|function harvest" contracts/ -A2 # Ownership check: existence vs ownership? grep -rn "_requireOwned\|ownerOf\|_isApprovedOrOwner\|_checkAuthorized" contracts/ -B5 # Silent modifiers grep -rn "modifier\b" contracts/ -A8 | grep -B3 "if (" | grep -v "require\|revert" # Uninitialized initializer grep -rn "function initialize\b" contracts/ -A3 grep -rn "_disableInitializers()" contracts/
| Protocol | Payout | Bug | |---|---|---| | Wormhole | $10M | Uninitialized UUPS proxy → anyone calls initialize() | | ZeroLend | n/a | split() uses existence check, not ownership check | | Alchemix | n/a | poke() missing onlyNewEpoch → infinite FLUX inflation | | Parity | $150M frozen | No access control on initWallet() in library |
> #3 Critical — 17% of Criticals.
1. List all state changes in function A (deposit/place/create)
2. List all state changes in function B (withdraw/update/cancel)
3. For each state change in A: does B have the corresponding reverse?
4. For each token transfer in A: does B have the corresponding refund?
If A does X but B doesn't do the reverse of X → BUG.solidityfunction place_order(OrderInput calldata order) external { token.safeTransferFrom(msg.sender, address(this), order.price); // takes tokens orders[orderId] = order; } function update_order(OrderInput calldata updatedOrder) external { // BUG: NO REFUND for sell orders when price decreases → tokens permanently stuck orders[orderId] = updatedOrder; }
solidityfunction swapForETH(uint256 amountIn) external { token.safeTransferFrom(msg.sender, address(this), amountIn); uint256 filled = dex.swap(amountIn); // partial fill possible _refundExcessEth(amountIn - filled); // BUG: refunds ETH only, not ERC20 }
solidityfunction deposit(uint256 assets, address receiver) public override { shares = _deposit(assets, receiver); // includes receipt validation } function mint(uint256 shares, address receiver) public override { assets = convertToAssets(shares); _mint(receiver, shares); // MISSING: _deposit() validation → mints without receiving assets }
bashgrep -rn "function place_\|function create_\|function add_\|function open_" contracts/ -A5 grep -rn "function update_\|function modify_\|function cancel_" contracts/ -A5 grep -rn "safeApprove\b" contracts/ # safeApprove without zero-reset before grep -rn "delete\b" contracts/ -B5 -A5 # delete before operation completes grep -rn "function deposit\|function mint\|function withdraw\|function redeem" contracts/ -A10
> #4 High — 22% of Highs. Single character change. Massive impact.
solidity// VeChain Stargate — post-exit reward drain: function _claimableDelegationPeriods(address delegator) internal view returns (uint256) { if (endPeriod > nextClaimablePeriod) { // BUG: should be >= return 0; // exited users get nothing } return nextClaimablePeriod - lastClaimedPeriod; // rewards for period AFTER exit }
> For every if (A > B): "What happens when A == B?" Is that correct?
> vs >= at period endblock.timestamp == deadline lock or unlock?break with > vs >=i <= array.length (should be i < array.length)>= amount allows exact full withdrawal?bash# Boundaries in comparisons grep -rn "Period\|Epoch\|Round\|Deadline\|period\|epoch\|deadline" contracts/ -A3 | grep "[<>][^=]" # Loop breaks grep -rn "\bbreak\b" contracts/ -B10 # Off-by-one in array access grep -rn "\.length\s*-\s*1\|i\s*<=\s*.*\.length\b" contracts/
> 12% of all reports. Largest individual payouts. $117M Mango, $70M Curve.
solidity// VULNERABLE: (, int256 price,,,) = priceFeed.latestRoundData(); return uint256(price); // If Chainlink node goes down, stale price returned indefinitely // CORRECT: (, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData(); require(block.timestamp - updatedAt <= MAX_PRICE_AGE, "Stale price"); require(price > 0, "Invalid price");
solidity// VULNERABLE: PythStructs.Price memory p = pyth.getPriceUnsafe(priceFeed); return p.price; // ignores p.conf (confidence interval) // CORRECT: require(p.conf * 10 <= uint64(p.price), "Price too uncertain"); // conf > 10% of price = untrustworthy
solidity// VULNERABLE: 60-second TWAP uint32[] memory secondsAgos = new uint32[](2); secondsAgos[0] = 60; secondsAgos[1] = 0; // Flash loan can shift price for entire 60s window // CORRECT: 1800s minimum TWAP (30 min)
solidity// VULNERABLE: only Uniswap spot price uint price = getUniswapSpotPrice(token); // flash loan manipulatable // CORRECT: Chainlink primary, Uniswap TWAP as fallback, require close agreement
bash# Missing staleness check grep -rn "latestRoundData" contracts/ -A5 | grep -v "updatedAt\|timestamp" # Pyth price usage — confidence interval checked? grep -rn "getPriceUnsafe\|getPrice\b" contracts/ -A8 | grep -v "conf\|confidence" # TWAP windows — short TWAP flag grep -rn "secondsAgo\|TWAP\|cardinality" contracts/ -A5
solidity// VULNERABLE — first depositor attack: // 1. Attacker deposits 1 wei → gets 1 share // 2. Attacker donates large amount directly (transfer, not deposit) // 3. Exchange rate: 1 share = (1 + donation) assets // 4. Victim deposits → rounds down to 0 shares → free donation to attacker // CORRECT: virtual shares (OpenZeppelin v4.9+) function _decimalsOffset() internal view virtual override returns (uint8) { return 9; // add 1e9 virtual shares + assets to prevent manipulation }
solidity// VULNERABLE: shares transferred, but lock records stay with original owner // → shares stuck, can't redeem → permanent freeze (Belong pattern) function transfer(address to, uint256 amount) external override { _transfer(msg.sender, to, amount); // moves shares // MISSING: transfer lock record from msg.sender to `to` }
bashgrep -rn "function transfer\|function transferFrom" contracts/ -A15 grep -rn "function deposit\|function mint\|function withdraw\|function redeem" contracts/ -A10
> 2016–present. CEI pattern prevents it. Still found in DeFi.
solidity// VULNERABLE (effects after interaction): function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); (bool success,) = msg.sender.call{value: amount}(""); // INTERACTION first require(success); balances[msg.sender] -= amount; // EFFECT after → reentrancy window } // CORRECT (CEI — Checks, Effects, Interactions): function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); // CHECK balances[msg.sender] -= amount; // EFFECT (bool success,) = msg.sender.call{value: amount}(""); // INTERACTION last require(success); }
bash# External calls before state updates grep -rn "\.call{value\|safeTransfer\|transfer(" contracts/ -B10 | grep -v "require\|revert" # Missing nonReentrant modifier on critical functions grep -rn "function withdraw\|function redeem\|function claim" contracts/ -A2 | grep -v "nonReentrant" # Storage slot for reentrancy guard grep -rn "nonReentrant\|ReentrancyGuard\|_notEntered" contracts/
solidity// Attack flow: // 1. Borrow $100M from Aave flash loan // 2. Dump token in Uniswap pool → crash spot price // 3. Protocol reads Uniswap spot → undercollateralized loans accepted // 4. Borrow max against cheap collateral // 5. Repay flash loan, keep profits
bashgrep -rn "getReserves\|getAmountsOut\|slot0\b" contracts/ -A5 # spot price from reserves = manipulatable with flash loan # slot0 = Uniswap V3 spot price = manipulatable
solidity// VULNERABLE: function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { bytes32 hash = keccak256(abi.encodePacked(owner, spender, value, deadline)); // MISSING: nonce not included → same signature usable multiple times require(ecrecover(hash, v, r, s) == owner); }
solidity// VULNERABLE: signature valid on mainnet AND testnet AND all forks bytes32 hash = keccak256(abi.encodePacked(params)); // MISSING: block.chainid not in hash → works on any chain
bashgrep -rn "ecrecover\|ECDSA\.recover" contracts/ -B20 # Check: does the signed hash include nonce + chainId + contract address? grep -rn "nonce\|_nonces\|nonces\[" contracts/
solidity// Implementation and proxy share storage layout // Proxy slot 0: _owner // Implementation slot 0: _initialized // → writing to _initialized overwrites _owner
solidity// If implementation can be initialized directly → anyone becomes owner of implementation // Attack: call initialize() on implementation contract → call upgradeTo() → replace logic
solidityfunction execute(address target, bytes calldata data) external onlyOwner { target.delegatecall(data); // target is validated, but what if owner is compromised? }
bash# UUPS initialization protection grep -rn "function initialize\b\|_disableInitializers\|initializer" contracts/ # Delegate call grep -rn "delegatecall\b" contracts/ -B3 -A5 # Storage layout — proxy uses EIP-1967 slots? grep -rn "0x360894\|EIP1967\|_IMPLEMENTATION_SLOT" contracts/
solidity// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "../src/VulnerableContract.sol"; contract ExploitTest is Test { VulnerableContract target; address attacker = makeAddr("attacker"); address victim = makeAddr("victim"); function setUp() public { // Fork mainnet at specific block vm.createSelectFork("mainnet", BLOCK_NUMBER); // Deploy or load target target = VulnerableContract(TARGET_ADDRESS); // Fund accounts deal(address(token), attacker, INITIAL_BALANCE); deal(address(token), victim, VICTIM_BALANCE); } function test_exploit() public { console.log("Attacker balance before:", token.balanceOf(attacker)); vm.startPrank(attacker); // Step 1: Setup conditions // Step 2: Execute exploit // Step 3: Verify impact vm.stopPrank(); console.log("Attacker balance after:", token.balanceOf(attacker)); assertGt(token.balanceOf(attacker), INITIAL_BALANCE, "Exploit failed"); } }
solidityvm.prank(address) // next call from address vm.startPrank(address) // all calls from address until stopPrank() vm.deal(address, amount) // set ETH balance deal(token, address, amount) // set ERC20 balance vm.warp(timestamp) // set block.timestamp vm.roll(blockNumber) // set block.number vm.createSelectFork("mainnet", blockNumber) // fork mainnet vm.expectRevert(bytes) // next call should revert vm.label(address, "name") // label for trace output vm.assume(condition) // fuzz: discard inputs where false
bash# Run specific test forge test --match-test test_exploit -vvvv # Run with fork forge test --match-test test_exploit -vvvv --fork-url $MAINNET_RPC # Gas report forge test --gas-report # Coverage forge coverage --report summary
Other measured skills in the registry, with their headline benchmark lift.