TORQUE

Evidence

Adversarial findings

Before mainnet, a dedicated pass tried to drain the vault or get a free position. Each attack is a test that plays the attacker (test/adversarial/Attacks.t.sol, 23 tests), plus a sandwich against the real Robinhood Chain pool on a fork. Each test asserts that the attack fails or that its gain is bounded. Full write-up: research/ADVERSARIAL.md.

What broke, and was fixed before deployment

A dead or permanently stale feed could have locked LP money.

  • LP withdrawals required both price checks to pass. If the Chainlink feed stopped updating (deprecation, delisting) or started reverting, withdrawals were refused for good, even with every position closed and all USDG idle.
  • A reverting feed also made totalAssets() revert, which bricked the vault.
  • A position its owner abandoned could never be knocked out without a feed, so its loan was stuck too.

The fixes:

  1. No revert on a broken feed. The feed is read with try/catch; a reverting or non-positive answer reads as "no price".
  2. LPs can always leave when no loans are open. NAV is then exactly idle cash, so withdrawals need no price check.
  3. Emergency unwind(id) once the feed is dead, after 7 days without a usable print (the longest normal freeze measured is 78 hours). Anyone may unwind at the pool's 30-minute average less 1%; the vault is repaid first.
  4. A dead-feed clock for a reverting feed. reportFeedDown() starts it, and any healthy read (open, close, knockOut, or the report itself) clears it.

Tested by test_attack_deadFeedCannotLockIdleLpFunds, test_attack_staleFeedWithOpenPositionUnwindsAfterSevenDays, test_attack_revertingFeedNeedsSevenDaysFromReport, test_attack_staleDownReportClearedByActivity and test_attack_unwindIntoCrashedSpotReverts.

What held

AttackResult
Reentrancy through a hook-calling token, on every external callEvery market re-entry reverts. Removing the guards from open and close makes the test fail
First-depositor inflationThe victim loses at most dust; the attacker cannot redeem more than they put in
Share-rounding farm, fuzzed round tripsNever ends with more USDG than it started with
mint() rounding past the $20 capRefused: the cap is checked twice
Knock-out by crashing the pool's spot priceNotKnockable: the trigger is Chainlink, not the pool
Crashing spot under a real knock-out to buy the forced sale cheapSlippage: the sale must fill within 1% of Chainlink
Sandwich on the real pool (fork)Front-running $1k, $10k or $100k cost the attacker $1.00, $10.00 and $99.95; the victim filled 0.10–0.17% from Chainlink (2026-10-03 run)
Closing your own position into a crashed spotUnderwater: a close that would short the vault reverts
LP running ahead of a known loss in the same blockRefused while the pool average lags; once it agrees, NAV carries the loss
LP and trader as one account, all in one blockCannot come out ahead: the open fee is a cost
Free position: fuzzed margin and leverage, immediate closeAn immediate round trip always costs the fee
Double claim, closing someone else's position, calling the swap callback, lend, setMarketAll refused

Residual risks we accept

These are stated on the Limits page: upstream trust in Paxos and Robinhood, capacity griefing of the open-interest cap, and the LP exit lag after a weekend move.