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

# Vault operations

> Mint, redeem, deposit and withdraw against an FxVault with viem: previews, status, stale prices, price-update payloads and what changes if the oracle changes.

Every synthetic currency is an [`FxVault`](/protocol/contracts/fx-vault) that converts the chain's dollar into the synthetic at the oracle rate and back. Traders normally reach it through the [Router](/protocol/contracts/router); this page is for integrations that mint or redeem directly, or that underwrite a vault. Examples use the wINR vault on Robinhood Chain (find every vault in [Addresses](/protocol/addresses)).

```ts theme={"system"}
import { createPublicClient, createWalletClient, http, parseUnits, stringToHex } from "viem";
import { fxVaultAbi, currencyRegistryAbi, erc20Abi } from "@launchpad/abis";
import deployment from "@launchpad/abis/deployments/4663.json";

const usdg = deployment.usdc;                        // the chain's dollar, 6 decimals
const vault = deployment.currencies.INR.vault;       // wINR FxVault
const wINR = deployment.currencies.INR.synth;        // 18 decimals
```

## Read the vault

```ts theme={"system"}
const [rate, fresh, assets, liability, equity, crBps] = await publicClient.readContract({ address: vault, abi: fxVaultAbi, functionName: "status" });
const params = await publicClient.readContract({ address: vault, abi: fxVaultAbi, functionName: "params" });
// params.mintFeeBps 30, redeemFeeBps 30, staleFeeBps 100, protocolFeeBps 2000, minCRBps 10000, liabilityCap 1_000_000e6
const paused = await publicClient.readContract({ address: vault, abi: fxVaultAbi, functionName: "paused" });
```

`status()` gives everything a UI needs: the rate the vault will use (INR per USD, 1e18), whether it is fresh (normal fees, underwriting open) or stale (1% fee, underwriting closed), raw-dollar `assets`, `liability` and signed `equity`, and the collateral ratio in bps (`2^256 - 1` when nothing is minted). It reverts `StalePrice(code, age)` once the rate is older than 5 days; handle that as "oracle offline" and fall back to `assets()` plus the last known rate if you need numbers.

## Mint

<Steps>
  <Step title="Preview">
    ```ts theme={"system"}
    const usdcIn = parseUnits("100", 6);
    const [synthOut, fee, isFresh] = await publicClient.readContract({ address: vault, abi: fxVaultAbi, functionName: "previewMint", args: [usdcIn] });
    // synthOut = (usdcIn - fee) * rate / 1e6, 18 decimals
    ```
  </Step>

  <Step title="Approve and mint">
    ```ts theme={"system"}
    await walletClient.writeContract({ address: usdg, abi: erc20Abi, functionName: "approve", args: [vault, usdcIn] });
    await walletClient.writeContract({
      address: vault, abi: fxVaultAbi, functionName: "mint",
      args: [usdcIn, (synthOut * 995n) / 1000n, []], value: 0n,   // 0.5% tolerance on the rate
    });
    ```
  </Step>
</Steps>

`mint` reverts `Slippage` below `minSynthOut`, `LiabilityCapExceeded` if the vault's total liability would pass 1,000,000 dollars, `CollateralRatioTooLow` if assets would fall below 100% of liability (cannot happen from a mint at 100% unless the vault is already under water), and `EnforcedPause` while paused. The synthetic lands in the caller's wallet and can be sent, traded on any curve in that currency, or redeemed.

## Redeem

No approval: the vault burns the caller's synthetic.

```ts theme={"system"}
const synthIn = parseUnits("8000", 18); // 8,000 wINR
const [usdcOut, redeemFee, freshNow] = await publicClient.readContract({ address: vault, abi: fxVaultAbi, functionName: "previewRedeem", args: [synthIn] });
await walletClient.writeContract({
  address: vault, abi: fxVaultAbi, functionName: "redeem",
  args: [synthIn, (usdcOut * 995n) / 1000n, []], value: 0n,
});
```

`previewRedeem` already includes the pro-rata haircut when `assets < liability`; the `Redeemed` event reports `haircut = true` in that case. Redeeming is never paused and never blocked by the collateral ratio. In the stale window both directions pay `staleFeeBps` (1%) instead of 0.3%.

## Underwrite (deposit and withdraw)

Underwriters supply equity, receive the vault's share token (`uw-wINR`, 18 decimals) and earn 80% of every mint and redeem fee. Both actions need a fresh price.

