Security review

A full internal review of the DOSS contracts: the trust model, what the code enforces, every finding we know about, and exactly what is tested. Published in full, including the uncomfortable parts, because that is the only kind of security document worth reading.

What this is, and what it is not

This is an internal security review, prepared by the DOSS development team. It is a serious line-by-line review, and every claim in it is checkable against the verified source code.

It is not an independent audit. No third-party security firm has reviewed these contracts. A review written by the people who wrote the code, however careful, cannot catch the class of mistakes the authors are blind to. Until an independent audit happens, DOSS remains unaudited and the deposit caps stay small. If you ever see this project claim "audited" without a named firm and a linked report, treat that as a red flag.

September 2026 · Solidity 0.8.26 · OpenZeppelin Contracts v5 · Uniswap v3 libraries

Scope

Four contracts, all deployed on Robinhood Chain and all in scope. Source is verified on the explorer so you can diff every line against this review.

ContractRoleAddress
DossVaultERC-4626 vault, strategy registry, deposit cap, withdrawal cascade 0x600a…cbDC
StrategyCLOne per stock/USDG pool: holds the LP position, oracle-priced valuation, harvests stamped per pair by the factory
StrategyFactoryDeploys a StrategyCL per pair, reading pool fee, token order and decimals from chain 0xaB0b…40c1
FeeCollectorReceives performance fees; owner can sweep to the treasury address only 0x1759…e637

Out of scope: the Uniswap v3 contracts and USDG itself (external, widely deployed), the off-chain keeper (it holds a scoped key and is covered by the trust model below), and the website.

Trust model · read this section if you read nothing else

Three keys can influence the system. What each one can and cannot do is the core of DOSS security:

KeyCanCannot
Owner (founder) Add/remove strategies, raise the deposit cap, pause deposits, set the agent key, point a strategy at a different price feed, set fees up to a hard 20% cap, set risk parameters Lower the cap, pause withdrawals, or transfer user funds to itself directly. But see S-1: feed control is indirect price control.
Agent (keeper) Move capital between the vault and registered strategies; open, move, close ranges (ticks must bracket the oracle price); harvest. Rate-limited to 6 actions per hour per strategy. Send funds anywhere except the vault, a registered strategy, or the fee collector. Every destination is hardcoded. This is fuzz-tested as an invariant (see coverage).
Price feeds Define the value of every stock position: share price, swap bounds, and harvest gains all follow the feed. Move pool prices or touch balances directly.
The honest summary: today, one operator holds all three. The owner key, the agent key, and the StaticFeed oracles are all operated by the founder. The contracts guarantee that the agent key alone cannot steal, and that pool-price manipulation by outsiders cannot move the share price. They do not protect you from the operator. If you do not trust the founder, do not deposit. This is exactly why the per-wallet and global deposit caps exist, and why they stay small until the trust points below are hardened.

What the contracts enforce

Properties that hold on-chain regardless of what any key does:

  • Withdrawals can never be paused. The pause flag stops deposits and agent actions only. The withdrawal path has no pause check, and the vault pulls from strategies automatically to cover redemptions.
  • The deposit cap can only go up. setDepositCap reverts on any decrease, so the owner cannot trap depositors behind a shrinking cap.
  • Share price is oracle-priced, never pool-priced. Positions are valued at the feed-implied price. Wash trading or sandwiching the pool does not move totalAssets().
  • Every range must bracket the oracle. rebalance and redeploy revert unless the oracle price sits strictly inside the ticks, so the agent cannot park liquidity somewhere nonsensical.
  • Internal swaps are oracle-bounded. Minimum output is derived from the feed price minus the pool fee and a 1% deviation allowance. A manipulated pool makes the swap revert instead of eating the loss.
  • Performance fee is capped at 20% in the bytecode, charged on gains above a high-water mark only, and deposits can never be skimmed as "profit": the mark rises with every deployment and shrinks pro-rata with every withdrawal.
  • First-depositor share inflation is mitigated with an ERC-4626 virtual-offset of 6, making the classic donation attack cost about a million times its payoff.
  • Ownership moves in two steps (Ownable2Step) on every contract, so a fat-fingered transfer cannot brick admin.

Findings

Everything we know about, ranked. "Acknowledged" means it is a deliberate design trade-off that is disclosed and mitigated rather than fixed in code.

IDSevFindingStatus
S-1High Feed control is valuation controlAcknowledged, mitigated by caps
S-2Medium A dead feed can temporarily freeze vault flowsAcknowledged, owner-recoverable
S-3Medium Owner powers act instantly, no timelock yetAcknowledged, timelock planned
S-4Low 18-decimal stock assumption is not enforced by the factoryOperational guard
S-5Low Harvested event always reports 0 compoundedCosmetic, fix in next revision
S-6Info Position mints use zero slippage minimumsBounded by prior oracle-checked swaps
S-7Info A leaked agent key can waste money on churnBounded by rate limit and brackets

