Skip to content
Security
Skill

/web3-case-study-role-misconfig

Case 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.

BOOST
From plugin
tradecatlabs-vibe-coding-cn
17k18 skills
Install
$ npx -y skills add tradecatlabs/vibe-coding-cn --skill web3-case-study-role-misconfig --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/web3-case-study-role-misconfig

Context preview

The summary Claude sees to decide when to auto-load this skill.

Case 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.

SKILL.md

web3-case-study-role-misconfig.SKILL.md
name: web3-case-study-role-misconfig
description: Case 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.
Contains: architecture walkthrough, all bug class verdicts, 2 findings (DISTRIBUTOR_ROLE never granted, dust harvest DoS), complete PoC templates, report drafts, validation steps.

CASE STUDY: ROLE MISCONFIGURATION IN A YIELD AGGREGATOR

> Bug Class: Access Control | Severity: Critical/Medium | Payout Range: $10K–$50K > This file shows how to apply the full 10-class methodology to a real yield aggregator target.

---

TARGET PROFILE (Anonymized)

| Field | Value | |-------|-------| | Protocol Type | Yield aggregator — stablecoin → lending protocol → harvest → DEX → reward token | | Max Bounty | $50K (Critical) | | TVL | Low (fresh program, under $100K) | | Core Contracts | Vault.sol, RewardsDistributor.sol | | Program Age | ~5 days when hunted (fresh = low competition) | | Prior Audits | Firm A (16 findings, all Risk Accepted) + Firm B (18 findings, all Risk Accepted) |

**Scorecard:** Max bounty (+2) + custom math (+1) + recent code (+1) + known prior audits (+1) + public source (+1) + program new (+2) = **8/10 → HUNT**

**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.

---

ARCHITECTURE + FUND FLOW

User deposits Stablecoin
    ↓ deposit(uint256 amount)
Vault.sol stores:
  - deposits[user] += amount
  - totalDeposited += amount
  - depositTimestamp[user] = block.timestamp
    ↓ safeTransferFrom(user, address(this), amount)
    ↓ lendingProtocol.supply(stablecoin, amount, address(this), 0)
  Interest-bearing token accrues in Vault.sol balance
    ↓ (periodic) _performHarvest()
  aToken balance > totalDeposited + DUST_THRESHOLD
    ↓ lendingProtocol.withdraw(stablecoin, harvestAmount - 1, address(this))
    ↓ dex.exactInputSingle(stablecoin → rewardToken)
    ↓ RewardsDistributor.distribute(rewardToken, amount)
  RewardsDistributor tracks:
  - cumulativeRewardPerShare updates
  - users can call claimFor(user) to collect rewardToken

User withdraws:
    ↓ withdraw(uint256 amount)
  if block.timestamp < depositTimestamp[user] + LOCK_PERIOD:
    withdrawFee applies (e.g. 0.5%)
  lendingProtocol.withdraw(stablecoin, amount, user)

**Key state variables:**

  • `deposits[user]` — user principal (stablecoin)
  • `totalDeposited` — sum of all principals
  • `depositTimestamp[user]` — last deposit time (affects withdrawal fee)
  • `cumulativeRewardPerShare` — reward index in RewardsDistributor
  • `lastClaimedReward[user]` — user's last reward index

---

KNOWN ISSUES (Risk Accepted by Team — Do NOT Submit)

Firm A Findings (16 total, all Risk Accepted)

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).

Firm B Findings (18 total, all Risk Accepted)

Including:

  • HAL-01: withdrawFee can be changed by owner (centralization)
  • HAL-05: deposit() resets depositTimestamp even on partial top-ups → **extends lock period for existing deposits**
  • HAL-08: Missing check for DISTRIBUTOR_ROLE being set *(flagged but did NOT verify it was never granted)*
  • Various gas and event issues

**Pattern:** Firm B flagged "missing check" but didn't verify the role was actually ungranted. This is the gap to exploit.

---

BUG CLASS VERDICTS

1. Accounting Desync — 2 FINDINGS

**Finding 1: The `-1` Stranding Pattern**

// In _performHarvest():
harvestAmount = aToken.balanceOf(address(this)) - totalDeposited - 1; // strands 1 wei

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).

**Finding 2: Dust Harvest DoS** ← VALID MEDIUM

Scenario: Accumulated harvest amount is very tiny (< DEX minimum swap)
1. harvest() calls dex.exactInputSingle(stablecoin → rewardToken)
2. DEX returns 0 (amount too small to produce any output)
3. RewardsDistributor.distribute(0) is called
4. If distribute() reverts on 0 amount → harvest is permanently frozen
5. Users can still withdraw principal but all future yield is lost

Verification: Check if distribute(0) reverts. Check DEX minimum swap threshold.

2. Access Control — 1 FINDING (CRITICAL/HIGH)

**Finding: DISTRIBUTOR_ROLE Never Granted** ← MAIN FINDING

// RewardsDistributor.sol
bytes32 public constant DISTRIBUTOR_ROLE = keccak256("DISTRIBUTOR_ROLE");

function claimFor(address user) external {
    require(hasRole(DISTRIBUTOR_ROLE, msg.sender), "Not distributor");
    // ... distribute rewardToken to user
}

**Problem:** `DISTRIBUTOR_ROLE` is defined but NEVER granted in the constructor or any initialization function. No address holds this role. `claimFor()` can never succeed — all reward tokens are permanently locked.

**How Firm B missed it:** They flagged "missing check for whether role is set" — but their fix recommendation was "add a require that checks the role exists." They didn't verify that `getRoleMemberCount(DISTRIBUTOR_ROLE) == 0` on the live deployment.

**Severity Assessment:**

  • If harvest HAS already happened: CRITICAL (funds locked forever)
  • If harvest never happened yet: HIGH (permanent lock when it does happen)
  • Impact × Likelihood × Exploitability: 3 × 3 × 3 = 27 → CRITICAL

**Verification commands:**

# Check if any address has DISTRIBUTOR_ROLE (replace with actual address)
cast call <REWARDS_DISTRIBUTOR_ADDR> \
  "getRoleMemberCount(bytes32)(uint256)" \
  "$(cast keccak 'DISTRIBUTOR_ROLE')"
# Expected: 0 = confirmed bug

# Alternative: Etherscan → Events → filter "RoleGranted"
# If no RoleGranted events with DISTRIBUTOR_ROLE hash = confirmed

#

Read more
Ships withtradecatlabs-vibe-coding-cn

从想法到产品的 AI 结对编程工作流标准:Prompt + Skill + Context + Quality Gate + 工程闭环 <!-- 徽章区域 (BADGES) --> 本仓库的 AI 解读链接:zread.ai/tukuaiai/vibe-coding-cn 🧠 六条核心命题

Get the whole plugin