```ts theme={"system"}
const usdcDeposit = parseUnits("5000", 6);
await walletClient.writeContract({ address: usdg, abi: erc20Abi, functionName: "approve", args: [vault, usdcDeposit] });

// expected shares: first depositor gets usdcIn * 1e12; later ones usdcIn * totalSupply / equity
const supply = await publicClient.readContract({ address: vault, abi: fxVaultAbi, functionName: "totalSupply" });
const expected = supply === 0n ? usdcDeposit * 10n ** 12n : (usdcDeposit * supply) / BigInt(equity);
await walletClient.writeContract({ address: vault, abi: fxVaultAbi, functionName: "deposit", args: [usdcDeposit, (expected * 99n) / 100n, []], value: 0n });

// later: withdraw a share of equity
const shares = await publicClient.readContract({ address: vault, abi: fxVaultAbi, functionName: "balanceOf", args: [account.address] });
const expectedOut = (shares * BigInt(equity)) / supply;
await walletClient.writeContract({ address: vault, abi: fxVaultAbi, functionName: "withdraw", args: [shares, (expectedOut * 99n) / 100n, []], value: 0n });
```

Reverts to expect: `NotFresh` outside the fresh window, `NoEquity` when equity is zero or negative (deposits after the first, and every withdrawal), `CollateralRatioTooLow` when a withdrawal would leave assets below `minCRBps` of liability, `InsufficientAssets` when the payout exceeds `assets()`. `donate(usdcIn)` adds equity without shares and needs only an approval.

An underwriter's position value is `shares * equity / totalSupply` in raw dollars; it rises with fees and falls when the currency strengthens against the dollar (liability grows). Historical `vaultSnapshot` rows from the indexer give equity over time for an APY estimate (see [Vaults query](/api/queries/vaults)).

## Price updates: `priceUpdate` and `value`

Every state-changing vault function is payable and takes `bytes[] priceUpdate`. The vault forwards `oracle.updateFee(priceUpdate)` to `oracle.update` and refunds the rest of `msg.value`. With the deployed `KeeperFxOracle` the fee is always 0: pass `[]` and `value: 0n`, as above.

If a vault were created against a `PythFxOracle`, the flow would be:

```ts theme={"system"}
// 1. fetch the Hermes update for the currency's feed id (contracts/script/PythFeeds.sol)
const res = await fetch(`${HERMES}/v2/updates/price/latest?ids[]=${feedId}&encoding=hex`, { headers: { Authorization: `Bearer ${key}` } });
const priceUpdate = (await res.json()).binary.data.map((d: string) => `0x${d}` as const);
// 2. ask the vault's oracle for the fee, in native units (18-decimal USDC on Arc)
const oracle = await publicClient.readContract({ address: vault, abi: fxVaultAbi, functionName: "oracle" });
const fee = await publicClient.readContract({ address: oracle, abi: pythFxOracleAbi, functionName: "updateFee", args: [priceUpdate] });
// 3. mint with the payload and the fee as value
await walletClient.writeContract({ address: vault, abi: fxVaultAbi, functionName: "mint", args: [usdcIn, minSynthOut, priceUpdate], value: fee });
```

<Warning>
  A vault's oracle is immutable and set at creation. `CurrencyRegistry.setOracle` changes what the factory sizes curves with and what the Router charges as a fee, not what any existing vault reads; the Router computes the fee from the registry's oracle and the vault checks it against its own. Today every vault and the registry point at the same `KeeperFxOracle`, and moving a currency to a Pyth-priced vault would need a new synthetic under a new code (the registry refuses a second vault per code). Query `vault.oracle()` rather than `registry.oracle()` when computing the fee for a direct vault call.
</Warning>

## Handling `StalePrice`

`rate()` and every function that calls it revert with `StalePrice(bytes32 code, uint256 age)` once the price is older than `maxAgeStale` (5 days). Decode it with viem's `decodeErrorResult` against `fxVaultAbi` (the error is declared on the oracle; `keeperFxOracleAbi` has it) or simply treat any revert from a preview as "vault unavailable". Curve trading in the quote asset keeps working through a stale oracle; only conversions to and from dollars stop. The keeper heartbeats hourly, so this state means the keeper has been down for days.

## Reading vault history

The indexer stores a `vaultEvent` (`mint`, `redeem`, `deposit`, `withdraw`, `donate`) and a `vaultSnapshot` (assets, liability, equity, `crBps`, synth and share supply at that block) for every event, plus `rate` rows from `RatePosted`. See [Vaults query](/api/queries/vaults) and [Rates query](/api/queries/rates).
