Web3 triage report
Skill Olaradiallysymmetrical491/web3-bug-bounty-hunting-ai-skills/web3-triage-report
Enable efficient smart contract bug bounty hunting with AI-powered skills built from thousands of real Immunefi and DeFiHack reports.
npx -y skills add Olaradiallysymmetrical491/web3-bug-bounty-hunting-ai-skills --skill web3-triage-reportAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Bug triage validation system, Immunefi report format, and 20 real paid bounty examples dissected. Use this when validating a finding before submitting, writing an Immunefi report, checking if a bug is actually valid, or studying real examples of paid vulnerabilities.
SKILL.md
30.1 KB, as published. Nobody here has run it
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
- Title = [Exact function] + [root cause] + [quantified impact]
- Comparison evidence = "Function A has X, Function B doesn't"
- Attack path = numbered steps, each with exact function call
- Quantified impact = "$X stolen" or "X% yield diluted"
- PoC output = actual console.log numbers, not just "test passes"
- 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:**
```solidity
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):
- Owner sets
minYieldAmountto minimum (1 * 10^6 = 1 USDC) harvestTimePeriodpasses (24 hours by default)- Yield accrued: 1 wei of aUSDC above totalSupply
canHarvest()returnstrue(time condition satisfied)- Harvester calls
harvest(0)withminOut = 0 yieldAmount = 1→ Aave withdraws 1 wei USDC- Uniswap
exactInputSingle(1 wei USDC → WBTC)returns0output rewardReceived = 0userRewards = 0 - 0 - 1 = type(uint256).max← ARITHMETIC UNDERFLOW- 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.solline X (deployed at0x...) - 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:**
```bash
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