| Metric | Value |
|---|---|
| Total agents | 8 (6 audit + 2 research) |
| Total tool uses (across agents) | 256 |
| Total tokens (agents only) | 434,961 |
| Main thread (orchestration) | ~115,000 est. |
| Total session tokens | ~550,000 |
| Contracts analyzed | 19 |
| Lines of code | ~4,100 LoC |
| Tokens per LoC | ~134 |
| Estimated API cost | ~$5-8 |
| Wall clock time | ~30 minutes (parallel) |
Rating equals novelty times exploitability times impact times submittability.
accrueDueTokens() pre-initialization griefing
src/DBR.sol:285-295 · agent DBR token mechanics
The bug
accrueDueTokens() is public and permissionless. When called for a user with no debt, it sets lastUpdated[user] = block.timestamp. But the conditional if(accrued > 0 || lastUpdated[user] == 0) means if lastUpdated was already set and debt == 0, the timestamp never gets refreshed.
Attack
1. Attacker calls accrueDueTokens(victim) at T1 -> lastUpdated[victim] = T1 2. One year passes. Victim borrows 1000 DOLA at T2 3. onBorrow() -> accrueDueTokens(): debt=0, lastUpdated!=0 -> accrued=0, condition FALSE, lastUpdated stays T1! 4. debts[victim] += 1000e18 5. balanceOf(victim) now computes: (T2+1 - T1) * 1000e18 / 365 days = ~1000 DBR consumed INSTANTLY (should be ~0.00003 DBR) 6. Victim immediately in deficit -> force replenished at penaltyImpact An attacker can grief any future borrower. The attack costs only gas. The attacker can also profit as the force replenisher, since that role earns a DOLA reward from the Market contract. Fix Always update
lastUpdated no matter the accrual amount.
src/feeds/ConvexCurvePriceFeed.sol:36-50, ConvexFraxSharePriceFeed.sol:33-47 · agent base feed infrastructure
Legacy feeds call raw Chainlink feeds directly, with no heartbeat check, no isPriceOutOfBounds, no fallback, and no staleness signal. During a circuit breaker event, say a CRV crash past minAnswer, collateral can be overvalued 10x.
src/DbrDistributor.sol:66-73 · agent DBR token mechanics
The onlyINVEscrow modifier places _ before validation. It's safe right now, since a revert rolls back, but the pattern is fragile. Code comments call this an intentional CEI violation for reentrancy protection. Likely out of scope, an acknowledged design choice.
src/feeds/WbtcPriceFeed.sol:70-71
Division by wbtcBtcPrice happens before the bounds check. A feed returning 0 triggers a division-by-zero panic, the fallback becomes unreachable, and the entire wBTC market freezes. Post-audit contract, and novel.
src/feeds/WbtcPriceFeed.sol:70-73
The BTC/USD Chainlink feed never gets checked for circuit breaker bounds. Only wBTC/BTC is checked. If BTC/USD hits its circuit breaker cap, the capped price passes through and never triggers the fallback. Post-audit, and novel.
src/feeds/WbtcPriceFeed.sol:123-131
The fallback uses Curve tricrypto plus Chainlink USDT/ETH plus ETH/USD with zero staleness, bounds, or negative checks. It returns updatedAt from the primary path, not the fallback. Post-audit, and novel.
src/feeds/ChainlinkBasePriceFeed.sol:77-93
When the primary feed is stale or out of bounds, the fallback price gets accepted with no staleness or bounds validation. Both feeds failing at once means an undetected bad price.
ConvexCurvePriceFeed.sol:42-46
A hardcoded 50% floor sits on the CvxCRV/CRV ratio. If CvxCRV depegs to 20%, the feed still reports 50%, overvaluing by 2.5x. Comments call it temporary, but it stays in production.
ChainlinkBasePriceFeed.sol:78-86
The fallback price never gets checked for positive or zero. Oracle.sol mitigates it with require(price > 0), but direct feed consumers like PessimisticFeed and CurveLPPessimisticFeed bypass that check.
DolaPriceFeed.sol:103-131
When pyUSD/USD and FRAX/USD go stale at the same time, a stale price gets returned with updatedAt = 0. Liquidations then proceed on stale data.
ChainlinkBasePriceFeed.sol:87-90, Oracle.sol:158-162
C4 duplicate risk
Staleness is not checked in Oracle.sol. BorrowController only protects borrows. C4 flagged the same systemic issue in October 2022.
Market.sol:658, 630, 469
C4 duplicate risk
Same systemic staleness gap. Not submittable as novel.
DolaFixedPriceFeed.sol:13
An acknowledged design choice. The contract carries a "don't use for external integrations" comment. A DOLA depeg is a protocol-level systemic risk.
| ID | Finding | File | Rating |
|---|---|---|---|
| L-01 | Double-pessimistic dampening (feed plus oracle) | PessimisticFeed.sol |
|
| L-02 | Bounds check uses primary aggregator after fallback | ChainlinkBasePriceFeed.sol |
|
| L-03 | BorrowController staleness only protects borrows | BorrowController.sol |
|
| L-04 | Mutable storage for critical addresses (ConvexCurve) | ConvexCurvePriceFeed.sol |
|
| L-05 | StYEthPriceFeed missing heartbeat staleness check | StYEthPriceFeed.sol |
|
| L-06 | StYEthPriceFeed fallback returns zero, market freeze | StYEthPriceFeed.sol |
|
| L-07 | InvPriceFeed sole Curve EMA (historically exploited) | InvPriceFeed.sol |
|
| L-08 | WbtcPriceFeed no heartbeat enforcement anywhere | WbtcPriceFeed.sol |
|
| L-09 | DBR transfer totalSupply desynchronization | DBR.sol |
|
| L-10 | Market rounding mismatch (dust bad debt) | Market.sol |
|
| L-11 | ALE dust token accumulation via balanceOf pattern | ALE.sol |
|
| L-12 | DolaPriceFeed stale pyUSD used when lower | DolaPriceFeed.sol |
| Finding | Target | Reward | Confidence |
|---|---|---|---|
| H-01 DBR accrueDueTokens griefing | Critical / High | $5K - $15K | 70% |
| M-05+06+07 WbtcPriceFeed bundle | Medium | $2K | 60% |
| Finding | Target | Reward | Confidence |
|---|---|---|---|
| H-02 Convex feeds bypass protections | High / Medium | $2K - $5K | 30% |
| M-01 Fallback not validated | Medium | $2K | 40% |
| M-03 Convex ratio floor | Medium | $2K | 30% |
| Finding | Reason |
|---|---|
| M-02, M-10 (staleness gaps) | C4 duplicate, same systemic issue flagged Oct 2022 |
| H-03 (modifier pattern) | Acknowledged design choice in code comments |
| M-09 (DolaFixed $1) | Design choice, acknowledged |
| All low findings | Below bounty threshold, not novel enough |
| Audit | Date | Scope | Overlap |
|---|---|---|---|
| Code4rena | Oct 2022 | FiRM core (8 contracts, 901 LOC) | M-02, M-10, L-09 |
| Nomoi | May 2023 | cvxCRV market launch | H-02 M-03 maybe |
| yAudit | Jan 2024 | sDOLA | None (different scope) |
| Sherlock Contest | Nov 2025 | Junior Tranches | None (different scope) |
| Sherlock Audit | Oct 2025 | Junior Tranches | None (different scope) |
| Contract | LoC | Agent | Coverage |
|---|---|---|---|
| Oracle.sol | 167 | Multiple | Full |
| Market.sol | 688 | Agent 9 | Full |
| DBR.sol | 393 | Agent 10 | Full |
| DbrDistributor.sol | 128 | Agent 10 | Full |
| ALE.sol | ~750 | Agent 3 | Full |
| BorrowController.sol | 172 | Multiple | Partial |
| ChainlinkBasePriceFeed.sol | 162 | Agent 8 | Full |
| PessimisticFeed.sol | 88 | Agent 8 | Full |
| ConvexCurvePriceFeed.sol | 84 | Agent 8 | Full |
| ConvexFraxSharePriceFeed.sol | 81 | Agent 8 | Full |
| CurveLPPessimisticFeed.sol | 109 | Multiple | Full |
| DolaFixedPriceFeed.sol | 56 | Agent 7 | Full |
| DolaPriceFeed.sol | 193 | Agent 7 | Full |
| USDeBeforeMaturityFeed.sol | 74 | Agent 7 | Full |
| FeedSwitch.sol | 131 | Agent 7 | Full |
| StYEthPriceFeed.sol | 214 | Agent 6 | Full |
| WstETHPriceFeed.sol | 200 | Agent 6 | Full |
| WbtcPriceFeed.sol | 132 | Agent 6 | Full |
| InvPriceFeed.sol | 186 | Agent 6 | Full |