S-1 · Feed control is valuation control High · acknowledged

Strategies currently read keeper-tended StaticFeeds, not decentralized oracles (no Chainlink network exists for these assets on this chain yet). Whoever owns a feed can set its price, and the share price follows the feed. A malicious feed owner could inflate the price and redeem shares against other depositors' USDG. The contracts cannot distinguish an honest feed from a dishonest one; this is the single largest trust point in the system. Mitigations today: the feed updater is a different key from the contract owner path users interact with, every feed write is a public on-chain transaction you can watch, the keeper cross-checks feed against official market prices and pool prices and goes flat on divergence, and the global plus per-wallet deposit caps bound the total at risk. The exit from this finding is an independent price source or a feed guarded by a timelock plus sanity bounds; until then the caps stay small.

S-2 · A dead feed can freeze flows Medium · acknowledged

totalAssets() sums every strategy's oracle-priced value, so if a feed goes stale (24h+1h tolerance) or breaks while its strategy holds stock, valuation reverts and deposits and share-priced withdrawals revert with it until the owner re-points the feed or relaxes staleness. No funds are lost and the LP position is untouched; this is an availability risk, not a solvency one. The keeper tends feeds continuously and alarms loudly when one goes quiet. A future revision should let withdrawals fall back to a last-known price band rather than reverting.

S-3 · Owner powers act instantly Medium · acknowledged

setFeed, setConfig, setPerformanceFee (≤20%), addStrategy and setAgent all take effect in the same block. Users get no reaction window to a hostile or mistaken owner action. A public timelock in front of the owner key is the planned fix and a precondition for raising the caps meaningfully. Until then, you are trusting the founder's keys, and the site says so.

S-4 · 18-decimal assumption Low

StrategyCL's valuation math assumes the stock token has 18 decimals. The factory reads decimals() from the token but does not revert when it is not 18; a strategy created for a non-18-decimal token would mis-value badly. Creation is owner-only and every Robinhood tokenized stock observed so far is 18 decimals, so this is guarded operationally; the next factory revision should enforce it in code.

S-5 · Harvested event under-reports Low

The Harvested event's compounded field is hardcoded to zero by a degenerate ternary. Funds are unaffected; only the event log is less useful than intended. Off-chain accounting reads balances, not this field. Will be fixed in the next contract revision (the deployed bytecode is immutable).

S-6 · Zero-min mints Info

mint passes amount0Min = amount1Min = 0. This is normally a sandwich risk, but here the token ratio being minted was just set by oracle-bounded swaps, and an attacker moving the pool against the mint mostly changes which residual token stays idle in the strategy rather than extracting value. Tightening this is still worthwhile in a future revision.

S-7 · Leaked agent key Info

A stolen agent key cannot move funds out (see the invariant tests) but could churn ranges to bleed swap fees and pool friction. The damage rate is capped by the 6-actions-per-hour limit and the oracle bracket requirement, and the owner can rotate the key with setAgent at any time.

Test coverage

The full Foundry suite runs on every change: 56 tests, all passing at the reviewed commit.

  • Invariant fuzzing, 128,000 randomized calls per invariant: invariant_agentNeverHoldsFunds and invariant_collectorReceivesOnlyUsdg drive rebalance, redeploy, de-risk, harvest, oracle moves and time passage in random interleavings and assert the agent path never accumulates funds and the collector only ever receives USDG.
  • Fuzz tests (512+ runs each): agentCannotExtract across arbitrary tick ranges; full deposit/withdraw round-trips across arbitrary amounts.
  • Principal accounting: deposits never skimmed as profit, the high-water mark rises with deployments and shrinks pro-rata with withdrawals, harvest charges gains only.
  • Multi-strategy: registry add/remove, the withdrawal cascade across strategies, cap and pause behavior, dust-tolerant removal.
  • A fork test replays the full daily cycle (enter, earn, de-risk, gap, redeploy, harvest) against real Uniswap v3 bytecode.

The keeper has its own suite (24 tests) covering the state machine: pre-bell de-risk, re-anchor gates, divergence folds, rotation brakes, and harvest guards.

Reproduce it yourself: the tests ship in the repository next to the contracts. forge test is the whole command.

The path to a real audit

In order, and honestly: verified source on the explorer (done), this public review (you are reading it), a timelock in front of the owner key, hardened price feeds (independent source or bounded and delayed updates), then an independent audit by a named firm with the report linked here. The deposit caps rise after those things, not before. Anything faster would be asking for trust the system has not earned yet.