Skip to main content
No Windrose contract has been audited. The code runs on Robinhood Chain with real value and on two testnets; the protections below are what exists, not a guarantee. Treat any deposit as at risk until an external audit is published.

Threats and what bounds them

Oracle latency and manipulation (vaults)

A vault mints and redeems at whatever its oracle returns. Two attacks follow: trading against a stale rate when the real market has moved (latency arbitrage), and a wrong rate posted by a compromised keeper.
  • Fees bound the profit of latency arbitrage: 0.3% each way when fresh, 1% during the stale window, so a round trip costs 0.6% to 2%, which exceeds typical intraday FX moves. Underwriters accept the residual risk in exchange for 80% of those fees.
  • The keeper oracle rejects any post that moves a rate by more than 15% (maxMoveBps = 1500), so one bad post is bounded; it never accepts a rate older than the stored one, and posts cannot be dated in the future.
  • The keeper key on the VPS holds only the keeper role and a little gas. The owner key (deployer) can forcePost any rate and re-allow-list keepers; that is the strongest single point of trust in the system today.
  • After 2 hours without a post the vault charges the stale fee and closes underwriting; after 5 days it refuses dollar conversions altogether (StalePrice). Curve trading in the quote asset is unaffected by the oracle at every stage.
What is not mitigated: a keeper that posts plausible but wrong prices within the 15% band, or an owner acting maliciously. The contract comment says never to use the keeper oracle as the sole price source for a mainnet vault holding real money; today it is, because no decentralised FX feed covers the sixty currencies on these chains. See Oracles.

Vault insolvency after a large FX move

If a currency strengthens against the dollar after minting, the dollar value of outstanding synthetic rises above the vault’s assets.
  • minCRBps gates mints and underwriter withdrawals, never redemptions. At the deployed 100% every mint is fully backed at its own rate; a higher setting (for example 110%) would require underwriter equity before minting.
  • Below 100% collateral, redeem scales the payout by assets / liability (the haircut), so early redeemers cannot drain the vault ahead of later ones, and invariant_redeemingEverythingNeverExceedsAssets holds under random mints, redeems, deposits, withdrawals and ±10% rate steps.
  • Underwriters lose first: their equity absorbs the move before any holder is haircut. With no underwriters, holders share the loss pro rata until fees rebuild a buffer.
  • The liability cap (1,000,000 dollars per vault) bounds the exposure of any single currency.

Curve manipulation and MEV

The curve has no external price input after creation, so nothing off-chain can be manipulated to move it; only trades do. A trader’s minTokensOut / minQuoteOutRaw is the sole protection against sandwiching, and every quote function is exact for the block it runs in. Fees make round trips strictly unprofitable (testFuzz_buySell_neverProfits) and the invariant suite checks solvency, token conservation and that the reserve never exceeds the graduation target. Creators have no special powers over a curve beyond claiming their fee share; supply, fees and reserves are immutable after initialize.

Graduation griefing

Anyone can pre-initialise the Uniswap v4 pool for a launch’s pair at an arbitrary price before graduation. The curve handles it: initializePool is a no-op on an initialised pool, the curve reads the actual sqrtPriceX96 and provides full-range liquidity at that price. A large mispricing would then be arbitraged by the first swap; the position itself remains locked. graduate() is permissionless and idempotent, and the router wraps its automatic attempt in try so a pool-side failure cannot block the filling buy (test_graduate_poolPreInitialisedByGriefer_stillWorks, test_buyWithUSDC_overshoot_refundsAndAutoGraduates).

Admin keys

Every admin function is listed here; there is no timelock and no multisig yet, and the deployer account holds every owner role (Ownable2Step, so a transfer takes transferOwnership plus acceptOwnership by the new owner). The one factory power worth underlining: setAddresses changes the locker and PositionManager that future graduations mint to, for every launch that has not graduated yet. Positions already minted are unaffected. Protocol fees flow to feeCollector (the deployer by default) through pull calls only.

Reentrancy

Router, LaunchFactory, FxVault and LiquidityLocker use OpenZeppelin’s ReentrancyGuard; BondingCurve uses ReentrancyGuardTransient (transient storage, which is why the chain must support Cancun). Every state-changing entry point that moves tokens is guarded, state is updated before external transfers (checks-effects-interactions), and the router holds no balances between calls. Launch tokens and synthetics are plain OpenZeppelin ERC-20s without hooks; the dollar and EURC are the issuers’ contracts.

Upgradeability and immutability

No proxies. BondingCurve clones are minimal proxies to an immutable implementation that locks itself in its constructor; every clone is initialised exactly once. FxVault.oracle, usdc, synthToken and code are immutables; Router.registry and usdc are immutables; LaunchFactory.curveImplementation and LiquidityLocker.posm are immutables. Changing behaviour means deploying new contracts and pointing the factory or registry at them, which never affects existing curves, vaults or positions.

The dollar blocklist (Arc)

Arc’s native USDC reverts any transfer touching a blocklisted address. Every fee and reward is therefore pull-claimed: claimCreatorFees, sweepProtocolFees, claimProtocolFees, LiquidityLocker.collect. The only pushed transfers go to msg.sender (refunds of untaken quote, sale proceeds, excess value), so a blocklisted caller can only fail their own transaction. USDG on Robinhood Chain is a Paxos token with its own compliance controls; the same pull design applies.

Randomness and ordering

Nothing reads PREVRANDAO (always 0 on Arc) or block numbers; deadlines and oracle ages are timestamps. Instant finality on both chains means events are final when emitted.

Test suite

forge test in contracts/ runs the unit, fuzz and invariant suites (256 fuzz runs and 64 invariant runs of depth 32 by default; 2,000 fuzz runs in the ci profile). Uniswap v4 is deployed from artifacts inside the tests, and mock ERC-20s stand in for the dollar because standard anvil cannot execute Arc’s native USDC.

Reporting

Report a vulnerability privately to the team before disclosing it; do not test against mainnet contracts holding other people’s funds. The testnets (Robinhood testnet, Arc testnet) run identical code and have faucets.