web3-triage-report
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
More from shuvonsec/web3-bug-bounty-hunting-ai-skills
web3-poc-foundry
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.
3web3-ai-tools
AI-powered tools for Web3 bug bounty automation. Use when you want to automate recon, run autonomous audits, or use AI agents for vulnerability discovery.
3web3-start-here
Master index for the web3 smart contract security knowledge base. Use this to navigate the skill chain. Read files in order — each ends with NEXT.
3web3-bug-classes
Complete reference for all 10 DeFi smart contract bug classes. Use this when hunting for specific vulnerability types, need attack patterns for accounting desync, access control, incomplete path, off-by-one, oracle manipulation, ERC4626 vaults, reentrancy, flash loans, signature replay, or proxy/upgrade bugs.
3web3-hunt-zksync-era
ZKsync Era (Immunefi) completed hunt — 0 findings after exhaustive 5-session audit. Use as a DEFENSE STUDY — learn what makes a protocol unhuntable, which patterns block all 10 bug classes, and when to abandon a target. Contains architecture breakdown, 25 tested attack vectors, and pre-dive scoring refinements for large L1 bridge protocols.
3web3-solidity-audit-mcp
MCP server integrating Slither + Aderyn + SWC patterns into Claude Code for smart contract auditing. Use when analyzing Solidity files, running DeFi-specific detectors, or generating invariants. 10 MCP tools, 86 SWC detectors, DeFi preset pack, CI/CD workflow.
3