> ## Documentation Index
> Fetch the complete documentation index at: https://docs.windrose.market/llms.txt
> Use this file to discover all available pages before exploring further.

# Security model

> The threat model as implemented: oracle risk, vault solvency, curve manipulation, graduation griefing, the admin surface, reentrancy, upgradeability, the blocklist, audit status and the test suite.

<Warning>
  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.
</Warning>

## 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](/protocol/contracts/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).

| Contract                                              | Owner can                                                                                                                                                                                                                                                                                                      | Owner cannot                                                                                                                      |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `CurrencyRegistry`                                    | replace the oracle for future launches, register or re-register a real stablecoin for a code, create a synthetic for a new code, enable or disable a code for new launches                                                                                                                                     | remove a code, change an existing curve's quote asset, touch a vault's parameters from here, create a second synthetic for a code |
| `FxVault` (each)                                      | set fees (each at most 10%), the protocol share, `minCRBps` (at least 100%) and the liability cap; set a guardian; pause and unpause mint and deposit; claim accrued protocol fees                                                                                                                             | pause redeem, withdraw or donate; move user or underwriter funds; change the oracle, the dollar or the synthetic                  |
| `KeeperFxOracle`                                      | allow-list keepers, enable codes, set the move limit and the age limits, `forcePost` any rate                                                                                                                                                                                                                  | nothing else: a wrong `forcePost` is the worst case, bounded by the vault fees and by users' slippage floors                      |
| `LaunchFactory`                                       | set the launch configuration for future launches (bounded: trade fee at most 10%, graduation fee at most 20%, supplies must add up), the registry, the pool fee and tick spacing, and the `poolManager`, `positionManager`, `permit2`, `locker` and `feeCollector` addresses read at graduation and fee sweeps | change a live curve's fees, supply, price or reserve; take a curve's reserve; block trading                                       |
| `LiquidityLocker`                                     | change the fee collector                                                                                                                                                                                                                                                                                       | withdraw, transfer or burn a position                                                                                             |
| `BondingCurve`, `LaunchToken`, `SynthToken`, `Router` | no 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.

| File                 | Covers                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BondingCurve.t.sol` | curve sizing by rate and decimals, disabled currencies, buy then sell solvency, the graduation cap, pool seeding at the curve price (synthetic, six-decimal dollar, EURC, pre-initialised pool), not-ready and double graduation, fee claims and sweeps, double initialisation, the creator's first buy in the quote asset and via the vault; fuzz: `testFuzz_buySell_neverProfits`, `testFuzz_manyBuys_respectBounds`                                   |
| `FxVault.t.sol`      | mint amounts and fees, capacity under a `minCRBps` above 100%, holder gains and underwriter losses when the currency moves, the haircut and its pro-rata property, withdrawals blocked below `minCR`, deposit and withdrawal round trips and share proportionality, negative-equity deposits, the stale window then rejection, pause scope, the liability cap, protocol fee claims, slippage, `OnlyVault`; fuzz: `testFuzz_redeemAll_neverExceedsAssets` |
| `Invariants.t.sol`   | `invariant_curveSolvent`, `invariant_tokenConservation`, `invariant_reserveBounded` (random buys and sells); `invariant_balanceCoversProtocolFees`, `invariant_redeemingEverythingNeverExceedsAssets`, `invariant_noSynthWithoutLiability` (random vault actions with ±10% rate moves)                                                                                                                                                                   |
| `Oracles.t.sol`      | Pyth normalisation and inversion, fresh-to-stale transitions, confidence rejection, fee refund and rejection, empty-update no-op, unsupported codes, the fixed USD rate, Chainlink inversion, the keeper's rules                                                                                                                                                                                                                                         |
| `Router.t.sol`       | dollar buys and sells on synthetic and USD launches, overshoot refund with auto-graduation, the EURC `UseQuoteAsset` revert, the quote-asset paths, expired deadlines, slippage                                                                                                                                                                                                                                                                          |

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