跳到主要内容
知仓学习社ZHICANG

web3-triage-report

Bug triage validation system, Immunefi report format, and 20 real paid bounty examples dissected. Use this when validating a finding before submitti…

不碰外部(只输出文字)无严重或高危命中tradecatlabs/vibe-coding-cn

它会碰到什么

扫了多少1 个文本文件,30 KB
它会碰到什么不碰外部(只输出文字)
命中总数0 处
命中统计严重 0 · 高 0 · 中 0 · 低 0

这一栏是扫描器报的事实,不是结论。命中多不等于有毒(安全工具、规则库、示例脚本本来就会包含危险写法),命中少也不等于干净。它和你手上的凭据、文件、网络有什么关系,需要你自己看。

技能内容

TRIAGE, REPORT WRITING & REAL EXAMPLES


PART 1: TRIAGE

THE 7-QUESTION GATE

Ask these IN ORDER before writing a single word of your report.

ONE wrong answer = STOP and move on.


Q1: Can an attacker use this RIGHT NOW, step by step?

Complete this template:

1. Setup:   [what I need]
2. Call:    [exact function, exact params]
3. Result:  [what I have that I didn't have before]
4. Cost:    [gas + capital]
5. ROI:     [profit / cost ratio]

If you cannot complete steps 2 and 3 with specific function calls: KILL IT.


Q2: Is the impact in the program's accepted impact list?

Go to the Immunefi program page. Find "Impacts in Scope."

Match your bug to one of these EXACTLY.

Example impact tiers:

  • "Direct theft of any user funds" — Critical
  • "Permanent freezing of funds" — Critical
  • "Protocol insolvency" — Critical
  • "Theft of unclaimed yield" — High
  • "Permanent freezing of unclaimed yield" — High
  • "Temporary freezing of funds" — High
  • "Smart contract unable to operate due to lack of token funds" — Medium
  • "Griefing (no profit motive, but damage to users)" — Medium
  • "Contract fails to deliver promised returns, but doesn't lose value" — Low

If your bug does not match any impact in scope: KILL IT.


Q3: Is the root cause in an in-scope contract?

Confirm the exact deployed address is in scope on the program page.

If the bug is in Aave, Uniswap, OpenZeppelin, or any external dependency: KILL IT.


Q4: Does it require admin/privileged access?

"Admin can drain funds" = centralization risk = KILL IT.

"Admin can set parameter X which under condition Y creates DoS" = borderline.

Salvage path: can the bug trigger WITHOUT the admin doing anything unusual?

  • If yes: valid
  • If no: likely invalid (requires admin mistake — almost always out of scope)

Q5: Is this already known/acknowledged in prior audits?

Find the audit reports for the protocol. Search for "Risk Accepted," "Acknowledged," "Won't Fix."

If your bug matches a known finding: KILL IT.

Edge case: if acknowledged finding + NEW code around it creates a new attack path → that is a new bug, not the acknowledged one. Must prove the new path.


Q6: Is the economic attack viable?

Attacker spends: gas + capital
Attacker gains: tokens stolen or protocol damaged

If profit < cost: KILL IT.

Example:

  • DoS via dust harvest: costs 1 wei USDC + gas, disables yield for $81K TVL → VIABLE.
  • Withdraw-fee arbitrage: fee (0.1%) > diluted yield from attack → NOT profitable → KILL IT.

Q7: Is this already public?

  • Is it on social media or in a disclosed report?
  • Was it previously submitted and disclosed?
  • Is the "sensitive" data visible in the UI already?

If yes: KILL IT.


THE SEVERITY MATRIX

Score = Impact × Likelihood × Exploitability (each 1–3)

| | Impact=1 (info leak) | Impact=2 (partial) | Impact=3 (theft/freeze) |

|--|--|--|--|

| L=1 E=1 | 1 (Info) | 2 (Low) | 3 (Low) |

| L=2 E=2 | 4 (Medium) | 8 (High) | 12 (High) |

| L=3 E=3 | 9 (High) | 18 (Critical) | 27 (Critical) |

Rule: When borderline, round DOWN. Over-classification destroys credibility.


THINK LIKE AN ATTACKER TEMPLATE

Before writing your report, fill in this attack scenario:

Protocol: [name]
Target contract: [address + function]
Preconditions: [what state must exist?]
Attack sequence:
  1. Attacker calls [exact function] with [exact params]
  2. [What happens in the contract]
  3. [What state changes]
  4. Attacker ends up with: [X more tokens / broken state / DoS]
Total cost: [gas estimate + capital requirement]
Total gain: [$X stolen / $Y TVL frozen]
Viable? [yes/no + reason]

If you can't fill in steps 1–4 with specific values, the bug is not ready to submit.


THINK LIKE A TRIAGER CHECKLIST

A triager reviewing your report will immediately check:

  • [ ] Does the title match an accepted impact?
  • [ ] Is the vulnerable function clearly identified (file + line)?
  • [ ] Is the root cause explained (not just "there is a bug")?
  • [ ] Is there comparison evidence ("function A has this, function B doesn't")?
  • [ ] Does the PoC run without errors?
  • [ ] Is the severity appropriate to the actual impact?
  • [ ] Is the bug already in the known issues list?
  • [ ] Does the fix make sense (proves you understand the root cause)?

If your report can't pass this checklist: revise before submitting.


SEVERITY DOWNGRADE TRIGGERS

| Condition | Severity drops |

|-----------|---------------|

| Requires specific admin configuration | -1 level |

| Impact limited to a small subset of users | -1 level |

| Requires long time window (>24h) to exploit | -1 level |

| Protocol can detect and pause before loss | -1 level |

| Impact is yield loss, not principal loss | -1 level |

| Bug is theoretical with no practical attack | Down to Info |

| Attack costs more than attacker gains | Invalid |


VALID vs INVALID COMPARISON TABLE

| Bug | Valid? | Reason |

|-----|--------|--------|

| DISTRIBUTOR_ROLE never granted → claimFor() permanently uncallable | Valid (Medium) | Deployment bug, not admin action, real impact on users |

| - 1 strands 1 wei per harvest | Valid (Low/Info) | Real, quantified, honest about minor impact |

| Front-run harvest (acknowledged in prior audit) | Invalid | Known issue = instant rejection |

| Admin can change fee to 100% | Invalid | Centralization risk = almost always OOS |

| Harvest DoS via dust (requires admin misconfiguration) | Borderline | Must prove it triggers without unusual admin action |

| ecrecover returns address(0) = anyone can pass | Valid (Critical) | No preconditions, direct theft |

| Contract uses spot price oracle | Valid (High/Critical) | Flash loan manipulation, well-documented impact |

| Missing slippage parameter | Valid (Medium) | MEV sandwich possible, quantifiable loss |

| GraphQL introspection enabled | Invalid | Info disclosure only, no exploitation path |

| Missing HSTS header | Invalid | Always rejected |


THE VALIDITY RATIO

Immunefi tracks your submission:triage ratio.

High invalid submission rate → your future reports get lower priority.

Target: 70%+ valid submissions.

Better to submit 3 valid bugs than 10 invalid ones.

A low-severity honest submission is better for your ratio than an overclaimed invalid one.


PART 2: REPORT WRITING

THE WINNING FORMULA

  1. Title = [Exact function] + [root cause] + [quantified impact]
  2. Comparison evidence = "Function A has X, Function B doesn't"
  3. Attack path = numbered steps, each with exact function call
  4. Quantified impact = "$X stolen" or "X% yield diluted"
  5. PoC output = actual console.log numbers, not just "test passes"
  6. 1-line fix = proves you understand the root cause

IMMUNEFI REPORT TEMPLATE (Complete)

## [Exact function in ContractName] — [root cause in 10 words] leads to [quantified impact]

### Example title:
"_performHarvest() in Ern.sol subtracts hardcoded 1 wei causing reward token
permanent lockup across all harvests"

---

## Summary

[2-3 sentences maximum. What is the bug, where is it, what does it enable.]

The `_performHarvest()` function in `Ern.sol` subtracts a hardcoded `1` from
`userRewards` without distributing or accounting for the remainder. This causes
1 wei of reward token to be permanently locked in the contract after every
harvest, and — more critically — causes a revert when Uniswap returns 0 output
for dust-amount swaps, permanently freezing the harvest function.

---

## Vulnerability Details

**Contract:** `Ern.sol`
**Function:** `_performHarvest()`, line 187
**Type:** Arithmetic error / Incomplete path

**Vulnerable Code:**

uint256 protocolFee = (rewardReceived * harvestFee) / 10000;

uint256 userRewards = rewardReceived - protocolFee - 1; // ← BUG: hardcoded -1

if (protocolFee > 0) REWARD_TOKEN.safeTransfer(owner(), protocolFee);

if (totalSharesSupply > 0) {

cumulativeRewardPerShare += (userRewards * 1e18) / totalSharesSupply;

}


**Comparison Evidence:**
Function `claimYield()` at line 120 correctly handles zero-amount cases.
`_performHarvest()` at line 187 does not account for the stranded `1 wei`
remainder, creating a silent fund loss on every harvest.

**Root Cause:**
The `- 1` subtraction creates a permanent accounting gap. The 1 wei:
- Is NOT sent to the protocol fee recipient (owner)
- Is NOT distributed to users via `cumulativeRewardPerShare`
- Remains locked in the contract indefinitely with no recovery mechanism

**Attack Path (numbered, each step is a specific function call):**
1. Owner sets `minYieldAmount` to minimum (1 * 10^6 = 1 USDC)
2. `harvestTimePeriod` passes (24 hours by default)
3. Yield accrued: 1 wei of aUSDC above totalSupply
4. `canHarvest()` returns `true` (time condition satisfied)
5. Harvester calls `harvest(0)` with `minOut = 0`
6. `yieldAmount = 1` → Aave withdraws 1 wei USDC
7. Uniswap `exactInputSingle(1 wei USDC → WBTC)` returns `0` output
8. `rewardReceived = 0`
9. `userRewards = 0 - 0 - 1 = type(uint256).max` ← ARITHMETIC UNDERFLOW
10. Transaction reverts. All future harvests permanently blocked.

---

## Impact

**Severity:** High
**Category:** Temporary freezing of funds

**Quantified Impact:**
- ernUSDC TVL: $69,300
- ernUSDT TVL: $12,000
- All accrued wBTC yield frozen for all depositors
- Recovery requires owner intervention or protocol upgrade

**Preconditions:**
[list any setup conditions required]

---

## Proof of Concept

[Working Foundry test that runs with forge test -vvv]
[Must include console.log output showing actual numbers]
[Must compile and pass cleanly]

**Expected Output:**

[PASS] testHarvestDoS()

Logs:

canHarvest: true

yieldAmount: 1

harvest() REVERTS: arithmetic underflow confirmed

All future harvests blocked until owner intervenes


---

## Recommended Fix

**Option 1 — Remove the unexplained `-1`:**

// Before:

uint256 userRewards = rewardReceived - protocolFee - 1;

// After:

uint256 userRewards = rewardReceived - protocolFee;


**Option 2 — Guard against zero rewardReceived:**

if (rewardReceived == 0) {

lastHarvest = block.timestamp;

return;

}


---

## References

- Vulnerable code: `ContractName.sol` line X (deployed at `0x...`)
- Related prior audit finding (if relevant): [explain why yours is DIFFERENT]
- CWE/weakness class: [e.g., CWE-191: Integer Underflow]

TITLE FORMULA

[ROOT CAUSE] in [function name] allows [WHO] to [IMPACT]

Examples:

  • Missing access control in setPassword() allows anyone to change the stored password
  • Reentrancy in refund() enables attacker to drain all ETH before state update
  • Spot price oracle in getPrice() enables flash loan manipulation of exchange rates
  • _performHarvest() subtracts hardcoded 1 wei causing permanent harvest DoS when Uniswap returns 0

IMPACT SELECTION GUIDE

Match your finding to one of these Immunefi tiers (program-specific — always verify):

| Impact | Tier | Typical payout range |

|--------|------|---------------------|

| Direct theft of user funds (no limit) | Critical | $50K–$10M |

| Permanent freezing of funds | Critical | $50K–$10M |

| Protocol insolvency | Critical | $50K–$10M |

| Theft of unclaimed yield | High | $10K–$100K |

| Permanent freezing of unclaimed yield | High | $10K–$100K |

| Temporary freezing of funds (>1 hour) | High | $5K–$50K |

| Contract can't operate due to lack of funds | Medium | $2K–$10K |

| Griefing (damage, no profit motive) | Medium | $2K–$10K |

| Contract fails to deliver promised returns | Low | $500–$2K |


PoC REQUIREMENTS

A PoC that wins bounties must:

  1. Be a working Foundry test (forge test -vvv passes)
  2. Fork mainnet at a specific block number
  3. Use real deployed contract addresses
  4. Show console.log output with actual dollar amounts or token counts
  5. Be reproducible by the triager with a single command

A PoC that gets rejected:

  • "Test passes" with no meaningful assertion
  • Compilation errors
  • Wrong mainnet addresses
  • No fork — just unit tests with mocks
  • No quantification of impact

COMMON REJECTION REASONS

  1. Vague impact: "Could potentially cause loss of funds" → Always quantify in USD
  2. No comparison evidence: "This is missing" without showing what sibling function has it
  3. PoC that doesn't run: Compilation errors, wrong addresses → test before submitting
  4. Wrong severity: Overclassifying → damages credibility for future reports
  5. Known issue not checked: Submitting what's already in the audit report → instant reject
  6. No fix provided: Shows you don't fully understand root cause
  7. Multiple variants of same bug: Submit ONE report per root cause
  8. Missing preconditions: Listing an admin action as if it's freely exploitable

WHAT TRIAGERS ACTUALLY WANT TO SEE

From analyzing winning reports across Immunefi competitions:

  • Comparison evidence is the #1 differentiator. "Function X has guard Y, Function Z doesn't" is more compelling than just saying "Function Z is missing guard Y."
  • Numbers matter more than words. "$69,300 TVL frozen" > "significant funds at risk."
  • The fix proves understanding. A 1-line fix that addresses root cause > a 10-line defensive patch.
  • Attack cost matters. "1 wei USDC + gas" > "substantial capital required."
  • Preconditions must be honest. Triagers will test the scenario. If you omit that it requires admin action, they will find it and reject.

PART 3: REAL EXAMPLES

20 paid bounty reports dissected — pattern, technique, key insight.


1. Wormhole — $10M (Uninitialized Proxy)

Protocol: Wormhole bridge

Payout: $10,000,000

Bug class: Upgrade patterns / Access control

Root cause: UUPS proxy implementation contract missing _disableInitializers() in constructor. Attacker called initialize() directly on the implementation, became guardian, upgraded proxy to malicious contract.

What the hunter did: Called implementation() on the UUPS proxy → got impl address → called initialize() directly on impl → became guardian → upgraded proxy.

Key insight: _disableInitializers() was missing from the impl constructor. Always check the IMPL contract directly, not the proxy.

Grep to replicate:

grep -rn "function initialize\b" contracts/ -A3
grep -rn "_disableInitializers()" contracts/
# If initialize has no protection on implementation → CRITICAL

2. ZeroLend — Existence vs. Ownership Check

Bug class: Access control / Logic error

Root cause: split() called _requireOwned(tokenId) which only checks the token exists — not that msg.sender owns it. Any caller could split any token.

What the hunter did: Read split(). Noticed _requireOwned. Looked up what it does. Found it only checks existence, not ownership. Called split(victimTokenId, 1).

Key insight: _requireOwned_checkAuthorized. Always verify what the access control function ACTUALLY validates, not just what its name implies.

Grep:

grep -rn "_requireOwned\|ownerOf\b" contracts/ -B5 -A5
# Read the implementation: does it check msg.sender == owner? Or just: does owner exist?

3. Alchemix — Missing onlyNewEpoch on poke()

Bug class: Missing guard on sibling function

Root cause: vote() and reset() had onlyNewEpoch(tokenId) modifier. poke() did not. Called poke() unlimited times per epoch to drain FLUX tokens.

What the hunter did: Listed all functions in the vote/veNFT system. Saw vote() and reset() had onlyNewEpoch. Checked poke() — it didn't. Called poke unlimited times per epoch.

Key insight: When you see a modifier on 2 out of 3 sibling functions, the 3rd is the bug. This is the "missing guard on sibling function" pattern — one of the most common Critical findings.

Grep:

grep -rn "function vote\|function poke\|function reset" contracts/ -A2
# Compare: do all have the same set of modifiers?

4. Yeet — startUnstake() Phantom Rewards

Bug class: Accounting desync

Root cause: startUnstake() decremented totalSupply but balanceOf(this) didn't change. harvest() computed yieldAmount = balanceOf(this) - totalSupply — now larger than real. Phantom yield created from the desync.

What the hunter did: Traced the flow. startUnstake() decrements totalSupply. harvest() computes yield from the difference. After startUnstake, the difference is now artificially inflated.

Key insight: Always trace what balanceOf(this) - totalSupply equals in each state transition.

Grep:

grep -rn "balanceOf(address(this)).*-.*total\|total.*-.*balanceOf(address(this))" contracts/
# Then: does totalSupply change without balanceOf changing?

5. Folks Finance — Zero-Amount Array Push

Bug class: Missing validation before array push

Root cause: increaseCollateral() called colPools.push(pool) BEFORE checking amount > 0. Depositing 0 pushed the pool entry. Call 5 times → 5 entries for same pool → getLoanLiquidity counted it 5×, inflating collateral.

What the hunter did: Read increaseCollateral(). Found colPools.push(pool) before the amount check. Deposited 0 five times to inflate collateral count.

Key insight: Check that array push operations have amount validation BEFORE the push — not after.

Grep:

grep -rn "\.push(" contracts/ -B5
# Is there: require(amount > 0) or if (amount == 0) return; BEFORE the push?

6. VeChain Stargate — > vs >= Infinite Drain

Bug class: Off-by-one / boundary condition

Root cause: if (endPeriod > nextClaimablePeriod) — the equal case (==) fell through to the active rewards branch instead of the completed branch. After exiting, attacker could claim infinite rewards.

What the hunter did: Read the delegation period claiming function. Found > comparison. Asked "what when equal?" Traced: equal case triggers infinite reward claim loop.

Key insight: At EVERY > comparison, ask "what when equal?" Boundary conditions are the most common source of off-by-one criticals.

Grep:

grep -rn "endPeriod\|exitPeriod\|lastPeriod" contracts/ | grep "[<>][^=]"
# For every strict comparison on period/epoch boundaries: test the equal case

7. Flare FAssets — Tautology in Proof Verification

Bug class: Logic error / Tautology

Root cause: require(sourceAddressesRoot == sourceAddressesRoot) — comparing a variable to itself. Always true. Non-payment proof verification was bypassed entirely.

What the hunter did: Read the non-payment proof validation. Found the tautological comparison. Recognized it as always-true.

Key insight: Run a tautology check on every codebase. Any variable compared against itself is a critical bypass.

Grep:

grep -rn "require\|assert" contracts/ | python3 -c "
import sys, re
for l in sys.stdin:
    if re.search(r'\b(\w{4,})\b.*==.*\b\1\b', l):
        print(l.strip())
"

8. Aurora — DelegateCall to Precompile ($6M)

Bug class: Non-standard EVM behavior

Root cause: On Aurora (custom EVM chain), delegatecall to precompile addresses caused balance deduction in the precompile's context — not the caller's. Attacker delegatecalled the precompile: ETH sent to NEAR, but caller's balance unchanged.

What the hunter did: On a custom EVM chain, tested delegatecall to precompile addresses. Found that balance accounting diverged from mainnet Ethereum behavior.

Key insight: On L2s and custom EVMs, test every opcode's behavior — it may differ from mainnet Ethereum. This is especially true for precompiles, gas mechanics, and storage.

Replicable on: Any chain with custom precompiles, EVM-Cosmos hybrids, Substrate-based EVMs.


9. Polygon MRC20 — Missing Balance Check on Gasless Transfer ($2.2M)

Bug class: Cryptography / Missing validation

Root cause: transferWithSig() did not check if from had sufficient balance. If ecrecover returned address(0) (invalid signature), from = address(0) and tokens were minted from the void.

What the hunter did: Read transferWithSig(). Checked: is from balance validated? No. Checked: if ecrecover returns address(0), does it revert? No. Called with invalid signature → minted from zero address.

Key insight: Always test ecrecover with an invalid signature. The return value must be compared against address(0) and must revert.

Grep:

grep -rn "ecrecover\|ECDSA\.recover" contracts/ -A5
# Is the return value compared against address(0)?
# Is the from-balance verified before transfer?

10. Evmos — Read the Docs ($150K)

Bug class: Configuration / Logic error

Root cause: Cosmos SDK documentation specifies that BlockedAddrs map must include all module accounts. The module account was not in BlockedAddrs. Sending tokens to it corrupted chain state and caused a halt.

What the hunter did: Read the Cosmos SDK documentation. Found the requirement. Read app.go. Found the module account was missing from BlockedAddrs. Demonstrated the chain halt.

Key insight: READ THE DOCS. The documentation describes the correct behavior. If the code doesn't match, that's the bug. This is especially true for framework-specific requirements (Cosmos SDK, Anchor, Move runtime).


11. Plume — safeApprove Without Cleanup

Bug class: Token standards / Deprecated pattern

Root cause: DEX wrapper used safeApprove(router, amountIn) before a partial swap with no cleanup. Second swap failed with "approve from non-zero to non-zero." First amount locked in wrapper permanently.

What the hunter did: Read the DEX wrapper. Found safeApprove. Asked: "What happens to leftover approval?" No cleanup. Traced: second call reverts, amount locked.

Key insight: safeApprove is deprecated for a reason — it fails if there's a non-zero existing approval. Must call safeApprove(spender, 0) after every swap.

Grep:

grep -rn "safeApprove\b" contracts/ -A8
# Is there: safeApprove(spender, 0) cleanup after the swap?

12. Raydium — remaining_accounts Validation ($505K)

Bug class: Solana-specific / Missing account validation

Root cause: increase_liquidity.rs used remaining_accounts[0] as TickArrayBitmapExtension with no validation that it was actually that account. Passing arbitrary account flipped tick bitmap, draining the pool.

What the hunter did: On Solana, read increase_liquidity.rs. Found remaining_accounts[0] used without validation. Passed arbitrary account to flip tick bitmap.

Key insight: On Solana, every remaining_accounts[n] must be validated with ownership check, discriminator check, or constraint attribute.

Grep (Solana):

grep -rn "remaining_accounts\[" src/ --include="*.rs" -A5
# Is there: AccountInfo constraint, owner check, discriminator check?

13. Shardeum — Duplicate Validator Signatures

Bug class: Cryptography / Missing deduplication

Root cause: validSignatures++ incremented without deduplication. One malicious validator stuffed its own signature 100× → reached 66% threshold alone, bypassing consensus.

What the hunter did: Read the consensus code. Found validSignatures++ with no deduplication check. Demonstrated one validator reaching threshold by repeating its signature.

Key insight: Any counter that increases per-signature must check for duplicate signers. if (seen.has(signer)) continue; seen.add(signer) is the fix pattern.

Grep:

grep -rn "validSignatures++\|signatureCount++" src/ -B5
# Is there: if (seen.has(signer)) continue; seen.add(signer)?

14. DFX Finance — 2-Decimal Token Rounding ($100K)

Bug class: Arithmetic / Precision loss

Root cause: Protocol supported EURS token (2 decimals). Deposit of 1 wei EURS: viewRawAmount(1) → division result rounds to 0 → deposit 0 underlying, receive non-zero LP shares.

What the hunter did: Noted the protocol supported EURS (2 decimals). Ran the calculation manually: 1 wei → 0 underlying after rounding → free LP shares.

Key insight: For any non-standard decimal token (2, 4, 6 decimals), test edge cases near 0. Division truncates toward zero, which creates deposit-with-0-cost attacks.

Grep:

grep -rn "\.decimals()\|EURS\|2.*decimal\|decimals.*2\b" contracts/
# For any non-standard decimal token: test edge cases near 0

15. Alchemix V3 — Fast Path Skips State Update

Bug class: Incomplete path / Fast path skip

Root cause: claimRedemption() had a fast path: if Transmuter already had funds, it transferred and burned NFT, then returned early. The slow path updated cumulativeEarmarked, _redemptionWeight, totalDebt. Fast path skipped all 3. Phantom debt + stranded collateral.

What the hunter did: Read claimRedemption(). Found if (transmuter has funds) { transfer; burn; return; }. Traced which state updates the slow path does that the fast path skips.

Key insight: For every early return or fast path: which state updates happen in normal flow but NOT here? The delta is the bug. This is the most common Critical pattern across Immunefi competition reports.

Grep:

grep -rn "if.*sufficient\|fast.*path\|return\b" contracts/ -B3 -A10
# For each early return: which state updates happen in normal flow but NOT here?

16. Belt Finance — Logic Error in Yield Aggregator ($1.05M)

Bug class: Oracle manipulation / Flash loan

Root cause: Vault's price per full share calculation included a temporary value before it was settled. Flash loan inflated the transient value → manipulated ppfs → borrow against inflated collateral.

What the hunter did: Read the vault's pricePerFullShare calculation. Found it included a value that could be temporarily inflated. Flash loaned to inflate it, then borrowed against the inflated collateral.

Key insight: Any vault with pricePerFullShare or exchangeRate that is readable AND writable in the same transaction is a flash loan oracle target. Check: can a single transaction inflate then read the rate?


17. Belong — ERC4626 First Depositor

Bug class: Arithmetic / First depositor inflation

Root cause: convertToShares had no virtual offset. First depositor could inflate exchange rate: deposit 1 wei → donate 999,999 → next depositor gets 0 shares for 999,999 deposit.

What the hunter did: Read convertToShares. No totalSupply() + 10**decimalsOffset() virtual offset. Executed the classic first depositor inflation: deposit 1 → donate → victim gets 0.

Key insight: No virtual offset in ERC4626 = first depositor inflation possible. Fix: OpenZeppelin's implementation adds totalSupply() + 10**_decimalsOffset() to numerator.

Grep:

grep -rn "convertToShares\|_convertToShares" contracts/ -A5
# Is there: totalSupply() + 10**decimalsOffset() or totalAssets() + 1?

18. Paradex — Negative Value in Account Transfer

Bug class: Type system / Non-EVM chain specifics

Root cause: On Starknet (Cairo), account_transfer_partial() took felt252 input (which can be negative). Validation amount >= min_amount passed even for negative values. balance - (-X) = balance + X → infinite balance.

What the hunter did: On Starknet, read account_transfer_partial(). Input type is felt252. Tested: does validation pass for negative felt252? Yes. balance - (-X) = balance + X.

Key insight: On Cairo/Starknet, felt252 can represent negative values. Any amount validation must check amount > 0 explicitly, not just amount >= minimum.


19. Anvil Protocol — uint256 → uint16 Truncation

Bug class: Arithmetic / Type truncation

Root cause: uint16((bigValue * 10_000) / collateral) — result cast to uint16 without range check. Value 65,537 truncates to 1. Collateral factor validation passed. Deposited $0.003, issued $10,000 LOC.

What the hunter did: Found uint16(...) cast in collateral factor calculation. Calculated: what input produces a value just above 65,535? That value truncates to 1. Passed that input.

Key insight: Any downcast (uint256 → uint16, uint128 → uint64) without a range check is a critical candidate if the result is used for security validation.

Grep:

grep -rn "uint16(\|uint8(\|as u16\|as u8" contracts/ src/
# Is there a range check before the downcast?

20. ResupplyFi — Near-Empty Vault Manipulation ($1.8M)

Bug class: Arithmetic / Exchange rate manipulation

Root cause: Protocol used virtual shares but with too-small virtual offset. Near-empty vault (1 existing share) still allowed exchange rate manipulation when total assets were very low.

What the hunter did: Found that virtual shares were present but the offset was small enough that with 1 existing share and very low TVL, the same inflation math still applied.

Key insight: Even WITH virtual shares, very low TVL vaults can still be vulnerable if the virtual offset is too small relative to the token's price. Test at TVL = 1 share.


THE 3 UNIVERSAL PATTERNS (From All 20 Examples)

Almost every bug comes from one of three root causes:

Pattern A: "I assumed function B was called, but it wasn't"

  • Fast path skip, early return, conditional execution
  • The fix: ensure ALL paths update ALL state variables
  • Examples: #15 (Alchemix V3 fast path), #4 (Yeet accounting desync)

Pattern B: "I assumed the check meant X, but it actually means Y"

  • _requireOwned = existence not ownership
  • > = doesn't include boundary
  • Modifier = silent bypass when missing
  • Examples: #2 (ZeroLend), #6 (VeChain), #3 (Alchemix poke)

Pattern C: "I assumed this can't happen, but it can"

  • "ecrecover can't return address(0)" → it can
  • "negative amounts can't be passed" → they can (felt252)
  • "this function won't be called twice" → it will (no onlyNewEpoch)
  • Examples: #9 (Polygon MRC20), #18 (Paradex), #3 (Alchemix)

→ NEXT: [06-methodology-research.md](06-methodology-research.md)

想直接用这个技能?

本站把开放许可(MIT / Apache 等)的技能按仓库打包整理到网盘,点一下转存到你自己的网盘,不用一个个从 GitHub 拉。许可未声明的技能只给原始仓库链接,不打包。