$npx -y skills add shuvonsec/web3-bug-bounty-hunting-ai-skills --skill web3-case-study-role-misconfigCase study - role misconfiguration bug class applied to a yield aggregator protocol. Use as a template for applying all 10 bug classes to a single target.
| 1 | # CASE STUDY: ROLE MISCONFIGURATION IN A YIELD AGGREGATOR |
| 2 | > Bug Class: Access Control | Severity: Critical/Medium | Payout Range: $10K–$50K |
| 3 | > This file shows how to apply the full 10-class methodology to a real yield aggregator target. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## TARGET PROFILE (Anonymized) |
| 8 | |
| 9 | | Field | Value | |
| 10 | |-------|-------| |
| 11 | | Protocol Type | Yield aggregator — stablecoin → lending protocol → harvest → DEX → reward token | |
| 12 | | Max Bounty | $50K (Critical) | |
| 13 | | TVL | Low (fresh program, under $100K) | |
| 14 | | Core Contracts | Vault.sol, RewardsDistributor.sol | |
| 15 | | Program Age | ~5 days when hunted (fresh = low competition) | |
| 16 | | Prior Audits | Firm A (16 findings, all Risk Accepted) + Firm B (18 findings, all Risk Accepted) | |
| 17 | |
| 18 | **Scorecard:** Max bounty (+2) + custom math (+1) + recent code (+1) + known prior audits (+1) + public source (+1) + program new (+2) = **8/10 → HUNT** |
| 19 | |
| 20 | **Why this scores high:** Fresh program on a live bounty platform + prior audits that accepted all risk = team is aware of issues but hasn't patched them. Hunt for what auditors missed or flagged but accepted. |
| 21 | |
| 22 | --- |
| 23 | |
| 24 | ## ARCHITECTURE + FUND FLOW |
| 25 | |
| 26 | ``` |
| 27 | User deposits Stablecoin |
| 28 | ↓ deposit(uint256 amount) |
| 29 | Vault.sol stores: |
| 30 | - deposits[user] += amount |
| 31 | - totalDeposited += amount |
| 32 | - depositTimestamp[user] = block.timestamp |
| 33 | ↓ safeTransferFrom(user, address(this), amount) |
| 34 | ↓ lendingProtocol.supply(stablecoin, amount, address(this), 0) |
| 35 | Interest-bearing token accrues in Vault.sol balance |
| 36 | ↓ (periodic) _performHarvest() |
| 37 | aToken balance > totalDeposited + DUST_THRESHOLD |
| 38 | ↓ lendingProtocol.withdraw(stablecoin, harvestAmount - 1, address(this)) |
| 39 | ↓ dex.exactInputSingle(stablecoin → rewardToken) |
| 40 | ↓ RewardsDistributor.distribute(rewardToken, amount) |
| 41 | RewardsDistributor tracks: |
| 42 | - cumulativeRewardPerShare updates |
| 43 | - users can call claimFor(user) to collect rewardToken |
| 44 | |
| 45 | User withdraws: |
| 46 | ↓ withdraw(uint256 amount) |
| 47 | if block.timestamp < depositTimestamp[user] + LOCK_PERIOD: |
| 48 | withdrawFee applies (e.g. 0.5%) |
| 49 | lendingProtocol.withdraw(stablecoin, amount, user) |
| 50 | ``` |
| 51 | |
| 52 | **Key state variables:** |
| 53 | - `deposits[user]` — user principal (stablecoin) |
| 54 | - `totalDeposited` — sum of all principals |
| 55 | - `depositTimestamp[user]` — last deposit time (affects withdrawal fee) |
| 56 | - `cumulativeRewardPerShare` — reward index in RewardsDistributor |
| 57 | - `lastClaimedReward[user]` — user's last reward index |
| 58 | |
| 59 | --- |
| 60 | |
| 61 | ## KNOWN ISSUES (Risk Accepted by Team — Do NOT Submit) |
| 62 | |
| 63 | ### Firm A Findings (16 total, all Risk Accepted) |
| 64 | All standard: missing events, gas optimizations, reentrancy guards present (CEI followed), centralization risks (owner can pause), single oracle (DEX swap is operational, not security-critical). |
| 65 | |
| 66 | ### Firm B Findings (18 total, all Risk Accepted) |
| 67 | Including: |
| 68 | - HAL-01: withdrawFee can be changed by owner (centralization) |
| 69 | - HAL-05: deposit() resets depositTimestamp even on partial top-ups → **extends lock period for existing deposits** |
| 70 | - HAL-08: Missing check for DISTRIBUTOR_ROLE being set *(flagged but did NOT verify it was never granted)* |
| 71 | - Various gas and event issues |
| 72 | |
| 73 | **Pattern:** Firm B flagged "missing check" but didn't verify the role was actually ungranted. This is the gap to exploit. |
| 74 | |
| 75 | --- |
| 76 | |
| 77 | ## BUG CLASS VERDICTS |
| 78 | |
| 79 | ### 1. Accounting Desync — 2 FINDINGS |
| 80 | |
| 81 | **Finding 1: The `-1` Stranding Pattern** |
| 82 | ```solidity |
| 83 | // In _performHarvest(): |
| 84 | harvestAmount = aToken.balanceOf(address(this)) - totalDeposited - 1; // strands 1 wei |
| 85 | ``` |
| 86 | The hardcoded `-1` strands 1 wei of stablecoin per harvest permanently. Over thousands of harvests, this accumulates. Severity: LOW/INFORMATIONAL (no user loss, just protocol dust accumulation). |
| 87 | |
| 88 | **Finding 2: Dust Harvest DoS** ← VALID MEDIUM |
| 89 | ``` |
| 90 | Scenario: Accumulated harvest amount is very tiny (< DEX minimum swap) |
| 91 | 1. harvest() calls dex.exactInputSingle(stablecoin → rewardToken) |
| 92 | 2. DEX returns 0 (amount too small to produce any output) |
| 93 | 3. RewardsDistributor.distribute(0) is called |
| 94 | 4. If distribute() reverts on 0 amount → harvest is permanently frozen |
| 95 | 5. Users can still withdraw principal but all future yield is lost |
| 96 | |
| 97 | Verification: Check if distribute(0) reverts. Check DEX minimum swap threshold. |
| 98 | ``` |
| 99 | |
| 100 | ### 2. Access Control — 1 FINDING (CRITICAL/HIGH) |
| 101 | **Finding: DISTRIBUTOR_ROLE Never Granted** ← MAIN FINDING |
| 102 | |
| 103 | ```solidity |
| 104 | // RewardsDistributor.sol |
| 105 | bytes32 public constant DISTRIBUTOR_ROLE = keccak256("DISTRIBUTOR_ROLE"); |
| 106 | |
| 107 | function claimFor(address user) external { |
| 108 | require(hasRole(DISTRIBUTOR_ROLE, msg.sender), "Not distributor"); |
| 109 | // ... distribute rewardToken to user |
| 110 | } |
| 111 | ``` |
| 112 | |
| 113 | **Problem:** `DISTRIBUTOR_ROLE` is defined but NEVER granted in the constructor or any init |