agentsclimarketplace

Web3 triage report

Skill Olaradiallysymmetrical491/web3-bug-bounty-hunting-ai-skills/web3-triage-report

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.From its SKILL.md

Install
npx -y skills add Olaradiallysymmetrical491/web3-bug-bounty-hunting-ai-skills --skill web3-triage-report

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 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.

SKILL.md

30.1 KB, ~7.6k tokens by cl100k_base, 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=11 (Info)2 (Low)3 (Low)
L=2 E=24 (Medium)8 (High)12 (High)
L=3 E=39 (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

ConditionSeverity 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 attackDown to Info
Attack costs more than attacker gainsInvalid

VALID vs INVALID COMPARISON TABLE

BugValid?Reason
DISTRIBUTOR_ROLE never granted → claimFor() permanently uncallableValid (Medium)Deployment bug, not admin action, real impact on users
- 1 strands 1 wei per harvestValid (Low/Info)Real, quantified, honest about minor impact
Front-run harvest (acknowledged in prior audit)InvalidKnown issue = instant rejection
Admin can change fee to 100%InvalidCentralization risk = almost always OOS
Harvest DoS via dust (requires admin misconfiguration)BorderlineMust prove it triggers without unusual admin action
ecrecover returns address(0) = anyone can passValid (Critical)No preconditions, direct theft
Contract uses spot price oracleValid (High/Critical)Flash loan manipulation, well-documented impact
Missing slippage parameterValid (Medium)MEV sandwich possible, quantifiable loss
GraphQL introspection enabledInvalidInfo disclosure only, no exploitation path
Missing HSTS headerInvalidAlways 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:**
```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):

  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:**
```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

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.