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

# FxVault and SynthToken

> One vault per synthetic currency: mint and redeem at the oracle rate, underwriter equity and shares, collateral ratio, the pro-rata haircut, fees, pausing and events.

An `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`](/protocol/contracts/currency-registry); addresses are in [Addresses](/protocol/addresses).

## Immutables and parameters

```solidity theme={"system"}
IERC20     public immutable usdc;        // the settlement dollar, 6 decimals
SynthToken public immutable synthToken;  // the synthetic, 18 decimals
IFxOracle  public immutable oracle;      // the oracle the registry pointed at when the vault was created
bytes32    public immutable code;        // "INR"

struct Params {
    uint16  mintFeeBps;      // fee on mint when the price is fresh
    uint16  redeemFeeBps;    // fee on redeem when the price is fresh
    uint16  staleFeeBps;     // fee on mint and redeem while the price is stale (FX market closed)
    uint16  protocolFeeBps;  // share of every fee that goes to the protocol, in bps of the fee
    uint32  minCRBps;        // minimum collateral ratio after a mint or an underwriter withdrawal; >= 10000
    uint128 liabilityCap;    // maximum liability in raw dollars
}
```

<Warning>
  `oracle` is immutable. Replacing the registry's oracle with `setOracle` does not change the oracle any existing vault reads, and `createSynthetic` refuses a second vault for the same code. See [Oracles](/protocol/contracts/oracles).
</Warning>

Deployed parameters on every chain:

| Parameter        | Value                                                       |
| ---------------- | ----------------------------------------------------------- |
| `mintFeeBps`     | 30 (0.3%)                                                   |
| `redeemFeeBps`   | 30 (0.3%)                                                   |
| `staleFeeBps`    | 100 (1%)                                                    |
| `protocolFeeBps` | 2000 (20% of each fee)                                      |
| `minCRBps`       | 10000 (100%)                                                |
| `liabilityCap`   | `1_000_000e6` (1,000,000 dollars of liability per currency) |

Bounds enforced by `_setParams` (`InvalidParams`): each fee at most 1000 bps (10%), `protocolFeeBps` at most 10000, `minCRBps` at least 10000.

## Accounting

```
assets     = dollar balance - protocolFeesAccrued            (raw dollars; 0 if fees exceed the balance)
liability  = ceil(synthSupply * 1e6 / rate)                  (dollar value of every synthetic outstanding)
equity     = assets - liability                              (int256, can be negative)
CR         = assets * 10000 / liability                      (bps; 2^256 - 1 when liability is 0)
```

```solidity theme={"system"}
function assets() public view returns (uint256);
function liabilityAt(uint256 rate_) public view returns (uint256);
function equityAt(uint256 rate_) public view returns (int256);
function status() external view returns (uint256 rate_, bool fresh, uint256 assets_, uint256 liability_, int256 equity_, uint256 crBps);
function params() external view returns (Params memory);
function synth() external view returns (address);
function code() external view returns (bytes32);
function protocolFeesAccrued() external view returns (uint256);
function guardian() external view returns (address);
```

`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

```solidity theme={"system"}
function previewMint(uint256 usdcIn) external view returns (uint256 synthOut, uint256 fee, bool fresh);
function previewRedeem(uint256 synthIn) external view returns (uint256 usdcOut, uint256 fee, bool fresh);
```

`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

```solidity theme={"system"}
function mint(uint256 usdcIn, uint256 minSynthOut, bytes[] calldata priceUpdate)
    external payable whenNotPaused nonReentrant returns (uint256 synthOut);
function redeem(uint256 synthIn, uint256 minUsdcOut, bytes[] calldata priceUpdate)
    external payable nonReentrant returns (uint256 usdcOut);
```

**`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

```solidity theme={"system"}
function deposit(uint256 usdcIn, uint256 minShares, bytes[] calldata priceUpdate)
    external payable whenNotPaused nonReentrant returns (uint256 shares);
function withdraw(uint256 shares, uint256 minUsdcOut, bytes[] calldata priceUpdate)
    external payable nonReentrant returns (uint256 usdcOut);
function donate(uint256 usdcIn) external nonReentrant;
```

Shares are the vault's own ERC-20 balance (`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

```solidity theme={"system"}
function claimProtocolFees(address to) external onlyOwner;
function setParams(Params calldata p) external onlyOwner;
function setGuardian(address g) external onlyOwner;
function pause() external;      // guardian or owner
function unpause() external onlyOwner;
```

`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

```solidity theme={"system"}
event Minted(address indexed user, uint256 usdcIn, uint256 synthOut, uint256 fee, uint256 rate, bool fresh);
event Redeemed(address indexed user, uint256 synthIn, uint256 usdcOut, uint256 fee, uint256 rate, bool fresh, bool haircut);
event Deposited(address indexed user, uint256 usdcIn, uint256 shares);
event Withdrawn(address indexed user, uint256 shares, uint256 usdcOut);
event Donated(address indexed from, uint256 usdcIn);
event ParamsSet(Params params);
event GuardianSet(address guardian);
event ProtocolFeesClaimed(address indexed to, uint256 amount);
```

Plus OpenZeppelin's `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](/api/queries/vaults)).

## Errors

| Error                                  | When                                                             |
| -------------------------------------- | ---------------------------------------------------------------- |
| `ZeroAmount()`                         | zero input                                                       |
| `Slippage()`                           | output zero or below the caller's minimum                        |
| `LiabilityCapExceeded()`               | mint would push liability above `liabilityCap`                   |
| `CollateralRatioTooLow()`              | mint or withdrawal would leave `CR < minCRBps`                   |
| `NotFresh()`                           | deposit or withdrawal while the price is stale                   |
| `NoEquity()`                           | deposit (after the first) or withdrawal with non-positive equity |
| `InsufficientAssets()`                 | withdrawal larger than `assets()`                                |
| `NotGuardian()`                        | `pause` by anyone but the guardian or owner                      |
| `InvalidParams()`                      | `setParams` out of bounds                                        |
| `InsufficientFee()` / `RefundFailed()` | oracle fee handling                                              |
| `StalePrice(code, age)`                | from the oracle, when the rate is older than the stale limit     |

## `SynthToken`

```solidity theme={"system"}
contract SynthToken is ERC20 {
    address public vault;
    function setVault(address vault_) external;          // deployer (the registry) only, once
    function mint(address to, uint256 amount) external;  // vault only
    function burn(address from, uint256 amount) external; // vault only
}
```

A plain 18-decimal ERC-20 with no fees, pausing or blocklist. `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`.
