FxVault turns dollars into a synthetic currency and back at the oracle rate. Users mint and redeem; underwriters (the app calls them backers) deposit dollars as equity, receive shares, earn the fees and are short the currency against the dollar. There are no debt positions and no liquidations: the vault’s whole balance backs every synthetic in circulation, and redemptions are scaled pro rata if the vault is ever under water.
Source: contracts/src/synth/FxVault.sol (ERC20 share token, Ownable2Step, ReentrancyGuard, Pausable) and contracts/src/synth/SynthToken.sol. Interface: contracts/src/interfaces/IFxVault.sol. One pair per currency is deployed by CurrencyRegistry.createSynthetic; addresses are in Addresses.
Immutables and parameters
Bounds enforced by
_setParams (InvalidParams): each fee at most 1000 bps (10%), protocolFeeBps at most 10000, minCRBps at least 10000.
Accounting
status() calls oracle.rate(code) and therefore reverts with StalePrice when the rate is older than the oracle’s stale limit (5 days). The indexer falls back to assets() and the last indexed rate in that case.
With minCRBps = 10000 and fees retained as assets, every mint leaves the vault at or above 100% at the rate it was minted at. Equity only falls below zero when the currency strengthens against the dollar after minting (the liability grows in dollar terms); it recovers as fees accrue, as underwriters deposit, or as the rate moves back.
Previews
previewMint: fee = usdcIn * (fresh ? mintFeeBps : staleFeeBps) / 10000, synthOut = (usdcIn - fee) * rate / 1e6. previewRedeem: gross = synthIn * 1e6 / rate, scaled by assets / liability when assets are below liability (the haircut), then fee = gross * (fresh ? redeemFeeBps : staleFeeBps) / 10000 and usdcOut = gross - fee. Both revert StalePrice beyond the stale limit. They match mint and redeem exactly for the same block.
User actions
mint (approve the dollar to the vault first): ZeroAmount for zero input; pushes priceUpdate to the oracle (see below); reads rate and fresh; pulls usdcIn; accrues the protocol share of the fee; mints synthOut to the caller (Slippage if zero or below minSynthOut); then checks the new liability against liabilityCap (LiabilityCapExceeded) and the collateral ratio (CollateralRatioTooLow when assets * 10000 < liability * minCRBps). Blocked while paused. Emits Minted.
redeem (no approval: the vault burns the caller’s synthetic directly): ZeroAmount; price push; computes gross, applies the haircut when assets < liability, takes the fee, Slippage if usdcOut is zero or below minUsdcOut; accrues the protocol share; burns synthIn from the caller; transfers the dollars. Never pausable, never blocked by the collateral ratio. Emits Redeemed with haircut = true when the payout was scaled.
Underwriter actions
balanceOf, totalSupply, transferable), named "<synth name> Underwriter" with the symbol uw-<synth symbol>, 18 decimals. They are a claim on equity, not on assets.
deposit requires a fresh price (NotFresh otherwise). The first deposit receives usdcIn * 1e12 shares (1 dollar = 1e18 shares). Later deposits receive usdcIn * totalSupply / equity, and revert NoEquity when equity is zero or negative (a vault under water cannot be recapitalised by buying shares; use donate). Slippage below minShares. Blocked while paused. Emits Deposited.
withdraw requires a fresh price and positive equity. usdcOut = shares * equity / totalSupply; Slippage if zero or below minUsdcOut; the shares are burned; InsufficientAssets if the payout exceeds assets(); CollateralRatioTooLow if the remaining assets would fall below minCRBps of the liability. Never pausable. Emits Withdrawn.
donate adds dollars to equity without minting shares, for recapitalising a vault or seeding a buffer. Emits Donated.
Underwriters are paid 80% of every mint and redeem fee (the fee minus protocolFeeBps), which accrues as assets and therefore as equity. They lose first when the currency strengthens: with minCRBps = 10000 a vault can be minted against with zero underwriter equity, in which case holders bear an adverse move pro rata at redemption until fees rebuild a buffer.
Fees, pausing and admin
protocolFeesAccrued grows by fee * protocolFeeBps / 10000 on every mint and redeem and is excluded from assets(); the owner pulls it with claimProtocolFees. pause blocks mint and deposit only (NotGuardian for anyone but the guardian or the owner); redeem, withdraw and donate can never be paused. setParams applies immediately to every later mint, redeem, deposit and withdrawal. The owner is the registry owner at creation (the deployer today); no guardian is set at deployment.
Price updates and value
Every payable function calls_pushPrice(priceUpdate): it forwards exactly oracle.updateFee(priceUpdate) to oracle.update, reverts InsufficientFee if msg.value is lower and refunds the rest (RefundFailed if the refund fails). With KeeperFxOracle the fee is always zero: pass [] and no value.
Events
Paused(address) / Unpaused(address) and the share token’s Transfer / Approval. usdcIn, usdcOut and fee are raw dollars; synthIn / synthOut and shares are 18-decimal; rate is the oracle rate used. The indexer writes a vaultEvent and a vaultSnapshot for each of the first five (see Vaults query).
Errors
SynthToken
OnlyVault guards mint and burn; setVault reverts OnlyDeployer for anyone but the registry that deployed it and VaultAlreadySet on a second call. The synthetic is the quote asset of every launch in its currency and the token seeded into that launch’s Uniswap v4 pool at graduation.
Properties the test suite checks
contracts/test/FxVault.t.sol covers mint amounts and fees, mint capacity under a minCRBps above 100%, redemption gains and losses when the currency moves, the pro-rata haircut, withdrawal blocking, share proportionality, negative-equity deposits, the stale window (stale fee, then rejection), pause scope, the liability cap, protocol fee claims, slippage and the OnlyVault guard, plus testFuzz_redeemAll_neverExceedsAssets. The invariant suite (random mint, redeem, deposit, withdraw and ±10% rate moves) holds invariant_balanceCoversProtocolFees, invariant_redeemingEverythingNeverExceedsAssets (redeeming the whole supply never pays more than assets()) and invariant_noSynthWithoutLiability.