# API Reference Source: https://docs.multiliquid.xyz/api-reference/introduction Public REST API for Multiliquid deployment metadata, transaction history, and protocol volume The Multiliquid REST API provides read-only access to live deployment metadata, canonical EVM transaction activity, and protocol volume analytics. ```text theme={null} https://api.multiliquid.xyz ``` All documented endpoints use `GET`, require no request body, and are publicly accessible without an API key. Explore the live API with Swagger UI Download the machine-readable OpenAPI schema ## Quick Start Retrieve the current Ethereum mainnet deployment: ```bash theme={null} curl "https://api.multiliquid.xyz/v1/info/evm?chain=mainnet" ``` ```typescript theme={null} const response = await fetch( "https://api.multiliquid.xyz/v1/info/evm?chain=mainnet", ); if (!response.ok) { throw new Error(`Multiliquid API request failed: ${response.status}`); } const deployment = await response.json(); console.log(deployment.swap_contract); ``` Deployment metadata uses `snake_case`. Transaction-history and volume responses use `camelCase`. ## Endpoints | Method | Endpoint | Description | | ------ | ------------------ | ------------------------------------------------------------ | | `GET` | `/` | API metadata and endpoint index | | `GET` | `/health` | Service health check | | `GET` | `/v1/status` | Current API status | | `GET` | `/v1/info` | EVM and SVM deployment metadata | | `GET` | `/v1/info/evm` | EVM contracts, assets, delegates, and adapters | | `GET` | `/v1/info/svm` | SVM programs, assets, LPs, pairs, and vaults | | `GET` | `/v1/transactions` | Canonical EVM economic transaction history | | `GET` | `/v1/volume` | Protocol swap volume over a selected time range, split by LP | `GET /api/status` remains available as a legacy alias for `GET /v1/status`. ## Deployment Metadata Deployment endpoints provide the addresses, asset identifiers, decimals, delegates, and pricing infrastructure needed by an integration. ### All Deployments ```http theme={null} GET /v1/info ``` | Parameter | Required | Values | Description | | --------- | -------- | ------------ | -------------------------------- | | `runtime` | No | `evm`, `svm` | Return only the selected runtime | When `runtime` is omitted, the response contains both `evm` and `svm`. When it is provided, the response contains only the selected runtime, keyed by chain. ```bash theme={null} curl "https://api.multiliquid.xyz/v1/info" curl "https://api.multiliquid.xyz/v1/info?runtime=evm" ``` ### EVM Deployment ```http theme={null} GET /v1/info/evm ``` | Parameter | Required | Values | Description | | --------- | -------- | -------------------- | ------------------------- | | `chain` | No | `mainnet`, `sepolia` | Return one EVM deployment | Omitting `chain` returns all EVM deployments keyed by chain. Supplying it returns the selected deployment object directly. An EVM deployment contains: | Field | Description | | ------------------------------- | -------------------------------------------------------------------------------------- | | `chain_id` | EVM chain ID | | `swap_contract` | `MultiliquidSwap` contract address | | `treasury_contract` | Liquid Treasury token address | | `stablecoin_delegate_contracts` | LP delegate addresses, stablecoin IDs, tokens, administrators, and optional guardrails | | `stable_assets` | Stable-asset names, token addresses, and decimals | | `rwa_assets` | RWA names, IDs, tokens, decimals, price adapters, and optional delegates | ```bash theme={null} curl "https://api.multiliquid.xyz/v1/info/evm?chain=mainnet" ``` The EVM SDK uses this endpoint through `loadEvmDeployment()` to merge current deployment metadata into its chain presets. ### SVM Deployment ```http theme={null} GET /v1/info/svm ``` | Parameter | Required | Values | Description | | --------- | -------- | ------------------- | ------------------------- | | `chain` | No | `mainnet`, `devnet` | Return one SVM deployment | Omitting `chain` returns all SVM deployments keyed by chain. Supplying it returns the selected deployment object directly. An SVM deployment contains: | Field | Description | | --------------- | -------------------------------------------------------------- | | `program_id` | Multiliquid Solana program ID | | `rwa_assets` | RWA mints and their price sources | | `stable_assets` | Stable-asset mints and their price sources | | `lp_info` | LP administrators, vault authorities, pairs, mints, and vaults | ```bash theme={null} curl "https://api.multiliquid.xyz/v1/info/svm?chain=mainnet" ``` ## Transaction History ```http theme={null} GET /v1/transactions ``` Returns canonical EVM economic transactions for one network. Each transaction includes its inclusion data, receipt outcome, optional decoded top-level call, submitted swap bounds, and realized swap or Treasury allowlist activity. ### Query Parameters | Parameter | Required | Description | | ----------------- | -------- | ---------------------------------------------------------------------------- | | `network` | Yes | Runtime-prefixed network, currently `evm-mainnet` or `evm-sepolia` | | `wallet` | No | Match the sender or an indexed user, operator, receiver, or allowlist wallet | | `outcome` | No | Receipt outcome: `success` or `revert` | | `activity` | No | Economic activity: `swap` or `allowlist` | | `entrypoint` | No | Exact decoded top-level Solidity entrypoint | | `transactionHash` | No | Exact `0x`-prefixed EVM transaction hash | | `limit` | No | Page size from `1` to `100`; defaults to `50` | | `cursor` | No | Opaque `nextCursor` value from the previous page | ```bash theme={null} curl "https://api.multiliquid.xyz/v1/transactions?network=evm-mainnet&limit=25" ``` Filters can be combined: ```bash theme={null} curl "https://api.multiliquid.xyz/v1/transactions?network=evm-mainnet&wallet=0x1111111111111111111111111111111111111111&outcome=success&activity=swap&limit=50" ``` ### Response | Field | Description | | -------------- | ------------------------------------------------------------------- | | `network` | Selected runtime-prefixed network | | `chainId` | EVM chain ID | | `transactions` | Transaction rows in descending canonical chain order | | `nextCursor` | Opaque continuation cursor, or `null` when no next page exists | | `coverage` | Historical-backfill and latest relevant stream-observation metadata | Each transaction contains: | Field | Description | | --------------------------------------------------- | ----------------------------------------------------------------- | | `transactionHash` | EVM transaction hash | | `inclusion` | Block number, block hash, transaction index, and timestamp | | `sender`, `recipient`, `nonce`, `value`, `selector` | Transaction envelope data | | `receipt` | `outcome`, `gasUsed`, and `effectiveGasPrice` | | `directCall` | Decoded top-level call and submitted swap legs, when available | | `activities` | Realized swap and Treasury allowlist events ordered by `logIndex` | `directCall.submittedSwapLegs` contains the bounds supplied in calldata. Realized amounts and fees remain in `activities`; submitted bounds never replace event results. A successful nested swap can have activities without a top-level `directCall`. A reverted decoded call can have a `directCall` without realized activities. ### Pagination Rows use descending keyset order. To request the next page, pass `nextCursor` back with the same network and filters: ```typescript theme={null} const query = new URLSearchParams({ network: "evm-mainnet", limit: "50", }); const firstPage = await fetch( `https://api.multiliquid.xyz/v1/transactions?${query}`, ).then((response) => response.json()); if (firstPage.nextCursor) { query.set("cursor", firstPage.nextCursor); const secondPage = await fetch( `https://api.multiliquid.xyz/v1/transactions?${query}`, ).then((response) => response.json()); } ``` The cursor is tied to the original filters and must be treated as opaque. ### Canonicality and Precision Transactions become visible at first inclusion and may be replaced if the indexed chain reorganizes. A `success` outcome describes receipt execution; it is not a finality indicator. Native amounts, swap bounds, fees, gas values, block positions, nonces, and timestamps are decimal strings so JSON consumers do not lose integer precision. Addresses, hashes, selectors, and byte-valued asset IDs are lowercase `0x`-prefixed hex. The endpoint intentionally does not infer token symbols, decimal-formatted token amounts, prices, business direction, pending state, or total result counts. ## Protocol Volume ```http theme={null} GET /v1/volume ``` Aggregates canonical realized swap legs across one half-open UTC range and returns both the protocol total and an LP breakdown. ### Query Parameters | Parameter | Required | Description | | ----------- | -------- | ------------------------------------------------------------------ | | `network` | Yes | Runtime-prefixed network, currently `evm-mainnet` or `evm-sepolia` | | `from` | No | Inclusive whole-second RFC 3339 UTC lower bound | | `to` | No | Exclusive whole-second RFC 3339 UTC upper bound | | `lpAddress` | No | One EVM stablecoin delegate address | If `to` is omitted, it defaults to the request time. If `from` is omitted, it defaults to 30 days before the resolved `to`. The maximum range is 365 days. ```bash theme={null} curl "https://api.multiliquid.xyz/v1/volume?network=evm-mainnet&from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z" ``` Filter the result to one LP: ```bash theme={null} curl "https://api.multiliquid.xyz/v1/volume?network=evm-mainnet&lpAddress=0x0630582db5a1949509f21B315677C40b9aEb7bDA" ``` On EVM, `lpAddress` is the stablecoin delegate used by the swap. Future SVM transaction support will use the LP vault authority. ### Volume Calculation Every realized swap event is counted once, including automatic prefund or sweep legs: * Stablecoin input is valued from `amountIn` using a \$1 proxy. * Stablecoin output is valued from `amountOut` plus all stablecoin-denominated event fees using a \$1 proxy. * RWA-to-RWA input is valued using the latest cached input-RWA adapter price. The endpoint aggregates the complete selected range; it does not return time buckets. `lpVolumes` splits the same result by LP. ### Response | Field | Description | | ---------------------- | -------------------------------------------------- | | `network`, `chainId` | Selected network and EVM chain ID | | `from`, `to` | Resolved half-open UTC range | | `pricedVolumeUsd` | Total priced volume with six fractional digits | | `totalLegCount` | All realized legs in the range | | `pricedLegCount` | Legs included in `pricedVolumeUsd` | | `unpricedLegCount` | Legs lacking a required RWA price | | `pricingComplete` | Whether every leg was priced | | `missingPriceAssetIds` | RWA IDs whose prices were unavailable | | `lpVolumes` | The same volume and pricing fields grouped by LP | | `usedPrices` | Cached RWA prices used for RWA-to-RWA legs | | `coverage` | Indexed historical and stream-observation coverage | USD volume values are decimal strings with six fractional digits. Adapter prices in `usedPrices` are 18-decimal USD-WAD strings and include their fetch time and observation block. If a required adapter price has not populated the cache, the endpoint still returns `200` with the known priced volume, `pricingComplete: false`, the unpriced leg count, and the missing asset IDs. Current RWA prices are refreshed at startup and every 24 hours. Historical RWA-to-RWA volume is therefore revalued when the cached adapter price changes. ## Coverage Both transaction-history and volume responses include: | Field | Description | | ------------------------------------------------- | ------------------------------------------------------- | | `coverage.historicalBackfill.startBlock` | First block in the configured historical range | | `coverage.historicalBackfill.nextBlock` | Next block scheduled for historical ingestion | | `coverage.historicalBackfill.indexedThroughBlock` | Last contiguously indexed historical block, or `null` | | `coverage.lastMatchingStreamObservation` | Most recent relevant streamed block and hash, or `null` | `historicalBackfill` describes contiguous historical progress. `lastMatchingStreamObservation` identifies the most recent relevant stream delivery and does not imply that every intervening block was observed. For volume responses, ingestion coverage and pricing completeness are separate: * `coverage` describes how much chain history has been indexed. * `pricingComplete` describes whether all indexed swap legs could be valued. ## Errors Invalid parameters return a JSON error: ```json theme={null} { "error": "error description", "status": 400 } ``` | Status | Meaning | | ------ | ------------------------------------------------------------------------ | | `200` | Request completed successfully | | `400` | Unsupported or invalid parameter, address, hash, range, limit, or cursor | | `500` | Internal indexed-data or database error | Always check the HTTP status before reading a response as a successful payload. ## Service Endpoints ```bash theme={null} curl "https://api.multiliquid.xyz/health" curl "https://api.multiliquid.xyz/v1/status" ``` `/health` and `/v1/status` return plain text. The root endpoint returns API metadata and a JSON index of currently exposed endpoints. Use live deployment metadata with the TypeScript SDK Review deployed contracts, assets, IDs, and adapters # Liquid Treasury Source: https://docs.multiliquid.xyz/applications/liquid-treasury Institutional stablecoin treasury management — T-Bill yield, instant 24/7 liquidity, and DeFi composability ## Overview Liquid Treasury enables institutions to earn competitive yields on stablecoin holdings through blue-chip Tokenized Money Market Funds (TMMFs) — with instant redemptions available 24/7, subject to availability. Built on the Multiliquid Protocol, Liquid Treasury delivers institutional-grade access to Treasury yields with full compliance, on-chain transparency, and seamless integration. Available to U.S. and non-U.S. Accredited Investors. KYC/KYB required for all depositors. Interest rates based on a discount to short-term U.S. Treasury Bill Rates. Real-world asset backing, not synthetic or DeFi-native. Redeem to stablecoins or blue-chip TMMFs instantly, 24/7/365, subject to availability. Multi-issuer TMMF reserve structure reduces counterparty risk. No single point of failure. ### The Problem Institutions must choose between earning yield **or** having liquidity. Traditional money market products force this tradeoff with settlement delays and redemption caps. No existing product offers all three: true instant liquidity, T-Bill yield, and issuer diversification. ### Why Liquid Treasury | Feature | Liquid Treasury | Traditional TMMFs | | :------------- | :-------------------------------------------- | :------------------- | | Liquidity | 24/7 instant, atomic, subject to availability | T+1 or daily windows | | Access | Programmatic and automated | Largely manual | | Infrastructure | On-chain native | Fiat + On-chain | | Eligibility | U.S. and non-U.S. Accredited Investors | Varies | ## The \$TSY Token | Property | Value | | :-------- | :----------------------------- | | Symbol | \$TSY | | Type | ERC-20 (and equivalent) | | Decimals | 18 | | Networks | Ethereum, Solana (coming soon) | | Bridge | LayerZero OFT | | Transfers | Permissionless | Deposit stablecoins to mint \$TSY tokens 1:1. Your token balance remains constant while interest accrues. Upon redemption, burn \$TSY to receive principal plus accrued interest atomically. \$TSY is freely-transferrable on secondary. As DeFi integrations expand, holders can use \$TSY as collateral in lending protocols, provide liquidity on DEXs, and integrate into yield strategies. ## How It Works Send USDC to Liquid Treasury and receive \$TSY tokens. Primary minters complete KYC/KYB onboarding. Deposits flow into the multi-issuer TMMF reserve, with a dedicated stablecoin buffer for instant redemptions. Start accruing yield immediately. Returns are contractually tied to prevailing U.S. Treasury Bill yields, providing transparent, market-linked returns. Interest is calculated daily based on the 3-Month U.S. T-Bill Oracle rate. Withdraw to stablecoins, subject to availability, or TMMFs instantly, 24/7. The dedicated liquidity buffer enables stablecoin redemptions without waiting for underlying TMMF redemptions. ## Reserve Architecture Liquid Treasury's reserve is split between yield-generating TMMF holdings and a stablecoin buffer that decouples redemption speed from settlement: Allocated across leading blue-chip Tokenized Money Market Funds, the yield-generating core of the reserve. Always available for atomic, in-kind redemptions. Stablecoin reserve enabling instant redemptions without redeeming underlying TMMFs. Buffer replenishes on T+1/T+2 cycles from new deposits and TMMF redemptions. If the stablecoin buffer hits 0%, all transactions will be reverted. Traditional tokenized Treasuries require liquidation of underlying assets for redemptions, triggering T+1/T+2 delays. Liquid Treasury's stablecoin buffer decouples redemption speed from settlement. The system monitors utilization and rebalances dynamically to maintain target liquidity levels. ## Legal Structure Liquid Treasury operates through a bankruptcy-remote Delaware Special Purpose Vehicle (SPV), providing clear legal separation and investor protection. * **Contractual Interest**: Interest is contractually tied to prevailing U.S. Treasury Bill yields, providing transparent, market-linked returns. * **Bankruptcy Remote**: SPV assets are segregated and bankruptcy-remote, providing structural protection. * **On-Chain Verifiable**: Real-time on-chain reserve verification for full transparency. * **Note Structure**: Depositors become lenders to the SPV and sign a note purchase agreement. ## DeFi Composability \$TSY is designed for seamless integration across DeFi: * **Permissionless Transfers**: Move \$TSY freely between wallets and protocols. * **DeFi-Ready**: Use as collateral, liquidity, or in yield strategies. * **Secondary Market Liquidity**: Trade or swap \$TSY on secondary markets. * **Soulbound Yield**: \$TSY is non-interest-bearing and freely transferrable. Only original minters can burn \$TSY and claim their yield. ## Compliance Required for all primary participants. Whitelist-enforced on-chain minting ensures only verified addresses receive newly issued tokens. Bank Secrecy Act and Anti-Money Laundering compliance. Blacklist enforcement for sanctioned or restricted addresses. Segregated reserve custody via audited smart contracts using OpenZeppelin standards. Real-time on-chain reserve verification. All state changes emit comprehensive events for auditability. ## Integration Options Liquid Treasury is designed for programmatic access — ideal for B2B2C platforms, trading desks, liquid funds, vault curators, exchanges, payment processors, and any business managing treasury operations at scale. * **Smart Contracts**: Direct on-chain interaction via ethers.js or web3.py for DeFi integrations * **REST API**: Read-only deployment metadata, canonical EVM transaction history, and LP volume analytics * **SDK**: TypeScript library for seamless integration into existing stacks * **CLI Tool**: Command-line interface for automated treasury operations and scripting workflows White-label ready — embed yield functionality into your platform via a simple one-time integration. Deposits and redemptions are submitted on-chain through the Multiliquid contracts or TypeScript SDK. The REST API supports deployment discovery, reporting, and analytics. See the [API Reference](/api-reference/introduction) for REST endpoints. For on-chain integration details, contract ABIs, and technical specifications, see the [Integration Guide](/evm/guides/integration). *** ## Technical Reference The sections below provide detailed smart contract documentation for developers integrating directly with Liquid Treasury's on-chain infrastructure. ### Contract Architecture #### Treasury Token ```solidity theme={null} contract Treasury is Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable, ERC20PermitUpgradeable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable ``` **Base Contracts**: * **ERC20Upgradeable**: Standard token functionality * **ERC20BurnableUpgradeable**: Burn capability * **ERC20PermitUpgradeable**: EIP-2612 gasless approvals * **AccessControlEnumerableUpgradeable**: Role-based permissions with enumeration * **PausableUpgradeable**: Emergency pause capability * **ReentrancyGuardUpgradeable**: Reentrancy attack protection * **UUPSUpgradeable**: Secure upgrade mechanism #### Deployment Model The treasury token can be deployed independently on multiple chains, connected via bridge contracts: ```mermaid theme={null} graph TB subgraph "Ethereum Mainnet" E_TREASURY[Treasury] E_BRIDGE1[Bridge A] E_BRIDGE2[Bridge B] end subgraph "Arbitrum" A_TREASURY[Treasury] A_BRIDGE1[Bridge A] A_BRIDGE2[Bridge B] end subgraph "Solana" S_TREASURY[Treasury] S_BRIDGE1[Bridge A] end E_BRIDGE1 <-->|Cross-Chain| A_BRIDGE1 E_BRIDGE1 <-->|Cross-Chain| S_BRIDGE1 E_BRIDGE2 <-->|Cross-Chain| A_BRIDGE2 E_BRIDGE1 -->|bridgeMint / burnFrom| E_TREASURY E_BRIDGE2 -->|bridgeMint / burnFrom| E_TREASURY A_BRIDGE1 -->|bridgeMint / burnFrom| A_TREASURY A_BRIDGE2 -->|bridgeMint / burnFrom| A_TREASURY S_BRIDGE1 -->|bridgeMint / burnFrom| S_TREASURY ``` Each deployment is independent but connected via bridges: * Same token name and symbol across chains * Separate role administration per chain * Bridge contracts handle cross-chain transfers via burn-and-mint ### Core Functions #### Minting **mint** (Primary Issuance) Mint tokens to whitelisted addresses only. Used for initial token issuance to institutional clients after KYC/KYB verification. ```solidity theme={null} function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) whenNotPaused nonReentrant; ``` **Requirements**: * Caller must have `MINTER_ROLE` * Recipient must be whitelisted (`whitelisted[to] == true`) * Contract must not be paused * Amount must be non-zero **Reverts**: `AccountNotWhitelisted(to)` if recipient is not whitelisted, `InvalidAmount()` if amount is zero Whitelist enforcement on primary issuance ensures KYC/KYB compliance. Only addresses that have completed the onboarding process can receive newly minted tokens. **bridgeMint** (Cross-Chain) Mint tokens for cross-chain bridge operations. No whitelist enforcement, as bridged tokens represent existing supply. ```solidity theme={null} function bridgeMint(address to, uint256 amount) external onlyRole(BRIDGE_ROLE) whenNotPaused nonReentrant; ``` **Requirements**: * Caller must have `BRIDGE_ROLE` * Recipient must not be the zero address * Contract must not be paused * Amount must be non-zero **Events**: Emits `BridgeMint(bridge, to, amount)` for off-chain monitoring before the standard `Transfer` event. #### Burning ```solidity theme={null} function burn(uint256 amount) public whenNotPaused; function burnFrom(address from, uint256 amount) public whenNotPaused; ``` Standard ERC-20 burn functions. `burnFrom` requires prior approval from the token holder. If the caller has `BRIDGE_ROLE`, a `BridgeBurn` event is emitted for off-chain monitoring in addition to the standard `Transfer` event. **Requirements**: * Amount must be non-zero * Contract must not be paused * `burnFrom` requires sufficient allowance #### Transfers Standard ERC-20 `transfer` and `transferFrom` functions. Transfers are permissionless with two exceptions: * **Blacklist enforcement**: Both sender and receiver are checked against the blacklist on every transfer (including mints and burns). Blacklisted addresses cannot send or receive tokens. * **Pause enforcement**: All transfers are blocked when the contract is paused. Whitelist enforcement applies **only** to `mint()` (primary issuance). Transfers between addresses are unrestricted beyond blacklist and pause checks. This enables \$TSY to trade freely on secondary markets and integrate with DeFi protocols. #### EIP-2612 Permit ```solidity theme={null} function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external; ``` Enables gasless approvals: 1. User signs an approval message off-chain 2. A relayer submits the `permit()` transaction (pays gas) 3. Approval is set without requiring a transaction from the user 4. Subsequent `transferFrom()` or `burnFrom()` can execute immediately ### Bridge Integration #### Bridge Management Bridges are managed through role-based access control. The `OPERATOR_ROLE` can add and remove bridges: ```solidity theme={null} function addBridge(address bridge) external onlyRole(OPERATOR_ROLE); function removeBridge(address bridge) external onlyRole(OPERATOR_ROLE); ``` Adding a bridge grants `BRIDGE_ROLE`; removing a bridge revokes it. Each operation emits `BridgeAdded` or `BridgeRemoved` events. Multiple bridges can operate simultaneously, providing redundancy, distributed risk, and competitive routing options. #### TreasuryOFTAdapter (LayerZero) A reference bridge implementation using LayerZero's OFT (Omnichain Fungible Token) protocol: * **Source chain**: Burns tokens via `Treasury.burnFrom()` (requires user approval of the adapter) * **Destination chain**: Mints tokens via `Treasury.bridgeMint()` (requires `BRIDGE_ROLE`) * Routes zero-address recipients to `0xdead` to prevent token loss **Setup**: 1. Deploy `TreasuryOFTAdapter` on each chain 2. Grant `BRIDGE_ROLE` to the adapter: `treasury.addBridge(adapterAddress)` 3. Configure LayerZero peers: `adapter.setPeer(dstEid, peerAddress)` ### Compliance Controls #### Whitelist Whitelist enforcement applies only to primary issuance minting (`mint()`). Managed by `WHITELISTER_ROLE`: ```solidity theme={null} function addToWhitelist(address account) external onlyRole(WHITELISTER_ROLE); function removeFromWhitelist(address account) external onlyRole(WHITELISTER_ROLE); ``` Whitelist status is stored in a public mapping: `mapping(address => bool) public whitelisted` **Lifecycle**: 1. Institution completes KYC/KYB onboarding 2. Compliance team adds the institution's address to the whitelist 3. Address can now receive primary issuance mints 4. If needed, address is removed from the whitelist during offboarding #### Blacklist Blacklisted addresses cannot send or receive tokens in any context (transfers, mints, burns). Managed by `BLACKLISTER_ROLE`: ```solidity theme={null} function addToBlacklist(address account) external onlyRole(BLACKLISTER_ROLE); function removeFromBlacklist(address account) external onlyRole(BLACKLISTER_ROLE); function addToBlacklistBatch(address[] calldata accounts) external onlyRole(BLACKLISTER_ROLE); function removeFromBlacklistBatch(address[] calldata accounts) external onlyRole(BLACKLISTER_ROLE); ``` Blacklist status is stored in a public mapping: `mapping(address => bool) public blacklisted` Batch operations enable efficient compliance actions across multiple addresses in a single transaction. ### Administrative Functions #### Force Burn ```solidity theme={null} function forceBurn(address from, uint256 amount) external onlyRole(OPERATOR_ROLE); ``` Burns tokens from any address without requiring approval. Bypasses pause and blacklist checks by calling `super._update()` directly. **Use cases**: Regulatory compliance (e.g., court orders), security incident response, recovery from compromised addresses. **Events**: Emits `ForcedBurn(operator, from, amount)`. #### Force Transfer ```solidity theme={null} function forceTransfer(address from, address to, uint256 amount) external onlyRole(OPERATOR_ROLE); ``` Transfers tokens between any addresses without approval. Bypasses pause and blacklist checks. **Use cases**: Wallet recovery with verified identity, regulatory compliance, emergency fund movement. **Events**: Emits `ForcedTransfer(operator, from, to, amount)`. #### Pause / Unpause ```solidity theme={null} function pause() external onlyRole(PAUSER_ROLE); function unpause() external onlyRole(PAUSER_ROLE); ``` Pausing blocks all standard token operations (mints, burns, transfers). Emergency functions (`forceBurn`, `forceTransfer`) remain operational when the contract is paused. *** ### Yield-Bearing Integration (Treasury Delegate) When integrated with MultiliquidSwap, the Liquid Treasury token becomes a yield-bearing asset through the **Treasury Delegate** contract. The delegate manages daily interest rate accrual, a credit-based redemption system, and swap operations. #### Overview The Treasury Delegate extends the standard Multiliquid delegate model with yield mechanics: 1. Users deposit RWAs or stablecoins via MultiliquidSwap and receive \$TSY tokens 2. The delegate tracks **credits** (deposit entitlements) separately from token balances 3. An off-chain worker posts daily interest rates sourced from the 3-Month US T-Bill Oracle 4. Credits accrue yield over time via a compounding multiplier 5. When users redeem, they receive their principal plus accumulated yield ```solidity theme={null} contract TreasuryDelegate is StablecoinDelegateBase, IYieldBearingDelegate ``` #### Token/Credit Separation \$TSY tokens (ERC-20) and credits are independent: * **Tokens** can be freely transferred on the open market * **Credits** track a user's yield entitlement and are non-transferable * Secondary market buyers receive tokens but **no credits** (no yield benefit until depositing via MultiliquidSwap) * Redemption requires sufficient credits to prevent negative backing | Scenario | Tokens | Credits | Can Redeem? | | :-------------------------------------- | --------------: | ---------------: | :----------------------------------------------: | | User deposits via MultiliquidSwap | Receives tokens | Receives credits | Yes | | User buys tokens on secondary market | Has tokens | No credits | No (must deposit first) | | User transfers tokens to another wallet | Loses tokens | Keeps credits | Yes, with tokens bought back on secondary market | #### Daily Rate System **Posting Rates** An off-chain worker posts daily interest rates sourced from the 3-Month US T-Bill Oracle : ```solidity theme={null} function postDailyRate(uint256 dayNumber, uint256 grossRate) external onlyRole(RATE_POSTER_ROLE); ``` * `dayNumber`: The day number (`block.timestamp / 1 days`) * `grossRate`: The APY before management fee (WAD-scaled, e.g., 4.5% = `4.5e16`) * The management fee is subtracted on-chain; the net APY is stored * The net APY is converted to a per-day compounding rate Rates must be posted sequentially (including weekend backfills) at 0:01 UTC of the following day. Yield does not accrue for days where rates have yet to be posted. Yield is backfilled upon bootup in the event of the offchain worker crashing. **Correcting Rates** ```solidity theme={null} function correctDailyRate(uint256 dayNumber, uint256 grossRate) external onlyRole(OPERATOR_ROLE); ``` Operators can correct or backfill past rates within the existing series (between `firstRateDay` and `lastRatePostingDay`). The operator cannot advance the rate posting frontier. **Management Fee** ```solidity theme={null} function setManagementFee(uint256 fee) external onlyIssuerAdmin; ``` The management fee is a WAD-scaled APY (e.g., 50 bps = `5e15`) subtracted from the gross rate at posting time. Only the LP admin can update it. #### Withheld Credits New deposits enter a **withheld credits** queue before becoming full credits. These withheld credits become full credits after 24 hours of yield-generation. Withheld credits are selected first when any withdrawals are being made. If they have not reached the 24 hour maturity period upon withdrawal, the credits have yield generated withheld (if any). This prevents timing exploits of the underlying RWA NAVs. Each withheld credit entry tracks: ```solidity theme={null} struct WithheldCredits { address tokenAddress; // Source token (RWA or stablecoin) uint256 credits; // Amount of credits withheld uint256 releaseTimestamp; // When credits can be released (24hr hold) uint256 earningStartTimestamp; // When credits start earning yield bool isRWA; // Determines yield calculation method } ``` **Yield earning rules differ by deposit type:** | Deposit Type | Earning Start | Yield Method | Rationale | | :----------------- | :------------- | :--------------------------------- | :-------------------------------------- | | RWA deposit | Immediate | Daily only (no partial days) | Already invested in underlying fund | | Stablecoin deposit | T+1 (next day) | Continuous (includes partial days) | Settlement delay for stablecoin backing | **Queue operations:** * **Additions**: New deposits are pushed to the back of the deque * **Reductions** (withdrawals): Consumed from the back (LIFO — most recent deposits first, which have accrued the least yield) * **Merges**: Released from the front (FIFO — oldest deposits mature first) when 24-hour hold period expires When withheld credits merge into main credits, a weighted average yield multiplier is calculated to blend existing and new credits. #### Interest Accrual Interest accrual is **permissionless** — anyone can trigger it for any user: ```solidity theme={null} function accrueInterest(address user, uint256 maxDays, uint256 maxRecords) external nonReentrant whenNotPaused returns ( uint256 daysAccrued, uint256 recordsMerged, bool dailyAccrualComplete, bool creditsFullyMerged ); function batchAccrueInterest(address[] calldata users) external nonReentrant whenNotPaused; ``` Both limits must be nonzero. Use `type(uint256).max` for uncapped daily accrual or to attempt to merge every currently eligible record. Main credits are accrued before eligible withheld-credit records are merged. `dailyAccrualComplete` reports whether all currently processable daily rates have been applied, while `creditsFullyMerged` reports whether no currently eligible withheld-credit record remains. Interest accrual is called daily after rate posting by an off-chain worker to make sure all accounts are up to date. **Yield multiplier**: Each user's effective redeemable value is `credits * yieldMultiplier / WAD`. The multiplier starts at `1e18` (1.0) and compounds daily. #### View Functions ```solidity theme={null} // Returns the yield-scaled value for a given redemption amount function getYieldAmount(address user, uint256 redeemValue, bool stablecoinWithdrawal) external view returns (uint256 yieldScaledAmount); // Returns $TSY tokens needed to achieve a target dollar value function getAmountForTargetValue(address user, uint256 targetValue, bool stablecoinWithdrawal) external view returns (uint256 treasuryNeeded); // Returns all withheld credit entries for a user function getWithheldCredits(address user) external view returns (WithheldCredits[] memory); // Returns the current day number (block.timestamp / 1 days) function getCurrentDay() external view returns (uint256); // Returns the net APY and posting status for a specific day function getDailyRate(uint256 dayNumber) external view returns (uint256 rate, bool posted); ``` Withheld credits are always valued at 1:1 in view functions (no yield applied). Only main credits receive the yield multiplier. For stablecoin withdrawals, a partial-day yield is applied using the last posted rate to account for intra-day accrual. *** ### Access Control #### Treasury Token Roles | Role | Permissions | Typical Holder | | :------------------- | :-------------------------------------------------------------------------------------------------------------- | :------------------------------------ | | `DEFAULT_ADMIN_ROLE` | Upgrade contract, manage all roles | Multi-sig governance | | `OPERATOR_ROLE` | Bridge management (add/remove), force burn, force transfer. **Role admin** for `BRIDGE_ROLE` and `MINTER_ROLE`. | Operations multi-sig | | `MINTER_ROLE` | Primary issuance minting (whitelist-enforced) | Treasury Delegate, authorized issuers | | `BRIDGE_ROLE` | Cross-chain bridge minting (no whitelist) | Bridge adapter contracts | | `PAUSER_ROLE` | Pause and unpause all token operations | Security team | | `WHITELISTER_ROLE` | Add/remove addresses from whitelist | Compliance team | | `BLACKLISTER_ROLE` | Add/remove addresses from blacklist (including batch) | Compliance team | #### Treasury Delegate Roles | Role | Permissions | Typical Holder | | :-------------------------- | :------------------------------------------------------------------------------------------------ | :----------------------- | | `DEFAULT_ADMIN_ROLE` | Upgrade delegate contract | Multi-sig governance | | `MULTILIQUID_SWAP_CONTRACT` | Execute swap operations (deployStablecoin, receiveStablecoin, etc.) | MultiliquidSwap contract | | LP Admin | Set management fee, configure custody addresses, manage RWA/stablecoin whitelists, pause delegate | Protocol team | | `RATE_POSTER_ROLE` | Post sequential daily interest rates | Off-chain rate worker | | `OPERATOR_ROLE` | Correct past daily rates | Operations multi-sig | | `PAUSE_ROLE` | Emergency pause of delegate operations | Security team | ### Events #### Treasury Token Events ```solidity theme={null} event AddedToWhitelist(address indexed account); event RemovedFromWhitelist(address indexed account); event AddedToBlacklist(address indexed account); event RemovedFromBlacklist(address indexed account); event BridgeMint(address indexed bridge, address indexed to, uint256 amount); event BridgeBurn(address indexed bridge, address indexed from, uint256 amount); event ForcedBurn(address indexed operator, address indexed from, uint256 amount); event ForcedTransfer(address indexed operator, address indexed from, address indexed to, uint256 amount); event BridgeAdded(address indexed bridge); event BridgeRemoved(address indexed bridge); ``` #### Treasury Delegate Events ```solidity theme={null} event DailyRatePosted(uint256 indexed dayNumber, uint256 rate, address indexed poster); event DailyRateCorrected(uint256 indexed dayNumber, uint256 oldRate, uint256 newRate, address indexed corrector); event InterestAccrued(address indexed user, uint256 fromDay, uint256 toDay, uint256 daysAccrued, uint256 multiplierAfter); event YieldClaimed(address indexed user, uint256 creditsRedeemed, uint256 yieldAmount, uint256 effectiveValue); event FirstRatePosted(uint256 dayNumber); event PendingCreditsMerged(address indexed user, uint256 pendingAmount, uint256 newCredits); event PendingCreditsAdded(address indexed user, uint256 amount, uint256 depositDay); event ManagementFeeUpdated(uint256 oldFee, uint256 newFee); event WithheldCreditsReduced(address indexed user, uint256 amount, uint256 remainingWithheld); event MainCreditsReduced(address indexed user, uint256 amount, uint256 remainingCredits); ``` ### Custom Errors #### Treasury Token Errors ```solidity theme={null} error AccountNotWhitelisted(address); error AccountBlacklisted(address); error ZeroAddress(); error InvalidAmount(); ``` #### Treasury Delegate Errors ```solidity theme={null} error RateAlreadyPosted(uint256 dayNumber); error CannotPostFutureRate(uint256 dayNumber, uint256 currentDay); error CannotPostRateForUnfinishedDay(uint256 dayNumber, uint256 currentDay); error CannotCorrectBeforeFirstRate(uint256 dayNumber, uint256 firstRateDay); error RateTooHigh(uint256 rate, uint256 maxRate); error ManagementFeeTooHigh(uint256 fee); error InsufficientCredits(uint256 requested, uint256 available); error MustPostSequentially(uint256 dayNumber, uint256 expectedDay); error InvalidTargetValues(uint256 feeTarget, uint256 feePlusRedemptionTarget, uint256 totalTarget); ``` ### Technical Specifications | Property | Value | | :--------------- | :----------------------- | | Token Standard | ERC-20 (with extensions) | | Decimals | 18 | | Upgrade Pattern | UUPS Proxy | | Solidity Version | 0.8.30 | *** Learn how to integrate with the Multiliquid Protocol # Contract ABIs Source: https://docs.multiliquid.xyz/evm/contracts/abis Production Application Binary Interfaces for Multiliquid Protocol smart contracts These production ABIs are generated from the current contracts and interfaces used by `Multiliquid/src/prod`. Use them with viem, ethers.js, or another EVM library to encode calls, decode results, and parse events. Each JSON block has a copy button in its top-right corner. The embedded ABIs are the complete Foundry artifact ABIs, including inherited functions, events, and custom errors. ## Production Interface * All route families use `quoteSwap(user, input)` and `swap(user, receiver, inputs, permit)`. * Delegated execution includes standing `swapInputAllowances`, EIP-712 `nonces`, and `DOMAIN_SEPARATOR()`. * Requested and internally derived legs emit the unified `Swap` event. * Treasury yield accrual uses `accrueInterest(user, maxDays, maxRecords)`. See [V1 to V2 Changes](/evm/contracts/v1-to-v2-changes) for the version comparison and integration migration checklist. ## MultiliquidSwap Primary production swap orchestrator. This ABI contains the singular route-tagged `quoteSwap` and batched `swap` entrypoints, delegated allowances, EIP-712 nonce/domain reads, asset configuration, protocol administration, and the unified `Swap` event. ```json MultiliquidSwap.json theme={null} [ { "type": "constructor", "inputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "BLACKLISTER_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "DEFAULT_ADMIN_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "DOMAIN_SEPARATOR", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "EXTERNAL_PAUSER_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "OPERATOR_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "UPGRADE_INTERFACE_VERSION", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" }, { "type": "function", "name": "adjustSwapInputAllowance", "inputs": [ { "name": "operator", "type": "address", "internalType": "address" }, { "name": "assetInID", "type": "bytes32", "internalType": "bytes32" }, { "name": "amount", "type": "uint256", "internalType": "uint256" }, { "name": "increase", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "autoLiquidityStablecoinIDs", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "blacklisted", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "configureStablecoinGuardrail", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "priceAdapter", "type": "address", "internalType": "address" }, { "name": "band", "type": "uint256", "internalType": "uint256" }, { "name": "enabled", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "discountRates", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" }, { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleAdmin", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMember", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "index", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMemberCount", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMembers", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "address[]", "internalType": "address[]" } ], "stateMutability": "view" }, { "type": "function", "name": "grantRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "hasRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "initialize", "inputs": [ { "name": "admin", "type": "address", "internalType": "address" }, { "name": "operator", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "issuerPaidProtocolFeeRates", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "multiliquidVault", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "nonces", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "pause", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "paused", "inputs": [], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "protocolFeeExempt", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "proxiableUUID", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "quoteSwap", "inputs": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "inputs", "type": "tuple", "internalType": "struct IMultiliquidSwap.SwapInputs", "components": [ { "name": "routeId", "type": "uint256", "internalType": "uint256" }, { "name": "assetInID", "type": "bytes32", "internalType": "bytes32" }, { "name": "assetOutID", "type": "bytes32", "internalType": "bytes32" }, { "name": "stablecoinDelegateID", "type": "bytes32", "internalType": "bytes32" }, { "name": "assetInAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAmt", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "outputs", "type": "tuple", "internalType": "struct IMultiliquidSwap.SwapResolved", "components": [ { "name": "tokenInAmt", "type": "uint256", "internalType": "uint256" }, { "name": "tokenOutAmt", "type": "uint256", "internalType": "uint256" }, { "name": "issuerFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "spreadFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" } ] } ], "stateMutability": "view" }, { "type": "function", "name": "redemptionFees", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" }, { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "renounceRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "callerConfirmation", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "revokeRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "rwaInfo", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "accepted", "type": "bool", "internalType": "bool" }, { "name": "additionalRiskControls", "type": "bool", "internalType": "bool" }, { "name": "decimals", "type": "uint8", "internalType": "uint8" }, { "name": "delegate", "type": "address", "internalType": "address" }, { "name": "assetAddress", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "rwaPriceAdapters", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "rwaWhitelistAdapters", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "setAutoLiquidityStablecoinID", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "autoLiquidityStablecoinID", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setBlacklisted", "inputs": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "_blacklisted", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setIssuerPaidProtocolFeeRate", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setMaxSwapLegs", "inputs": [ { "name": "newMaxSwapLegs", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setMultiliquidVault", "inputs": [ { "name": "vault", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setProtocolFeeExempt", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "exempt", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWAAcceptance", "inputs": [ { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "_delegate", "type": "address", "internalType": "address" }, { "name": "_assetAddress", "type": "address", "internalType": "address" }, { "name": "_accepted", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWADiscountRate", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWARedemptionFee", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRwaPriceAdapter", "inputs": [ { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "priceAdapter", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRwaWhitelistAdapter", "inputs": [ { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "adapter", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setSpreadProtocolTakeRate", "inputs": [ { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinAcceptance", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "_delegate", "type": "address", "internalType": "address" }, { "name": "_assetAddress", "type": "address", "internalType": "address" }, { "name": "_accepted", "type": "bool", "internalType": "bool" }, { "name": "_yieldBearing", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinAcceptanceFee", "inputs": [ { "name": "stablecoinOutID", "type": "bytes32", "internalType": "bytes32" }, { "name": "stablecoin", "type": "address", "internalType": "address" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinRedemptionFee", "inputs": [ { "name": "stablecoinOutID", "type": "bytes32", "internalType": "bytes32" }, { "name": "stablecoin", "type": "address", "internalType": "address" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinUSDValue", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "value", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "spreadProtocolTakeRate", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinAcceptanceFees", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" }, { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinGuardrailBands", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinGuardrailEnabled", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinInfo", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "accepted", "type": "bool", "internalType": "bool" }, { "name": "yieldBearing", "type": "bool", "internalType": "bool" }, { "name": "decimals", "type": "uint8", "internalType": "uint8" }, { "name": "delegate", "type": "address", "internalType": "address" }, { "name": "assetAddress", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinPriceAdapters", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinRedemptionFees", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" }, { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinUSDValue", "inputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "supportsInterface", "inputs": [ { "name": "interfaceId", "type": "bytes4", "internalType": "bytes4" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "swap", "inputs": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "inputs", "type": "tuple[]", "internalType": "struct IMultiliquidSwap.SwapInputs[]", "components": [ { "name": "routeId", "type": "uint256", "internalType": "uint256" }, { "name": "assetInID", "type": "bytes32", "internalType": "bytes32" }, { "name": "assetOutID", "type": "bytes32", "internalType": "bytes32" }, { "name": "stablecoinDelegateID", "type": "bytes32", "internalType": "bytes32" }, { "name": "assetInAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAmt", "type": "uint256", "internalType": "uint256" } ] }, { "name": "permit", "type": "tuple", "internalType": "struct IMultiliquidSwap.ApprovalPermit", "components": [ { "name": "deadline", "type": "uint256", "internalType": "uint256" }, { "name": "signature", "type": "bytes", "internalType": "bytes" } ] } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "swapInputAllowances", "inputs": [ { "name": "", "type": "address", "internalType": "address" }, { "name": "", "type": "address", "internalType": "address" }, { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "unpause", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "upgradeToAndCall", "inputs": [ { "name": "newImplementation", "type": "address", "internalType": "address" }, { "name": "data", "type": "bytes", "internalType": "bytes" } ], "outputs": [], "stateMutability": "payable" }, { "type": "event", "name": "AutoLiquidityStablecoinIDSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "autoLiquidityStablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" } ], "anonymous": false }, { "type": "event", "name": "DiscountRateSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Initialized", "inputs": [ { "name": "version", "type": "uint64", "indexed": false, "internalType": "uint64" } ], "anonymous": false }, { "type": "event", "name": "IssuerPaidProtocolFeeRateSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "oldRate", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "newRate", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "MultiliquidVaultSet", "inputs": [ { "name": "oldVault", "type": "address", "indexed": true, "internalType": "address" }, { "name": "newVault", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "Paused", "inputs": [ { "name": "account", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "ProtocolFeeExemptSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "exempt", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "RWAAcceptanceSet", "inputs": [ { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "delegate", "type": "address", "indexed": true, "internalType": "address" }, { "name": "assetAddress", "type": "address", "indexed": true, "internalType": "address" }, { "name": "decimals", "type": "uint8", "indexed": false, "internalType": "uint8" }, { "name": "accepted", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "RedemptionFeeSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "RoleAdminChanged", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "previousAdminRole", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "newAdminRole", "type": "bytes32", "indexed": true, "internalType": "bytes32" } ], "anonymous": false }, { "type": "event", "name": "RoleGranted", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "sender", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RoleRevoked", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "sender", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RwaPriceAdapterSet", "inputs": [ { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "priceAdapter", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RwaWhitelistAdapterSet", "inputs": [ { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "adapter", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "SpreadProtocolTakeRateSet", "inputs": [ { "name": "oldRate", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "newRate", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "StablecoinAcceptanceFeeSet", "inputs": [ { "name": "stablecoinOutID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "stablecoinIn", "type": "address", "indexed": true, "internalType": "address" }, { "name": "rate", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "StablecoinAcceptanceSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "delegate", "type": "address", "indexed": true, "internalType": "address" }, { "name": "assetAddress", "type": "address", "indexed": true, "internalType": "address" }, { "name": "decimals", "type": "uint8", "indexed": false, "internalType": "uint8" }, { "name": "accepted", "type": "bool", "indexed": false, "internalType": "bool" }, { "name": "yieldBearing", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "StablecoinPriceGuardrailSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "priceAdapter", "type": "address", "indexed": true, "internalType": "address" }, { "name": "band", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "StablecoinRedemptionFeeSet", "inputs": [ { "name": "stablecoinOutID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "stablecoinIn", "type": "address", "indexed": true, "internalType": "address" }, { "name": "rate", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "StablecoinUSDValueSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "value", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Swap", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "assetInID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "assetOutID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "operator", "type": "address", "indexed": false, "internalType": "address" }, { "name": "receiver", "type": "address", "indexed": false, "internalType": "address" }, { "name": "routeId", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "stablecoinDelegateID", "type": "bytes32", "indexed": false, "internalType": "bytes32" }, { "name": "amountIn", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "amountOut", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "issuerFeeAmt", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "spreadFeeAmt", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "SwapInputAllowanceSet", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "operator", "type": "address", "indexed": true, "internalType": "address" }, { "name": "assetInID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "SwapInputAllowanceSpent", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "operator", "type": "address", "indexed": true, "internalType": "address" }, { "name": "assetInID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "amountSpent", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "remainingAmount", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Unpaused", "inputs": [ { "name": "account", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "Upgraded", "inputs": [ { "name": "implementation", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "UserBlacklistUpdated", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "blacklisted", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "error", "name": "AccessControlBadConfirmation", "inputs": [] }, { "type": "error", "name": "AccessControlUnauthorizedAccount", "inputs": [ { "name": "account", "type": "address", "internalType": "address" }, { "name": "neededRole", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "AddressEmptyCode", "inputs": [ { "name": "target", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "AssetIDAlreadyRegistered", "inputs": [ { "name": "assetID", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "ERC1967InvalidImplementation", "inputs": [ { "name": "implementation", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ERC1967NonPayable", "inputs": [] }, { "type": "error", "name": "EnforcedPause", "inputs": [] }, { "type": "error", "name": "ExpectedPause", "inputs": [] }, { "type": "error", "name": "FailedCall", "inputs": [] }, { "type": "error", "name": "FailedToDeployRWA", "inputs": [] }, { "type": "error", "name": "FailedToDeployStablecoin", "inputs": [] }, { "type": "error", "name": "IdenticalAssets", "inputs": [] }, { "type": "error", "name": "IdenticalStablecoins", "inputs": [] }, { "type": "error", "name": "InsufficientRWAInput", "inputs": [] }, { "type": "error", "name": "InsufficientRWAOutput", "inputs": [] }, { "type": "error", "name": "InsufficientStablecoinInput", "inputs": [] }, { "type": "error", "name": "InsufficientStablecoinOutput", "inputs": [] }, { "type": "error", "name": "InsufficientSwapAllowance", "inputs": [] }, { "type": "error", "name": "InvalidAdmin", "inputs": [] }, { "type": "error", "name": "InvalidAssetAddress", "inputs": [] }, { "type": "error", "name": "InvalidDelegate", "inputs": [] }, { "type": "error", "name": "InvalidInitialization", "inputs": [] }, { "type": "error", "name": "InvalidLegCount", "inputs": [] }, { "type": "error", "name": "InvalidPrice", "inputs": [] }, { "type": "error", "name": "InvalidRate", "inputs": [] }, { "type": "error", "name": "InvalidSignature", "inputs": [] }, { "type": "error", "name": "InvalidStablecoinDelegateID", "inputs": [ { "name": "actual", "type": "bytes32", "internalType": "bytes32" }, { "name": "expected", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "InvalidSwapAddress", "inputs": [ { "name": "user", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "InvalidVault", "inputs": [] }, { "type": "error", "name": "NotInitializing", "inputs": [] }, { "type": "error", "name": "NotStablecoinDelegate", "inputs": [] }, { "type": "error", "name": "PermitExpired", "inputs": [] }, { "type": "error", "name": "PriceAdapterNotSet", "inputs": [ { "name": "tokenID", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "RWANotAccepted", "inputs": [] }, { "type": "error", "name": "RWAValidityCheckFailed", "inputs": [] }, { "type": "error", "name": "ReentrancyGuardReentrantCall", "inputs": [] }, { "type": "error", "name": "StablecoinGuardrailBandNotSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "StablecoinNotAccepted", "inputs": [] }, { "type": "error", "name": "StablecoinPriceAdapterNotSet", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "StablecoinPriceOutsideBand", "inputs": [ { "name": "stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "adminValue", "type": "uint256", "internalType": "uint256" }, { "name": "oraclePrice", "type": "uint256", "internalType": "uint256" }, { "name": "band", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "USDValueNotSet", "inputs": [ { "name": "tokenID", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "UUPSUnauthorizedCallContext", "inputs": [] }, { "type": "error", "name": "UUPSUnsupportedProxiableUUID", "inputs": [ { "name": "slot", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "UnsupportedDecimals", "inputs": [ { "name": "decimals", "type": "uint8", "internalType": "uint8" } ] }, { "type": "error", "name": "UnsupportedRouteId", "inputs": [ { "name": "routeId", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "UserBlacklisted", "inputs": [ { "name": "user", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "UserNotWhitelisted", "inputs": [ { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "user", "type": "address", "internalType": "address" } ] } ] ``` ## Treasury Upgradeable Treasury Yield ERC-20 contract, formerly documented here as `LiquidTreasury`. It includes ERC-20, EIP-2612 permit, access-control, whitelist, blacklist, bridge, pause, forced-transfer, and UUPS functionality. ```json Treasury.json theme={null} [ { "type": "constructor", "inputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "BLACKLISTER_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "BRIDGE_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "DEFAULT_ADMIN_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "DOMAIN_SEPARATOR", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "MINTER_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "OPERATOR_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "PAUSER_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "UPGRADE_INTERFACE_VERSION", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" }, { "type": "function", "name": "WHITELISTER_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "addBridge", "inputs": [ { "name": "bridge", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "addToBlacklist", "inputs": [ { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "addToBlacklistBatch", "inputs": [ { "name": "accounts", "type": "address[]", "internalType": "address[]" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "addToWhitelist", "inputs": [ { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "allowance", "inputs": [ { "name": "owner", "type": "address", "internalType": "address" }, { "name": "spender", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "approve", "inputs": [ { "name": "spender", "type": "address", "internalType": "address" }, { "name": "value", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "balanceOf", "inputs": [ { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "blacklisted", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "bridgeMint", "inputs": [ { "name": "to", "type": "address", "internalType": "address" }, { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "burn", "inputs": [ { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "burnFrom", "inputs": [ { "name": "from", "type": "address", "internalType": "address" }, { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "decimals", "inputs": [], "outputs": [ { "name": "", "type": "uint8", "internalType": "uint8" } ], "stateMutability": "view" }, { "type": "function", "name": "eip712Domain", "inputs": [], "outputs": [ { "name": "fields", "type": "bytes1", "internalType": "bytes1" }, { "name": "name", "type": "string", "internalType": "string" }, { "name": "version", "type": "string", "internalType": "string" }, { "name": "chainId", "type": "uint256", "internalType": "uint256" }, { "name": "verifyingContract", "type": "address", "internalType": "address" }, { "name": "salt", "type": "bytes32", "internalType": "bytes32" }, { "name": "extensions", "type": "uint256[]", "internalType": "uint256[]" } ], "stateMutability": "view" }, { "type": "function", "name": "forceBurn", "inputs": [ { "name": "from", "type": "address", "internalType": "address" }, { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "forceTransfer", "inputs": [ { "name": "from", "type": "address", "internalType": "address" }, { "name": "to", "type": "address", "internalType": "address" }, { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "getRoleAdmin", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMember", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "index", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMemberCount", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMembers", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "address[]", "internalType": "address[]" } ], "stateMutability": "view" }, { "type": "function", "name": "grantRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "hasRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "initialize", "inputs": [ { "name": "name_", "type": "string", "internalType": "string" }, { "name": "symbol_", "type": "string", "internalType": "string" }, { "name": "admin_", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "mint", "inputs": [ { "name": "to", "type": "address", "internalType": "address" }, { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "name", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" }, { "type": "function", "name": "nonces", "inputs": [ { "name": "owner", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "pause", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "paused", "inputs": [], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "permit", "inputs": [ { "name": "owner", "type": "address", "internalType": "address" }, { "name": "spender", "type": "address", "internalType": "address" }, { "name": "value", "type": "uint256", "internalType": "uint256" }, { "name": "deadline", "type": "uint256", "internalType": "uint256" }, { "name": "v", "type": "uint8", "internalType": "uint8" }, { "name": "r", "type": "bytes32", "internalType": "bytes32" }, { "name": "s", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "proxiableUUID", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "removeBridge", "inputs": [ { "name": "bridge", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeFromBlacklist", "inputs": [ { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeFromBlacklistBatch", "inputs": [ { "name": "accounts", "type": "address[]", "internalType": "address[]" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeFromWhitelist", "inputs": [ { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "renounceRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "callerConfirmation", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "revokeRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "supportsInterface", "inputs": [ { "name": "interfaceId", "type": "bytes4", "internalType": "bytes4" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "symbol", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" }, { "type": "function", "name": "totalSupply", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "transfer", "inputs": [ { "name": "to", "type": "address", "internalType": "address" }, { "name": "value", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "transferFrom", "inputs": [ { "name": "from", "type": "address", "internalType": "address" }, { "name": "to", "type": "address", "internalType": "address" }, { "name": "value", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "unpause", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "upgradeToAndCall", "inputs": [ { "name": "newImplementation", "type": "address", "internalType": "address" }, { "name": "data", "type": "bytes", "internalType": "bytes" } ], "outputs": [], "stateMutability": "payable" }, { "type": "function", "name": "whitelisted", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "event", "name": "AddedToBlacklist", "inputs": [ { "name": "account", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "AddedToWhitelist", "inputs": [ { "name": "account", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "Approval", "inputs": [ { "name": "owner", "type": "address", "indexed": true, "internalType": "address" }, { "name": "spender", "type": "address", "indexed": true, "internalType": "address" }, { "name": "value", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "BridgeAdded", "inputs": [ { "name": "bridge", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "BridgeBurn", "inputs": [ { "name": "bridge", "type": "address", "indexed": true, "internalType": "address" }, { "name": "from", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "BridgeMint", "inputs": [ { "name": "bridge", "type": "address", "indexed": true, "internalType": "address" }, { "name": "to", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "BridgeRemoved", "inputs": [ { "name": "bridge", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "EIP712DomainChanged", "inputs": [], "anonymous": false }, { "type": "event", "name": "ForcedBurn", "inputs": [ { "name": "operator", "type": "address", "indexed": true, "internalType": "address" }, { "name": "from", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "ForcedTransfer", "inputs": [ { "name": "operator", "type": "address", "indexed": true, "internalType": "address" }, { "name": "from", "type": "address", "indexed": true, "internalType": "address" }, { "name": "to", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Initialized", "inputs": [ { "name": "version", "type": "uint64", "indexed": false, "internalType": "uint64" } ], "anonymous": false }, { "type": "event", "name": "Paused", "inputs": [ { "name": "account", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RemovedFromBlacklist", "inputs": [ { "name": "account", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RemovedFromWhitelist", "inputs": [ { "name": "account", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RoleAdminChanged", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "previousAdminRole", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "newAdminRole", "type": "bytes32", "indexed": true, "internalType": "bytes32" } ], "anonymous": false }, { "type": "event", "name": "RoleGranted", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "sender", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RoleRevoked", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "sender", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "Transfer", "inputs": [ { "name": "from", "type": "address", "indexed": true, "internalType": "address" }, { "name": "to", "type": "address", "indexed": true, "internalType": "address" }, { "name": "value", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Unpaused", "inputs": [ { "name": "account", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "Upgraded", "inputs": [ { "name": "implementation", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "error", "name": "AccessControlBadConfirmation", "inputs": [] }, { "type": "error", "name": "AccessControlUnauthorizedAccount", "inputs": [ { "name": "account", "type": "address", "internalType": "address" }, { "name": "neededRole", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "AccountBlacklisted", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "AccountNotBlacklisted", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "AccountNotWhitelisted", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "AddressEmptyCode", "inputs": [ { "name": "target", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ECDSAInvalidSignature", "inputs": [] }, { "type": "error", "name": "ECDSAInvalidSignatureLength", "inputs": [ { "name": "length", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "ECDSAInvalidSignatureS", "inputs": [ { "name": "s", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "ERC1967InvalidImplementation", "inputs": [ { "name": "implementation", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ERC1967NonPayable", "inputs": [] }, { "type": "error", "name": "ERC20InsufficientAllowance", "inputs": [ { "name": "spender", "type": "address", "internalType": "address" }, { "name": "allowance", "type": "uint256", "internalType": "uint256" }, { "name": "needed", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "ERC20InsufficientBalance", "inputs": [ { "name": "sender", "type": "address", "internalType": "address" }, { "name": "balance", "type": "uint256", "internalType": "uint256" }, { "name": "needed", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "ERC20InvalidApprover", "inputs": [ { "name": "approver", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ERC20InvalidReceiver", "inputs": [ { "name": "receiver", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ERC20InvalidSender", "inputs": [ { "name": "sender", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ERC20InvalidSpender", "inputs": [ { "name": "spender", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ERC2612ExpiredSignature", "inputs": [ { "name": "deadline", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "ERC2612InvalidSigner", "inputs": [ { "name": "signer", "type": "address", "internalType": "address" }, { "name": "owner", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "EnforcedPause", "inputs": [] }, { "type": "error", "name": "ExpectedPause", "inputs": [] }, { "type": "error", "name": "FailedCall", "inputs": [] }, { "type": "error", "name": "InvalidAccountNonce", "inputs": [ { "name": "account", "type": "address", "internalType": "address" }, { "name": "currentNonce", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "InvalidAmount", "inputs": [] }, { "type": "error", "name": "InvalidInitialization", "inputs": [] }, { "type": "error", "name": "NotInitializing", "inputs": [] }, { "type": "error", "name": "ReentrancyGuardReentrantCall", "inputs": [] }, { "type": "error", "name": "UUPSUnauthorizedCallContext", "inputs": [] }, { "type": "error", "name": "UUPSUnsupportedProxiableUUID", "inputs": [ { "name": "slot", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "ZeroAddress", "inputs": [] } ] ``` ## TreasuryDelegate Yield-bearing stablecoin delegate used by the Treasury Yield registration. It includes delegate settlement and LP administration plus yield-rate, credit, multiplier, and bounded interest-accrual functionality. ```json TreasuryDelegate.json theme={null} [ { "type": "constructor", "inputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "DEFAULT_ADMIN_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "MULTILIQUID_SWAP_CONTRACT", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "OPERATOR_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "PAUSE_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "RATE_POSTER_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "UPGRADE_INTERFACE_VERSION", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" }, { "type": "function", "name": "accrueInterest", "inputs": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "maxDays", "type": "uint256", "internalType": "uint256" }, { "name": "maxRecords", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "daysAccrued", "type": "uint256", "internalType": "uint256" }, { "name": "recordsMerged", "type": "uint256", "internalType": "uint256" }, { "name": "dailyAccrualComplete", "type": "bool", "internalType": "bool" }, { "name": "creditsFullyMerged", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "addIssuerAdmin", "inputs": [ { "name": "issuerAdmin", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "addRWAColdStorageAddress", "inputs": [ { "name": "coldStorageAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "addStablecoinColdStorageAddress", "inputs": [ { "name": "coldStorageAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "balanceSheetCustodyAddress", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "batchAccrueInterest", "inputs": [ { "name": "users", "type": "address[]", "internalType": "address[]" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "blacklist", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "correctDailyRate", "inputs": [ { "name": "dayNumber", "type": "uint256", "internalType": "uint256" }, { "name": "grossRate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "credits", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "dailyRates", "inputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "deployStablecoin", "inputs": [ { "name": "params", "type": "tuple", "internalType": "struct IStablecoinDelegate.DelegateParams", "components": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" }, { "name": "protocolFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetInAddress", "type": "address", "internalType": "address" }, { "name": "assetInAmount", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAddress", "type": "address", "internalType": "address" }, { "name": "assetOutAmount", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "success", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "exchangeRWAs", "inputs": [ { "name": "params", "type": "tuple", "internalType": "struct IStablecoinDelegate.DelegateParams", "components": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" }, { "name": "protocolFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetInAddress", "type": "address", "internalType": "address" }, { "name": "assetInAmount", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAddress", "type": "address", "internalType": "address" }, { "name": "assetOutAmount", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "success", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "exchangeStablecoins", "inputs": [ { "name": "params", "type": "tuple", "internalType": "struct IStablecoinDelegate.DelegateParams", "components": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" }, { "name": "protocolFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetInAddress", "type": "address", "internalType": "address" }, { "name": "assetInAmount", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAddress", "type": "address", "internalType": "address" }, { "name": "assetOutAmount", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "success", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "firstRateDay", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "getAmountForTargetValue", "inputs": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "targetValue", "type": "uint256", "internalType": "uint256" }, { "name": "stablecoinWithdrawal", "type": "bool", "internalType": "bool" } ], "outputs": [ { "name": "treasuryNeeded", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "getCurrentDay", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "getDailyRate", "inputs": [ { "name": "dayNumber", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "rate", "type": "uint256", "internalType": "uint256" }, { "name": "posted", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "getRWAColdStorageAddresses", "inputs": [], "outputs": [ { "name": "addresses", "type": "address[]", "internalType": "address[]" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleAdmin", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMember", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "index", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMemberCount", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMembers", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "address[]", "internalType": "address[]" } ], "stateMutability": "view" }, { "type": "function", "name": "getStablecoinColdStorageAddresses", "inputs": [], "outputs": [ { "name": "addresses", "type": "address[]", "internalType": "address[]" } ], "stateMutability": "view" }, { "type": "function", "name": "getWithheldCredits", "inputs": [ { "name": "user", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "tuple[]", "internalType": "struct WithheldCreditsDeque.WithheldCredits[]", "components": [ { "name": "tokenAddress", "type": "address", "internalType": "address" }, { "name": "credits", "type": "uint256", "internalType": "uint256" }, { "name": "releaseTimestamp", "type": "uint256", "internalType": "uint256" }, { "name": "earningStartTimestamp", "type": "uint256", "internalType": "uint256" }, { "name": "isRWA", "type": "bool", "internalType": "bool" } ] } ], "stateMutability": "view" }, { "type": "function", "name": "getYieldAmount", "inputs": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "redeemValue", "type": "uint256", "internalType": "uint256" }, { "name": "stablecoinWithdrawal", "type": "bool", "internalType": "bool" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "grantRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "hasRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "initialize", "inputs": [ { "name": "multiliquidAdmin", "type": "address", "internalType": "address" }, { "name": "initialIssuerAdmin", "type": "address", "internalType": "address" }, { "name": "_multiliquidSwap", "type": "address", "internalType": "address" }, { "name": "_stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "_stablecoinAddress", "type": "address", "internalType": "address" }, { "name": "initialRate", "type": "uint256", "internalType": "uint256" }, { "name": "initialManagementFee", "type": "uint256", "internalType": "uint256" }, { "name": "ratePoster", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "issuerAdmins", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "lastAccrualDay", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "lastRatePostingDay", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "managementFee", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "maxDailyRate", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "maxFallbackRateSeconds", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "multiliquidSwap", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "pause", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "pauseMultiliquid", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "paused", "inputs": [], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "postDailyRate", "inputs": [ { "name": "dayNumber", "type": "uint256", "internalType": "uint256" }, { "name": "grossRate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "proxiableUUID", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "ratePosted", "inputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "receiveStablecoin", "inputs": [ { "name": "params", "type": "tuple", "internalType": "struct IStablecoinDelegate.DelegateParams", "components": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" }, { "name": "protocolFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetInAddress", "type": "address", "internalType": "address" }, { "name": "assetInAmount", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAddress", "type": "address", "internalType": "address" }, { "name": "assetOutAmount", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "success", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeIssuerAdmin", "inputs": [ { "name": "issuerAdmin", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeRWAColdStorageAddress", "inputs": [ { "name": "coldStorageAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeStablecoinColdStorageAddress", "inputs": [ { "name": "coldStorageAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "renounceRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "callerConfirmation", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "revokeRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "rwaWhitelist", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "setAutoLiquidityStablecoinID", "inputs": [ { "name": "autoLiquidityStablecoinID", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setBlacklist", "inputs": [ { "name": "account", "type": "address", "internalType": "address" }, { "name": "isBlacklisted", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setManagementFee", "inputs": [ { "name": "fee", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setMaxDailyRate", "inputs": [ { "name": "maxRate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setMaxFallbackRateSeconds", "inputs": [ { "name": "maxSeconds", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWACustodyAddress", "inputs": [ { "name": "_balanceSheetCustodyAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWADiscountRate", "inputs": [ { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWARedemptionFee", "inputs": [ { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinAcceptanceFee", "inputs": [ { "name": "stablecoin", "type": "address", "internalType": "address" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinCustodyAddress", "inputs": [ { "name": "_stablecoinCustodyAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinRedemptionFee", "inputs": [ { "name": "stablecoin", "type": "address", "internalType": "address" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setUserYieldMultiplier", "inputs": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "newMultiplier", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "stablecoinAddress", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinCustodyAddress", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinID", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinWhitelist", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "supportsInterface", "inputs": [ { "name": "interfaceId", "type": "bytes4", "internalType": "bytes4" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "totalWithheldCredits", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "treasury", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "contract ITreasury" } ], "stateMutability": "view" }, { "type": "function", "name": "unpause", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "unpauseMultiliquid", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "upgradeToAndCall", "inputs": [ { "name": "newImplementation", "type": "address", "internalType": "address" }, { "name": "data", "type": "bytes", "internalType": "bytes" } ], "outputs": [], "stateMutability": "payable" }, { "type": "function", "name": "whitelistRWA", "inputs": [ { "name": "rwa", "type": "address", "internalType": "address" }, { "name": "accepted", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "whitelistStablecoin", "inputs": [ { "name": "stablecoin", "type": "address", "internalType": "address" }, { "name": "accepted", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "withdrawStablecoin", "inputs": [ { "name": "to", "type": "address", "internalType": "address" }, { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "yieldMultiplier", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "event", "name": "AutoLiquidityStablecoinIDSet", "inputs": [ { "name": "autoLiquidityStablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" } ], "anonymous": false }, { "type": "event", "name": "BlacklistUpdated", "inputs": [ { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "blacklisted", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "DailyRateCorrected", "inputs": [ { "name": "dayNumber", "type": "uint256", "indexed": true, "internalType": "uint256" }, { "name": "oldRate", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "newRate", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "corrector", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "DailyRatePosted", "inputs": [ { "name": "dayNumber", "type": "uint256", "indexed": true, "internalType": "uint256" }, { "name": "rate", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "poster", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "FirstRatePosted", "inputs": [ { "name": "dayNumber", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Initialized", "inputs": [ { "name": "version", "type": "uint64", "indexed": false, "internalType": "uint64" } ], "anonymous": false }, { "type": "event", "name": "InterestAccrued", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "fromDay", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "toDay", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "daysAccrued", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "multiplierAfter", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "IssuerAdminAdded", "inputs": [ { "name": "issuerAdmin", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "IssuerAdminRemoved", "inputs": [ { "name": "issuerAdmin", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "MainCreditsReduced", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "remainingCredits", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "ManagementFeeUpdated", "inputs": [ { "name": "oldFee", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "newFee", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "MaxDailyRateUpdated", "inputs": [ { "name": "oldRate", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "newRate", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "MaxFallbackRateSecondsUpdated", "inputs": [ { "name": "oldWindow", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "newWindow", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Paused", "inputs": [ { "name": "account", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "PendingCreditsAdded", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "depositDay", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "PendingCreditsMerged", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "pendingAmount", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "newCredits", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "RWAColdStorageAddressAdded", "inputs": [ { "name": "coldStorageAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RWAColdStorageAddressRemoved", "inputs": [ { "name": "coldStorageAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RWACustodyAddressSet", "inputs": [ { "name": "balanceSheetCustodyAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RWADiscountRateSet", "inputs": [ { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "indexed": true, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "RWAWhitelist", "inputs": [ { "name": "rwa", "type": "address", "indexed": true, "internalType": "address" }, { "name": "accepted", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "RedemptionFeeSet", "inputs": [ { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "indexed": true, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "RoleAdminChanged", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "previousAdminRole", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "newAdminRole", "type": "bytes32", "indexed": true, "internalType": "bytes32" } ], "anonymous": false }, { "type": "event", "name": "RoleGranted", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "sender", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RoleRevoked", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "sender", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "StablecoinAcceptanceFeeSet", "inputs": [ { "name": "stablecoin", "type": "address", "indexed": true, "internalType": "address" }, { "name": "rate", "type": "uint256", "indexed": true, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "StablecoinColdStorageAddressAdded", "inputs": [ { "name": "coldStorageAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "StablecoinColdStorageAddressRemoved", "inputs": [ { "name": "coldStorageAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "StablecoinCustodyAddressSet", "inputs": [ { "name": "stablecoinCustodyAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "StablecoinRedemptionFeeSet", "inputs": [ { "name": "stablecoin", "type": "address", "indexed": true, "internalType": "address" }, { "name": "rate", "type": "uint256", "indexed": true, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "StablecoinWhitelist", "inputs": [ { "name": "stablecoin", "type": "address", "indexed": true, "internalType": "address" }, { "name": "accepted", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "StablecoinWithdrawn", "inputs": [ { "name": "to", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Unpaused", "inputs": [ { "name": "account", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "Upgraded", "inputs": [ { "name": "implementation", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "WithheldCreditsReduced", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "remainingWithheld", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "YieldClaimed", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "creditsRedeemed", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "yieldAmount", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "effectiveValue", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "YieldMultiplierUpdated", "inputs": [ { "name": "user", "type": "address", "indexed": true, "internalType": "address" }, { "name": "oldMultiplier", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "newMultiplier", "type": "uint256", "indexed": false, "internalType": "uint256" }, { "name": "admin", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "error", "name": "AccessControlBadConfirmation", "inputs": [] }, { "type": "error", "name": "AccessControlUnauthorizedAccount", "inputs": [ { "name": "account", "type": "address", "internalType": "address" }, { "name": "neededRole", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "AddressBlacklisted", "inputs": [ { "name": "account", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "AddressEmptyCode", "inputs": [ { "name": "target", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "AddressNotAllowed", "inputs": [] }, { "type": "error", "name": "CannotCorrectBeforeFirstRate", "inputs": [ { "name": "dayNumber", "type": "uint256", "internalType": "uint256" }, { "name": "firstRateDay", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "CannotPostFutureRate", "inputs": [ { "name": "dayNumber", "type": "uint256", "internalType": "uint256" }, { "name": "currentDay", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "CannotPostRateForUnfinishedDay", "inputs": [ { "name": "dayNumber", "type": "uint256", "internalType": "uint256" }, { "name": "currentDay", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "CannotRemoveSelf", "inputs": [] }, { "type": "error", "name": "ERC1967InvalidImplementation", "inputs": [ { "name": "implementation", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ERC1967NonPayable", "inputs": [] }, { "type": "error", "name": "EnforcedPause", "inputs": [] }, { "type": "error", "name": "ExpectedPause", "inputs": [] }, { "type": "error", "name": "FailedCall", "inputs": [] }, { "type": "error", "name": "InsufficientCredits", "inputs": [ { "name": "requested", "type": "uint256", "internalType": "uint256" }, { "name": "available", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "InvalidFallbackRateWindow", "inputs": [] }, { "type": "error", "name": "InvalidInitialization", "inputs": [] }, { "type": "error", "name": "InvalidInitializationParams", "inputs": [] }, { "type": "error", "name": "InvalidIssuerAdmin", "inputs": [] }, { "type": "error", "name": "InvalidMaxDays", "inputs": [] }, { "type": "error", "name": "InvalidMaxRecords", "inputs": [] }, { "type": "error", "name": "InvalidMultiliquidSwap", "inputs": [] }, { "type": "error", "name": "InvalidRate", "inputs": [] }, { "type": "error", "name": "InvalidStablecoinAsset", "inputs": [ { "name": "asset", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "InvalidStablecoinExchange", "inputs": [ { "name": "assetIn", "type": "address", "internalType": "address" }, { "name": "assetOut", "type": "address", "internalType": "address" }, { "name": "issuerStablecoin", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "InvalidTargetValues", "inputs": [ { "name": "feeTarget", "type": "uint256", "internalType": "uint256" }, { "name": "feePlusRedemptionTarget", "type": "uint256", "internalType": "uint256" }, { "name": "totalTarget", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "InvalidYieldMultiplier", "inputs": [ { "name": "multiplier", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "ManagementFeeTooHigh", "inputs": [ { "name": "fee", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "MustPostSequentially", "inputs": [ { "name": "dayNumber", "type": "uint256", "internalType": "uint256" }, { "name": "expectedDay", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "NotInitializing", "inputs": [] }, { "type": "error", "name": "NotIssuerAdmin", "inputs": [] }, { "type": "error", "name": "OutOfBounds", "inputs": [] }, { "type": "error", "name": "QueueEmpty", "inputs": [] }, { "type": "error", "name": "QueueFull", "inputs": [] }, { "type": "error", "name": "RWANotWhitelisted", "inputs": [] }, { "type": "error", "name": "RateAlreadyPosted", "inputs": [ { "name": "dayNumber", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "RateTooHigh", "inputs": [ { "name": "rate", "type": "uint256", "internalType": "uint256" }, { "name": "maxRate", "type": "uint256", "internalType": "uint256" } ] }, { "type": "error", "name": "ReentrancyGuardReentrantCall", "inputs": [] }, { "type": "error", "name": "SafeERC20FailedOperation", "inputs": [ { "name": "token", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "SetRWACustodyAddress", "inputs": [] }, { "type": "error", "name": "SetStablecoinCustodyAddress", "inputs": [] }, { "type": "error", "name": "StablecoinNotWhitelisted", "inputs": [ { "name": "stablecoin", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "UUPSUnauthorizedCallContext", "inputs": [] }, { "type": "error", "name": "UUPSUnsupportedProxiableUUID", "inputs": [ { "name": "slot", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "ZeroAddressNotAllowed", "inputs": [] } ] ``` ## UniformLabsDelegateV2 Production balance-sheet stablecoin delegate implementation. Its inherited ABI includes custody and whitelist reads, LP-admin configuration, settlement entrypoints, pause controls, and UUPS administration. ```json UniformLabsDelegateV2.json theme={null} [ { "type": "constructor", "inputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "DEFAULT_ADMIN_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "MULTILIQUID_SWAP_CONTRACT", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "PAUSE_ROLE", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "UPGRADE_INTERFACE_VERSION", "inputs": [], "outputs": [ { "name": "", "type": "string", "internalType": "string" } ], "stateMutability": "view" }, { "type": "function", "name": "addIssuerAdmin", "inputs": [ { "name": "issuerAdmin", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "addRWAColdStorageAddress", "inputs": [ { "name": "coldStorageAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "addStablecoinColdStorageAddress", "inputs": [ { "name": "coldStorageAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "balanceSheetCustodyAddress", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "blacklist", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "deployStablecoin", "inputs": [ { "name": "params", "type": "tuple", "internalType": "struct IStablecoinDelegate.DelegateParams", "components": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" }, { "name": "protocolFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetInAddress", "type": "address", "internalType": "address" }, { "name": "assetInAmount", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAddress", "type": "address", "internalType": "address" }, { "name": "assetOutAmount", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "success", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "exchangeRWAs", "inputs": [ { "name": "params", "type": "tuple", "internalType": "struct IStablecoinDelegate.DelegateParams", "components": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" }, { "name": "protocolFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetInAddress", "type": "address", "internalType": "address" }, { "name": "assetInAmount", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAddress", "type": "address", "internalType": "address" }, { "name": "assetOutAmount", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "success", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "exchangeStablecoins", "inputs": [ { "name": "params", "type": "tuple", "internalType": "struct IStablecoinDelegate.DelegateParams", "components": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" }, { "name": "protocolFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetInAddress", "type": "address", "internalType": "address" }, { "name": "assetInAmount", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAddress", "type": "address", "internalType": "address" }, { "name": "assetOutAmount", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "success", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "getRWAColdStorageAddresses", "inputs": [], "outputs": [ { "name": "addresses", "type": "address[]", "internalType": "address[]" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleAdmin", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMember", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "index", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMemberCount", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" }, { "type": "function", "name": "getRoleMembers", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [ { "name": "", "type": "address[]", "internalType": "address[]" } ], "stateMutability": "view" }, { "type": "function", "name": "getStablecoinColdStorageAddresses", "inputs": [], "outputs": [ { "name": "addresses", "type": "address[]", "internalType": "address[]" } ], "stateMutability": "view" }, { "type": "function", "name": "grantRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "hasRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "initialize", "inputs": [ { "name": "multiliquidAdmin", "type": "address", "internalType": "address" }, { "name": "initialIssuerAdmin", "type": "address", "internalType": "address" }, { "name": "_multiliquidSwap", "type": "address", "internalType": "address" }, { "name": "_stablecoinID", "type": "bytes32", "internalType": "bytes32" }, { "name": "_stablecoinAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "issuerAdmins", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "multiliquidSwap", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "pause", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "pauseMultiliquid", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "paused", "inputs": [], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "proxiableUUID", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "receiveStablecoin", "inputs": [ { "name": "params", "type": "tuple", "internalType": "struct IStablecoinDelegate.DelegateParams", "components": [ { "name": "user", "type": "address", "internalType": "address" }, { "name": "receiver", "type": "address", "internalType": "address" }, { "name": "vault", "type": "address", "internalType": "address" }, { "name": "protocolFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "lpFeeAmt", "type": "uint256", "internalType": "uint256" }, { "name": "assetInAddress", "type": "address", "internalType": "address" }, { "name": "assetInAmount", "type": "uint256", "internalType": "uint256" }, { "name": "assetOutAddress", "type": "address", "internalType": "address" }, { "name": "assetOutAmount", "type": "uint256", "internalType": "uint256" } ] } ], "outputs": [ { "name": "success", "type": "bool", "internalType": "bool" } ], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeIssuerAdmin", "inputs": [ { "name": "issuerAdmin", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeRWAColdStorageAddress", "inputs": [ { "name": "coldStorageAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "removeStablecoinColdStorageAddress", "inputs": [ { "name": "coldStorageAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "renounceRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "callerConfirmation", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "revokeRole", "inputs": [ { "name": "role", "type": "bytes32", "internalType": "bytes32" }, { "name": "account", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "rwaWhitelist", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "setAutoLiquidityStablecoinID", "inputs": [ { "name": "autoLiquidityStablecoinID", "type": "bytes32", "internalType": "bytes32" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setBlacklist", "inputs": [ { "name": "account", "type": "address", "internalType": "address" }, { "name": "isBlacklisted", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWACustodyAddress", "inputs": [ { "name": "_balanceSheetCustodyAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWADiscountRate", "inputs": [ { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setRWARedemptionFee", "inputs": [ { "name": "rwaID", "type": "bytes32", "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinAcceptanceFee", "inputs": [ { "name": "stablecoin", "type": "address", "internalType": "address" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinCustodyAddress", "inputs": [ { "name": "_stablecoinCustodyAddress", "type": "address", "internalType": "address" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "setStablecoinRedemptionFee", "inputs": [ { "name": "stablecoin", "type": "address", "internalType": "address" }, { "name": "rate", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "stablecoinAddress", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinCustodyAddress", "inputs": [], "outputs": [ { "name": "", "type": "address", "internalType": "address" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinID", "inputs": [], "outputs": [ { "name": "", "type": "bytes32", "internalType": "bytes32" } ], "stateMutability": "view" }, { "type": "function", "name": "stablecoinWhitelist", "inputs": [ { "name": "", "type": "address", "internalType": "address" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "supportsInterface", "inputs": [ { "name": "interfaceId", "type": "bytes4", "internalType": "bytes4" } ], "outputs": [ { "name": "", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" }, { "type": "function", "name": "unpause", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "unpauseMultiliquid", "inputs": [], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "upgradeToAndCall", "inputs": [ { "name": "newImplementation", "type": "address", "internalType": "address" }, { "name": "data", "type": "bytes", "internalType": "bytes" } ], "outputs": [], "stateMutability": "payable" }, { "type": "function", "name": "whitelistRWA", "inputs": [ { "name": "rwa", "type": "address", "internalType": "address" }, { "name": "accepted", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "whitelistStablecoin", "inputs": [ { "name": "stablecoin", "type": "address", "internalType": "address" }, { "name": "accepted", "type": "bool", "internalType": "bool" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "function", "name": "withdrawStablecoin", "inputs": [ { "name": "to", "type": "address", "internalType": "address" }, { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [], "stateMutability": "nonpayable" }, { "type": "event", "name": "AutoLiquidityStablecoinIDSet", "inputs": [ { "name": "autoLiquidityStablecoinID", "type": "bytes32", "indexed": true, "internalType": "bytes32" } ], "anonymous": false }, { "type": "event", "name": "BlacklistUpdated", "inputs": [ { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "blacklisted", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "Initialized", "inputs": [ { "name": "version", "type": "uint64", "indexed": false, "internalType": "uint64" } ], "anonymous": false }, { "type": "event", "name": "IssuerAdminAdded", "inputs": [ { "name": "issuerAdmin", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "IssuerAdminRemoved", "inputs": [ { "name": "issuerAdmin", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "Paused", "inputs": [ { "name": "account", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RWAColdStorageAddressAdded", "inputs": [ { "name": "coldStorageAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RWAColdStorageAddressRemoved", "inputs": [ { "name": "coldStorageAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RWACustodyAddressSet", "inputs": [ { "name": "balanceSheetCustodyAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RWADiscountRateSet", "inputs": [ { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "indexed": true, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "RWAWhitelist", "inputs": [ { "name": "rwa", "type": "address", "indexed": true, "internalType": "address" }, { "name": "accepted", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "RedemptionFeeSet", "inputs": [ { "name": "rwaID", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "rate", "type": "uint256", "indexed": true, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "RoleAdminChanged", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "previousAdminRole", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "newAdminRole", "type": "bytes32", "indexed": true, "internalType": "bytes32" } ], "anonymous": false }, { "type": "event", "name": "RoleGranted", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "sender", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "RoleRevoked", "inputs": [ { "name": "role", "type": "bytes32", "indexed": true, "internalType": "bytes32" }, { "name": "account", "type": "address", "indexed": true, "internalType": "address" }, { "name": "sender", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "StablecoinAcceptanceFeeSet", "inputs": [ { "name": "stablecoin", "type": "address", "indexed": true, "internalType": "address" }, { "name": "rate", "type": "uint256", "indexed": true, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "StablecoinColdStorageAddressAdded", "inputs": [ { "name": "coldStorageAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "StablecoinColdStorageAddressRemoved", "inputs": [ { "name": "coldStorageAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "StablecoinCustodyAddressSet", "inputs": [ { "name": "stablecoinCustodyAddress", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "StablecoinRedemptionFeeSet", "inputs": [ { "name": "stablecoin", "type": "address", "indexed": true, "internalType": "address" }, { "name": "rate", "type": "uint256", "indexed": true, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "StablecoinWhitelist", "inputs": [ { "name": "stablecoin", "type": "address", "indexed": true, "internalType": "address" }, { "name": "accepted", "type": "bool", "indexed": false, "internalType": "bool" } ], "anonymous": false }, { "type": "event", "name": "StablecoinWithdrawn", "inputs": [ { "name": "to", "type": "address", "indexed": true, "internalType": "address" }, { "name": "amount", "type": "uint256", "indexed": false, "internalType": "uint256" } ], "anonymous": false }, { "type": "event", "name": "Unpaused", "inputs": [ { "name": "account", "type": "address", "indexed": false, "internalType": "address" } ], "anonymous": false }, { "type": "event", "name": "Upgraded", "inputs": [ { "name": "implementation", "type": "address", "indexed": true, "internalType": "address" } ], "anonymous": false }, { "type": "error", "name": "AccessControlBadConfirmation", "inputs": [] }, { "type": "error", "name": "AccessControlUnauthorizedAccount", "inputs": [ { "name": "account", "type": "address", "internalType": "address" }, { "name": "neededRole", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "AddressBlacklisted", "inputs": [ { "name": "account", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "AddressEmptyCode", "inputs": [ { "name": "target", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "AddressNotAllowed", "inputs": [] }, { "type": "error", "name": "CannotRemoveSelf", "inputs": [] }, { "type": "error", "name": "ERC1967InvalidImplementation", "inputs": [ { "name": "implementation", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "ERC1967NonPayable", "inputs": [] }, { "type": "error", "name": "EnforcedPause", "inputs": [] }, { "type": "error", "name": "ExpectedPause", "inputs": [] }, { "type": "error", "name": "FailedCall", "inputs": [] }, { "type": "error", "name": "InvalidInitialization", "inputs": [] }, { "type": "error", "name": "InvalidInitializationParams", "inputs": [] }, { "type": "error", "name": "InvalidIssuerAdmin", "inputs": [] }, { "type": "error", "name": "InvalidMultiliquidSwap", "inputs": [] }, { "type": "error", "name": "InvalidRate", "inputs": [] }, { "type": "error", "name": "InvalidStablecoinAsset", "inputs": [ { "name": "asset", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "InvalidStablecoinExchange", "inputs": [ { "name": "assetIn", "type": "address", "internalType": "address" }, { "name": "assetOut", "type": "address", "internalType": "address" }, { "name": "issuerStablecoin", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "NotInitializing", "inputs": [] }, { "type": "error", "name": "NotIssuerAdmin", "inputs": [] }, { "type": "error", "name": "RWANotWhitelisted", "inputs": [] }, { "type": "error", "name": "ReentrancyGuardReentrantCall", "inputs": [] }, { "type": "error", "name": "SafeERC20FailedOperation", "inputs": [ { "name": "token", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "SetRWACustodyAddress", "inputs": [] }, { "type": "error", "name": "SetStablecoinCustodyAddress", "inputs": [] }, { "type": "error", "name": "StablecoinNotWhitelisted", "inputs": [ { "name": "stablecoin", "type": "address", "internalType": "address" } ] }, { "type": "error", "name": "UUPSUnauthorizedCallContext", "inputs": [] }, { "type": "error", "name": "UUPSUnsupportedProxiableUUID", "inputs": [ { "name": "slot", "type": "bytes32", "internalType": "bytes32" } ] }, { "type": "error", "name": "ZeroAddressNotAllowed", "inputs": [] } ] ``` ## IPriceAdapter Common read interface implemented by Multiliquid RWA and stablecoin price adapters. ```json IPriceAdapter.json theme={null} [ { "type": "function", "name": "getPrice", "inputs": [], "outputs": [ { "name": "", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" } ] ``` ## IWhitelistAdapter Common compliance interface used to validate an asset transfer from one address to another for a specified amount. ```json IWhitelistAdapter.json theme={null} [ { "type": "function", "name": "isWhitelisted", "inputs": [ { "name": "from", "type": "address", "internalType": "address" }, { "name": "to", "type": "address", "internalType": "address" }, { "name": "amount", "type": "uint256", "internalType": "uint256" } ], "outputs": [ { "name": "whitelisted", "type": "bool", "internalType": "bool" } ], "stateMutability": "view" } ] ``` ## SDK ABI Exports The v0.2.0 TypeScript SDK exposes const-asserted ABIs optimized for integration: | Export | Surface | | --------------------------- | --------------------------------------------------------------- | | `multiliquidSwapAbi` | Swap, quote, authorization, protocol reads, events, and errors | | `stablecoinDelegateBaseAbi` | Delegate discovery and LP-administration methods | | `balanceSheetDelegateAbi` | Balance-sheet delegate settlement methods | | `yieldBearingDelegateAbi` | Yield calculations and accrual methods | | `treasuryDelegateAbi` | Treasury credit, rate, and yield-accounting reads | | `priceAdapterAbi` | Standard `getPrice()` interface | | `whitelistAdapterAbi` | Standard `isWhitelisted(from, to, amount)` interface | | `erc20Abi` | ERC-20 metadata, balances, allowances, approvals, and transfers | ```typescript theme={null} import { multiliquidSwapAbi, priceAdapterAbi, stablecoinDelegateBaseAbi, treasuryDelegateAbi, whitelistAdapterAbi, } from "@uniformlabs/multiliquid-evm-sdk"; ``` ## viem Example ```typescript theme={null} import { multiliquidSwapAbi, type SwapInputs, } from "@uniformlabs/multiliquid-evm-sdk"; const quote = await publicClient.readContract({ address: multiliquidSwapAddress, abi: multiliquidSwapAbi, functionName: "quoteSwap", args: [userAddress, input satisfies SwapInputs], }); ``` For route construction, delegated authorization, simulation, and auto-liquidity workflows, see the [Integration Guide](/evm/guides/integration). # MultiliquidSwap Contract Source: https://docs.multiliquid.xyz/evm/contracts/multiliquid-swap The route, pricing, authorization, and settlement orchestrator for Multiliquid on EVM ## Overview `MultiliquidSwap` is the central execution layer for Multiliquid on EVM. It prices and settles atomic exchanges across permissioned RWAs and stablecoins while coordinating LP delegates, RWA risk controls, price adapters, whitelist adapters, and protocol fee configuration. One `quoteSwap` function prices every route and one `swap` function settles single-leg or multi-leg transactions. Every asset is converted through 18-decimal USD values with deterministic exact-in and exact-out math. Users can transact directly or authorize operators through standing allowances and one-shot EIP-712 permits. Stablecoin delegates can use automatic prefund and sweep legs backed by a designated liquidity stablecoin. **Production contract**: `src/prod/MultiliquidSwap.sol` ## Architecture ```mermaid theme={null} flowchart LR U[User] --> S[MultiliquidSwap] O[Operator] --> S S --> P[Price Adapters] S --> W[Whitelist Adapters] S --> R[RWA Delegates] S --> D[Stablecoin Delegate] D --> C[LP Custody] S --> V[Protocol Fee Vault] ``` The swap contract owns routing, pricing, authorization, protocol-level controls, and atomic execution. Asset movement is delegated to the selected stablecoin LP domain. Optional RWA delegates apply stateful asset-specific controls, while whitelist adapters preflight recipient eligibility. The contract is upgradeable through UUPS and uses OpenZeppelin access control, pausing, and reentrancy protection. ## Unified Route Model Every requested leg uses the same structure: ```solidity theme={null} struct SwapInputs { uint256 routeId; bytes32 assetInID; bytes32 assetOutID; bytes32 stablecoinDelegateID; uint256 assetInAmt; uint256 assetOutAmt; } ``` `stablecoinDelegateID` selects the LP domain whose custody, whitelists, fee schedules, and settlement implementation apply to the leg. | Route ID | Direction | Amount mode | | -------: | --------------------------------- | ----------- | | `0` | Stablecoin → RWA | Exact-in | | `1` | Stablecoin → RWA | Exact-out | | `2` | RWA → stablecoin | Exact-in | | `3` | RWA → stablecoin | Exact-out | | `4` | RWA → RWA | Exact-in | | `5` | RWA → RWA | Exact-out | | `6` | Stablecoin → stablecoin | Exact-in | | `7` | Stablecoin → stablecoin | Exact-out | | `8` | Prefunded RWA → stablecoin | Exact-in | | `9` | Prefunded RWA → stablecoin | Exact-out | | `10` | Prefunded stablecoin → stablecoin | Exact-in | | `11` | Prefunded stablecoin → stablecoin | Exact-out | Even route IDs are exact-in and odd route IDs are exact-out. ### Amount Semantics | Mode | `assetInAmt` | `assetOutAmt` | | --------- | ------------------------ | ------------------------- | | Exact-in | Exact input spent | Minimum acceptable output | | Exact-out | Maximum acceptable input | Exact output received | The route configuration determines which stablecoin delegate is valid: * Stablecoin → RWA uses the input stablecoin ID. * RWA → stablecoin uses the output stablecoin ID. * RWA → RWA uses an accepted stablecoin LP domain selected by the caller. * Stablecoin → stablecoin can use either the input or output stablecoin delegate. * Prefunded routes follow the same delegate rule as their corresponding base route. ## Quoting `quoteSwap` prices one requested leg: ```solidity theme={null} function quoteSwap(address user, SwapInputs calldata inputs) external view returns (SwapResolved memory outputs); ``` ```solidity theme={null} struct SwapResolved { uint256 tokenInAmt; uint256 tokenOutAmt; uint256 issuerFeeAmt; uint256 spreadFeeAmt; uint256 lpFeeAmt; } ``` The quote validates the route, asset registrations, delegate selection, price availability, and stablecoin guardrails. It uses current fee and yield-accounting state without moving tokens, spending approvals, or mutating accrued interest. For exact-in routes, the quote uses `assetInAmt` and treats `assetOutAmt` only as an execution bound. For exact-out routes, it uses `assetOutAmt` and treats `assetInAmt` only as an execution bound. Quotes are prospective and do not require the user or custody accounts to hold the quoted assets. Applications can exercise the complete settlement path with an `eth_call` simulation before submission. ## Swap Execution All requested legs settle through one entrypoint: ```solidity theme={null} function swap( address user, address receiver, SwapInputs[] calldata inputs, ApprovalPermit calldata permit ) external; ``` * `user` owns the input assets and yield credits. * `msg.sender` is the operator submitting the transaction. * `receiver` receives every requested leg's output. * `inputs` is an ordered array of one or more bounded swap legs. * `permit` is optional EIP-712 authorization for delegated execution. Every transaction is atomic. Route validation, pricing, authorization, yield synchronization, token transfers, internally derived liquidity legs, allowance spending, and nonce changes all revert together if any leg fails. ### Multi-Leg Transactions The contract executes requested legs in array order up to the operator-configured maximum. Each leg is independently specified and priced. Outputs are not implicitly assigned as the next leg's input; applications can construct chained amounts off-chain and submit the resulting ordered batch. Multi-leg execution supports: * Portfolio rebalancing * Batch order settlement * Multi-route institutional workflows * Atomic execution around auto-liquidity legs ## User, Operator, and Receiver Separating the economic user, transaction operator, and output receiver enables relayers, institutional operators, smart-account workflows, scheduled execution, and settlement to a designated custody account. The contract supports three authorization modes: | Mode | Authorization | Receiver | | ------------------ | --------------------------------------------------------------- | ------------------------------------ | | Direct | `msg.sender == user` | Any nonzero, non-blacklisted address | | Standing allowance | Per user, operator, and input asset | Must equal `user` | | EIP-712 permit | Signed exact execution, receiver, operator, nonce, and deadline | Signed receiver | ERC-20 token allowance to the selected stablecoin delegate remains separate from protocol-level operator authorization. ### Standing Swap Allowances ```solidity theme={null} function adjustSwapInputAllowance( address operator, bytes32 assetInID, uint256 amount, bool increase ) external; ``` Allowances are stored as: ```solidity theme={null} swapInputAllowances[user][operator][assetInID] ``` The function adds or subtracts a delta. Exact-out swaps consume the resolved input rather than the submitted maximum. `type(uint256).max` acts as an infinite allowance and is not decremented. Standing allowances can only authorize a delegated swap when `receiver == user`, keeping the reusable permission tied to the user's own output account. ### EIP-712 One-Shot Permits The one-shot permit allows the user, operator, and receiver to be three different addresses for one signed execution. The user owns the inputs and signs the authorization, the operator submits the transaction, and the receiver receives the output. The signed `SwapInputs[]` can contain one requested leg or an ordered multi-leg batch. Batch authorization is supported because `swap` accepts an array, but the permit's defining purpose is one-shot authorization across separate user, operator, and receiver identities. ```solidity theme={null} struct ApprovalPermit { uint256 deadline; bytes signature; } ``` The `SwapBatchApproval` typed message binds: * User, operator, and receiver * The complete ordered `SwapInputs[]` * Every route, asset, delegate, amount, and execution bound * The user's current nonce * The permit deadline * Chain ID and verifying contract The domain name is `MultiliquidSwap` and the domain version is `2`. Signatures support standard ECDSA, compact EIP-2098, ERC-1271 contract accounts, and EIP-7702 delegated EOAs. One nonce authorizes the complete signed execution, whether single-leg or multi-leg. A reverted transaction rolls back the nonce with the rest of settlement. ## Pricing Engine Multiliquid uses deterministic NAV-based pricing rather than an AMM curve. The engine: 1. Normalizes input amounts to 18-decimal WAD precision. 2. Converts the input asset into a common USD-denominated route unit. 3. Applies the selected LP's fee schedule and protocol fee policy. 4. Applies yield-adjusted value where the input is yield-bearing. 5. Converts the remaining route value into output-token decimals. 6. Rounds conservatively for exact-in and exact-out guarantees. RWA prices come from `IPriceAdapter.getPrice()`. Stablecoins use an operator-configured WAD value and can additionally enforce an oracle deviation guardrail. ### Fee Outputs `SwapResolved` and the `Swap` event report three fee amounts: | Field | Meaning | | -------------- | --------------------------------------------------------------------------------------------------------------------------- | | `issuerFeeAmt` | Configured LP-funded protocol fee reported separately from the user's route value; the selected delegate handles settlement | | `spreadFeeAmt` | Protocol share of the LP spread calculated from discount, acceptance, and redemption settings | | `lpFeeAmt` | Explicit LP fee retained or minted for the selected delegate, such as a redemption or acceptance fee | The protocol can configure LP-funded fee rates per stablecoin, a global protocol take on LP spread, and fee exemptions for selected stablecoins. LP delegates configure their own RWA discounts, RWA redemption fees, stablecoin acceptance fees, and stablecoin redemption fees. Treasury's portfolio management fee belongs to the yield-bearing delegate and is applied to posted APY before user yield accrues. ## Auto-Liquidity A stablecoin delegate can designate an accepted stablecoin—typically a yield-bearing asset—through `autoLiquidityStablecoinIDs`. ### Prefund Routes `8`–`11` signal that a stablecoin-output swap may require liquidity immediately before the user leg. The contract: 1. Quotes the requested output requirement. 2. Builds a stablecoin-to-stablecoin exact-out leg from the configured auto-liquidity asset. 3. Executes that leg from the selected delegate's stablecoin custody account. 4. Executes the user's requested route. The derived LP leg uses the custody account's standing `swapInputAllowance` with `MultiliquidSwap` itself as operator. The user authorizes only the requested leg. ### Sweep Eligible stablecoin-input routes can sweep the stablecoin balance received by custody into the configured auto-liquidity asset after the requested leg. The sweep: * Uses the custody balance delta measured inside the transaction * Executes only when a standing LP allowance is present * Quotes and sets its own exact-in output bound * Settles atomically with the requested swap Every derived leg emits its own `Swap` event. ## Asset and Compliance Controls ### RWA Registration `setRWAAcceptance` records: * Acceptance status * Token address and decimals * Optional RWA delegate for stateful risk controls Each accepted RWA also uses a price adapter. An optional whitelist adapter can validate the actual custody sender, receiver, and transfer amount before an RWA leaves custody. ### Stablecoin Registration `setStablecoinAcceptance` records: * Acceptance status * Token address and decimals * Stablecoin delegate * Yield-bearing status A stablecoin with a delegate can participate in all applicable route families. A registration without a delegate can serve as the non-delegate side of a stablecoin-to-stablecoin route. ### Stablecoin Guardrails ```solidity theme={null} function configureStablecoinGuardrail( bytes32 stablecoinID, address priceAdapter, uint256 band, bool enabled ) external; ``` When enabled, the contract compares the stablecoin's configured USD value with its oracle price and requires the percentage deviation to remain within the WAD-scaled band. ### Blacklist The protocol blacklist applies once per batch to the user, operator, and receiver. Stablecoin delegates can apply an additional LP-controlled blacklist during settlement. ## Administration | Function | Purpose | Authority | | ------------------------------ | -------------------------------------- | -------------------- | | `setRWAAcceptance` | Register or remove an RWA | `OPERATOR_ROLE` | | `setStablecoinAcceptance` | Register or remove a stablecoin | `OPERATOR_ROLE` | | `setRwaPriceAdapter` | Configure RWA pricing | `OPERATOR_ROLE` | | `setRwaWhitelistAdapter` | Configure RWA eligibility checks | `OPERATOR_ROLE` | | `setStablecoinUSDValue` | Configure stablecoin route value | `OPERATOR_ROLE` | | `configureStablecoinGuardrail` | Configure stablecoin oracle protection | `OPERATOR_ROLE` | | `setIssuerPaidProtocolFeeRate` | Configure LP-funded protocol fee | `OPERATOR_ROLE` | | `setSpreadProtocolTakeRate` | Configure protocol share of LP spread | `OPERATOR_ROLE` | | `setProtocolFeeExempt` | Configure stablecoin fee exemption | `OPERATOR_ROLE` | | `setMaxSwapLegs` | Set the batch leg limit | `OPERATOR_ROLE` | | `setBlacklisted` | Update protocol blacklist | `BLACKLISTER_ROLE` | | `setMultiliquidVault` | Update protocol fee recipient | `DEFAULT_ADMIN_ROLE` | Fee schedule and auto-liquidity setters are callable only through the registered stablecoin delegate, which applies LP-admin authorization. ## Access Control | Role | Capabilities | | ---------------------- | --------------------------------------------------------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | Contract upgrades, role administration, and protocol fee vault | | `OPERATOR_ROLE` | Asset configuration, price and guardrail configuration, fee policy, batch limit, pause, and unpause | | `BLACKLISTER_ROLE` | Protocol blacklist management | | `EXTERNAL_PAUSER_ROLE` | Pause-only operational authority | ## Unified Swap Event Every requested and internally derived leg emits: ```solidity theme={null} event Swap( address indexed user, bytes32 indexed assetInID, bytes32 indexed assetOutID, address operator, address receiver, uint256 routeId, bytes32 stablecoinDelegateID, uint256 amountIn, uint256 amountOut, uint256 issuerFeeAmt, uint256 spreadFeeAmt, uint256 lpFeeAmt ); ``` The event contains enough information to reconstruct route direction, execution identity, selected LP domain, resolved amounts, and fee allocation for each leg. Learn how LP domains manage settlement, custody, fees, and liquidity # Price Adapters Source: https://docs.multiliquid.xyz/evm/contracts/price-adapters USD-denominated pricing and stablecoin guardrails for Multiliquid ## Overview Price adapters give Multiliquid a consistent USD price for assets with different pricing sources. Every adapter implements the same read-only interface and returns an 18-decimal WAD value, allowing `MultiliquidSwap` to use one pricing engine for every supported route. Registered RWA adapters provide the prices used to quote and execute swaps. Optional stablecoin adapters validate configured USD values against live oracle prices. ## Interface and units All price adapters implement `IPriceAdapter`: ```solidity theme={null} interface IPriceAdapter { /// @return price USD price in 18-decimal WAD format function getPrice() external view returns (uint256 price); } ``` The returned price is denominated in USD with 18 decimals: | USD price | WAD value | | --------- | --------------------------: | | \$1.00 | `1_000_000_000_000_000_000` | | \$1.05 | `1_050_000_000_000_000_000` | | \$0.99 | `990_000_000_000_000_000` | `MultiliquidSwap` rejects a zero price. Individual adapters may also validate source-specific conditions such as oracle freshness, completed rounds, and positive values. ## How pricing is used ```mermaid theme={null} flowchart LR Source["NAV contract, oracle, or
authorized price setter"] Adapter["IPriceAdapter.getPrice()
18-decimal USD WAD"] Swap["MultiliquidSwap
quoteSwap() / swap()"] Amounts["Resolved token amounts
and fee amounts"] Source --> Adapter --> Swap --> Amounts ``` For RWA routes, `MultiliquidSwap` reads the adapter registered for each RWA ID. For stablecoins, the protocol uses an administrator-configured USD value and can optionally compare it with a registered price adapter before accepting that value. ## Adapter catalog | Adapter | Price source | Normalization | Primary use | | ------------------------------ | ------------------------------- | ----------------------------------------------- | ------------------------------------------------ | | `ULTRAAdapter` | ULTRA manager exchange rate | Manager rate and basis-point denominator to WAD | ULTRA | | `USTBAdapter` | Chainlink-compatible feed | 6 decimals to WAD | USTB | | `FalconXUSDCPriceAdapter` | Pareto credit vault `priceAA()` | 6 decimals to WAD | AA\_FalconXUSDC | | `ChainlinkPriceAdapter` | Chainlink-compatible feed | Feed decimals to WAD | General oracle pricing and stablecoin guardrails | | `ChroniclePriceAdapter` | Chronicle oracle | Native WAD | General oracle pricing and stablecoin guardrails | | `DollarPeggedAdapter` | Constant | Returns exactly `1e18` | Assets priced at \$1.00 | | `ThirdPartySetterPriceAdapter` | Authorized onchain price setter | Price supplied in WAD | Published NAVs | ## Direct NAV adapters ### ULTRAAdapter `ULTRAAdapter` reads `lastSetMintExchangeRate()` and `BPS_DENOMINATOR()` from the ULTRA manager, then normalizes the result to 18 decimals: ```solidity theme={null} price = ultraManager.lastSetMintExchangeRate() * 1e18 / ultraManager.BPS_DENOMINATOR(); ``` The adapter rejects a zero manager address at deployment and a zero calculated price. ### USTBAdapter `USTBAdapter` reads the latest answer from a Chainlink-compatible feed and multiplies the 6-decimal answer by `1e12`. It rejects a zero feed address and non-positive answers. ### FalconXUSDCPriceAdapter `FalconXUSDCPriceAdapter` reads `priceAA()` from the Pareto credit vault for `AA_FalconXUSDC`. Because the vault reports the price in the 6-decimal USDC underlying unit, the adapter multiplies it by `1e12`. It rejects a zero vault address and a zero price. ## General oracle adapters ### ChainlinkPriceAdapter `ChainlinkPriceAdapter` supports feeds with up to 18 decimals and calculates its WAD multiplier from the feed's `decimals()` value at deployment. Each price read validates: * The answer is positive. * The update belongs to a completed round. * The update timestamp is present and is not in the future. * The update is no older than `MAX_AGE`. Addresses with `OPERATOR_ROLE` can update `MAX_AGE` to a nonzero value with `setMaxAge()`. The adapter emits `MaxAgeUpdated` whenever the threshold changes. ### ChroniclePriceAdapter `ChroniclePriceAdapter` reads `tryReadWithAge()` from a Chronicle oracle. Chronicle prices are already WAD-denominated, so the adapter returns the value without decimal scaling. Each price read requires a valid, nonzero value with a timestamp that is not in the future or older than `MAX_AGE`. Addresses with `OPERATOR_ROLE` can update the nonzero freshness threshold with `setMaxAge()`. ## Fixed and managed adapters ### DollarPeggedAdapter `DollarPeggedAdapter` always returns `1e18`, representing exactly \$1.00. It has no external dependencies or mutable state and is suitable for assets whose protocol price is fixed at one dollar. ### ThirdPartySetterPriceAdapter `ThirdPartySetterPriceAdapter` stores a WAD-denominated price published by an authorized account: * `DEFAULT_ADMIN_ROLE` manages price setters. * `PRICE_SETTER_ROLE` authorizes calls to `setPrice()`. * `getPrice()` returns the current value and reverts while that value is zero. * `PriceUpdated` records the new price and the account that submitted it. The production VBILL adapter uses this model for its published NAV. ## Stablecoin price guardrails A stablecoin registration includes an administrator-configured USD value. An optional price adapter and deviation threshold can add an oracle guardrail to that value. When a guardrail is enabled, `MultiliquidSwap` compares the configured value with the adapter's live WAD price. The configured value is accepted only when its absolute deviation from the oracle price is within the stablecoin's configured threshold. ```text theme={null} deviation = abs(configured USD value - oracle price) / configured USD value ``` This keeps the stablecoin value used for route math explicit while allowing onchain oracle validation. The same `IPriceAdapter` interface supports both Chainlink and Chronicle guardrails. ## Production asset assignments | Asset | Adapter implementation | | --------------- | ------------------------------ | | ULTRA | `ULTRAAdapter` | | WTGXX | `DollarPeggedAdapter` | | BENJI | `DollarPeggedAdapter` | | USTB | `USTBAdapter` | | VBILL | `ThirdPartySetterPriceAdapter` | | USCC | `ChainlinkPriceAdapter` | | AA\_FalconXUSDC | `FalconXUSDCPriceAdapter` | | USDC guardrail | `ChainlinkPriceAdapter` | See [Deployments](/evm/deployments) for the corresponding production addresses. ## Integration considerations * Treat every adapter price as an 18-decimal USD WAD, independent of the token's own decimals. * Use `quoteSwap()` to obtain the exact token and fee amounts produced by the protocol's registered adapters. * Monitor adapter-specific update events and source freshness where the adapter has mutable pricing or freshness parameters. * A successful price read validates pricing data; token inventory, custody balances, allowances, and recipient eligibility are enforced during settlement. Review the integration-facing changes introduced by the current protocol architecture. # RWA Delegate Contracts Source: https://docs.multiliquid.xyz/evm/contracts/rwa-delegate Optional stateful risk controls for Real World Asset integrations ## Overview RWA delegates add stateful, asset-specific risk controls to an accepted RWA. They are useful when an issuer wants Multiliquid settlement to enforce limits or operating rules beyond the token's own transfer restrictions. `MultiliquidSwap` records whether an RWA has additional risk controls. When enabled, it calls the registered delegate immediately before settlement. An RWA can be accepted without a delegate when token-native controls and whitelist adapters provide the required policy. Delegates can track volume, time windows, user state, or other protocol-specific limits. **Base contract**: `src/prod/RWADelegate.sol` ## RWA Controls in Context Multiliquid separates three complementary control layers: | Layer | Purpose | Execution model | | ------------------------- | --------------------------------------------------------------------------- | ------------------------------------- | | Token-native restrictions | Enforce rules inside the RWA token | Applied by the token during transfer | | Whitelist adapter | Preflight transfer eligibility for the custody sender, receiver, and amount | Read-only call from `MultiliquidSwap` | | RWA delegate | Apply stateful risk policy around RWA inflows and outflows | Stateful call from `MultiliquidSwap` | Stablecoin delegates separately decide whether their LP domain accepts a given RWA and execute the actual token movement. ## Core Interface ```solidity theme={null} interface IRWADelegate { function checkRWAIn( address to, uint256 amount ) external returns (bool success); function checkRWAOut( address from, uint256 amount ) external returns (bool success); } ``` Both methods are callable only by the registered `MultiliquidSwap` contract in the production base implementation. ### `checkRWAIn` Validates an RWA entering a recipient account: * Stablecoin → RWA calls it for the receiver. * RWA → RWA calls it for the output RWA and receiver. ```solidity theme={null} function checkRWAIn(address to, uint256 amount) external returns (bool success); ``` ### `checkRWAOut` Validates an RWA leaving its source account: * RWA → stablecoin calls it for the user. * RWA → RWA calls it for the input RWA and user. ```solidity theme={null} function checkRWAOut(address from, uint256 amount) external returns (bool success); ``` Returning `false` causes `MultiliquidSwap` to revert with `RWAValidityCheckFailed`. Concrete delegates can also revert with a more specific custom error. ## Base Contract `RWADelegate` provides: * UUPS upgradeability * Enumerable role management * Independent issuer-admin membership * Reentrancy protection * Issuer and protocol pause paths * Registered RWA ID, token address, and `MultiliquidSwap` address * Default pass-through implementations of `checkRWAIn` and `checkRWAOut` Concrete RWA delegates override one or both checks with the issuer's desired stateful policy. ## ULTRA Delegate The production ULTRA delegate enforces a per-address daily volume limit in both directions. ### Volume Window * The same global `dailyVolumeLimit` applies to every address. * Each address has its own accumulated volume. * Both RWA inflows and outflows add to the address's current total. * The accounting window resets daily at 06:00 UTC, corresponding to 14:00 Singapore time. * A zero limit disables swap volume through the delegate. ```solidity theme={null} struct UserVolumeData { uint256 volume; uint32 lastResetDay; uint96 reserved; } mapping(address => UserVolumeData) public userVolumes; uint256 public dailyVolumeLimit; ``` Before accepting a movement, the delegate computes: ```solidity theme={null} uint256 newVolume = userData.volume + amount; if (newVolume > dailyVolumeLimit) { revert DailyVolumeLimitExceeded( user, amount, userData.volume, dailyVolumeLimit ); } ``` The update is atomic with the swap, so reverted settlement does not consume volume. ### Reads and Administration ```solidity theme={null} function getUserVolume(address user) external view returns ( uint256 volume, uint256 limit, uint256 resetTime ); function setDailyVolumeLimit(uint256 newLimit) external; ``` `getUserVolume` reports the effective volume for the current window, the configured limit, and the next reset timestamp. Issuer admins set the global limit. ### Event ```solidity theme={null} event DailyVolumeLimitUpdated( uint256 oldLimit, uint256 newLimit ); ``` ## Registration An operator registers an RWA and its optional delegate through `MultiliquidSwap`: ```solidity theme={null} multiliquidSwap.setRWAAcceptance( rwaID, rwaDelegateAddress, rwaTokenAddress, true ); multiliquidSwap.setRwaPriceAdapter( rwaID, priceAdapterAddress ); multiliquidSwap.setRwaWhitelistAdapter( rwaID, whitelistAdapterAddress ); ``` Passing `address(0)` as the RWA delegate disables the stateful delegate layer. Passing `address(0)` as the whitelist adapter disables the protocol-level whitelist preflight. The RWA token's native transfer behavior continues to apply in either case. `setRWAAcceptance` reads and stores the token's decimals and supports tokens with up to 18 decimals. ## Initialization Concrete delegates initialize: ```solidity theme={null} function initialize( address multiliquidAdmin, address initialIssuerAdmin, address multiliquidSwap, bytes32 rwaID, address rwaAddress ) external; ``` The Multiliquid admin and issuer admin must be distinct, nonzero accounts. Initialization assigns: * Upgrade and role administration to `DEFAULT_ADMIN_ROLE` * Stateful check access to the `MULTILIQUID_SWAP_CONTRACT` role * Issuer operating authority to the independent issuer-admin set * Protocol pause authority to `PAUSE_ROLE` ## Pause Controls Issuer admins control the issuer path: ```solidity theme={null} function pause() external; function unpause() external; ``` Multiliquid's `PAUSE_ROLE` controls the protocol path: ```solidity theme={null} function pauseMultiliquid() external; function unpauseMultiliquid() external; ``` While paused, both risk-check methods stop, which prevents settlement through that RWA delegate. ## Access Control | Authority | Capabilities | | --------------------------- | ---------------------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | UUPS upgrades and OpenZeppelin role administration | | Issuer admin | RWA risk parameters, issuer-admin membership, pause, and unpause | | `MULTILIQUID_SWAP_CONTRACT` | `checkRWAIn` and `checkRWAOut` | | `PAUSE_ROLE` | Multiliquid protocol pause path | Learn how Multiliquid sources WAD-denominated asset prices # Stablecoin Delegate Contracts Source: https://docs.multiliquid.xyz/evm/contracts/stablecoin-delegate LP-controlled settlement domains for custody, fees, compliance, and liquidity ## Overview Stablecoin delegates connect `MultiliquidSwap` to a specific Liquidity Provider (LP). Each delegate defines how assets move, which collateral it accepts, where reserves are held, how LP fees are configured, and which administrative controls apply. The selected `stablecoinDelegateID` gives every route a clear LP domain. That domain owns the settlement policy even when the route exchanges two RWAs or two stablecoins. LP admins manage accepted assets, fee schedules, custody, liquidity, and delegate-level controls. Delegates move or mint assets only when called by the registered `MultiliquidSwap` contract. Hot custody and informational cold-storage addresses are directly readable on-chain. Delegates can designate an auto-liquidity stablecoin for prefund and sweep workflows. **Base contract**: `src/prod/StablecoinDelegateBase.sol` ## Delegate Architecture `MultiliquidSwap` calculates resolved amounts and calls one of four settlement methods. The delegate then performs the token-specific transfer, mint, or burn operations. ### Production Delegate Families | Family | Settlement model | | ---------------------- | ----------------------------------------------------------------------------- | | Balance sheet | Transfers existing inventory between users, custody, and the protocol vault | | Mint/burn | Mints the delegated stablecoin on issuance and burns it on redemption | | Yield-bearing Treasury | Mints and burns Treasury while tracking credits, rates, and yield multipliers | All families inherit the same custody, LP-admin, whitelist, fee, blacklist, pause, and upgrade framework. ## Settlement Interface ```solidity theme={null} struct DelegateParams { address user; address receiver; address vault; uint256 protocolFeeAmt; uint256 lpFeeAmt; address assetInAddress; uint256 assetInAmount; address assetOutAddress; uint256 assetOutAmount; } ``` | Function | Route family | Delegate responsibility | | --------------------- | ----------------------- | ----------------------------------------------------------------------------- | | `deployStablecoin` | RWA → stablecoin | Receive the RWA and deliver the configured stablecoin | | `receiveStablecoin` | Stablecoin → RWA | Receive or burn stablecoin and deliver the RWA | | `exchangeRWAs` | RWA → RWA | Receive one accepted RWA and deliver another | | `exchangeStablecoins` | Stablecoin → stablecoin | Exchange the delegated stablecoin against an accepted counterparty stablecoin | Each method is callable only by the role assigned to `MultiliquidSwap`, is non-reentrant, and respects delegate pause and blacklist state. ### User and Receiver `user` is the source of the input asset and `receiver` is the destination for the output asset. Delegates validate both identities and send output directly to the requested receiver. ### Protocol and LP Fees `protocolFeeAmt` contains the protocol amounts resolved by `MultiliquidSwap`. `lpFeeAmt` contains the explicit LP fee for the route. The concrete delegate determines whether those amounts are transferred from custody, taken from input, or minted according to the stablecoin's settlement model. ## Balance-Sheet Settlement `BalanceSheetStablecoinDelegate` uses pre-existing liquidity: * RWA inputs move from the user to `balanceSheetCustodyAddress`. * RWA outputs move from RWA custody to the receiver. * Stablecoin inputs move into `stablecoinCustodyAddress`. * Stablecoin outputs and applicable protocol fees move out of stablecoin custody. * RWA and stablecoin custody accounts approve the delegate as ERC-20 spender. This model supports stablecoins that do not expose mint and burn permissions to the delegate. ## Mint-and-Burn Settlement `MintBurnStablecoinDelegate` represents integrations that expose controlled mint and burn authority: * RWA → stablecoin routes mint stablecoin output to the receiver. * Stablecoin → RWA routes burn the user's delegated stablecoin. * RWA → RWA routes can mint protocol and LP fees without moving the delegated stablecoin through the user's wallet. * Stablecoin → stablecoin routes mint or burn the delegated stablecoin and custody the counterparty stablecoin. Concrete implementations can adapt the base behavior to a token's native mint and burn interface. ## Treasury Yield Delegate `TreasuryDelegate` combines stablecoin settlement with on-chain yield accounting. ### Credits and Yield * `credits[user]` tracks released backing credits. * New deposits enter a 24-hour withheld-credit queue. * `totalWithheldCredits[user]` provides the aggregate pending amount. * `yieldMultiplier[user]` converts released credits into effective redeemable value. * `lastAccrualDay[user]` records the user's synchronized rate day. RWA-backed deposits begin earning from their deposit timestamp. Stablecoin-backed deposits apply the configured earning-start rules. Withheld credits remain redeemable at face value and merge into released credits as they mature. ### Daily Rates Rate posters submit sequential gross APY values. The delegate subtracts the portfolio management fee and stores the resulting net APY and derived daily compounding rate. ```solidity theme={null} function postDailyRate(uint256 dayNumber, uint256 grossRate) external; function correctDailyRate(uint256 dayNumber, uint256 grossRate) external; ``` The delegate supports an optional maximum daily rate and a bounded fallback window for partial-day stablecoin withdrawal yield. ### Interest Accrual ```solidity theme={null} function accrueInterest( address user, uint256 maxDays, uint256 maxRecords ) external returns ( uint256 daysProcessed, uint256 recordsProcessed, bool daysComplete, bool recordsComplete ); ``` `maxDays` bounds posted-rate processing and `maxRecords` bounds withheld-credit merging. Both values must be nonzero. `batchAccrueInterest` synchronizes multiple users without explicit per-user limits. Swap settlement synchronizes the relevant user's Treasury state before final pricing. Read-only `quoteSwap` uses stored state without changing it. ## LP Administration LP admins are stored independently from OpenZeppelin role membership and can add or remove other LP admins. ### Asset Eligibility ```solidity theme={null} function whitelistRWA(address rwa, bool accepted) external; function whitelistStablecoin(address stablecoin, bool accepted) external; ``` The RWA whitelist determines which RWA tokens the LP domain will accept or deliver. The stablecoin whitelist determines which counterparty stablecoins can be exchanged against the delegated stablecoin. These delegate lists complement protocol-level asset acceptance, RWA recipient whitelist adapters, token-native compliance, and optional RWA delegates. ### Fee Configuration LP admins configure WAD-scaled rates through the delegate: ```solidity theme={null} function setRWADiscountRate(bytes32 rwaID, uint256 rate) external; function setRWARedemptionFee(bytes32 rwaID, uint256 rate) external; function setStablecoinAcceptanceFee(address stablecoin, uint256 rate) external; function setStablecoinRedemptionFee(address stablecoin, uint256 rate) external; ``` The delegate forwards these settings to its registered stablecoin domain in `MultiliquidSwap`. Rates must remain below `1e18`. ### Auto-Liquidity ```solidity theme={null} function setAutoLiquidityStablecoinID( bytes32 autoLiquidityStablecoinID ) external; ``` The selected asset is used by `MultiliquidSwap` for eligible prefund and sweep legs. Passing `bytes32(0)` disables automatic liquidity for the delegate. ### Custody ```solidity theme={null} function setRWACustodyAddress(address custodyAddress) external; function setStablecoinCustodyAddress(address custodyAddress) external; ``` Both hot custody addresses must be configured before settlement. They can point to separate accounts and are exposed independently on-chain. Delegates also maintain informational reserve disclosures: ```solidity theme={null} function getRWAColdStorageAddresses() external view returns (address[] memory); function getStablecoinColdStorageAddresses() external view returns (address[] memory); ``` LP admins can add and remove entries for both reserve categories. ### Delegate Controls LP admins can: * Apply an additional delegate-level blacklist * Pause and unpause LP settlement * Withdraw stablecoin held directly by the delegate * Update hot custody accounts * Manage LP-admin membership Multiliquid's `PAUSE_ROLE` has a separate `pauseMultiliquid` and `unpauseMultiliquid` path for protocol operations. ## Token Allowances Stablecoin delegates execute ERC-20 `transferFrom` calls and are the token spender for user and custody transfers. Typical integrations configure: * User allowance to the selected delegate for each input token * RWA custody allowance to the delegate for RWA output inventory * Stablecoin custody allowance to the delegate for stablecoin output and fee inventory Protocol-level standing swap allowances and EIP-712 permits authorize the operator; they do not replace ERC-20 token allowances. ## Access Control | Authority | Capabilities | | --------------------------- | -------------------------------------------------------------------------------------------- | | `DEFAULT_ADMIN_ROLE` | UUPS upgrades and OpenZeppelin role administration | | LP admin | Whitelists, fee schedules, custody, reserve disclosures, LP blacklist, and LP pause controls | | `MULTILIQUID_SWAP_CONTRACT` | Settlement entrypoints | | `PAUSE_ROLE` | Protocol pause and unpause path | | Treasury `RATE_POSTER_ROLE` | Sequential daily-rate posting | | Treasury `OPERATOR_ROLE` | Rate correction and yield operating parameters | ## Core Events The base delegate emits events for: * RWA and stablecoin whitelist updates * RWA discounts and redemption fees * Stablecoin acceptance and redemption fees * Auto-liquidity configuration * Hot custody changes * Cold-storage additions and removals * Delegate blacklist changes * LP-admin membership * Pause, unpause, and upgrades `TreasuryDelegate` additionally emits daily-rate, interest-accrual, credit, management-fee, and yield-multiplier events. Explore asset-specific stateful risk controls # Updates: v1 to v2 Source: https://docs.multiliquid.xyz/evm/contracts/v1-to-v2-changes Integration-facing changes in Multiliquid V2 ## Overview Multiliquid V2 keeps the protocol's NAV-based exchange model while consolidating the integration surface and adding delegated execution, atomic batching, and automatic liquidity management. This page collects the V1-to-V2 comparison in one place; the other Core Components pages describe the current protocol directly. One quote function and one swap function cover every exact-in and exact-out route. Standing allowances and EIP-712 permits support operators, relayers, and institutional execution. One signed transaction can execute multiple independently specified legs. LPs can opt into automatic prefunding and sweeping with a designated liquidity stablecoin. ## At a glance | Area | V1 | V2 | | ------------------ | --------------------------------------- | ----------------------------------------------------- | | Quote surface | Route-specific calculation functions | `quoteSwap(user, inputs)` | | Execution surface | Route-specific swap functions | `swap(user, receiver, inputs[], permit)` | | Routing | Function selection determined the route | `routeId` selects one of 12 route variants | | Identities | The caller was the user and receiver | User, operator, and receiver are explicit | | Operator approval | Direct execution | Standing input allowances or one-shot EIP-712 permits | | Transaction shape | One swap operation | Ordered, atomic multi-leg batches | | Protocol fees | User-paid volume tiers | LP-funded fee plus a protocol share of LP spread | | Liquidity workflow | Liquidity managed outside the swap call | Optional automatic prefund and sweep legs | | Events | Route-specific execution events | One `Swap` event for every requested or derived leg | ## Unified pricing and execution V1 exposed separate functions for each route direction and amount mode. Quote functions mirrored the execution functions, so integrations selected both a route-specific calculation method and its matching swap method. V2 represents every route with the same `SwapInputs` structure: ```solidity theme={null} struct SwapInputs { uint256 routeId; bytes32 assetInID; bytes32 assetOutID; bytes32 stablecoinDelegateID; uint256 assetInAmt; uint256 assetOutAmt; } ``` Integrations now use: ```solidity theme={null} function quoteSwap(address user, SwapInputs calldata inputs) external view returns (SwapResolved memory outputs); function swap( address user, address receiver, SwapInputs[] calldata inputs, ApprovalPermit calldata permit ) external; ``` The route ID encodes both direction and amount mode: | Route IDs | Direction | Amount modes | | ---------: | --------------------------------- | ------------------- | | `0`, `1` | Stablecoin → RWA | Exact-in, exact-out | | `2`, `3` | RWA → stablecoin | Exact-in, exact-out | | `4`, `5` | RWA → RWA | Exact-in, exact-out | | `6`, `7` | Stablecoin → stablecoin | Exact-in, exact-out | | `8`, `9` | Prefunded RWA → stablecoin | Exact-in, exact-out | | `10`, `11` | Prefunded stablecoin → stablecoin | Exact-in, exact-out | Even route IDs are exact-in and odd route IDs are exact-out. The unified pricing engine applies the same decimal normalization, NAV conversion, fee accounting, and conservative rounding rules across route families. ## User, operator, and receiver separation V1 used `msg.sender` as the economic user and output recipient. V2 names each role independently: * `user` owns the input assets, protocol permissions, and yield credits. * `operator` is `msg.sender`, the account that submits the transaction. * `receiver` receives the requested output assets. Direct swaps remain straightforward: the user submits the transaction and can choose a receiver. Delegated swaps allow a router, relayer, smart account, or institutional operator to execute within the scope authorized by the user. ## Fee model V1 charged the user a global protocol fee tier based on the size of an individual transaction. V2 separates protocol economics from LP pricing: | Fee component | Paid by | Purpose | | ----------------------- | --------------------------- | ----------------------------------------------------------------------------------------------- | | Treasury management fee | Yield generated by Treasury | The configured management rate is separated from the posted portfolio APY | | LP-funded protocol fee | Stablecoin LP domain | Configurable rate reported as `issuerFeeAmt`, separate from the user's route value | | Protocol spread fee | User route value | Protocol share of the spread created by the LP's discount and redemption or acceptance settings | | Explicit LP fee | Route-dependent | Redemption or acceptance fee reported as `lpFeeAmt` when the route applies it explicitly | `SwapResolved` and the unified `Swap` event expose the amounts as `issuerFeeAmt`, `spreadFeeAmt`, and `lpFeeAmt`. ### Worked RWA-to-stablecoin example Assume: * Gross RWA value: `$1,000` (`500` tokens at `$2.00`) * LP discount: `10%` * RWA redemption fee used to measure the LP spread: `20%` * Protocol share of that spread: `5%` * LP-funded protocol fee: `10 bps` The LP spread is: ```text theme={null} 1 / (1 - redemption fee) - (1 - discount) = 1 / (1 - 0.20) - (1 - 0.10) = 1.25 - 0.90 = 0.35 ``` The protocol spread rate is `5% × 0.35 = 1.75%`. Applied to the gross value, that is `$17.50`. | Component | Amount | | ---------------------------------------------- | ----------: | | Gross RWA value | `$1,000.00` | | 10% LP discount | `-$100.00` | | 1.75% protocol spread fee | `-$17.50` | | Stablecoin output to user | `$882.50` | | 10 bps LP-funded protocol fee | `$1.00` | | Total protocol fees | `$18.50` | | Aggregate stablecoin settlement from LP domain | `$901.00` | The LP-funded fee is reported separately and does not reduce the user's `$882.50` output. For a balance-sheet delegate, the LP domain settles the user output and both protocol fee amounts. Mint-and-burn delegates mint the same resolved amounts to their respective recipients. ## Durable swap allowances V2 adds protocol-level standing permissions: ```solidity theme={null} swapInputAllowances[user][operator][assetInID] ``` Users update them with: ```solidity theme={null} function adjustSwapInputAllowance( address operator, bytes32 assetInID, uint256 amount, bool increase ) external; ``` Key behavior: * Authorization is scoped to one user, operator, and input asset. * The allowance is spent by the resolved input amount, including on exact-out routes. * `type(uint256).max` acts as an infinite allowance. * A standing allowance can be used only when `receiver == user`. * ERC-20 approval to the relevant stablecoin delegate is still required for token movement. This model supports trusted routers, programmatic rebalancing, triggered orders, and LP auto-liquidity without granting a reusable right to redirect a user's outputs. ## One-shot EIP-712 permits V2 also supports “sign once, execute once” authorization with independent user, operator, and receiver addresses. The user owns the inputs and signs the permit, the operator submits the transaction, and the receiver receives the output. All three roles can use different addresses. The signed `SwapInputs[]` can contain one requested leg or an ordered multi-leg batch. Batch execution is supported, but identity separation is the permit's primary purpose. The permit binds: * The user, operator, and receiver * The full ordered `SwapInputs[]` array * Every route ID, asset ID, delegate ID, amount, and execution bound * The user's current nonce * The signature deadline * The chain ID and `MultiliquidSwap` verifying contract The EIP-712 domain name is `MultiliquidSwap` and its version is `2`. Signatures support standard ECDSA, compact EIP-2098, ERC-1271 contract accounts, and EIP-7702 delegated EOAs. A successful swap consumes one nonce for the complete signed execution. If any part of settlement reverts, the nonce change reverts with it. This authorization mode supports gas-sponsored flows, delayed execution, market-maker fills, and institutional signing policies without creating a durable protocol allowance. ## Ordered multi-leg swaps V2 accepts an ordered array of requested legs in one `swap` call. Every leg can use a different route, asset pair, LP delegate, and exact-in or exact-out mode. The complete batch is atomic and is limited by the protocol's configured maximum leg count. Legs do not implicitly feed their outputs into the next leg. Applications that want chained execution calculate the intermediate bounded amounts offchain and submit those legs explicitly. This structure supports portfolio rebalancing, multi-order execution, batch optimization, and signed workflows that must either settle completely or not at all. ## Automatic liquidity prefund and sweep V2 lets a stablecoin LP configure an `autoLiquidityStablecoinID`, typically selecting a yield-bearing stablecoin held by its custody account. ### Prefund Prefunded routes `8`–`11` express that the requested stablecoin-output leg can derive an internal stablecoin-to-stablecoin exact-out leg immediately before execution. The derived leg: 1. Calculates the stablecoin amount needed for the requested swap. 2. Uses the LP custody account as the LP user and receiver. 3. Converts the configured auto-liquidity stablecoin into the required output stablecoin. 4. Spends the LP's standing swap allowance to `MultiliquidSwap`. 5. Settles atomically with the user's requested leg. The prefund conversion is skipped when the configured auto-liquidity stablecoin is already the required output asset. ### Sweep After eligible stablecoin-input routes, the contract can measure stablecoin inventory received by the LP custody account and convert that balance into the configured auto-liquidity stablecoin. A sufficient standing allowance from the custody account opts into the sweep; without one, the requested user leg can still proceed without the derived sweep. Together, prefund and sweep let an LP hold working liquidity in a designated yield-bearing asset while making other accepted stablecoins available at execution time. ## Integration migration checklist 1. Replace route-specific quote calls with `quoteSwap(user, inputs)`. 2. Replace route-specific execution calls with `swap(user, receiver, inputs[], permit)`. 3. Map each direction and exact-in or exact-out mode to route IDs `0`–`11`. 4. Build exact-in bounds as an exact `assetInAmt` and minimum `assetOutAmt`; build exact-out bounds as a maximum `assetInAmt` and exact `assetOutAmt`. 5. Handle `issuerFeeAmt`, `spreadFeeAmt`, and `lpFeeAmt` in quotes and events. 6. Choose direct execution, a standing input allowance, or an EIP-712 one-shot permit. 7. Treat multi-leg inputs as an ordered atomic batch without implicit chaining. 8. Continue managing ERC-20 approvals separately from protocol operator authorization. 9. Index the unified `Swap` event for both requested and internally derived liquidity legs. Build quotes, approvals, permits, and swaps with the Multiliquid EVM SDK. # Deployments Source: https://docs.multiliquid.xyz/evm/deployments Deployed contract addresses across mainnet and testnet environments ## Overview This page lists the deployed contract addresses for the Multiliquid Protocol across all supported networks. Use these addresses to interact with the protocol contracts. ## Ethereum Mainnet **Network**: Ethereum Mainnet (Chain ID: 1) ### Core Protocol Contracts | Contract | Address | | :------------------------- | :------------------------------------------- | | MultiliquidSwap | `0xD66211cf522Ba23B59789ffF6524c1f03E13E20c` | | ULTRA Delegate | `0x95ce23e21561D9284e6af4e6293Bb4F35A0c2fd6` | | Uniform Labs USDC Delegate | `0x0630582db5a1949509f21B315677C40b9aEb7bDA` | | Treasury Yield Delegate | `0x1f2C73a2578B3d49843C9a40f473Ef6768f6d6a3` | | Treasury USDC Delegate | `0x8A982eDc1578394Ed03F8b24d3B78A924fCF3Abc` | | Metalayer USDC Delegate | `0xD49499321F771E18C5aA0D93152D7E2B20DA5b48` | | Treasury ERC-20 | `0x6f4E688a151dfA05B20Cb3151e6066d9fd5d7b32` | ### Price Adapters | Adapter | Address | Asset | | :---------------------------- | :------------------------------------------- | :-------------- | | ULTRAAdapter | `0xC105907F77693995583e1aE94C52d19Bad4111F6` | ULTRA | | BENJIAdapter | `0x04f27ab64370ccD64ef3e27aa13A1b2D479e0308` | BENJI | | WTGXXAdapter | `0x47f713d79A20b83CcFaf631FBaaf43A90Ec73238` | WTGXX | | USTBAdapter | `0x055473C83D4Af1D165a44D5AF4E8C95EB12f7E66` | USTB | | VBILLAdapter | `0x826873065A64cAe7b004B1becC5F0b80A00651b5` | VBILL | | USCC Adapter | `0x8F9545F4579C724B9945544beef81f3dF1334F4b` | USCC | | AA\_FalconXUSDC Price Adapter | `0xb653B9F44Eb103692544EEE8E8cEF6a5529fe8c0` | AA\_FalconXUSDC | ### Stablecoin Price Guardrails | Registration | Adapter | | :---------------- | :------------------------------------------- | | Uniform Labs USDC | `0xe3997b805a0bD5ff6f2D44dfC35BD99730FF5325` | | Metalayer USDC | `0xe3997b805a0bD5ff6f2D44dfC35BD99730FF5325` | | Treasury USDC | `0xe3997b805a0bD5ff6f2D44dfC35BD99730FF5325` | The three USDC registrations share the same USDC price adapter. Treasury Yield does not use a stablecoin price guardrail adapter. ### RWA Token Addresses | Asset | Address | Pricing/NAV Method | | :-------------- | :------------------------------------------- | :------------------------- | | ULTRA | `0x50293DD8889B931EB3441d2664dce8396640B419` | NAV accrues to token | | USTB | `0x43415eB6ff9DB7E26A15b704e7A3eDCe97d31C4e` | NAV accrues to token | | BENJI | `0x3DDc84940Ab509C11B20B76B466933f40b750dc9` | Target \$1.00 NAV | | WTGXX | `0x1feCF3d9d4Fee7f2c02917A66028a48C6706c179` | Target \$1.00 NAV | | VBILL | `0x2255718832bC9fD3bE1CaF75084F4803DA14FF01` | NAV accrues to token | | USCC | `0x14d60E7FDC0D71d8611742720E4C50E7a974020c` | Chainlink price adapter | | AA\_FalconXUSDC | `0xC26A6Fa2C37b38E549a4a1807543801Db684f99C` | Credit-vault price adapter | ### Stablecoin Addresses | Asset | Address | | :------------- | :------------------------------------------- | | USDC | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | Treasury Yield | `0x6f4E688a151dfA05B20Cb3151e6066d9fd5d7b32` | Uniform Labs USDC, Metalayer USDC, and Treasury USDC are separate protocol registrations of the same USDC ERC-20. ### Asset IDs Asset IDs (`bytes32`) are used for all contract interactions with MultiliquidSwap. **RWA Assets**: | Asset | Asset ID | | :-------------- | :------------------------------------------------------------------- | | ULTRA | `0x2e6e8e8a8e57b239de932d023d4b828ead7c5efab47641f4a9e63fe75a892fcd` | | WTGXX | `0xd6f4e7d1065d311484763d3eff16f9b2fb4ddd23fc099d4210f64a09db73fc21` | | BENJI | `0x509a71b6090f9efb1ef94c245f59c388968bbaf7c535a026bb5123c821bc547c` | | USTB | `0x6f640388b2d2075b1c9f2f195fa3d1e4b26167f5e25357294d44c4b7c877da68` | | VBILL | `0x380987a047d6c65f063ded9d8518bb3941db5b3c082f4a3c09e1049b66859ddf` | | USCC | `0x9cae6845cba75e8f8e01cfc92ea7b3fe83810159b1a8f258540508ecca5edb29` | | AA\_FalconXUSDC | `0xd9a6f94ca343fe1aa384ba88ba924cd8d23619123a6dede0407410525b5b8762` | **Stablecoin Registrations**: | Asset | Asset ID | | :---------------- | :------------------------------------------------------------------- | | Uniform Labs USDC | `0x58e27f2731863499e6a23a1662ebeed9041d6b40996ea9ca7f4ebc54201823b4` | | Treasury Yield | `0x780a0ed07e618523cabd294ee648537f4d185a074f97ecd50188d7f394e1e927` | | Metalayer USDC | `0x2f5c040b301a61f7770bc3ba613522e8d8d60a9f2cecf6721d3870fe6627de8b` | | Treasury USDC | `0xfd07a2f44bab2d27f5af4a5a579abfc543010ce2f97597a5e0d9e13be6b1fb26` | *** ## Ethereum Sepolia (Testnet) **Network**: Ethereum Sepolia (Chain ID: 11155111) **Status**: Active Testing **Core Protocol Contracts**: | Contract | Address | | :------------------------ | :------------------------------------------- | | MultiliquidSwap (Proxy) | `0x0cc2ff62B7d5f3584045e12490eA9f5c06Fce43F` | | MultiliquidSwap (Impl) | `0x51A69a70f247968DB4144d325519aCAEe0287480` | | USDC Delegate (Proxy) | `0x8De2F6a2A012a4e5A7C73e8ADA7141B0F380B4B0` | | USDC Delegate (Impl) | `0x5A5b80EBb36440998763B92F0f603593603B2142` | | Treasury (Proxy) | `0x072Db736308c58465e9FB0961327bFd4bF28E809` | | Treasury (Impl) | `0xFd69198Cb56A38453336F25916138AA176E0cB82` | | Treasury Delegate (Proxy) | `0x2Eb8EC371E1e76b6718CCD59567c9E158F46a905` | | Treasury Delegate (Impl) | `0x1Cff23E604f26fA862B3da873994b74c41ed5be7` | | Treasury Whitelister | `0x1C7a203FD77B594f5596bbd518A39783CF14Ad2B` | **Test Token Addresses**: | Asset | Address | Type | | :--------------------------- | :------------------------------------------- | :-------------- | | Mock USDC | `0x092BB220f7Ec652EB871D395D5f957F5f06E1e2D` | Test Stablecoin | | Mock RWA (with whitelist) | `0x1b2F0A294a3B8162FB4cB5356c689d296B750609` | Test RWA Token | | Mock RWA (without whitelist) | `0x063EE9692eB3E4328a279961922d2e41CB9DAAA8` | Test RWA Token | **Price Adapters**: | Adapter | Address | | :--------------------------- | :------------------------------------------- | | Mock RWA (with whitelist) | `0x9Be448606d4970025c20D455fC5e701D2706A20d` | | Mock RWA (without whitelist) | `0x2b71DFD87e13B97128B20338Cf6Cc64Aa1c5EAC0` | **Faucets**: * Sepolia ETH: [https://faucet.quicknode.com/ethereum/sepolia](https://faucet.quicknode.com/ethereum/sepolia) * Test Tokens: Mint directly using Etherscan's write contract interface *** ## Block Explorers | Network | Explorer | | :--------------- | :------------------------------------------------------------- | | Ethereum Mainnet | [https://etherscan.io/](https://etherscan.io/) | | Ethereum Sepolia | [https://sepolia.etherscan.io/](https://sepolia.etherscan.io/) | *** ## Contract Verification All deployed contracts are verified on their respective block explorers. To verify: 1. Navigate to the contract address on the block explorer 2. Click the "Contract" tab 3. Verify the "Contract Source Code" section shows a green checkmark 4. Review the source code and ABI *** ## Additional RWA Information ### ULTRA (Delta Wellington Ultra Short Treasury On-Chain Fund) * **Token**: `0x50293DD8889B931EB3441d2664dce8396640B419` * **Manager**: `0x9056777AD890ECe386D646a5c698a9A6a779000B` * **NAV Method**: `lastSetMintExchangeRate()` — NAV accrues to token over time ### WTGXX (WisdomTree US Dollar Digital Fund) * **Ethereum**: `0x1feCF3d9d4Fee7f2c02917A66028a48C6706c179` * **Arbitrum**: `0xFEb26F0943C3885B2CB85A9F933975356c81C33d` * **Avalanche**: `0x870FD36B3bf7f5abeEEa2C8D4abdF1dc4E33109d` * **Base**: `0x5096b85Ed11798fDdCB8b5CB27C399c04689c435` * **Optimism**: `0x870FD36B3bf7f5abeEEa2C8D4abdF1dc4E33109d` * **NAV**: Target \$1.00 ### BENJI (Franklin OnChain US Government Money Fund) * **Ethereum**: `0x3DDc84940Ab509C11B20B76B466933f40b750dc9` * **NAV Method**: `lastKnownPrice()` — Returns 1e18 (\$1.00) * **Resources**: [https://digitalassets.franklintempleton.com/benji/benji-contracts/](https://digitalassets.franklintempleton.com/benji/benji-contracts/) ### USTB (Superstate Short Duration US Government Securities Fund) * **Token**: `0x43415eB6ff9DB7E26A15b704e7A3eDCe97d31C4e` * **NAV Method**: `getChainlinkPrice()` — NAV accrues to token ### VBILL (VanEck Treasury Fund) * **Token**: `0x2255718832bC9fD3bE1CaF75084F4803DA14FF01` ### USCC * **Token**: `0x14d60E7FDC0D71d8611742720E4C50E7a974020c` * **Price Adapter**: `0x8F9545F4579C724B9945544beef81f3dF1334F4b` ### AA\_FalconXUSDC * **Token**: `0xC26A6Fa2C37b38E549a4a1807543801Db684f99C` * **Price Adapter**: `0xb653B9F44Eb103692544EEE8E8cEF6a5529fe8c0` *** This page will be updated as contracts are deployed to additional networks. Bookmark this page for the latest deployment information. Learn how to integrate with deployed contracts # Auto-Sweep Source: https://docs.multiliquid.xyz/evm/guides/auto-sweep Liquid Treasury auto-sweep configuration for an EVM Liquidity Provider Auto-Sweep allows a Liquidity Provider (LP) to hold working capital in Liquid Treasury while `MultiliquidSwap` coordinates settlement conversions at execution time. The feature has two complementary operations: * **Prefund** runs before a user-requested stablecoin-output swap. It redeems the LP's Liquid Treasury token (`$TSY`) into the stablecoin required for settlement. * **Sweep** runs after an eligible stablecoin-input swap. It deposits the stablecoin balance received by the LP vault during that swap into Liquid Treasury. Both operations are internal LP-owned swap legs. The LP does not add them to the user's calldata or permit. The user authorizes a **user-requested leg**, and `MultiliquidSwap` derives any **LP prefund or sweep leg** from current execution state using the LP vault's separate permissions. ## How It Works ```mermaid theme={null} sequenceDiagram actor User actor Operator as Execution Operator participant Swap as MultiliquidSwap participant LP as LP Vault participant Treasury as Treasury Delegate participant Requested as User-Route Delegate User-->>Operator: Authorize user-requested route Operator->>Swap: Submit user-requested leg opt User requested prefunded route 8–11 Swap->>Treasury: Derive exact-out Liquid Treasury redemption Treasury->>LP: Supply required stablecoin inventory end Swap->>Requested: Execute user-requested leg opt Eligible user input and LP sweep allowance Swap->>LP: Measure received balance delta Swap->>Treasury: Deposit delta into Liquid Treasury Treasury->>LP: Return $TSY and Liquid Treasury credits end Swap-->>Operator: Emit one Swap event per executed leg ``` ### Prefund Prefund is explicit in the **user-requested route**. It is not a separate route submitted by the LP: 1. The user-facing integrator selects [route `8`, `9`, `10`, or `11`](/evm/guides/integration#route-model), and the execution operator submits it for the user. 2. `MultiliquidSwap` quotes that user-requested leg at execution time. 3. The contract derives an LP-owned stablecoin-to-stablecoin exact-out leg from Liquid Treasury into the stablecoin needed by the user-requested leg. 4. The LP vault is the user and receiver of the derived leg; `MultiliquidSwap` is its operator. 5. The derived LP prefund and user-requested leg settle atomically. The contract skips a no-op prefund when Liquid Treasury already supplies the required inventory. Otherwise, a user-requested prefunded route requires sufficient `$TSY` balance, Liquid Treasury credits, token approval, protocol allowance, and Treasury Delegate settlement capacity. ### Sweep Sweep is automatic on eligible user-requested routes once the LP vault has opted in for the user's input asset: 1. Before the user-requested leg, the contract reads the LP vault's input-stablecoin balance. 2. The user-requested leg settles into the LP vault. 3. The contract reads the balance again and uses only the positive delta as the sweep input. 4. A zero standing protocol allowance from the LP vault skips the sweep. 5. A positive allowance causes the contract to derive an LP-owned exact-in deposit into Liquid Treasury. The sweep does not consume stablecoin inventory that was already in the LP vault before the user-requested leg. Its minimum `$TSY` output is quoted internally at execution time, and the derived LP leg settles atomically with the user-requested swap. Liquid Treasury deposits create `$TSY` tokens and Treasury Delegate credits for the LP vault. The [Liquid Treasury application page](/applications/liquid-treasury#yield-bearing-integration-treasury-delegate) describes token and credit accounting. ## Route Behavior This table starts with the route requested and authorized by the **user**. The last two columns show whether `MultiliquidSwap` can add a separate Liquid Treasury leg owned by the LP. | User-requested route | IDs | Derived Liquid Treasury prefund | Derived Liquid Treasury sweep | | --------------------------------- | ---------: | ------------------------------- | ----------------------------- | | Stablecoin → RWA | `0`, `1` | No | Eligible | | RWA → stablecoin | `2`, `3` | No | No | | RWA → RWA | `4`, `5` | No | No | | Stablecoin → stablecoin | `6`, `7` | No | Eligible | | Prefunded RWA → stablecoin | `8`, `9` | Yes | No | | Prefunded stablecoin → stablecoin | `10`, `11` | Yes | Eligible | “Eligible” does not mean the user controls the LP sweep. A sweep executes only when the LP has configured Liquid Treasury and the LP vault has granted a sufficient standing allowance for the user-requested input asset. Prefund may be skipped when the derived conversion would be an identity operation. The contract reads Auto-Sweep configuration from the user-requested leg's `stablecoinDelegateID`. For stablecoin-to-stablecoin routes, the integrator's input-side or output-side delegate selection therefore determines which LP domain, configuration, and custody account apply. The LP provides integration partners with the stablecoin delegate ID that identifies the LP domain. ## Who Controls Each Part Liquid Treasury Auto-Sweep is controlled at three levels: | Control | Owner | Effect | | --------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------- | | Liquid Treasury destination | The LP admin | Sets `TSY_YIELD` for the LP delegate; zero disables all automatic LP legs | | Standing input allowance | The LP vault | Enables derived spending from the LP vault for one input asset, such as USDC | | User-requested route ID | User-facing integrator + user | Routes `8`–`11` ask the contract to derive prefund; the user authorizes this route selection | This separation supports several operating modes: | Mode | LP configuration | Required user-facing integration behavior | | ----------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Disabled | The configured destination is `bytes32(0)` | The integrator does not submit prefunded routes; they revert while disabled | | Sweep only | Liquid Treasury is configured and only selected sweep inputs are authorized | The integrator submits base routes `0`–`7`; eligible user inputs can trigger sweep | | Prefund only | Liquid Treasury is configured, `$TSY` is authorized, and sweep-input allowances remain zero | The integrator selects prefunded routes `8`–`11` where LP inventory is needed | | Prefund and sweep | Liquid Treasury, `$TSY` prefund inventory, and selected sweep inputs are authorized | The integrator selects prefunded routes where needed; base routes remain available | Standing allowances can be bounded and replenished as they are spent, or set to `type(uint256).max`. A maximum allowance is treated as unlimited and is not decremented by successful swaps. ## LP Enablement Steps This section assumes the LP stablecoin delegate is initialized, its custody address is configured, and it accepts Liquid Treasury. The examples assume an initialized `ml` client from the [EVM Integration Guide](/evm/guides/integration). Narrative references use LP terminology; `ml.issuerAdmin` remains the exact SDK module name for the delegate's on-chain admin interface. The LP admin sets `TSY_YIELD` as the `autoLiquidityStablecoinID` for the existing LP stablecoin delegate. The LP vault approves the Treasury Delegate to pull USDC for Liquid Treasury sweeps. Approval for `$TSY` is required only when the LP also uses prefunded JIT redemption. The LP vault authorizes `MultiliquidSwap` to spend USDC for derived Liquid Treasury sweeps. A separate `$TSY` allowance enables optional prefund. The LP operations team confirms the configured Liquid Treasury ID and previews an eligible sweep. The execution operator simulates the final user-requested transaction. ### Configuration Values ```typescript theme={null} import { mainnet } from "@uniformlabs/multiliquid-evm-sdk"; const lpStablecoinID = mainnet.assetIds.stablecoin.USDC; const sweepInputStablecoinID = mainnet.assetIds.stablecoin.USDC; const liquidTreasuryID = mainnet.assetIds.stablecoin.TSY_YIELD; const [lpVault, sweepInputInfo, liquidTreasuryInfo, treasuryDelegate] = await Promise.all([ ml.delegates.getStablecoinCustodyAddress(lpStablecoinID), ml.assets.getStablecoinInfo(sweepInputStablecoinID), ml.assets.getStablecoinInfo(liquidTreasuryID), ml.delegates.getDelegateAddress(liquidTreasuryID), ]); ``` The example uses USDC as the sweep input and Liquid Treasury as the destination. A separate LP stablecoin registration can replace `lpStablecoinID` when the LP domain is not the Uniform Labs USDC registration. ### 1. LP Admin: Select Liquid Treasury The LP admin sets Liquid Treasury as the destination for the existing LP stablecoin delegate: ```typescript theme={null} await ml.issuerAdmin.setAutoLiquidityStablecoinID({ stablecoinID: lpStablecoinID, autoLiquidityStablecoinID: liquidTreasuryID, }); ``` The delegate forwards the setting to `MultiliquidSwap`. The transaction emits `AutoLiquidityStablecoinIDSet`. The setting applies only to user-requested routes whose `stablecoinDelegateID` selects this LP domain. ### 2. LP Vault: Approve the Treasury Delegate ERC-20 approval authorizes the Treasury Delegate to pull tokens from the LP vault during settlement. The wallet client below must sign as the LP vault. USDC approval enables the Liquid Treasury sweep: ```typescript theme={null} import { maxUint256 } from "viem"; import { erc20Abi } from "@uniformlabs/multiliquid-evm-sdk"; await lpVaultWalletClient.writeContract({ account: lpVaultWalletClient.account, address: sweepInputInfo.assetAddress, abi: erc20Abi, functionName: "approve", args: [treasuryDelegate, maxUint256], chain: lpVaultWalletClient.chain, }); ``` An LP that also uses prefunded JIT redemption grants a separate `$TSY` approval: ```typescript theme={null} await lpVaultWalletClient.writeContract({ account: lpVaultWalletClient.account, address: liquidTreasuryInfo.assetAddress, abi: erc20Abi, functionName: "approve", args: [treasuryDelegate, maxUint256], chain: lpVaultWalletClient.chain, }); ``` The Liquid Treasury administrator separately maintains the Treasury Delegate custody approvals and settlement inventory assumed by this guide. ### 3. LP Vault: Grant Standing Protocol Allowances `swapInputAllowance` is separate from ERC-20 approval. It authorizes `MultiliquidSwap` to insert and operate a derived LP leg. The LP creates an SDK client whose wallet signs as the LP vault: ```typescript theme={null} import { createMultiliquidClient } from "@uniformlabs/multiliquid-evm-sdk"; const mlAsLpVault = createMultiliquidClient({ deployment: ml.deployment, publicClient, walletClient: lpVaultWalletClient, }); ``` The helper below moves the current standing allowance to a chosen target: ```typescript theme={null} import { maxUint256, type Hex } from "viem"; const swapOperator = mlAsLpVault.deployment.addresses.multiliquidSwap; async function setAutoLegAllowance(assetInID: Hex, target: bigint) { const current = await mlAsLpVault.swap.getSwapInputAllowance( lpVault, swapOperator, assetInID, ); if (current === target) return; await mlAsLpVault.swap.adjustSwapInputAllowance({ operator: swapOperator, assetInID, amount: target > current ? target - current : current - target, increase: target > current, }); } // Required for USDC sweep into Liquid Treasury. await setAutoLegAllowance(sweepInputStablecoinID, maxUint256); // Optional: required only for prefunded JIT redemption from $TSY. await setAutoLegAllowance(liquidTreasuryID, maxUint256); ``` For the USDC allowance: * `0` leaves Liquid Treasury sweep disabled for USDC; the user-requested leg can continue without the sweep. * A sufficient positive allowance enables USDC sweep into Liquid Treasury. * A positive but insufficient allowance causes the complete user transaction to revert atomically. `type(uint256).max` is treated as unlimited and is not decremented by successful swaps. A bounded allowance is decremented as it is spent and must include enough headroom for execution-time amounts. ### 4. LP + Operator: Verify Enablement The LP operations team first confirms that the LP domain points to Liquid Treasury: ```typescript theme={null} const configuredDestination = await ml.swap.getAutoLiquidityStablecoinID(lpStablecoinID); if (configuredDestination !== liquidTreasuryID) { throw new Error("Liquid Treasury is not configured for this LP domain"); } ``` The LP operations team can then preview an eligible USDC-input route: ```typescript theme={null} import { ROUTE_IDS } from "@uniformlabs/multiliquid-evm-sdk"; const userRequestedInput = { routeId: ROUTE_IDS.STABLE_TO_RWA_EXACT_IN, assetInID: sweepInputStablecoinID, assetOutID: rwaID, stablecoinDelegateID: lpStablecoinID, assetInAmt: exactInput, assetOutAmt: minimumOutput, }; const preview = await ml.swap.getAutoLiquidityPreview({ user: userAddress, input: userRequestedInput, }); console.log("LP vault:", preview.lpVault); console.log("Liquid Treasury sweep:", preview.sweep); ``` The preview reports USDC sweep eligibility, the estimated input amount, the LP vault balance, and the standing protocol allowance. The final sweep amount remains unknown until execution because `MultiliquidSwap` measures the LP vault balance delta around the user-requested leg. The execution operator should simulate the final bounded transaction with the actual user, operator, receiver, and permit: ```typescript theme={null} const quote = await ml.quote.quoteSwap({ user: userAddress, input: userRequestedInput, simulate: true, operator: operatorAddress, receiver: receiverAddress, permit, }); console.log("Settlement simulation:", quote.simulation); ``` Simulation exercises current user funds, LP balances, ERC-20 approvals, standing allowances, prices, whitelists, and delegate settlement state. ## Change or Disable Auto-Sweep ### Disable One Sweep Input The LP vault sets that input asset's standing protocol allowance to zero using the `setAutoLegAllowance` helper. This disables Liquid Treasury sweep for that asset without affecting other sweep inputs or user-requested swaps. Revoking the protocol allowance does not revoke the token's ERC-20 approval. The LP vault revokes both controls if the Treasury Delegate should no longer be able to pull that asset from custody. ### Stop Using Prefund but Keep Sweep The LP cannot rewrite the user's route during execution. The LP coordinates with user-facing integrators to use base route IDs `2`, `3`, `6`, or `7` instead of prefunded routes `8`–`11`. This stops Liquid Treasury prefund requests while leaving Liquid Treasury sweep available on otherwise eligible stablecoin-input routes. ### Disable All Automatic Legs ```typescript theme={null} import { zeroHash } from "viem"; await ml.issuerAdmin.setAutoLiquidityStablecoinID({ stablecoinID: lpStablecoinID, autoLiquidityStablecoinID: zeroHash, }); ``` Setting zero disables Liquid Treasury prefund and sweep for the LP delegate. The LP admin should notify integration partners before disabling it: a user-requested prefunded route does not fall back to its base route and will revert while Auto-Sweep is disabled. Existing protocol allowances and ERC-20 approvals remain stored, so the LP vault removes them separately when retiring the configuration. ### Re-Enable Liquid Treasury The LP admin can restore `liquidTreasuryID` after confirming the Treasury Delegate whitelist, per-asset terms, custody inventory, token approvals, and protocol allowances. The restored configuration applies immediately to subsequent user-requested swaps that select the LP delegate. ## Monitoring For LP reconciliation, every user-requested and derived LP leg emits its own unified `Swap` event: * Derived Liquid Treasury prefund events use stablecoin-to-stablecoin exact-out route `7`. * Derived Liquid Treasury sweep events use stablecoin-to-stablecoin exact-in route `6`. * For derived LP events, the event's user and receiver are the LP vault, and its operator is `MultiliquidSwap`. * For the user-requested event, the user, receiver, operator, and requested route remain the user transaction's values. The REST API counts automatic Liquid Treasury prefund and sweep legs as realized swap legs in transaction history and volume analytics. The LP reporting pipeline should distinguish the user-requested leg from internal Liquid Treasury legs using the route together with the user, receiver, and operator; route `6` or `7` alone is not sufficient because users can also request those routes directly. Liquid Treasury token, credit, and yield mechanics Automatic-leg execution mechanics # Integration Guide Source: https://docs.multiliquid.xyz/evm/guides/integration Integrate with Multiliquid on EVM chains using the TypeScript SDK ## Integration Overview The official Multiliquid TypeScript SDK is built on [viem](https://viem.sh) and provides type-safe access to quoting, swap execution, delegated authorization, auto-liquidity previews, asset and delegate reads, LP administration, yield accounting, events, and multicall. The SDK follows the production `MultiliquidSwap` interface directly: * One route-tagged `quoteSwap(user, input)` view function quotes every route. * One `swap(user, receiver, inputs, permit)` function executes one or more requested legs. * Standing input allowances and EIP-712 one-shot permits authorize delegated execution. * Prefunded route IDs let the contract derive just-in-time LP liquidity legs internally. Understand route IDs, exact-in, exact-out, and delegate selection Price hypothetical swaps or test complete settlement Use standing allowances or one-time EIP-712 permits LP prefund, sweep, and JIT liquidity configuration workflows Read assets, prices, fees, eligibility, and protocol status Manage delegate configuration directly or through a multisig ## Installation ```bash theme={null} npm install @uniformlabs/multiliquid-evm-sdk@latest viem@^2.37.9 ``` Install [viem](https://viem.sh) directly because your application creates and passes viem clients to the SDK. Node.js `>=18` is required. ## Client Initialization Create a read-only client with a `publicClient`, or include a `walletClient` for swaps, approval changes, LP-admin writes, and yield accrual. ```typescript theme={null} import { createPublicClient, createWalletClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { mainnet as viemMainnet } from "viem/chains"; import { createMultiliquidClient, mainnet, } from "@uniformlabs/multiliquid-evm-sdk"; const transport = http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"); const publicClient = createPublicClient({ chain: viemMainnet, transport, }); const account = privateKeyToAccount("0x..."); const walletClient = createWalletClient({ account, chain: viemMainnet, transport, }); const ml = createMultiliquidClient({ deployment: mainnet, publicClient, walletClient, }); ``` The client exposes eight modules: | Module | Purpose | | ---------------- | --------------------------------------------------------------------------------- | | `ml.quote` | Single-leg quotes and optional execution simulation | | `ml.swap` | Swap execution, approvals, EIP-712 signing, calldata, and auto-liquidity previews | | `ml.assets` | Asset, price, fee, eligibility, and protocol-status reads | | `ml.delegates` | Delegate discovery, custody addresses, and whitelist reads | | `ml.issuerAdmin` | Delegate LP-admin reads, writes, and calldata builders | | `ml.yield` | Yield rates, credits, calculations, and accrual | | `ml.events` | Historical, live, and raw `Swap` event handling | | `ml.multicall` | Fully typed batching of arbitrary read calls | The selected deployment and underlying viem clients remain available as `ml.deployment`, `ml.publicClient`, and `ml.walletClient`. ### Live Deployment Metadata The static `mainnet` and `sepolia` presets ship with the package. To load the latest EVM deployment metadata from the Multiliquid API: ```typescript theme={null} import { createMultiliquidClient, loadEvmDeployment, } from "@uniformlabs/multiliquid-evm-sdk"; const deployment = await loadEvmDeployment({ chain: "mainnet" }); const ml = createMultiliquidClient({ deployment, publicClient, walletClient, }); ``` `loadEvmDeployment` validates the response, merges current contract addresses and assets into the selected chain preset, and rejects mismatched chain IDs or malformed addresses. ### Asset IDs The protocol identifies assets by `bytes32` IDs rather than token addresses: ```typescript theme={null} import { mainnet } from "@uniformlabs/multiliquid-evm-sdk"; mainnet.assetIds.rwa.ULTRA; mainnet.assetIds.rwa.WTGXX; mainnet.assetIds.rwa.BENJI; mainnet.assetIds.rwa.USTB; mainnet.assetIds.rwa.VBILL; mainnet.assetIds.stablecoin.USDC; mainnet.assetIds.stablecoin.TSY_YIELD; ``` Asset IDs are not derived from token addresses. Always use the IDs in the selected deployment config or the [Deployments](/evm/deployments) page. ## Route Model Every route uses the same `SwapInputs` structure: ```typescript theme={null} type SwapInputs = { routeId: bigint; assetInID: `0x${string}`; assetOutID: `0x${string}`; stablecoinDelegateID: `0x${string}`; assetInAmt: bigint; assetOutAmt: bigint; }; ``` The route ID selects the asset family and amount direction: | Route ID | Exported constant | Route | | -------: | -------------------------------------- | -------------------------------------------- | | `0` | `STABLE_TO_RWA_EXACT_IN` | Stablecoin → RWA, exact-in | | `1` | `STABLE_TO_RWA_EXACT_OUT` | Stablecoin → RWA, exact-out | | `2` | `RWA_TO_STABLE_EXACT_IN` | RWA → stablecoin, exact-in | | `3` | `RWA_TO_STABLE_EXACT_OUT` | RWA → stablecoin, exact-out | | `4` | `RWA_TO_RWA_EXACT_IN` | RWA → RWA, exact-in | | `5` | `RWA_TO_RWA_EXACT_OUT` | RWA → RWA, exact-out | | `6` | `STABLE_TO_STABLE_EXACT_IN` | Stablecoin → stablecoin, exact-in | | `7` | `STABLE_TO_STABLE_EXACT_OUT` | Stablecoin → stablecoin, exact-out | | `8` | `PREFUNDED_RWA_TO_STABLE_EXACT_IN` | Prefunded RWA → stablecoin, exact-in | | `9` | `PREFUNDED_RWA_TO_STABLE_EXACT_OUT` | Prefunded RWA → stablecoin, exact-out | | `10` | `PREFUNDED_STABLE_TO_STABLE_EXACT_IN` | Prefunded stablecoin → stablecoin, exact-in | | `11` | `PREFUNDED_STABLE_TO_STABLE_EXACT_OUT` | Prefunded stablecoin → stablecoin, exact-out | Even route IDs are exact-in and odd route IDs are exact-out. ### Amount Semantics | Direction | Pricing amount | Execution bound | | --------- | --------------------------------- | ----------------------------------- | | Exact-in | `assetInAmt` is the exact input | `assetOutAmt` is the minimum output | | Exact-out | `assetOutAmt` is the exact output | `assetInAmt` is the maximum input | The view quote ignores the execution-bound field. Settlement recalculates the quote from current state and enforces that signed or submitted bound. ### Delegate Selection `stablecoinDelegateID` selects the stablecoin LP domain that provides custody, compliance, and fee configuration: * Stablecoin → RWA: use `assetInID`. * RWA → stablecoin: use `assetOutID`. * RWA → RWA: select the accepted stablecoin delegate used for the route. * Stablecoin → stablecoin: select either `assetInID` or `assetOutID`. The selected side determines which LP delegate executes the exchange. * Prefunded aliases use the same delegate rule as their base route family. For stablecoin-to-stablecoin routes, input-delegate and output-delegate forms are distinct signed instructions because `stablecoinDelegateID` is included in `SwapInputs`. ## NAV-Based Pricing and Bounds Multiliquid pricing is based on NAV, configured fee schedules, decimal normalization, and—where applicable—yield state. It does not use AMM reserves or apply price impact based on trade size. The quote can still change between signing and execution when an oracle price, fee, guardrail, protocol exemption, or yield state changes. Integrations must deliberately set: * A minimum `assetOutAmt` for exact-in swaps. * A maximum `assetInAmt` for exact-out swaps. Using the quoted amount exactly provides zero tolerance and may cause a safe revert if state moves. The SDK never writes quote results back into execution bounds automatically. ## Quoting `ml.quote.quoteSwap` calls the single on-chain `quoteSwap(user, input)` view function for every route. ### Prospective Exact-In Quote ```typescript theme={null} import { ROUTE_IDS, type SwapInputs } from "@uniformlabs/multiliquid-evm-sdk"; const quoteInput: SwapInputs = { routeId: ROUTE_IDS.STABLE_TO_RWA_EXACT_IN, assetInID: mainnet.assetIds.stablecoin.USDC, assetOutID: mainnet.assetIds.rwa.ULTRA, stablecoinDelegateID: mainnet.assetIds.stablecoin.USDC, assetInAmt: 1_000_000n, // exact 1 USDC in assetOutAmt: 0n, // ignored by the view quote }; const quote = await ml.quote.quoteSwap({ user: account.address, input: quoteInput, }); console.log("Input:", quote.tokenInAmount); console.log("Output:", quote.tokenOutAmount); console.log("LP-funded protocol fee:", quote.issuerFee); console.log("Spread fee:", quote.spreadFee); console.log("LP fee:", quote.lpFee); ``` The result also contains `routeFamily`, `direction`, `timestamp`, and the normalized input. ### Prospective Exact-Out Quote ```typescript theme={null} const exactOutInput: SwapInputs = { routeId: ROUTE_IDS.RWA_TO_STABLE_EXACT_OUT, assetInID: mainnet.assetIds.rwa.ULTRA, assetOutID: mainnet.assetIds.stablecoin.USDC, stablecoinDelegateID: mainnet.assetIds.stablecoin.USDC, assetInAmt: 0n, // ignored by the view quote assetOutAmt: 1_000_000n, // exact 1 USDC out }; const exactOutQuote = await ml.quote.quoteSwap({ user: account.address, input: exactOutInput, }); console.log("Required RWA input:", exactOutQuote.tokenInAmount); ``` ### What a View Quote Validates The view quote validates route configuration and current pricing state, including: * Route and delegate selection * Asset acceptance * Price adapters and nonzero RWA prices * Stablecoin price guardrails * Current fee configuration * User-specific state needed to price yield-bearing inputs It does not check: * User token balances * ERC-20 allowances * Standing delegated swap allowances * RWA recipient whitelisting * LP or delegate custody inventory * Whether all settlement transfers can currently complete This means an unfunded address can request a prospective price. For yield-bearing inputs, use the address whose stored yield state you intend to model. ### Quote Versus Simulation Pass `simulate: true` to additionally run the real `swap(user, receiver, [input], permit)` path as an `eth_call`: ```typescript theme={null} const boundedInput: SwapInputs = { ...quoteInput, assetOutAmt: quote.tokenOutAmount, // choose your minimum output policy }; const checkedQuote = await ml.quote.quoteSwap({ user: account.address, input: boundedInput, simulate: true, }); if (checkedQuote.simulation?.success) { console.log("Estimated gas:", checkedQuote.simulation.gasEstimate); } else { console.log("Would revert:", checkedQuote.simulation?.error); } ``` | Mode | Purpose | | ------------------ | ---------------------------------------------------------------------------------------------- | | Default view quote | Prospective pricing without requiring user or custody balances | | `simulate: true` | Full settlement check against current balances, allowances, whitelists, custody, and liquidity | Simulation does not submit a transaction, but it does exercise current settlement conditions. An unfunded user can receive a view quote while the simulated swap correctly fails. For delegated simulation, provide the operator, receiver, and permit so `msg.sender` and authorization match the intended submission: ```typescript theme={null} const checkedDelegatedQuote = await ml.quote.quoteSwap({ user: userAddress, operator: operatorAddress, receiver: receiverAddress, input: boundedInput, permit, simulate: true, }); ``` ## Executing Swaps All routes use `ml.swap.swap`. It maps directly to `swap(user, receiver, inputs, permit)` and returns a transaction hash without waiting for a receipt. ### Direct Swap When the connected wallet is the user, `user` defaults to the wallet account and `receiver` defaults to the user: ```typescript theme={null} const txHash = await ml.swap.swap({ inputs: [boundedInput], }); const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, }); ``` Do not attach an EIP-712 swap permit to a direct user-submitted swap. The contract rejects a permit when the operator and user are the same address. ### Multi-Leg Batch A single transaction can execute an ordered array of requested legs: ```typescript theme={null} const inputs: SwapInputs[] = [ boundedInput, { routeId: ROUTE_IDS.RWA_TO_RWA_EXACT_IN, assetInID: mainnet.assetIds.rwa.ULTRA, assetOutID: mainnet.assetIds.rwa.USTB, stablecoinDelegateID: mainnet.assetIds.stablecoin.USDC, assetInAmt: quote.tokenOutAmount, assetOutAmt: minimumUstbOutput, }, ]; const txHash = await ml.swap.swap({ inputs }); ``` The entire batch is atomic. A failure in any requested or internally derived leg reverts every transfer, approval spend, and permit nonce change. ### ERC-20 Token Allowance For each requested leg, the selected stablecoin delegate executes token movements and is the ERC-20 spender for the user's input. The SDK exports `erc20Abi`, but does not automatically approve tokens or provide ERC-2612 helpers. ```typescript theme={null} import { erc20Abi } from "@uniformlabs/multiliquid-evm-sdk"; const inputToken = mainnet.addresses.tokens?.[boundedInput.assetInID]; if (inputToken === undefined) { throw new Error("Input token address is not configured"); } const delegate = await ml.delegates.getDelegateAddress( boundedInput.stablecoinDelegateID, ); await walletClient.writeContract({ account, chain: viemMainnet, address: inputToken, abi: erc20Abi, functionName: "approve", args: [delegate, boundedInput.assetInAmt], }); ``` For exact-out swaps, approve at least the chosen maximum `assetInAmt`. Token allowance is separate from Multiliquid's delegated swap authorization described below. ### Build Calldata Without Sending For smart-account batches, relayers, multisigs, or custom transaction submission: ```typescript theme={null} const calldata = ml.swap.buildSwapCalldata({ user: account.address, receiver: account.address, inputs: [boundedInput], }); ``` Omitted receivers resolve to the user and omitted permits resolve to an empty permit. ## Delegated Authorization Multiliquid supports two protocol-level mechanisms for an operator to submit swaps using another user's assets: | Authorization | Lifetime | Receiver rule | | ------------------------ | ----------------------------------------- | ------------------------------------------ | | Standing input allowance | Reusable per operator and input asset | Output must return to the user | | EIP-712 one-shot permit | One signed execution, nonce, and deadline | Any receiver explicitly signed by the user | Neither mechanism replaces the ERC-20 allowance granted to the selected stablecoin delegate. ### Standing Input Allowances The user adjusts an allowance keyed by `user`, `operator`, and `assetInID`: ```typescript theme={null} await userMl.swap.adjustSwapInputAllowance({ operator: operatorAddress, assetInID: mainnet.assetIds.stablecoin.USDC, amount: 100_000_000n, increase: true, }); const remaining = await userMl.swap.getSwapInputAllowance( userAddress, operatorAddress, mainnet.assetIds.stablecoin.USDC, ); ``` `adjustSwapInputAllowance` applies a delta: * `increase: true` adds `amount`. * `increase: false` subtracts `amount` and reverts on underflow. * `2^256 - 1` behaves as an infinite allowance and is not decremented. * Exact-out routes consume the resolved input, not the submitted maximum. The operator submits without a permit and must return output to the user: ```typescript theme={null} const txHash = await operatorMl.swap.swap({ user: userAddress, receiver: userAddress, inputs: [boundedInput], }); ``` Direct user swaps do not consume standing allowances. ### EIP-712 One-Shot Permits The primary purpose of an EIP-712 one-shot permit is to separate the three execution identities for one authorized transaction: * The **user** owns the input assets and signs the permit. * The **operator** submits the transaction and can be different from the user. * The **receiver** receives the output and can be different from both the user and operator. The permit can authorize a single requested leg or an ordered multi-leg batch because it binds the complete `SwapInputs[]` array. Batch support is available, but identity separation—not batching—is the reason to use the permit. For one-time authorization, the user signs the code-defined `SwapBatchApproval` typed message: ```typescript theme={null} const deadline = BigInt(Math.floor(Date.now() / 1000) + 15 * 60); const permit = await userMl.swap.signSwapApproval({ user: userAddress, operator: operatorAddress, receiver: receiverAddress, inputs: [boundedInput], deadline, }); ``` `signSwapApproval` reads the current nonce automatically when `nonce` is omitted. The operator must submit the exact same user, receiver, ordered input array, and permit: ```typescript theme={null} const txHash = await operatorMl.swap.swap({ user: userAddress, receiver: receiverAddress, inputs: [boundedInput], permit, }); ``` The signed message binds: * User, operator, and receiver * The complete ordered `SwapInputs[]` array * Every route ID, asset ID, delegate ID, amount, and execution bound * Current user nonce and deadline * Chain ID and verifying `MultiliquidSwap` address The EIP-712 domain is: ```typescript theme={null} const domain = { name: "MultiliquidSwap", version: "2", chainId, verifyingContract: multiliquidSwapAddress, }; ``` The domain version is the protocol's EIP-712 version (`"2"`), independent of the npm package version. Read the nonce and domain separator directly when needed: ```typescript theme={null} const nonce = await ml.swap.getSwapNonce(userAddress); const domainSeparator = await ml.swap.getDomainSeparator(); ``` For ERC-1271 smart accounts or external signing infrastructure, build the typed data without invoking the connected wallet: ```typescript theme={null} const typedData = ml.swap.buildSwapApprovalTypedData({ user: smartAccountAddress, operator: operatorAddress, receiver: receiverAddress, inputs: [boundedInput], nonce, deadline, }); ``` The contract accepts standard 65-byte ECDSA, compact EIP-2098 signatures, ERC-1271 contract signatures, and EIP-7702 delegated-account ECDSA signatures. `signSwapApproval` requires the connected signer to match `user`; use `buildSwapApprovalTypedData` for contract-account signing flows. Successful permit execution increments the user's nonce once for the signed transaction, whether it contains one requested leg or a multi-leg batch. A later settlement revert rolls the nonce change back, so the same permit can be retried before its deadline if no other permit consumed that nonce. ## Auto-Sweep Auto-Sweep lets an LP designate one stablecoin for working capital. Prefunded routes can convert that asset into required stablecoin inventory before a user swap, while eligible stablecoin-input routes can sweep the LP's newly received balance back into the designated asset afterward. Complete LP enablement, allowance, preview, JIT funding, and disablement workflow ## Querying Assets The `ml.assets` module provides read-only asset, price, fee, and eligibility information. ### Asset Information and Prices ```typescript theme={null} const ultraInfo = await ml.assets.getRWAInfo(mainnet.assetIds.rwa.ULTRA); const usdcInfo = await ml.assets.getStablecoinInfo( mainnet.assetIds.stablecoin.USDC, ); const allRWAs = await ml.assets.getAllRWAInfo(); const allStablecoins = await ml.assets.getAllStablecoinInfo(); const ultraPrice = await ml.assets.getRWAPrice(mainnet.assetIds.rwa.ULTRA); const usdcValue = await ml.assets.getStablecoinUSDValue( mainnet.assetIds.stablecoin.USDC, ); const allPrices = await ml.assets.getAllPrices(); ``` Prices and fee rates use WAD precision where `1e18` represents `1.0`. ### Fee Configuration ```typescript theme={null} const stablecoinID = mainnet.assetIds.stablecoin.USDC; const rwaID = mainnet.assetIds.rwa.ULTRA; const feeConfig = await ml.assets.getProtocolFeeConfig(stablecoinID); console.log("Exempt:", feeConfig.protocolFeeExempt); console.log( "Effective LP-funded protocol fee:", feeConfig.effectiveIssuerPaidProtocolFeeRate, ); console.log( "Effective spread take:", feeConfig.effectiveSpreadProtocolTakeRate, ); const discountRate = await ml.assets.getDiscountRate(stablecoinID, rwaID); const redemptionFee = await ml.assets.getRedemptionFee(stablecoinID, rwaID); const usdcAddress = (await ml.assets.getStablecoinInfo(stablecoinID)) .assetAddress; const acceptanceFee = await ml.assets.getStablecoinAcceptanceFee( mainnet.assetIds.stablecoin.TSY_YIELD, usdcAddress, ); const stableRedemptionFee = await ml.assets.getStablecoinRedemptionFee( mainnet.assetIds.stablecoin.TSY_YIELD, usdcAddress, ); ``` Individual reads are also available through `isProtocolFeeExempt`, `getIssuerPaidProtocolFeeRate`, and `getSpreadProtocolTakeRate`. ### Eligibility and Protocol Status ```typescript theme={null} const paused = await ml.assets.isPaused(); const blacklisted = await ml.assets.isBlacklisted(userAddress); const whitelisted = await ml.assets.isWhitelistedForRWA( mainnet.assetIds.rwa.ULTRA, custodyAddress, receiverAddress, rwaAmount, ); ``` `isWhitelistedForRWA` checks the configured RWA whitelist adapter. A route can still have additional delegate or token-level controls that are only fully exercised during simulation or execution. ## Delegate Queries ```typescript theme={null} const stablecoinID = mainnet.assetIds.stablecoin.USDC; const delegate = await ml.delegates.getDelegateInfo(stablecoinID); console.log("Address:", delegate.address); console.log("Type:", delegate.type); // "balanceSheet" or "yield" console.log("Stablecoin:", delegate.stablecoinAddress); console.log("RWA custody:", delegate.rwaCustodyAddress); console.log("Stablecoin custody:", delegate.stablecoinCustodyAddress); console.log("RWA cold storage:", delegate.rwaColdStorageAddresses); console.log( "Stablecoin cold storage:", delegate.stablecoinColdStorageAddresses, ); ``` For individual reads: ```typescript theme={null} const delegateAddress = await ml.delegates.getDelegateAddress(stablecoinID); const rwaCustody = await ml.delegates.getRWACustodyAddress(stablecoinID); const stableCustody = await ml.delegates.getStablecoinCustodyAddress(stablecoinID); const rwaColdStorage = await ml.delegates.getRWAColdStorageAddresses(stablecoinID); const stablecoinColdStorage = await ml.delegates.getStablecoinColdStorageAddresses(stablecoinID); const acceptsRwa = await ml.delegates.isRWAWhitelisted( stablecoinID, rwaTokenAddress, ); const acceptsStablecoin = await ml.delegates.isStablecoinWhitelisted( stablecoinID, backingStablecoinAddress, ); ``` ## LP Admin Operations The `ml.issuerAdmin` module resolves the selected stablecoin delegate and executes LP-admin methods. The module retains its legacy code-facing name. Reads require a `publicClient`; writes require an authorized wallet account. ```typescript theme={null} const stablecoinID = mainnet.assetIds.stablecoin.USDC; const isAdmin = await ml.issuerAdmin.isIssuerAdmin( stablecoinID, account.address, ); if (isAdmin) { await ml.issuerAdmin.setRWADiscountRate({ stablecoinID, rwaID: mainnet.assetIds.rwa.ULTRA, rate: 500_000_000_000_000n, // 5 bps in WAD precision }); } ``` Supported writes include: * `addIssuerAdmin` and `removeIssuerAdmin` * `whitelistRWA` and `whitelistStablecoin` * `setRWADiscountRate` and `setRWARedemptionFee` * `setStablecoinAcceptanceFee` and `setStablecoinRedemptionFee` * `setAutoLiquidityStablecoinID` * `setRWACustodyAddress` and `setStablecoinCustodyAddress` * `setBlacklist` * `addRWAColdStorageAddress` and `removeRWAColdStorageAddress` * `addStablecoinColdStorageAddress` and `removeStablecoinColdStorageAddress` * Delegate `pause` and `unpause` For multisigs, smart accounts, or offline proposal construction: ```typescript theme={null} const data = ml.issuerAdmin.buildIssuerAdminCalldata("whitelistRWA", { rwa: rwaTokenAddress, accepted: true, }); const transaction = await ml.issuerAdmin.buildIssuerAdminTransaction( stablecoinID, "whitelistRWA", { rwa: rwaTokenAddress, accepted: true, }, ); // transaction.to is the resolved delegate. // transaction.data is the encoded delegate call. ``` ## Yield Operations The yield module rejects stablecoins that are not configured as yield-bearing. ```typescript theme={null} const tsy = mainnet.assetIds.stablecoin.TSY_YIELD; const currentDay = await ml.yield.getCurrentDay(tsy); const currentRate = await ml.yield.getDailyRate(tsy, currentDay); const recentRates = await ml.yield.getDailyRates( tsy, currentDay - 7n, currentDay, ); const credits = await ml.yield.getCredits(tsy, userAddress); const withheldTotal = await ml.yield.getTotalWithheldCredits(tsy, userAddress); const withheldRecords = await ml.yield.getWithheldCredits(tsy, userAddress); const multiplier = await ml.yield.getYieldMultiplier(tsy, userAddress); const lastAccrualDay = await ml.yield.getLastAccrualDay(tsy, userAddress); ``` Calculate yield-adjusted amounts without changing state: ```typescript theme={null} const effectiveValue = await ml.yield.getYieldAmount({ stablecoinID: tsy, user: userAddress, redeemValue: 1_000_000_000_000_000_000n, stablecoinWithdrawal: true, }); const requiredCredits = await ml.yield.getAmountForTargetValue({ stablecoinID: tsy, user: userAddress, targetDollars: 1_000_000_000_000_000_000n, stablecoinWithdrawal: true, }); ``` Accrue all currently available work by omitting limits, or bound both daily accrual and withheld-record merges for predictable gas: ```typescript theme={null} await ml.yield.accrueInterest(tsy, userAddress); await ml.yield.accrueInterest(tsy, userAddress, 365n, 100n); await ml.yield.batchAccrueInterest(tsy, [firstUserAddress, secondUserAddress]); ``` Both explicit limits must be nonzero. The unlimited form passes `uint256.max` for both limits, matching the swap contract's internal synchronization path. ## Event Monitoring The SDK exposes the production contract's unified `Swap` event. ### Historical Events ```typescript theme={null} const events = await ml.events.getEvents({ user: userAddress, stablecoinID: mainnet.assetIds.stablecoin.USDC, fromBlock: 19_000_000n, toBlock: "latest", }); for (const event of events) { console.log("Route:", event.routeId); console.log("Operator:", event.operator); console.log("Receiver:", event.receiver); console.log("Input:", event.assetInID, event.amountIn); console.log("Output:", event.assetOutID, event.amountOut); console.log("Fees:", event.issuerFee, event.spreadFee, event.lpFee); } ``` Filters support `user`, `rwaID`, `stablecoinID`, `eventTypes: ["Swap"]`, `fromBlock`, and `toBlock`. ### Live Events ```typescript theme={null} const unwatch = ml.events.watchEvents({ user: userAddress }, (event) => { console.log("New swap:", event.log.transactionHash); }); // Stop watching. unwatch(); ``` Parse an arbitrary raw log with: ```typescript theme={null} const parsed = ml.events.parseLog(rawLog); if (parsed?.type === "Swap") { console.log(parsed.amountIn, parsed.amountOut); } ``` Internal prefund and sweep executions also emit `Swap` events with their actual LP user, operator, receiver, route, and amounts. ## Multicall `ml.multicall.read` batches arbitrary pure/view calls while preserving each return type: ```typescript theme={null} import { multiliquidSwapAbi, priceAdapterAbi, } from "@uniformlabs/multiliquid-evm-sdk"; const adapter = mainnet.addresses.priceAdapters?.[mainnet.assetIds.rwa.ULTRA]; if (adapter === undefined) { throw new Error("Missing ULTRA price adapter"); } const [ultraInfo, ultraPrice] = await ml.multicall.read([ { address: mainnet.addresses.multiliquidSwap, abi: multiliquidSwapAbi, functionName: "rwaInfo", args: [mainnet.assetIds.rwa.ULTRA], }, { address: adapter, abi: priceAdapterAbi, functionName: "getPrice", }, ]); ``` The SDK uses the deployment's `multicall3` address when configured and otherwise falls back to the canonical Multicall3 address. ## Error Handling Contract calls are decoded into typed SDK errors when the revert ABI is known. Standard Solidity `Error(string)` and `Panic(uint256)` payloads are also preserved. ```typescript theme={null} import { ContractPausedError, InsufficientRWAOutputError, InvalidPriceError, MultiliquidContractError, PermitExpiredError, StablecoinPriceOutsideBandError, UserBlacklistedError, } from "@uniformlabs/multiliquid-evm-sdk"; try { await ml.swap.swap({ inputs: [boundedInput] }); } catch (error) { if (error instanceof ContractPausedError) { console.log("Protocol is paused"); } else if (error instanceof UserBlacklistedError) { console.log("Blocked address:", error.args.user); } else if (error instanceof PermitExpiredError) { console.log("The delegated permit expired"); } else if (error instanceof InvalidPriceError) { console.log("An RWA price adapter returned zero"); } else if (error instanceof InsufficientRWAOutputError) { console.log("The exact-in minimum output was not met"); } else if (error instanceof StablecoinPriceOutsideBandError) { console.log("Oracle price:", error.args.oraclePrice); } else if (error instanceof MultiliquidContractError) { console.log(error.errorName, error.args); } } ``` When using `simulate: true`, execution errors are returned in `quote.simulation.error` rather than submitted on-chain. ## Custom Deployments Provide a `ChainDeployment` for another EVM deployment: ```typescript theme={null} import type { Address, Hex } from "viem"; import type { ChainDeployment } from "@uniformlabs/multiliquid-evm-sdk"; const myRwaID = "0x..." as Hex; const myStablecoinID = "0x..." as Hex; const deployment: ChainDeployment = { chainId: 42161, addresses: { multiliquidSwap: "0x..." as Address, delegates: { [myStablecoinID]: "0x..." as Address }, rwaDelegates: { [myRwaID]: "0x..." as Address }, priceAdapters: { [myRwaID]: "0x..." as Address }, tokens: { [myRwaID]: "0x..." as Address, [myStablecoinID]: "0x..." as Address, }, multicall3: "0x..." as Address, }, assetIds: { rwa: { MY_RWA: myRwaID }, stablecoin: { MY_STABLECOIN: myStablecoinID }, }, }; ``` Addresses omitted from the optional maps are resolved on-chain where the corresponding module supports resolution. ## Constants and ABIs The package exports route IDs, role IDs, precision constants, Multicall3 defaults, and const-asserted ABIs for direct viem usage: ```typescript theme={null} import { CANONICAL_MULTICALL3_ADDRESS, DEFAULT_EVM_INFO_API_BASE_URL, ROLES, ROUTE_IDS, WAD, balanceSheetDelegateAbi, erc20Abi, multiliquidSwapAbi, priceAdapterAbi, stablecoinDelegateBaseAbi, treasuryDelegateAbi, whitelistAdapterAbi, yieldBearingDelegateAbi, } from "@uniformlabs/multiliquid-evm-sdk"; ``` ## Testing on Sepolia ```typescript theme={null} import { createPublicClient, http } from "viem"; import { sepolia as viemSepolia } from "viem/chains"; import { createMultiliquidClient, sepolia, } from "@uniformlabs/multiliquid-evm-sdk"; const sepoliaPublicClient = createPublicClient({ chain: viemSepolia, transport: http(), }); const testMl = createMultiliquidClient({ deployment: sepolia, publicClient: sepoliaPublicClient, }); sepolia.assetIds.rwa.MOCK_RWA_WHITELIST; sepolia.assetIds.rwa.MOCK_RWA_NO_WHITELIST; sepolia.assetIds.stablecoin.USDC; sepolia.assetIds.stablecoin.TSY_YIELD; ``` ## Integration Best Practices 1. Build the exact bounded `SwapInputs` before signing or simulating. 2. Re-quote unchanged signed inputs immediately before JIT funding and submission. 3. Distinguish ERC-20 delegate allowance from Multiliquid operator authorization. 4. Use `simulate: true` when current user funds and custody conditions should be tested. 5. Use prospective view quotes when the user or LP is intentionally not funded yet. 6. Keep EIP-712 deadlines short and serialize permits per user nonce. 7. Use multicall and bulk asset methods for dashboards and discovery. 8. Wait for receipts in your application; SDK write methods return transaction hashes. 9. Test direct, standing-allowance, permit, exact-in, exact-out, and prefunded routes on Sepolia. ## Support and Resources * **Protocol Website**: [https://www.multiliquid.xyz/](https://www.multiliquid.xyz/) * **Deployment Addresses**: [Deployments](/evm/deployments) * **Contract ABIs**: [Contract ABIs](/evm/contracts/abis) * **Security Guidance**: [Security](/evm/overview/security) # Protocol Architecture Source: https://docs.multiliquid.xyz/evm/overview/architecture Comprehensive technical overview of the Multiliquid Protocol's modular design and smart contract infrastructure ## System Overview The Multiliquid Protocol is built on a modular, upgradeable architecture that enables atomic swaps between multiple permissioned Real World Asset (RWA) tokens and multiple stablecoins. The system is designed for institutional-grade operations with comprehensive risk management, transparent fee structures, and extensible integration patterns. ### Design Principles 1. **Modularity**: Asset-specific logic isolated in delegate contracts 2. **Upgradeability**: UUPS proxy pattern enables secure evolution 3. **Atomicity**: All operations either fully succeed or fully revert 4. **Transparency**: All state changes emit comprehensive events 5. **Extensibility**: New assets can be integrated without modifying core logic ## Core Contract System The protocol consists of four primary contract categories that work together to enable secure, atomic swaps between RWAs and stablecoins. ### How the Contracts Work Together At the heart of the system, **MultiliquidSwap** acts as the central orchestrator that coordinates all swap operations. When a user initiates a swap, MultiliquidSwap queries **Price Adapters** to obtain real-time USD valuations for the assets involved, then calculates the exact amounts using WAD mathematics and applies the appropriate protocol fees. For added security, it calls **RWA Delegates** (if configured) to enforce volume limits and compliance checks before any tokens move. Finally, it delegates the actual token transfers to **Stablecoin Delegates**, which handle the asset-specific logic—whether minting new stablecoins, transferring from custody, or burning tokens. This separation of concerns ensures that each contract focuses on its specialized responsibility while maintaining atomic execution, meaning every swap either completes entirely or reverts with no partial state changes. ### 1. MultiliquidSwap Contract **Location**: `MultiliquidSwap.sol` The central orchestrator managing all swap operations, fee calculations, and asset interactions. #### Core Responsibilities * **Swap Orchestration**: Coordinates stablecoin↔RWA, RWA↔RWA, and stablecoin↔stablecoin routes with ExactIn and ExactOut variants * **Fee Management**: Resolves LP-paid protocol fees, spread fees, and LP-configured asset fees * **Price Calculation**: Integrates with price adapters for USD-denominated valuation * **Access Control**: Enforces role-based permissions for all operations * **Quote Generation**: Provides a view function for off-chain quote calculation Complete function signatures and implementation details ### 2. Stablecoin Delegate System **Base Contract**: `StablecoinDelegateBase.sol` Stablecoin delegates handle asset-specific integration logic for each supported stablecoin. #### Core Responsibilities * **Stablecoin Operations**: Mint, burn, or transfer stablecoins based on delegate type * **RWA Whitelist Management**: Maintain per-Liquidity Provider RWA whitelists controlled by LP admins * **Fee Configuration**: Set discount rates, redemption fees, and acceptance fees per RWA * **Risk Controls**: Pause mechanisms and compliance hooks for metadata validation * **Custody Management**: Manage RWA token custody addresses (for balance sheet delegates) #### Delegate Types * **Mint/Burn Delegates**: Direct integration with stablecoins supporting native minting * **Balance Sheet Delegates**: Custody-based model for existing stablecoin liquidity balances without mint/burn integration * **Yield-Bearing Delegates**: Credit and yield accounting layered onto stablecoin settlement Complete interface and implementation details ### 3. RWA Delegate System **Base Contract**: `RWADelegate.sol` RWA delegates provide **optional** risk management and compliance controls for specific RWA tokens. Only deployed when custom controls beyond token-native functionality are needed. #### Core Responsibilities * **Volume Limits**: Enforce daily swap limits per wallet with configurable reset windows * **Risk Validation**: Validate RWA transfers with `checkRWAIn()` and `checkRWAOut()` hooks * **Pause Controls**: RWA-admin-controlled pause mechanism independent from protocol-wide pause #### When to Use RWA delegates are only required when additional risk management is needed beyond the RWA token's native controls (e.g., built-in KYC, transfer restrictions). Complete interface and implementation details ### 4. Price Adapter System **Interface**: `IPriceAdapter.sol` Price adapters provide USD-denominated pricing for all assets in the protocol. #### Core Responsibilities * **Price Provision**: Return USD price in 18-decimal WAD format (1e18 = \$1.00 USD) * **Asset-Specific Integration**: Query published NAV of the RWA * **Standardization**: Normalize pricing across different RWA token architectures Complete adapter implementations and integration patterns ### 5. Application Layer: Liquid Treasury The protocol's modular architecture enables higher-level financial products built on the core swap infrastructure. Liquid Treasury is the first such application. **Liquid Treasury Token** (`Treasury.sol`): * Multi-chain institutional treasury token with whitelist and blacklist compliance controls * Dual minting modes: whitelist-enforced primary issuance and bridge-agnostic cross-chain minting * Emergency controls (force burn, force transfer) and EIP-2612 permit support **Treasury Delegate** (`TreasuryDelegate.sol`): * Yield-bearing stablecoin delegate integrating Liquid Treasury with MultiliquidSwap * Daily interest rate accrual based on underlying tokenized money market fund performance * Withheld credits system with 24-hour hold period for risk management * Permissionless interest accrual with bounded processing for gas safety Complete token mechanics, yield system, compliance controls, and bridge integration ## Operational Flow ### Swap Execution Flow ```mermaid theme={null} sequenceDiagram actor User actor Operator participant MultiliquidSwap participant Pricing participant Policy participant StablecoinDelegate participant Assets User-->>Operator: Authorize transaction Operator->>MultiliquidSwap: Submit one or more swap legs MultiliquidSwap->>MultiliquidSwap: Validate inputs and authorization MultiliquidSwap->>Pricing: Resolve prices and fees MultiliquidSwap->>Policy: Check applicable controls MultiliquidSwap->>StablecoinDelegate: Send resolved settlement StablecoinDelegate->>Assets: Transfer, mint, or burn StablecoinDelegate-->>MultiliquidSwap: Swap complete MultiliquidSwap-->>Operator: Emit Swap event ``` ## Data Structures ### RWA Information ```solidity theme={null} struct RWAInfo { bool accepted; // Whether RWA is accepted for swaps bool additionalRiskControls; // Whether delegate risk checks are enabled uint8 decimals; // Token decimals (e.g., 6, 18) address delegate; // Optional delegate address (address(0) if none) address assetAddress; // Address of the RWA token contract } ``` ### Stablecoin Information ```solidity theme={null} struct StablecoinInfo { bool accepted; // Whether stablecoin is accepted bool yieldBearing; // Whether the stablecoin accrues yield uint8 decimals; // Token decimals address delegate; // LP delegate, or zero for swap-only counterparty assets address assetAddress; // Address of the stablecoin token contract } ``` ### Fee Model The protocol separates LP-paid protocol fees, the protocol's share of LP spreads, and LP-configured asset fees. Each Liquidity Provider configures spreads and fees independently for every accepted asset within its own delegate domain. ## Permission Model The protocol implements a comprehensive role-based access control system: ### Global Roles **DEFAULT\_ADMIN\_ROLE** * Can upgrade contracts via UUPS * Administers roles according to each contract's configured role hierarchy * Configures the Multiliquid protocol fee vault * Intended for multi-sig governance **OPERATOR\_ROLE** * Day-to-day operational control * Can accept new RWAs and stablecoins * Can update price adapters * Can configure protocol fee policy and stablecoin guardrails * Can pause and unpause the MultiliquidSwap contract (emergency and maintenance) * Cannot upgrade contracts **PAUSE\_ROLE** *(Delegate Contracts)* * Exists on stablecoin and RWA delegate contracts, not on MultiliquidSwap * Can pause individual delegate operations * Provides granular, per-delegate emergency controls ### Asset-Specific Roles **Liquidity Provider Admin** Each stablecoin delegate has its own Liquidity Provider admin who can: * Set RWA discount rates for their stablecoin * Set RWA redemption fees for their stablecoin * Manage the RWA whitelist for their stablecoin * Pause their specific delegate * Configure custody addresses **RWA Admin** Each RWA delegate has its own RWA admin who can: * Configure volume limits * Adjust risk parameters * Pause their specific delegate ### Special Role **MULTILIQUID\_SWAP\_CONTRACT** * Granted to the MultiliquidSwap contract address * Allows the swap contract to call delegate functions * Ensures only the central orchestrator can trigger swaps ## Upgrade Mechanism All core contracts use the UUPS (Universal Upgradeable Proxy Standard) pattern: ### Upgrade Process 1. **Preparation**: New implementation contract is deployed 2. **Testing**: Comprehensive testing on testnet with identical configuration 3. **Proposal**: DEFAULT\_ADMIN\_ROLE proposes upgrade 4. **Execution**: Authorized upgrade execution 5. **Verification**: Post-upgrade validation and monitoring ### Upgrade Safety * **Storage Gaps**: All upgradeable contracts include storage gaps for future variables * **Initialization Guards**: Prevents re-initialization of upgraded contracts * **Access Control**: Only DEFAULT\_ADMIN\_ROLE can execute upgrades * **Event Emission**: All upgrades emit events for transparency ### Storage Layout Preservation When upgrading contracts, the storage layout must be preserved: * New variables must preserve the slots of existing state * Existing variables cannot be removed or reordered * Storage gaps provide buffer for new variables ## Event System The protocol emits events for settlement and major configuration changes: ### Swap Events Every requested and internally derived leg emits the unified `Swap` event. It identifies the user, operator, receiver, assets, route, Liquidity Provider domain, resolved amounts, and fee amounts. ### Administrative Events Dedicated administrative events cover asset registration, pricing, guardrails, fee policy, custody, Liquidity Provider settings, blacklists, and auto-liquidity. ## Extensibility The protocol's modular architecture enables straightforward extension: ### Adding a New Stablecoin 1. **Deploy Delegate**: Create stablecoin delegate (mint/burn or balance sheet) 2. **Configure Delegate**: Set custody addresses, initial whitelists 3. **Configure USD Value**: Set the route value and optional oracle guardrail 4. **Register with MultiliquidSwap**: Add the stablecoin to protocol configuration 5. **Test Integration**: Validate swaps on testnet 6. **Production Deployment**: Enable on mainnet ### Adding a New RWA 1. **Technical Analysis**: Review RWA token contract for compatibility 2. **Deploy Delegate** (optional): Only if custom risk controls needed 3. **Deploy Price Adapter**: Create adapter for USD pricing 4. **Register with MultiliquidSwap**: Add the RWA and its optional policy adapters 5. **Whitelist Configuration**: Liquidity Providers accept the new RWA 6. **Test Integration**: Validate swaps on testnet 7. **Production Deployment**: Enable on mainnet *** Review the comprehensive security measures, access controls, and risk management framework # Multiliquid Protocol Source: https://docs.multiliquid.xyz/evm/overview/index Decentralized infrastructure for atomic swaps between permissioned Real World Assets and stablecoins ## Overview Multiliquid is a decentralized protocol that enables institutional-grade atomic swaps between multiple permissioned Real World Asset (RWA) tokens and multiple stablecoins. The protocol provides a secure, transparent, and efficient infrastructure for digital asset exchange in regulated markets. Explore the protocol's modular design and core contracts Review security measures and risk management controls Learn how to integrate with the Multiliquid Protocol ## Key Features ### Atomic Swap Execution The protocol ensures complete transaction atomicity—either all conditions are fulfilled or none are. This eliminates partial executions and guarantees consistent state across all operations. ### Multi-Asset Support * **Any RWA Token**: Support for multiple permissioned Real World Asset tokens * **Any Stablecoin**: Integration with multiple institutional stablecoin liquidity providers and balance sheet providers * **Extensible Design**: Modular architecture enables seamless onboarding of new assets ### Institutional-Grade Security * **Upgradeable Contracts**: UUPS proxy pattern for secure evolution without disruption * **Role-Based Access Control**: Granular permissions using OpenZeppelin's battle-tested framework * **Risk Management**: Asset-specific controls including volume limits, discount rates, fees, pause mechanisms, and compliance checks * **Emergency Controls**: Multi-signature governance and emergency pause capabilities ### Transparent Fee Structure * **LP-Paid Protocol Fee**: Configurable per Liquidity Provider domain * **Spread Fee**: Protocol participation in spreads configured by Liquidity Providers * **LP Flexibility**: Liquidity Providers configure spreads and fees for each accepted asset independently ## How It Works The protocol operates through a coordinated system of smart contracts: 1. **MultiliquidSwap Contract**: Central orchestrator managing all swap operations, fee calculations, and asset interactions 2. **Delegate Contracts**: Asset-specific modules handling: * Stablecoin minting/burning or external integration * RWA token risk management and custody * Compliance and volume controls 3. **Price Adapters**: Modular pricing for RWAs and optional stablecoin guardrails 4. **Whitelist Management**: Liquidity Provider asset acceptance and RWA-specific eligibility checks ### Swap Process ```mermaid theme={null} sequenceDiagram actor User actor Operator participant MultiliquidSwap participant Pricing participant Policy participant StablecoinDelegate participant Assets User-->>Operator: Authorize transaction Operator->>MultiliquidSwap: Submit swap MultiliquidSwap->>Pricing: Resolve prices and fees MultiliquidSwap->>Policy: Check applicable controls MultiliquidSwap->>StablecoinDelegate: Request settlement StablecoinDelegate->>Assets: Transfer, mint, or burn MultiliquidSwap-->>Operator: Emit Swap event ``` ## Supported Assets The protocol is designed to support a growing ecosystem of institutional-grade digital assets: ### Real World Assets (RWAs) * Permissioned tokenized securities * Tokenized treasury products * Institutional digital assets with compliance requirements ### Stablecoins * Mint/burn stablecoins * Pre-deposited liquidity provided by balance sheet providers without mint/burn integration ## Protocol Governance The protocol employs a multi-tiered permission model: * **Protocol Administrators**: Manage core system upgrades and global parameters * **Operators**: Handle day-to-day operations including price updates and asset acceptance * **Emergency Roles**: Pause capabilities for critical security scenarios * **Liquidity Provider Admins**: Control all custody and risk management parameters for their Liquidity Provider ## Technology Stack Built on industry-standard frameworks: * **Solidity 0.8.30**: Checked arithmetic and modern language features * **OpenZeppelin Upgradeable**: Audited proxy patterns and access control * **Solady**: Gas-optimized mathematical operations * **UUPS Proxies**: Minimal proxy overhead with secure upgrade mechanisms ## Integration Options **EVM SDK**: The official TypeScript SDK supports quoting, transaction construction, delegated authorization, simulation, protocol reads, and event monitoring. **Direct Smart Contract Integration**: Multiliquid allows for direct swap integration on Ethereum Mainnet. For developer integration details, contract ABIs, and technical specifications, see the [Integration Guide](/evm/guides/integration). ## Next Steps Deep dive into the protocol's modular design, contract interactions, and swap mechanics Examine security measures, audit results, and risk management frameworks Browse detailed contract documentation and function specifications Yield-bearing institutional treasury token with cross-chain bridge support *** The Multiliquid Protocol handles significant value and operates in regulated markets. All integrations should undergo thorough testing and security review. Contact the Multiliquid team before production deployment. # Security & Risk Management Source: https://docs.multiliquid.xyz/evm/overview/security Comprehensive security measures, risk controls, and operational safeguards in the Multiliquid Protocol ## Security Overview The Multiliquid Protocol is built with institutional-grade security as a core requirement. Every contract, function, and integration point has been designed with security-first principles, incorporating multiple layers of protection, comprehensive access controls, and battle-tested frameworks. See our audits [here](https://github.com/uniformlabs/audits). OpenZeppelin frameworks and secure coding practices Multi-tiered role-based permission model Asset-specific controls and volume limits Pause capabilities and incident response ## Smart Contract Security ### Industry-Standard Frameworks The protocol uses audited, production-tested libraries: **OpenZeppelin Upgradeable Contracts** * **Usage**: Access control, proxy patterns, reentrancy protection, safe external operations * **Security**: Industry-standard, extensively audited by multiple firms * **Key Modules**: * `AccessControlUpgradeable`: Role-based permissions * `UUPSUpgradeable`: Secure upgrade mechanism * `ReentrancyGuardUpgradeable`: Reentrancy attack prevention * `PausableUpgradeable`: Emergency pause functionality **Solady** * **Usage**: Gas-optimized mathematical operations * **Security**: Extensively tested, overflow-safe operations * **Key Module**: `FixedPointMathLib` for WAD/RAY mathematics ### Core Security Mechanisms #### 1. Reentrancy Protection The unified `swap()` entrypoint, stablecoin settlement methods, and stateful RWA checks use reentrancy guards. Requested legs, derived liquidity operations, and delegate state changes revert atomically if settlement fails. #### 2. SafeERC20 Operations Stablecoin delegates use OpenZeppelin's `SafeERC20` library for ERC20 transfers: * Handles non-standard ERC20 implementations * Reverts on failed transfers (even if token returns `false`) * Protects against tokens with unusual return values ```solidity theme={null} using SafeERC20 for IERC20; // Safe transfer with automatic revert on failure IERC20(rwaToken).safeTransferFrom(user, custody, amount); ``` #### 3. Integer Overflow Protection * **Solidity 0.8.30**: Built-in overflow/underflow protection * **Checked Arithmetic**: All operations automatically revert on overflow * **WAD Mathematics**: Consistent 18-decimal precision with explicit conservative rounding #### 4. Input Validation Swap execution validates inputs before settlement: * Asset IDs must correspond to accepted assets * Amounts must be non-zero * Addresses must be non-zero ### Upgrade Safety #### UUPS Proxy Pattern The protocol uses the **Universal Upgradeable Proxy Standard (UUPS)**: **Security Advantages**: * Upgrade logic lives in implementation (not proxy) * Smaller proxy contract reduces attack surface * Explicit authorization required for upgrades * Event emission for all upgrades **Upgrade Authorization**: ```solidity theme={null} function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { // Only DEFAULT_ADMIN_ROLE (typically multi-sig) can upgrade } ``` #### Storage Layout Protection * **Storage Gaps**: All upgradeable contracts include `__gap` arrays * **Layout Verification**: Automated tools verify storage compatibility * **Initialization Guards**: `initializer` modifier prevents re-initialization ## Access Control System The protocol implements a sophisticated multi-tier permission model using OpenZeppelin's `AccessControl`: ### Global Roles #### DEFAULT\_ADMIN\_ROLE **Permissions**: * Authorize UUPS upgrades on contracts where the role is held * Administer roles according to each contract's configured role hierarchy * Configure the protocol fee vault on MultiliquidSwap **Security Considerations**: * Should never be held by EOA (Externally Owned Account) * Requires multi-party consensus for all operations #### OPERATOR\_ROLE **Permissions**: * Accept new RWAs and stablecoins * Update price adapters * Configure protocol fee policy and stablecoin guardrails * Manage operational parameters and swap batch limits **Security Considerations**: * Should never be held by EOA (Externally Owned Account) * Requires multi-party consensus for all operations **Restrictions**: * Cannot upgrade contracts * Cannot directly transfer user funds #### PAUSE\_ROLE **Permissions**: * Pause and unpause stablecoin and RWA delegate contracts through the Multiliquid pause path **Intended Holder**: Security operations team **Use Cases**: * Smart contract vulnerability discovered * Suspicious trading activity detected * Oracle manipulation detected * Regulatory requirement On MultiliquidSwap itself, `OPERATOR_ROLE` can pause and unpause. `EXTERNAL_PAUSER_ROLE` can pause but cannot unpause. ### Asset-Specific Roles #### Liquidity Provider Admin Each stablecoin delegate has an independent Liquidity Provider admin role: **Permissions**: * Set RWA discount rates for their stablecoin * Set RWA redemption fees for their stablecoin * Add/remove RWAs from their whitelist * Pause their specific delegate * Configure custody addresses **Security Boundary**: * Cannot affect other stablecoins * Cannot modify core protocol logic * Cannot access other Liquidity Providers' funds **Example**: ```solidity theme={null} stablecoinDelegate.setRWADiscountRate(rwaID, 1e16); // 1% discount ``` #### RWA Admin Each RWA delegate (when deployed) has an independent RWA admin role: **Permissions**: * Configure volume limits * Adjust risk parameters * Pause their specific delegate **Security Boundary**: * Cannot affect other RWAs * Cannot modify core protocol logic * Cannot override Liquidity Provider controls ### Permission Hierarchy ```mermaid theme={null} graph TD DEPLOYMENT ADMIN[DEFAULT_ADMIN_ROLE] OP[OPERATOR_ROLE] PAUSE[PAUSE_ROLE
Security Team] SC_ADMIN[Liquidity Provider Admin] RWA_ADMIN[RWA Admin] ADMIN -->|Grants/Revokes| OP ADMIN -->|Grants/Revokes| PAUSE DEPLOYMENT -->|Assigns| ADMIN DEPLOYMENT -->|Assigns| SC_ADMIN DEPLOYMENT -->|Assigns| RWA_ADMIN ADMIN -->|Can Upgrade| CONTRACTS[Smart Contracts] OP -->|Can Configure| PARAMS[Protocol Parameters] PAUSE -->|Can Pause| DELEGATES[Delegate Contracts] OP -->|Can Pause| CONTRACTS SC_ADMIN -->|Can Configure| SC_DELEGATE[Stablecoin Delegate] RWA_ADMIN -->|Can Configure| RWA_DELEGATE[RWA Delegate] ``` ## Risk Management Controls ### Volume Limits RWA delegates can implement daily volume limits to prevent: * Market manipulation through large swaps * Sudden liquidity drains * Compliance violations **Implementation Example (ULTRA)**: * **Daily Limit**: One RWA-admin-configured limit applied independently to each address * **Reset Time**: 2pm Singapore Time (UTC+8) * **Tracking**: Cumulative inflow and outflow volume in fixed daily windows * **Enforcement**: Automatic rejection when limit exceeded ### Whitelist Management **Per-Stablecoin RWA Whitelisting**: * Each Liquidity Provider controls which RWAs they accept * Granular risk management per Liquidity Provider * Prevents unwanted asset exposure **Whitelist Modification**: * Only the Liquidity Provider admin can modify their whitelist * Events emitted for transparency * Takes effect immediately **Benefits**: * Liquidity Providers maintain control over their risk profile * Flexible onboarding without protocol-wide impact * Compliance alignment with Liquidity Provider policies ### Slippage Protection ExactIn swaps specify an exact input and minimum acceptable output. ExactOut swaps specify an exact output and maximum acceptable input. If the calculated amount violates the slippage bounds, the transaction reverts atomically. ### Rate Validation LP-configured discounts, redemption fees, and stablecoin acceptance or redemption fees must remain below 100% WAD. Protocol LP-paid and spread-take rates cannot exceed 100%. ## Emergency Mechanisms ### Pause Functionality The protocol implements multi-level pause capabilities: #### Global Pause (MultiliquidSwap) **Trigger**: `OPERATOR_ROLE` or the pause-only `EXTERNAL_PAUSER_ROLE` calls `pause()` **Effects**: * All swap functions revert * Price queries still available * Administrative functions still accessible **Use Cases**: * Critical vulnerability discovered * Oracle manipulation detected * Regulatory requirement * Smart contract upgrade preparation #### Delegate-Level Pause **Stablecoin Delegate Pause**: * Triggered by the Liquidity Provider admin or `PAUSE_ROLE` * Affects only swaps involving that stablecoin delegate * Other stablecoin delegates remain operational **RWA Delegate Pause**: * Triggered by the RWA admin or `PAUSE_ROLE` * Affects only swaps involving that RWA * Other RWAs remain operational ### Unpause Requirements Unpausing requires: 1. Root cause analysis completed 2. Fix implemented and tested (if applicable) 3. Security review of changes 4. Explicit unpause transaction by authorized role ## Audit and Review Process ### Pre-Deployment Security Security validation includes: 1. **Extensive Internal Code Review and Testing** * Unit, fuzz, invariant, mutation, and fork testing * Security-focused checklist validation * Gas optimization review * Slither static analysis 2. **Testnet Deployment** * Full system deployment on testnet * Integration testing with real user flows * Stress testing with edge cases * Multi-day operational validation 3. **Independent Security Review** * Completed reviews listed in the [Multiliquid audits repository](https://github.com/uniformlabs/audits) 4. **Final Verification and Deployment** * Mainnet deployment scripts used * Role assignments reviewed and documented ### Post-Deployment Monitoring Continuous monitoring includes: * **Event Indexing**: All contract events logged and analyzed * **Transaction Monitoring**: Unusual patterns detected * **Price Oracle Monitoring**: Deviation alerts for price feeds ### Incident Response In the event of a security incident: 1. **Detection**: Monitoring systems alert security team 2. **Assessment**: Rapid evaluation of severity and impact 3. **Pause** (if necessary): Immediate pause of affected components 4. **Communication**: Transparent disclosure to stakeholders 5. **Remediation**: Fix development and testing 6. **Deployment**: Upgrade execution by the authorized role 7. **Post-Mortem**: Detailed report and process improvement ## Operational Security Best Practices For institutions integrating with the protocol: ### Key Management * **Use Hardware Wallets**: For all signing operations * **Multi-Sig Wallets**: Minimum 2-of-3 for operational roles * **Geographic Distribution**: Key holders in different locations * **Regular Rotation**: Periodic key refresh schedule ### Transaction Security * **Simulation**: Use transaction simulation before signing * **Verification**: Double-check recipient addresses and amounts * **Gas Limits**: Set reasonable limits to prevent griefing * **Nonce Management**: Track nonces to prevent replay attacks ## Known Limitations and Assumptions ### Token Compatibility The protocol assumes: * Tokens do not implement transfer fees or rebasing * Tokens do not have transfer requirements that break atomicity * Tokens follow standard ERC20 behavior * Price oracles remain available and accurate If integrating a token with non-standard behavior, additional analysis and potentially custom delegate development is required. *** ## Security Contacts For security vulnerabilities or concerns: **DO NOT** disclose security vulnerabilities publicly. Contact the Multiliquid team directly through secure channels. * **Website**: [https://www.multiliquid.xyz/](https://www.multiliquid.xyz/) * **Public Repository**: [https://github.com/uniformlabs/Multiliquid](https://github.com/uniformlabs/Multiliquid) *** This security documentation is a living document and will be updated as the protocol evolves, new security measures are implemented, and audit results become available. *** Explore the central orchestrator contract managing all swap operations # Deployments Source: https://docs.multiliquid.xyz/svm/deployments Deployed program and account addresses on Solana mainnet and devnet ## Overview This page lists the deployed program and account addresses for the Multiliquid Protocol on Solana. Use these addresses to interact with the program on-chain. ## Solana Mainnet **Network**: Solana Mainnet-Beta ### Program | Component | Address | | :---------------------- | :--------------------------------------------- | | Swap Program | `HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6` | | IDL Account | `9Z5DrheNqtW8KF4eCa5kqNbP8yBfkc1Jnp56974GvSDk` | | Global Config (PDA) | `5aENbrcLXt586S2CMzSkp7rvrLWUbyaAZJ6UgCRRCDiC` | | Program Authority (PDA) | `2a5pbsefjueEs9gwhG75W8AoVPM32GLQRx9eSDQhrzDq` | | ProgramData Account | `EXNXWtg1QBVNXAQJ6qCj7mk7LHr3QpgMhY3ozTKbfaYR` | | Upgrade Authority | `GV5QXqcBYXCkDYnanSBwGVRbnVKq6hE3oSZCWuTsgvrZ` | ### Configuration | Parameter | Value | | :----------------- | :--------------------------------------------- | | Admin | `GV5QXqcBYXCkDYnanSBwGVRbnVKq6hE3oSZCWuTsgvrZ` | | Fee Wallet | `JAgoEMKKyUFBB9sC1JnacHDBW6wxejkFaweVp62pE5se` | | Protocol Fees | 10 bps (0.1%) | | Global Pause State | Unpaused | ### Configured Asset Accounts | Asset | Mint Address | Asset Config | NAV Source | | :---- | :--------------------------------------------- | :--------------------------------------------- | :------------------------------------------------------- | | USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | `DC6iagctYZBt5MKCrdHnbEZiR2QsNFesa8YeSzQvAGUf` | Pyth Push `Dpw1EAVrSB1ibxiDQyTAW6Zip3J4Btk2x4SgApQCeFbX` | | ACRED | `FubtUcvhSCr3VPXEcxouoQjKQ7NWTCzXyECe76B7L3f8` | `GnHsBPaUwHY4uzoHNcSXCirMVoJEgqNQFwBbmbdA4kqa` | Pyth Push `6gyQ2TKvvV1JB5oWDobndv6BLRWcJzeBNk9PLQ5uPQms` | | BENJI | `5Tu84fKBpe9vfXeotjvfvWdWbAjy3hqsExvuHgFqFxA1` | `6BuEdpN7e7LNpfmWsLsSA1mh9uYRSMNMpricUHK54sxG` | Hardcoded \$1.00 | | USCC | `BTRR3sj1Bn2ZjuemgbeQ6SCtf84iXS81CS7UDTSxUCaK` | `3V7ZeDpnwdXj3fzGcFmDcW8E3xPPow1u2DaMKFJYvh7J` | Pyth Push `823Y4cV7XH2TzkB9NdHfTRoCKLrqXv8EgQP5nzEG43Hp` | | USTB | `CCz3SGVziFeLYk2xfEstkiqJfYkjaSWb2GCABYsVcjo2` | `GpSFgXan1qCza5J2tQJTK552qWk9oPiRyhH5qjPrRnLW` | Pyth Push `EqggHKbjePzmXAX6MW3EsgjiJ4mhkbb8j5s5KfGs1gLq` | | VBILL | `34mJztT9am2jybSukvjNqRjgJBZqHJsHnivArx1P4xy1` | `Hpcv1yieWYUNjpMQgVmpyrneXjbPt9VXgHVpjkpLtD4H` | Hardcoded \$1.00 | | WTGXX | `Em46fxxwgY2RRoUbBMSbEjJwY62x3ESMNdhnsGpEKewm` | `HBGEbgFe6oseRsaw8gTKLEiyvWk6ABCRrLxBvxgyNJgq` | Hardcoded \$1.00 | ONYC is not currently configured in the deployed mainnet program; setup was deferred while its Pyth state is `coming_soon`. ### Trading Pairs All pairs use USDC as the stablecoin side. **Liquidity Provider**: `C8Mi6kn7ajFWuNe4ZmsR9A6fdqRYhzXFoqVBGMsdJ2Uf` **Vault Authority (PDA)**: `83pyj2KSuWt4AFFWeMvuMY1HXbQoafSxP1tFQEudGCwU` | Pair | Pair Account (PDA) | | :----------- | :--------------------------------------------- | | USDC / ACRED | `8Au4HtMC4jUzYUYJmHVuCjezuo2dCFQoPwbCPmvHC5ds` | | USDC / BENJI | `HwAEmrBPoYLBqCQ3yRdELnJLxisyxo54PHyMTDc2yr2J` | | USDC / USCC | `5w6cm4CeEVLz9ayuKse3ZNWmsjQip7UBmLifM3yQMFuA` | | USDC / USTB | `HTzC1qsVU4FdVC6LFty454TBYvDUUjs98QJLb7DVQAjH` | | USDC / VBILL | `DtLjkaYkK3EQg72g5BkQrzbK1NNEKxjUAELtGW7HAMpV` | | USDC / WTGXX | `D3ESUMZ8mqrAW5ckYH4JSKWThnZvGCAKxQctDyQ5Lp1S` | **Liquidity Provider**: `7AzejdDM3AJpqxru7kkSLN2Lwisra87azsiawYM3J8jA` **Vault Authority (PDA)**: `8gmaC392zVSxbeJYcSZcgRRgvm7XsXfjhTzr7DsAQKvR` | Pair | Pair Account (PDA) | | :----------- | :--------------------------------------------- | | USDC / USCC | `6tDrDBBWotfqwT7YcGFZv2vDHeyarjPqGcHnsz7Fsxpp` | | USDC / USTB | `BUGHd6BQJzYDpukqpZ6i5Y5iJmtqcBnR4exaTqrZUAaS` | | USDC / WTGXX | `2ecdYCFovT8BJmKkMFwM64oftAUdDpLL1qCpsxt3swZy` | LP vault token accounts are associated token accounts owned by the LP's `vault_authority` PDA. Protocol fee vault token accounts are associated token accounts owned by the global `program_authority` PDA. ## Solana Devnet **Network**: Solana Devnet ### Program | Component | Address | | :---------------------- | :--------------------------------------------- | | Swap Program | `HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6` | | IDL Account | `9Z5DrheNqtW8KF4eCa5kqNbP8yBfkc1Jnp56974GvSDk` | | Global Config (PDA) | `5aENbrcLXt586S2CMzSkp7rvrLWUbyaAZJ6UgCRRCDiC` | | Program Authority (PDA) | `2a5pbsefjueEs9gwhG75W8AoVPM32GLQRx9eSDQhrzDq` | | ProgramData Account | `EXNXWtg1QBVNXAQJ6qCj7mk7LHr3QpgMhY3ozTKbfaYR` | | Upgrade Authority | `8nxkQVjEjmd4Vv51BDnHbBALg3n9SSgNJDNEoZDGBzsR` | ### Configuration | Parameter | Value | | :----------------- | :--------------------------------------------- | | Admin | `8nxkQVjEjmd4Vv51BDnHbBALg3n9SSgNJDNEoZDGBzsR` | | Fee Wallet | `8nxkQVjEjmd4Vv51BDnHbBALg3n9SSgNJDNEoZDGBzsR` | | Protocol Fees | 10 bps (0.1%) | | Global Pause State | Unpaused | ### Configured Asset Accounts | Asset | Mint Address | Token Program | Asset Config | | :--------------- | :--------------------------------------------- | :------------ | :--------------------------------------------- | | MOCK\_USDC | `AeQz4qPhAFxZshmv9gE3dbxyph2uaqyM4JnWk4DeH8Jo` | SPL Token | `P8PjTbQZ9xBUNMSskKuu86S3tVPCzdMZkrHtgXKYDD8` | | MOCK\_RWA | `9fBsNQ3rL2FdAYKXptwJ9nbarCDh57Xqzbsr68p9Te6j` | SPL Token | `6JsJwfXHiDRGSGdZVD6DAkqpTxJcmQWMpViH69RoadTC` | | MOCK\_USDC\_2022 | `6KkK2kC2y93tUWoWAsN1DNiuiVrrRyuX1VsKdJqKRnhg` | Token-2022 | `7p1s2S7vWfYhy5w4GThLv6ka7Xhu7bFsdUSHGKHgbTNu` | | MOCK\_RWA\_2022 | `CKY7KPNrJH35CTbJYmAVKhffwA11drLESTKJ6HiPfaD2` | Token-2022 | `xCrC4QJAvDLCemuwj8QZjMxcmbmsT42LxPZ3b7kFUbM` | | MOCK\_USDT | `BzW7XMExTPaSH8Fo28BPvkr2o9MpvDG8Dyuzudhnr59J` | Token-2022 | `9FAjk88ue3QwSdiCvdTjyKsLZJ5CK2DkYi9xQCcZBPUK` | | MOCK\_RWA2 | `53WBk4Dwh4rU7MNMigUNtTovRWyRrbXvgdN1StUpLviY` | Token-2022 | `B7fMhZf2yz4fd6K5dsZwmwjUy281GWMMJeJMPCuLfnro` | ### Trading Pairs **Liquidity Provider**: `8nxkQVjEjmd4Vv51BDnHbBALg3n9SSgNJDNEoZDGBzsR` **Vault Authority (PDA)**: `6XxYY262sign584DLWTLrkFUDFo1hEQk7CZZVB2EkeBc` | Pair | Pair Account (PDA) | | :--------------------------------- | :--------------------------------------------- | | MOCK\_USDC / MOCK\_RWA | `CFdG1zintcNbQjGwUJ4ZJb86Ais7NucmGuXjGniZsNCV` | | MOCK\_USDC\_2022 / MOCK\_RWA\_2022 | `GSBFj2iTJy6ms32B7V9ro4WxM5RuaQCT2WPVt8TZ4M3m` | | MOCK\_USDT / MOCK\_RWA2 | `B69NNMF2TjXWmJ7m5RHeDn1KxzFZr9LBR7JYtNwiJ1k` | **Liquidity Provider**: `Ek8hY6H1HzDWv9RsyeyhjgRrdeZCnRdvDJzdBh73c3TY` **Vault Authority (PDA)**: `CexkPUNbyy4YmJKdKfvipKrmdkVQxBDAUFRAUAL92m7Z` | Pair | Pair Account (PDA) | | :--------------------- | :--------------------------------------------- | | MOCK\_USDC / MOCK\_RWA | `HYeKWhRBKEPP1xPKNtFyYGysbwNv6bZv1Eg92Kou7ztc` | *** View the full program IDL and usage instructions *** ## Block Explorers | Network | Explorer | | :------------- | :----------------------------------------------------------------------------------------- | | Solana Mainnet | [https://explorer.solana.com/](https://explorer.solana.com/) | | Solana Devnet | [https://explorer.solana.com/?cluster=devnet](https://explorer.solana.com/?cluster=devnet) | | Solscan | [https://solscan.io/](https://solscan.io/) | *** This page will be updated as new assets and pairs are deployed. Bookmark this page for the latest deployment information. Learn how to execute swaps on the Multiliquid Program # Integration Guide Source: https://docs.multiliquid.xyz/svm/guides/integration Developer guide for integrating with the Multiliquid Protocol on Solana using the TypeScript SDK ## Integration Overview The Multiliquid Protocol on Solana can be integrated via the official TypeScript SDK, which provides a complete interface for quoting, building, and executing swaps against the on-chain program, along with liquidity-provider administration flows. The SDK is built on [Solana Kit](https://github.com/anza-xyz/kit) (`@solana/kit`) and uses native `bigint` values for token amounts. There is no Anchor or `@solana/web3.js` runtime dependency. Install and configure the TypeScript SDK Find available trading pairs Get swap quotes using client-side math or simulation Build and submit swap transactions Create, update, and close pairs and manage liquidity Implement a rolling 24-hour laddered pricing model as an LP ## Installation ```bash theme={null} npm install @uniformlabs/multiliquid-svm-sdk@latest @solana/kit ``` The package declares runtime dependencies on: * `@solana/kit` * `@solana-program/token-2022` Install `@solana/kit` directly when importing Kit helpers such as `address`, `createSolanaRpc`, and `signTransaction` in application code. Node.js `>=24` is required. ## Client Initialization The SDK provides a `MultiliquidClient` class that wraps all functionality. Addresses are Kit `Address` values (branded strings), created with `address()`: ```typescript theme={null} import { address, createSolanaRpc } from "@solana/kit"; import { MultiliquidClient } from "@uniformlabs/multiliquid-svm-sdk"; const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com"); const client = new MultiliquidClient({ rpc, cluster: "mainnet-beta", // "devnet" | "mainnet-beta" commitment: "confirmed", // optional, default: "confirmed" }); const user = address("..."); ``` `cluster` selects the built-in pair registry and default program address. Both devnet and mainnet use the same swap program ID: `HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6`. Pass `programAddress` to override that default. Optional `registryApiUrl`, `registryFetchTimeoutMs`, and `onRegistryError` configure metadata-API pair discovery. ## Pair Discovery ### Metadata Registry `getPairs()` queries the Multiliquid metadata API and falls back to the bundled registry: ```typescript theme={null} const pairs = await client.getPairs(); // Returns registered pairs for the configured cluster const usdcPairs = await client.getPairs({ quoteMint: address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), }); ``` Each returned entry includes the pair PDA, both mints in the pair account's stored quote/base order, the liquidity provider, and token decimals: ```typescript theme={null} for (const pair of pairs) { console.log(pair.label); // e.g. "USDC / USTB" console.log(pair.pair); console.log(pair.quoteMint); console.log(pair.baseMint); console.log(pair.liquidityProvider); console.log(pair.quoteDecimals); // e.g. 6 console.log(pair.baseDecimals); // e.g. 6 for USTB } // Use the entry directly in swap params const pair = pairs[0]; const quote = await client.getQuote({ user, liquidityProvider: pair.liquidityProvider, inputMint: pair.quoteMint, outputMint: pair.baseMint, amount: 1_000_000_000n, swapType: SwapType.ExactIn, }); ``` `getOfflinePairs()` returns the bundled registry without an HTTP call. ### On-Chain Discovery (RPC) For dynamically discovering pairs not in the registry: ```typescript theme={null} const pairs = await client.discoverPairs({ quoteMint: USDC, }); // Returns the same PairRegistryEntry format as getPairs() ``` ## Checking Pair Status Before executing a swap, verify the pair is active. Callers pass the tokens being spent and received; the SDK resolves pair ordering: ```typescript theme={null} const status = await client.checkPauseStatus(inputMint, outputMint, lp); if (status.anyPaused) { console.log("Swap blocked:", status.pauseReasons); // e.g. ["ProgramPaused"], ["PairPaused"], ["InputPaused"], ["OutputPaused"] } ``` Pause checks cover the global config, the pair, the input and output asset configs, and any LP-stable configs on those sides. All must be unpaused for swaps to execute. Pass `pairAddress` when both on-chain pair orderings exist for the same LP and mint pair. ## Quoting The SDK offers two quoting methods: client-side math replication and on-chain simulation. Callers specify the tokens spent and received. The SDK checks both possible on-chain pair orderings and translates the result to the protocol's quote-to-base or base-to-quote direction internally. If both orderings exist for the same LP and mint pair, pass `pairAddress` to disambiguate. ### Client-Side Quote Replicates the on-chain Rust math exactly using `bigint`. Fetches current NAV prices from oracles and computes the swap result: ```typescript theme={null} import { address } from "@solana/kit"; import { SwapType, toHumanReadable } from "@uniformlabs/multiliquid-svm-sdk"; const USDC = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const USTB = address("CCz3SGVziFeLYk2xfEstkiqJfYkjaSWb2GCABYsVcjo2"); const LP = address("C8Mi6kn7ajFWuNe4ZmsR9A6fdqRYhzXFoqVBGMsdJ2Uf"); const quote = await client.getQuote({ user, liquidityProvider: LP, inputMint: USDC, outputMint: USTB, amount: 1_000_000_000n, // 1000 USDC (6 decimals) swapType: SwapType.ExactIn, }); console.log("Output:", toHumanReadable(quote.amountOut, 6)); console.log("Protocol fees:", quote.protocolFees.toString()); console.log("Input NAV:", quote.inputNav.toString()); console.log("Output NAV:", quote.outputNav.toString()); console.log("Fee token:", quote.feeTokenMint); ``` `protocolFees` is denominated in `feeTokenMint` and is paid from the LP fee vault. It is not deducted from the trader's input. The `SwapQuote` includes: | Field | Description | | :------------------- | :--------------------------------------- | | `amountIn` | Total input amount | | `amountOut` | Output amount received | | `protocolFees` | Protocol fee collected in `feeTokenMint` | | `issuerProtocolFees` | Issuer protocol-fee component | | `spreadProtocolFees` | Spread protocol-fee component | | `discountAmount` | LP fee amount (redemption or discount) | | `amountInForVault` | Input amount credited to the LP vault | | `inputNav` | Input-asset NAV price (9 decimals) | | `outputNav` | Output-asset NAV price (9 decimals) | | `feeTokenMint` | Mint used to collect protocol fees | | `feeTokenNav` | Fee-token NAV price (9 decimals) | ### Simulation Quote Builds a swap instruction with ATA creation disabled, simulates it against the validator, and parses the emitted event: ```typescript theme={null} const simQuote = await client.getQuoteViaSimulation({ user, liquidityProvider: LP, inputMint: USDC, outputMint: USTB, amount: 1_000_000_000n, swapType: SwapType.ExactIn, }); console.log("Output:", simQuote.amountOut.toString()); console.log("Compute units:", simQuote.computeUnitsConsumed.toString()); ``` The simulation quote also returns `computeUnitsConsumed` as `bigint`, which is useful for setting compute-budget instructions. ## Executing Swaps The SDK is **instruction-first**: the primary API returns Kit `Instruction` objects for maximum composability. A convenience method for building unsigned Kit transactions is also available. ### Building a Swap Transaction ```typescript theme={null} import { getBase64EncodedWireTransaction, signTransaction } from "@solana/kit"; const { transaction } = await client.buildSwapTransaction({ user, liquidityProvider: LP, inputMint: USDC, outputMint: USTB, amount: 1_000_000_000n, // 1000 USDC swapType: SwapType.ExactIn, minAmountOut: 990_000_000n, // slippage protection }); const signedTransaction = await signTransaction([userSigner.keyPair], transaction); const signature = await rpc .sendTransaction(getBase64EncodedWireTransaction(signedTransaction), { encoding: "base64", preflightCommitment: "confirmed", }) .send(); ``` The returned transaction is unsigned. A signer whose address matches `user` must sign before submission. ### Building Individual Instructions For more control, build the swap instruction separately and compose it with other instructions (for example, compute budget): ```typescript theme={null} import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from "@solana-program/compute-budget"; import { buildUnsignedTransaction } from "@uniformlabs/multiliquid-svm-sdk"; const { instruction, setupInstructions } = await client.buildSwapInstruction({ user, liquidityProvider: LP, inputMint: USDC, outputMint: USTB, amount: 1_000_000_000n, swapType: SwapType.ExactIn, minAmountOut: 990_000_000n, }); // setupInstructions contains ATA creation if needed (autoCreateAta defaults to true). // The builder also resolves Token-2022 transfer-hook accounts for known hook mints. const transaction = await buildUnsignedTransaction(rpc, user, [ getSetComputeUnitLimitInstruction({ units: 150_000 }), getSetComputeUnitPriceInstruction({ microLamports: 50_000 }), ...setupInstructions, instruction, ]); ``` The SDK does not include compute-budget instructions automatically. Integrators that need priority fees or explicit compute limits can compose `@solana-program/compute-budget` instructions as shown above. Use `getQuoteViaSimulation()` to measure actual compute units consumed. Convenience swap and liquidity builders automatically plan treasury CPI accounts, associated token accounts, NAV and APY remaining accounts, and Token-2022 transfer-hook accounts when the LP has treasury enabled. ### Swap Examples **Buy RWA with USDC (ExactIn)** Swap exactly 1000 USDC for USTB, accepting at minimum 990 USTB: ```typescript theme={null} const { transaction } = await client.buildSwapTransaction({ user, liquidityProvider: LP, inputMint: USDC, outputMint: USTB, amount: 1_000_000_000n, // 1000 USDC (6 decimals) swapType: SwapType.ExactIn, minAmountOut: 990_000_000n, // min 990 USTB (6 decimals) }); ``` **Sell RWA for USDC (ExactIn)** Swap exactly 100 USTB for USDC, accepting at minimum 95 USDC: ```typescript theme={null} const { transaction } = await client.buildSwapTransaction({ user, liquidityProvider: LP, inputMint: USTB, outputMint: USDC, amount: 100_000_000n, // 100 USTB (6 decimals) swapType: SwapType.ExactIn, minAmountOut: 95_000_000n, // min 95 USDC (6 decimals) }); ``` **Buy Exact RWA Amount (ExactOut)** Receive exactly 100 USTB, spending at most 105 USDC: ```typescript theme={null} const { transaction } = await client.buildSwapTransaction({ user, liquidityProvider: LP, inputMint: USDC, outputMint: USTB, amount: 100_000_000n, // exact 100 USTB out (6 decimals) swapType: SwapType.ExactOut, maxAmountIn: 105_000_000n, // max 105 USDC (6 decimals) }); ``` **Sell RWA for Exact USDC (ExactOut)** Receive exactly 1000 USDC, spending at most 1010 USTB: ```typescript theme={null} const { transaction } = await client.buildSwapTransaction({ user, liquidityProvider: LP, inputMint: USTB, outputMint: USDC, amount: 1_000_000_000n, // exact 1000 USDC out (6 decimals) swapType: SwapType.ExactOut, maxAmountIn: 1_010_000_000n, // max 1010 USTB (6 decimals) }); ``` ### Swap Parameters Reference | Parameter | Type | Description | | :----------------------- | :------------------------- | :------------------------------------------------------------------------------------- | | `user` | `Address` | Transaction fee payer and swap signer | | `liquidityProvider` | `Address` | LP that owns the pair | | `inputMint` | `Address` | Token the trader spends | | `outputMint` | `Address` | Token the trader receives | | `pairAddress` | `Address` (optional) | Required only when both on-chain pair orderings exist | | `amount` | `bigint` | Primary amount in native token units (input for ExactIn, output for ExactOut) | | `swapType` | `SwapType` | `ExactIn` or `ExactOut` | | `minAmountOut` | `bigint` (optional) | Minimum output for ExactIn swaps | | `maxAmountIn` | `bigint` (optional) | Maximum input for ExactOut swaps | | `userInputTokenAccount` | `Address` (optional) | Override the user's input ATA | | `userOutputTokenAccount` | `Address` (optional) | Override the user's output ATA | | `autoCreateAta` | `boolean` (optional) | Auto-create ATAs if missing (default: `true`) | | `remainingAccounts` | `AccountMeta[]` (optional) | Extra accounts appended after SDK-derived oracle, treasury, and transfer-hook accounts | Swap instruction and transaction results expose the resolved `onchainDirection` (`SwapDirection.QuoteToBase` or `SwapDirection.BaseToQuote`). ## LP Admin and Liquidity Liquidity providers create, configure, and close their own pairs, and manage vault balances, using the same instruction-first API. The SDK derives PDAs, vault authorities, token program IDs, and Token-2022 transfer-hook accounts automatically. The LP signs every builder below. For `close_pair`, the global-config `admin` is referenced as a non-signing account; the SDK fetches it from `GlobalConfig` when it is not supplied via `admin`. ### Permissioned Token Whitelisting For every permissioned mint used by an LP, the token issuer must separately whitelist (allowlist) both of these addresses: 1. **LP admin address** — the `liquidityProvider` wallet that owns and authorizes the LP's source and recipient token accounts. 2. **Vault authority PDA** — the LP-specific PDA derived from `["vault_authority", liquidityProvider]` that owns the program's vault ATAs. These are different addresses with independent issuer permissions. Whitelisting the LP admin address does **not** whitelist the vault authority PDA, and whitelisting the vault authority PDA does **not** whitelist the LP admin address. After the relevant token accounts exist, confirm that the issuer has thawed both the LP-owned token account and the PDA-owned vault ATA for each permissioned mint. `init_pair` can create the vault ATA, but the Multiliquid program and SDK cannot grant issuer approval or bypass a frozen-account restriction. `add_liquidity`, `remove_liquidity`, and swaps involving that vault will fail until the required accounts are authorized and thawed. Derive the address that must be submitted to the issuer before funding the pair. Kit PDA helpers are asynchronous: ```typescript theme={null} const lpAdmin = liquidityProvider; const [vaultAuthority] = await client.deriveVaultAuthority(lpAdmin); console.log("LP admin to whitelist:", lpAdmin); console.log("Vault authority PDA to whitelist:", vaultAuthority); ``` Token-2022 program detection and transfer-hook account resolution are automatic, but issuer whitelisting and token-account thawing are external onboarding steps. ### Create a Pair `init_pair` is LP-signed: the liquidity provider pays for the Pair PDA, LP-stable configs, vault ATAs, and UserVaultInfo accounts. Callers pass unordered `mintA` and `mintB` values. The builder compares the decoded 32-byte public keys, derives the canonical quote/base PDA order, and places optional LP-stable configs according to each side's on-chain `AssetType`. Stable-on-base, stable-on-quote, and RWA-to-RWA pairs are supported. Fee combinations where `redemptionFeeBps + discountRateBps >= 10_000` are rejected. ```typescript theme={null} const { instruction: initPairIx } = await client.buildInitPairInstruction({ liquidityProvider, mintA: USDC, mintB: USTB, redemptionFeeBps: 50, // 0.5% quote → base fee discountRateBps: 25, // 0.25% base → quote fee }); // Or get an unsigned Kit transaction directly: const { transaction: initPairTx } = await client.buildInitPairTransaction({ liquidityProvider, mintA: USDC, mintB: USTB, redemptionFeeBps: 50, discountRateBps: 25, }); ``` ### Update Pair Configuration Update and close builders resolve both possible PDA mint orders and use the existing pair account's stored quote/base layout. Pass `pairAddress` when both orders exist. ```typescript theme={null} const { instruction: updatePairIx } = await client.buildUpdatePairInstruction({ liquidityProvider, mintA: USDC, mintB: USTB, redemptionFeeBps: 10, discountRateBps: 15, paused: false, }); ``` ### Add and Remove Liquidity ```typescript theme={null} const { instruction: addLiquidityIx, setupInstructions } = await client.buildAddLiquidityInstruction({ liquidityProvider, mint: USDC, amount: 1_000_000_000n, }); const { transaction: removeLiquidityTx } = await client.buildRemoveLiquidityTransaction({ liquidityProvider, mint: USTB, amount: 500_000_000n, }); ``` Use `lpTokenAccount` when the liquidity provider uses a non-ATA token account. Set `autoCreateAta: false` if token accounts are managed externally, and use `remainingAccounts` only for extra accounts beyond those derived by the SDK. ### Close a Pair `close_pair` is LP-signed and reclaims rent for the Pair PDA, and — when the pair was the last consumer of a shared vault — the vault ATA and UserVaultInfo PDA. Any remaining vault balances are returned to the LP's recipient token accounts. The SDK reads `UserVaultInfo.used` for both vaults and only resolves Token-2022 transfer-hook accounts when the shared-vault counter shows the close will transfer. ```typescript theme={null} const { instruction: closePairIx, setupInstructions } = await client.buildClosePairInstruction({ liquidityProvider, mintA: USDC, mintB: USTB, // Optional overrides: // lpMintATokenAccount, lpMintBTokenAccount — non-ATA recipient accounts // autoCreateAta: false — manage recipient ATAs externally // admin — cached GlobalConfig admin }); const { transaction: closePairTx } = await client.buildClosePairTransaction({ liquidityProvider, mintA: USDC, mintB: USTB, }); ``` For Token-2022 mints with a transfer hook, the LP recipient token account must already exist on-chain when the builder runs if the close will transfer vault balances. The builder resolves hook accounts from current on-chain data, so a recipient ATA that only exists as a setup instruction will be rejected. Pre-create the recipient account or pass it via `lpMintATokenAccount` / `lpMintBTokenAccount`. Hook account resolution uses a build-time snapshot of the vault balance, while the program transfers the execution-time balance during `close_pair`. ## Event Parsing Parse `SwapExecuted` events from transaction logs: ```typescript theme={null} // From a transaction signature const events = await client.parseSwapEventsFromTransaction(signature); for (const event of events) { console.log("Amount in:", event.amountIn.toString()); console.log("Amount out:", event.amountOut.toString()); console.log("Protocol fee:", event.protocolFeeAmount.toString()); console.log("Direction:", event.swapDirection); } ``` ```typescript theme={null} // From raw logs (e.g., from websocket subscription) const events = client.parseSwapEventsFromLogs(logs); ``` ## Error Handling The SDK provides structured error parsing for on-chain program errors: ```typescript theme={null} try { const signedTransaction = await signTransaction([userSigner.keyPair], transaction); await rpc .sendTransaction(getBase64EncodedWireTransaction(signedTransaction), { encoding: "base64", preflightCommitment: "confirmed", }) .send(); } catch (error) { const parsed = client.parseSwapError(error); if (parsed && "category" in parsed) { console.log("Error:", parsed.name, "-", parsed.message); console.log("Category:", parsed.category); switch (parsed.category) { case "slippage": // AmountOutTooLow or AmountInTooHigh — re-quote with fresh state break; case "paused": // ProgramPaused, PairPaused, or asset/LP pause — wait or skip break; case "oracle": // InvalidNav — NAV source unavailable or divergent break; case "liquidity": // InsufficientLiquidity — try smaller amount or different pair break; case "user_funds": // Trader input account missing or underfunded break; case "input_validation": // Fix input parameters break; case "math": // MathOverflow/Underflow — trade may be too small or too large break; } } } ``` ## Amount Formatting Convert between native token amounts (with decimals) and human-readable strings. Formatting uses decimal strings and `bigint`; no floating-point arithmetic is involved: ```typescript theme={null} import { toHumanReadable, toNativeAmount } from "@uniformlabs/multiliquid-svm-sdk"; // Native to human-readable toHumanReadable(100_000_000n, 6); // "100" toHumanReadable(99_800_000_000n, 9); // "99.8" // Human-readable to native toNativeAmount("100", 6); // 100_000_000n toNativeAmount("99.8", 9); // 99_800_000_000n ``` ## Reading On-Chain State ### Fetch All Swap State (Single RPC Call) ```typescript theme={null} const state = await client.fetchSwapState(inputMint, outputMint, lp); console.log("Protocol fees:", state.globalConfig.protocolFeesBps, "bps"); console.log("Redemption fee:", state.pair.redemptionFeeBps, "bps"); console.log("Discount rate:", state.pair.discountRateBps, "bps"); console.log("Pair paused:", state.pair.paused); console.log("Input LP paused:", state.inputLpStableConfig?.paused); console.log("Output LP paused:", state.outputLpStableConfig?.paused); console.log("Program paused:", state.globalConfig.paused); ``` ### Fetch Individual Accounts ```typescript theme={null} const globalConfig = await client.fetchGlobalConfig(); const pair = await client.fetchPair(pairAddress); const assetConfig = await client.fetchAssetConfig(configAddress); const lpStableConfig = await client.fetchLpStableConfig(lpStableConfigAddress); ``` ## PDA Derivation All program accounts are deterministic PDAs. Kit PDA derivation is asynchronous; every helper returns `Promise`, whose resolved value is `[address, bump]`: ```typescript theme={null} const [globalConfig] = await client.deriveGlobalConfig(); const [assetConfig] = await client.deriveAssetConfig(mint); const [pair] = await client.derivePair(lp, quoteMint, baseMint); const [vaultAuthority] = await client.deriveVaultAuthority(lp); const [vault] = await client.deriveVault(mint, lp); const [feeVault] = await client.deriveFeeVault(feeMint); const [lpStableConfig] = await client.deriveLpStableConfig(stableMint, lp); const [programAuthority] = await client.deriveProgramAuthority(); ``` Derive an existing pair with the registry entry's stored `quoteMint` / `baseMint`. For a new pair, canonicalize the unordered mints first: ```typescript theme={null} import { canonicalizePairMints } from "@uniformlabs/multiliquid-svm-sdk"; const { quoteMint, baseMint } = canonicalizePairMints(USDC, USTB); const [pairAddress] = await client.derivePair(lp, quoteMint, baseMint); ``` LP vault token accounts are ATAs for `(mint, vaultAuthority)`, where `vaultAuthority` is the LP-specific PDA derived from `["vault_authority", lp]`. Fee vault token accounts are ATAs for `(feeMint, programAuthority)`, where `programAuthority` is the global PDA derived from `["program_authority"]`. When deriving vault or fee-vault addresses manually for Token-2022 mints, pass the Token-2022 program ID as the optional `tokenProgram` argument. Swap and liquidity builders detect the mint owner and derive the correct ATAs automatically. ## Integration Best Practices ### Security 1. **Use Hardware Wallets**: Never store private keys in code or environment variables for production 2. **Simulate First**: Always call `getQuote()` or `getQuoteViaSimulation()` before executing 3. **Set Slippage**: Always provide `minAmountOut` or `maxAmountIn` for production swaps 4. **Check Pause State**: Call `checkPauseStatus()` before building transactions ### Performance 1. **Use Client-Side Quotes**: `getQuote()` is faster than `getQuoteViaSimulation()` for most use cases 2. **Set Compute Budget**: Use `computeUnitsConsumed` from simulation to set appropriate compute limits 3. **Reuse Client**: Create one `MultiliquidClient` instance and reuse across operations 4. **Use the Registry**: `getPairs()` uses the metadata API with a bundled fallback; `getOfflinePairs()` avoids network calls ### Operational 1. **Priority Fees**: Set appropriate priority fees via compute-budget instructions for time-sensitive operations 2. **Confirmation**: Wait for `confirmed` or `finalized` commitment before treating a swap as complete 3. **Event Monitoring**: Use `parseSwapEventsFromTransaction()` to verify swap results after execution 4. **Error Recovery**: Use `parseSwapError()` to categorize failures and implement appropriate retry logic ## Testing ### Devnet Configure the client for devnet testing: ```typescript theme={null} const rpc = createSolanaRpc("https://api.devnet.solana.com"); const client = new MultiliquidClient({ rpc, cluster: "devnet", }); // Devnet pairs use mock tokens — see SDK registry for addresses const devnetPairs = await client.getPairs(); ``` ### Mainnet ```typescript theme={null} const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com"); const client = new MultiliquidClient({ rpc, cluster: "mainnet-beta", }); ``` ## Support and Resources * **Protocol Website**: [https://www.multiliquid.xyz/](https://www.multiliquid.xyz/) * **Program IDL**: See [Program IDL](/svm/idl) page * **Deployment Addresses**: See [Deployments](/svm/deployments) page # Ladder Pricing Model Source: https://docs.multiliquid.xyz/svm/guides/ladder-pricing-model How an LP can implement a rolling 24-hour laddered pricing model on Multiliquid SVM using pair fee updates and vault liquidity management ## Overview The Multiliquid SVM program does not implement a native laddered pricing model on chain. Instead, each pair stores one live discount rate and one live redemption fee. An LP can still build a laddered pricing model on top of those fields by using an off-chain worker to monitor rolling volume and update fees as thresholds are crossed. * Each `Pair` has one `redemption_fee_bps` and one `discount_rate_bps`, which can be updated by the LP admin every block if needed * Each `Pair` sources liquidity from two vaults: one vault for the stablecoin and one vault for the RWA * Swaps consume whatever fee values are currently stored in the pair Given that design, a laddered pricing model has to be enforced operationally. This page uses `discount_rate_bps` for the primary example, but the same approach can be mirrored onto `redemption_fee_bps`. **Example scenario:** An LP admin wants to keep up to \$2,500,000 of live stablecoin inventory available for `RWA1` while applying a tiered discount schedule across rolling 24-hour `AssetToStable` volume. The first \~\$2.5M should clear at a 1% discount, giving the user 99% of the `RWA1` NAV value in stablecoin. From \$2.5M to \$5M, the discount is set at 5%. From \$5M to \$10M, the discount is set at 15%. | Rolling 24-hour cumulative volume | Active `discount_rate_bps` | Discount | | :-------------------------------- | :------------------------- | :------- | | `$0` to `< $2.5M` | `100` | `1.00%` | | `$2.5M` to `< $5M` | `500` | `5.00%` | | `$5M` to `< $10M` | `1500` | `15.00%` | This pricing model is enforced by an off-chain worker that tracks cumulative rolling 24-hour volume for swaps on `RWA1`. 1. The LP keeps between `$1,000,000` and `$2,500,000` of live stablecoin inventory in the vault at any given time 2. An off-chain worker measures rolling 24-hour volume 3. The worker decides which tier is currently active 4. When a threshold is crossed, the worker updates `discount_rate_bps` via `update_pair` 5. The LP uses `add_liquidity` to top up the stablecoin vault when balances fall below the chosen threshold, for example `$1,000,000`, potentially sourcing capital from a yield-generating product such as Liquid Treasury ## What The Program Gives You ### Pair Fee Fields The pair stores only the **currently active** fee values: ```rust theme={null} pub struct Pair { pub redemption_fee_bps: u16, pub discount_rate_bps: u16, pub stable_coin_mint_address: Pubkey, pub asset_token_mint_address: Pubkey, pub liquidity_provider: Pubkey, pub paused: bool, pub bump: u8, } ``` Operationally, this means: * `redemption_fee_bps` is the live fee for `StableToAsset` * `discount_rate_bps` is the live fee for `AssetToStable` * the current ladder rung is represented by the current value stored in the pair ### Live Fee Updates The LP can rotate the active rung with [`update_pair`](/svm/instructions/pair#update_pair): ```rust theme={null} pub fn update_pair( ctx: Context, redemption_fee_bps: u16, discount_rate_bps: u16, paused: bool, ) -> Result<()> ``` This is the key mechanism that makes the ladder possible despite only having one fee field per direction. ### Shared Vault Liquidity Liquidity is not stored per pair. It is stored per **LP + mint**: * one vault per LP and stable mint * one vault per LP and RWA mint On chain, the LP component is represented by the `vault_authority` PDA derived from `["vault_authority", liquidity_provider]`. Each vault is the associated token account for `(mint, vault_authority)`, not a token account owned by the global program authority. So: * `add_liquidity` deposits into the LP's vault for a mint * `remove_liquidity` withdraws from that same vault * if the LP has multiple pairs using the same mint, those pairs share the same vault balance This shared-vault model is what lets the LP keep only a bounded amount of capital on chain while still supporting more cumulative daily turnover. ## Directional Fee And Inventory Mechanics The two fee fields are directional: | Swap direction | Pair field used | Inventory that leaves LP vault | Inventory that enters LP vault | | :-------------- | :------------------- | :----------------------------- | :----------------------------- | | `StableToAsset` | `redemption_fee_bps` | RWA | Stablecoin | | `AssetToStable` | `discount_rate_bps` | Stablecoin | RWA | For the scenario in this page, the ladder is a **discount ladder**, so the relevant direction is: * user sells RWA to the LP * LP pays out stablecoin * the live price lever is `discount_rate_bps` * the constrained inventory is the LP's **stablecoin vault** That is why this model can be run with: * a rolling 24-hour counter off chain * periodic `update_pair` calls that change only `discount_rate_bps` * periodic stablecoin `add_liquidity` and `remove_liquidity` calls In many deployments, `redemption_fee_bps` can stay fixed while only `discount_rate_bps` changes with the ladder. If the LP also wants a ladder on the opposite direction, the same pattern applies: * maintain a separate rolling notional book for `StableToAsset` * choose the active `redemption_fee_bps` from that book * pass both the current `redemption_fee_bps` and current `discount_rate_bps` into `update_pair` In other words, the pair does not store an entire schedule. It stores the **currently active rung for each direction**. ## Reference Ladder For the example strategy, define the active `discount_rate_bps` from rolling 24-hour notional: | Rolling 24-hour cumulative volume | Active `discount_rate_bps` | Discount | | :-------------------------------- | :------------------------- | :------- | | `$0` to `< $2,500,000` | `100` | `1.00%` | | `$2,500,000` to `< $5,000,000` | `500` | `5.00%` | | `$5,000,000` to `< $10,000,000` | `1500` | `15.00%` | This should be read as: * there is one active rung at any point in time * the worker recomputes the rolling 24-hour total * the worker updates the pair to the fee corresponding to the current band The program does not transition bands automatically. The LP's worker does. ## How To Measure The Rolling 24-Hour Volume For a discount ladder on `AssetToStable`, the cleanest operational metric is: ```text theme={null} stable_outflow_per_fill = amount_out + protocol_fee_amount ``` Why this works: * on `AssetToStable`, the user receives stablecoin from the LP stable vault * protocol fees are also paid out of that same stable vault * so `amount_out + protocol_fee_amount` is the total stable inventory consumed by the fill That makes the rolling ladder straightforward to compute: ```text theme={null} rolling_24h_stable_outflow = Σ(amount_out + protocol_fee_amount) for all AssetToStable swaps in the last 24 hours ``` For a `$1.00` stablecoin such as USDC, that rolling stable outflow is already a practical dollar proxy. For `AssetToStable`, do not rely on the raw `amount_in` field from the `SwapExecuted` event as your primary volume metric. In this direction, the useful inventory measure is stablecoin consumed from the LP vault, which is `amount_out + protocol_fee_amount`. ## How The Live Inventory Band Works Assume the LP wants to keep up to `$2,500,000` of stablecoin live in the vault, and refill when the balance drops below `$1,000,000`. Define: * target stable vault balance: `$2,500,000` * refill floor: `$1,000,000` * refill amount: `target - current_vault_balance` Then the worker applies the following policy: * if the stable vault balance is below `$1,000,000`, call `add_liquidity` with the delta back to `$2,500,000` * if the stable vault balance is above `$2,500,000`, call `remove_liquidity` for the excess This keeps the live capital band between `$1,000,000` and `$2,500,000` while allowing the LP to manage on-chain inventory independently from the rolling fee ladder. That is the core distinction: * **live liquidity** is what is currently sitting in the vault * **rolling 24-hour volume** is what has traded through the vault over time The LP can therefore run a rolling ladder that tops out at `$10,000,000` while keeping between `$1,000,000` and `$2,500,000` of stablecoin inventory on chain, replenishing inventory as needed during the day. ## Why `remove_liquidity` Matters `remove_liquidity` is just as important as `add_liquidity`. In this model: * `AssetToStable` flow drains stablecoin from the stable vault and accumulates RWA in the RWA vault * `StableToAsset` flow does the opposite: it adds stablecoin to the stable vault and drains RWA from the RWA vault That means the worker can use `remove_liquidity` in two practical ways: * sweep excess stablecoin out of the stable vault when reverse flow pushes it above the `$2,500,000` cap * sweep accumulated RWA out of the RWA vault after users sell RWA into the LP This lets the LP keep only the intended amount of on-chain capital exposed to the pair while moving the rest back to custody, treasury, or an execution wallet. ## Single-Pair And Multi-Pair Variants ### Single Pair For one `RWA / stablecoin` pair, the model is simple: * track rolling 24-hour volume for that pair * update that pair's `discount_rate_bps` * manage the pair's shared stable vault balance ### Multiple Pairs Sharing The Same Stablecoin If the LP wants one ladder across several pairs that all share the same stablecoin: * the stable vault is already shared across those pairs because the vault is keyed by `LP + stable mint` * the worker should aggregate rolling notional across the full set of managed pairs * the worker should push the same active `discount_rate_bps` to every pair in that set * stablecoin rebalancing is done once at the shared stable vault This is the cleanest version of a cross-pair ladder on the current program. ### Multiple Pairs Across Different Stablecoins If the LP spans multiple stablecoins: * each stablecoin still has its own vault * the worker must normalize fills into a common USD measure off chain * `update_pair` must still be called per pair * liquidity rebalancing must still be done per stable vault So the ladder can still be global across the LP's book, but the accounting is entirely off chain. Contact the Multiliquid team for guidance on building an off-chain worker that tracks rolling 24-hour volume, rotates pair fees with `update_pair`, and rebalances vault inventory with `add_liquidity` and `remove_liquidity`. ## What To Do At 10M Rolling Volume Because this page assumes no pause controls, the `$10,000,000` level should be treated as a **soft operating budget**, not as a hard protocol-enforced stop. In practice, the worker usually does one or both of the following once rolling 24-hour volume reaches the final band: * keep the pair on the highest configured discount tier * stop topping the stable vault back up after it drains below the LP's desired exposure That is enough to express the pricing model operationally, but it is important to be precise about the limit: * the fee schedule is controllable * the live inventory band is controllable * the exact rolling-volume ceiling is **not** enforced atomically by the program Without an on-chain rolling-volume counter, fee-tier transitions are not atomic with swap execution. A trade can cross a threshold before the worker's next `update_pair` transaction lands. The model works well as an LP-admin policy, but it is not a hard on-chain invariant. ## Practical Summary The current SVM swap program is sufficient to run this laddered model today: * use `discount_rate_bps` as the current active rung for `AssetToStable` * keep `redemption_fee_bps` fixed unless the LP also wants a ladder on `StableToAsset` * measure rolling 24-hour stable outflow off chain * update the pair fee when the rolling total enters a new band * keep only `$1,000,000` to `$2,500,000` of live stablecoin inventory in the vault with `add_liquidity` and `remove_liquidity` * if the ladder spans multiple pairs, aggregate volume off chain and push the selected rung to each managed pair * treat the `$10,000,000` threshold as an operational target rather than a hard on-chain invariant That is the correct way to implement a laddered LP pricing model on the current protocol without adding any new on-chain state. *** Full reference for pair instructions, account structures, and liquidity management SDK installation, quoting, and swap execution # Program IDL Source: https://docs.multiliquid.xyz/svm/idl Interface Definition Language for the Multiliquid Swap Program on Solana The program IDL (Interface Definition Language) is required for interacting with the Multiliquid Swap Program via the Anchor framework or `@solana/web3.js`. It defines all instructions, accounts, types, and events. The code block includes a built-in copy button. Click the copy icon in the top-right corner to copy the full IDL to your clipboard. ## Swap Program Handles all swap operations between RWAs and stablecoins, asset configuration, pair management, liquidity operations, and fee collection. **Program ID**: `HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6` ```json swap_program.json theme={null} { "address": "HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6", "metadata": { "name": "swap_program", "version": "0.1.0", "spec": "0.1.0", "description": "Created with Anchor" }, "instructions": [ { "name": "add_liquidity", "discriminator": [ 181, 157, 89, 67, 143, 182, 52, 72 ], "accounts": [ { "name": "liquidity_provider", "docs": [ "the liquidity provider that provides the tokens" ], "signer": true }, { "name": "mint_address", "docs": [ "the mint address of the token" ] }, { "name": "lp_token_account", "docs": [ "the token account of the liquidity provider" ], "writable": true }, { "name": "vault_token_account", "docs": [ "the vault token account (for the program)" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "lp_vault_authority" }, { "kind": "account", "path": "token_program" }, { "kind": "account", "path": "mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "lp_vault_authority", "docs": [ "the LP-specific vault authority used in vault ATA constraints" ], "pda": { "seeds": [ { "kind": "const", "value": [ 118, 97, 117, 108, 116, 95, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "global_config", "docs": [ "the global config, used to check if the program is paused" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "token_program", "docs": [ "the token program, could be either the token2022 or the legacy token program" ] } ], "args": [ { "name": "amount", "type": "u64" } ] }, { "name": "claim_fees", "discriminator": [ 82, 251, 233, 156, 12, 52, 184, 202 ], "accounts": [ { "name": "stable_coin_mint_address", "docs": [ "the mint address of the stable coin" ] }, { "name": "fee_wallet_token_account", "docs": [ "the token account of the fee wallet" ], "writable": true }, { "name": "program_authority", "docs": [ "the program authority, used to sign the CPI transfer" ], "pda": { "seeds": [ { "kind": "const", "value": [ 112, 114, 111, 103, 114, 97, 109, 95, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] } ] } }, { "name": "fee_token_account", "docs": [ "the fee vault token account, used to transfer the tokens to the fee wallet" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "program_authority" }, { "kind": "account", "path": "token_program" }, { "kind": "account", "path": "stable_coin_mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "global_config", "docs": [ "the global config, used to check if the program is paused, and check fee wallet" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "token_program", "docs": [ "the token program, could be either the token2022 or the legacy token program" ] } ], "args": [ { "name": "fee_wallet", "type": "pubkey" } ] }, { "name": "close_pair", "discriminator": [ 45, 8, 194, 47, 65, 139, 172, 120 ], "accounts": [ { "name": "admin", "docs": [ "the admin, checked by has_one constraint with global config" ], "writable": true, "relations": [ "global_config" ] }, { "name": "liquidity_provider", "docs": [ "the liquidity provider, will receive tokens and reclaimed rent back if necessary" ], "writable": true, "signer": true }, { "name": "pair", "docs": [ "the pair account, will be closed" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 112, 97, 105, 114 ] }, { "kind": "account", "path": "liquidity_provider" }, { "kind": "account", "path": "stable_coin_mint_address" }, { "kind": "account", "path": "asset_token_mint_address" } ] } }, { "name": "stable_coin_mint_address", "docs": [ "the mint address of the pair's stable coin" ] }, { "name": "asset_token_mint_address", "docs": [ "the mint address of the pair's asset token" ] }, { "name": "lp_vault_authority", "docs": [ "the LP-specific vault authority, used to sign LP vault transfers" ], "pda": { "seeds": [ { "kind": "const", "value": [ 118, 97, 117, 108, 116, 95, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "stable_coin_vault_token_account", "docs": [ "the vault token account for the stable coin" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "lp_vault_authority" }, { "kind": "account", "path": "token_program_stable" }, { "kind": "account", "path": "stable_coin_mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "asset_token_vault_token_account", "docs": [ "the vault token account for the asset token" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "lp_vault_authority" }, { "kind": "account", "path": "token_program_asset" }, { "kind": "account", "path": "asset_token_mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "lp_stable_token_account", "docs": [ "the token account of the liquidity provider for the stable coin" ], "writable": true }, { "name": "lp_asset_token_account", "docs": [ "the token account of the liquidity provider for the asset token" ], "writable": true }, { "name": "lp_vault_stable_pda", "docs": [ "the LP vault info for the stable coin" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 117, 115, 101, 114, 95, 118, 97, 117, 108, 116, 95, 105, 110, 102, 111 ] }, { "kind": "account", "path": "stable_coin_mint_address" }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "lp_vault_asset_pda", "docs": [ "the LP vault info for the asset token" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 117, 115, 101, 114, 95, 118, 97, 117, 108, 116, 95, 105, 110, 102, 111 ] }, { "kind": "account", "path": "asset_token_mint_address" }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "asset_config_stable", "docs": [ "the asset config (stable)" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "stable_coin_mint_address" } ] } }, { "name": "asset_config_rwa", "docs": [ "the asset config (asset)" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "asset_token_mint_address" } ] } }, { "name": "global_config", "docs": [ "the global config, used to check if the program is paused, and check admin" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "token_program_stable", "docs": [ "the token program for the stable coin, could be either the token2022 or the legacy token program" ] }, { "name": "token_program_asset", "docs": [ "the token program for the asset token, could be either the token2022 or the legacy token program" ] } ], "args": [] }, { "name": "confirm_new_admin", "discriminator": [ 3, 109, 83, 170, 106, 54, 173, 117 ], "accounts": [ { "name": "global_config", "docs": [ "the global config account, will be updated" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "new_admin", "signer": true } ], "args": [] }, { "name": "init_asset_config_account", "discriminator": [ 32, 66, 21, 159, 176, 230, 59, 11 ], "accounts": [ { "name": "asset_config", "docs": [ "the RWA config account, will be initialized if needed" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "mint_address" } ] } }, { "name": "mint_address", "docs": [ "the mint address of the RWA token" ] }, { "name": "admin", "writable": true, "signer": true, "relations": [ "global_config" ] }, { "name": "fee_vault_token_account", "docs": [ "the fee vault token account, used to store the fees" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "program_authority" }, { "kind": "account", "path": "token_program" }, { "kind": "account", "path": "mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "program_authority", "docs": [ "the program authority" ], "pda": { "seeds": [ { "kind": "const", "value": [ 112, 114, 111, 103, 114, 97, 109, 95, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] } ] } }, { "name": "global_config", "docs": [ "the global config, used to check admin" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "token_program", "docs": [ "the token program for the asset token, could be either the token2022 or the legacy token program" ] }, { "name": "system_program", "docs": [ "the system program, used to initialize the account" ], "address": "11111111111111111111111111111111" }, { "name": "associated_token_program", "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" } ], "args": [ { "name": "nav_data", "type": { "vec": { "defined": { "name": "NavData" } } } }, { "name": "price_difference_bps", "type": "u16" }, { "name": "asset_type", "type": { "defined": { "name": "AssetType" } } } ] }, { "name": "init_global_config", "discriminator": [ 140, 136, 214, 48, 87, 0, 120, 255 ], "accounts": [ { "name": "global_config", "docs": [ "the global config account, will be initialized" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "admin", "docs": [ "the admin, will be the admin of the global config" ], "writable": true, "signer": true }, { "name": "system_program", "docs": [ "the system program, used to initialize the account" ], "address": "11111111111111111111111111111111" } ], "args": [ { "name": "fee_wallet", "type": "pubkey" }, { "name": "protocol_fees_bps", "type": "u16" } ] }, { "name": "init_pair", "discriminator": [ 210, 79, 92, 60, 153, 69, 57, 157 ], "accounts": [ { "name": "admin", "docs": [ "the admin, checked by has_one constraint with global config" ], "relations": [ "global_config" ] }, { "name": "liquidity_provider", "docs": [ "the liquidity provider, must sign to initialize its own pair" ], "writable": true, "signer": true }, { "name": "pair", "docs": [ "the pair account, will be initialized" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 112, 97, 105, 114 ] }, { "kind": "account", "path": "liquidity_provider" }, { "kind": "account", "path": "stable_coin_mint_address" }, { "kind": "account", "path": "asset_token_mint_address" } ] } }, { "name": "stable_coin_mint_address", "docs": [ "the mint address of the pair's stable coin" ] }, { "name": "asset_token_mint_address", "docs": [ "the mint address of the pair's asset token" ] }, { "name": "lp_vault_authority", "docs": [ "the LP-specific vault authority used to own LP's token accounts" ], "pda": { "seeds": [ { "kind": "const", "value": [ 118, 97, 117, 108, 116, 95, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "stable_coin_vault_token_account", "docs": [ "the vault token account for the stable coin" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "lp_vault_authority" }, { "kind": "account", "path": "token_program_stable" }, { "kind": "account", "path": "stable_coin_mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "asset_token_vault_token_account", "docs": [ "the vault token account for the asset token" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "lp_vault_authority" }, { "kind": "account", "path": "token_program_asset" }, { "kind": "account", "path": "asset_token_mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "lp_vault_stable_pda", "docs": [ "the LP vault info for the stable coin" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 117, 115, 101, 114, 95, 118, 97, 117, 108, 116, 95, 105, 110, 102, 111 ] }, { "kind": "account", "path": "stable_coin_mint_address" }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "lp_vault_asset_pda", "docs": [ "the LP vault info for the asset token" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 117, 115, 101, 114, 95, 118, 97, 117, 108, 116, 95, 105, 110, 102, 111 ] }, { "kind": "account", "path": "asset_token_mint_address" }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "global_config", "docs": [ "the global config, used to check admin" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "lp_stable_config", "docs": [ "the LP stable config, will be initialized if needed" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 108, 112, 95, 115, 116, 97, 98, 108, 101, 95, 99, 111, 110, 102, 105, 103 ] }, { "kind": "account", "path": "stable_coin_mint_address" }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "asset_config_stable", "docs": [ "the asset config (stable)" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "stable_coin_mint_address" } ] } }, { "name": "asset_config_rwa", "docs": [ "the asset config (asset)" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "asset_token_mint_address" } ] } }, { "name": "token_program_stable", "docs": [ "the token program for the stable coin, could be either the token2022 or the legacy token program" ] }, { "name": "token_program_asset", "docs": [ "the token program for the asset token, could be either the token2022 or the legacy token program" ] }, { "name": "associated_token_program", "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" }, { "name": "system_program", "docs": [ "the system program, used to initialize the account" ], "address": "11111111111111111111111111111111" } ], "args": [ { "name": "redemption_fee_bps", "type": "u16" }, { "name": "discount_rate_bps", "type": "u16" } ] }, { "name": "remove_liquidity", "discriminator": [ 80, 85, 209, 72, 24, 206, 177, 108 ], "accounts": [ { "name": "liquidity_provider", "docs": [ "the liquidity provider, will receive the tokens back" ], "signer": true }, { "name": "mint_address", "docs": [ "the mint address of the token" ] }, { "name": "lp_token_account", "docs": [ "the token account of the liquidity provider" ], "writable": true }, { "name": "vault_token_account", "docs": [ "the vault token account for the token" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "lp_vault_authority" }, { "kind": "account", "path": "token_program" }, { "kind": "account", "path": "mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "lp_vault_authority", "docs": [ "the LP-specific vault authority, used to sign the CPI transfers" ], "pda": { "seeds": [ { "kind": "const", "value": [ 118, 97, 117, 108, 116, 95, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "global_config", "docs": [ "the global config, used to check if the program is paused" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "token_program", "docs": [ "the token program for the token, could be either the token2022 or the legacy token program" ] } ], "args": [ { "name": "amount", "type": "u64" } ] }, { "name": "set_new_admin", "discriminator": [ 62, 156, 4, 148, 79, 162, 148, 252 ], "accounts": [ { "name": "global_config", "docs": [ "the global config account, will be updated" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "admin", "docs": [ "the multi liquid admin" ], "signer": true, "relations": [ "global_config" ] } ], "args": [ { "name": "new_admin", "type": "pubkey" } ] }, { "name": "set_paused_for_asset", "discriminator": [ 116, 141, 145, 189, 67, 108, 178, 238 ], "accounts": [ { "name": "global_config", "docs": [ "global config account, used to check the admin" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "asset_config", "docs": [ "RWA config account, used to update the paused state" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "mint_address" } ] } }, { "name": "mint_address", "docs": [ "the mint address of the RWA token" ] }, { "name": "admin", "docs": [ "the multi liquid admin" ], "writable": true, "signer": true, "relations": [ "global_config" ] } ], "args": [ { "name": "paused", "type": "bool" } ] }, { "name": "set_paused_for_lp_stable_config", "discriminator": [ 214, 35, 14, 32, 51, 46, 101, 150 ], "accounts": [ { "name": "global_config", "docs": [ "global config account, used to check if the program is paused" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "lp_stable_config", "docs": [ "LP stable config account, used to update the paused state" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 108, 112, 95, 115, 116, 97, 98, 108, 101, 95, 99, 111, 110, 102, 105, 103 ] }, { "kind": "account", "path": "mint_address" }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "mint_address", "docs": [ "the mint address of the stable coin" ] }, { "name": "liquidity_provider", "docs": [ "the liquidity provider" ] }, { "name": "signer", "docs": [ "the signer, could be either multiliquid admin, or the liquidity provider" ], "signer": true } ], "args": [ { "name": "paused", "type": "bool" } ] }, { "name": "swap", "discriminator": [ 248, 198, 158, 145, 225, 117, 135, 200 ], "accounts": [ { "name": "user", "docs": [ "the trader" ], "signer": true }, { "name": "pair", "docs": [ "the pair account" ], "pda": { "seeds": [ { "kind": "const", "value": [ 112, 97, 105, 114 ] }, { "kind": "account", "path": "liquidity_provider" }, { "kind": "account", "path": "stable_coin_mint_address" }, { "kind": "account", "path": "asset_token_mint_address" } ] } }, { "name": "program_authority", "docs": [ "the program authority, used to sign the CPI transfers" ], "pda": { "seeds": [ { "kind": "const", "value": [ 112, 114, 111, 103, 114, 97, 109, 95, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] } ] } }, { "name": "liquidity_provider", "docs": [ "liquidity provider, must be the liquidity provider of the pair" ] }, { "name": "lp_vault_authority", "docs": [ "the LP-specific vault authority, used to sign LP vault transfers" ], "pda": { "seeds": [ { "kind": "const", "value": [ 118, 97, 117, 108, 116, 95, 97, 117, 116, 104, 111, 114, 105, 116, 121 ] }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "stable_coin_mint_address", "docs": [ "the mint address of the stable coin" ] }, { "name": "asset_token_mint_address", "docs": [ "the mint address of the asset token" ] }, { "name": "asset_token_user_token_account", "docs": [ "the token account of the asset token, owned by the trader" ], "writable": true }, { "name": "stable_coin_user_token_account", "docs": [ "the token account of the stable coin, owned by the trader" ], "writable": true }, { "name": "stable_coin_vault_token_account", "docs": [ "the vault token account for the stable coin" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "lp_vault_authority" }, { "kind": "account", "path": "token_program_stable" }, { "kind": "account", "path": "stable_coin_mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "asset_token_vault_token_account", "docs": [ "the vault token account for the asset token" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "lp_vault_authority" }, { "kind": "account", "path": "token_program_asset" }, { "kind": "account", "path": "asset_token_mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "global_config", "docs": [ "the global config, used to check if the program is paused" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "fee_token_account", "docs": [ "the fee vault token account for the stable coin" ], "writable": true, "pda": { "seeds": [ { "kind": "account", "path": "program_authority" }, { "kind": "account", "path": "token_program_stable" }, { "kind": "account", "path": "stable_coin_mint_address" } ], "program": { "kind": "const", "value": [ 140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218, 255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89 ] } } }, { "name": "rwa_config", "docs": [ "the RWA config account for the asset token" ], "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "asset_token_mint_address" } ] } }, { "name": "stable_config", "docs": [ "the stable config account for the stable coin" ], "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "stable_coin_mint_address" } ] } }, { "name": "lp_stable_config", "docs": [ "the LP stable config account for the stable coin" ], "pda": { "seeds": [ { "kind": "const", "value": [ 108, 112, 95, 115, 116, 97, 98, 108, 101, 95, 99, 111, 110, 102, 105, 103 ] }, { "kind": "account", "path": "stable_coin_mint_address" }, { "kind": "account", "path": "liquidity_provider" } ] } }, { "name": "token_program_stable", "docs": [ "the token program for the stable coin, could be either the token2022 or the legacy token program" ] }, { "name": "token_program_asset", "docs": [ "the token program for the asset token, could be either the token2022 or the legacy token program" ] } ], "args": [ { "name": "amount", "type": "u64" }, { "name": "min_amount_out", "type": { "option": "u64" } }, { "name": "max_amount_in", "type": { "option": "u64" } }, { "name": "swap_direction", "type": { "defined": { "name": "SwapDirection" } } }, { "name": "swap_type", "type": { "defined": { "name": "SwapType" } } } ] }, { "name": "update_asset_config_account", "discriminator": [ 217, 201, 225, 177, 234, 99, 234, 162 ], "accounts": [ { "name": "asset_config", "docs": [ "the RWA config account, will be updated" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 97, 115, 115, 101, 116 ] }, { "kind": "account", "path": "mint_address" } ] } }, { "name": "mint_address", "docs": [ "the mint address of the RWA token (used in seeds)" ] }, { "name": "admin", "docs": [ "the multi liquid admin" ], "signer": true, "relations": [ "global_config" ] }, { "name": "global_config", "docs": [ "the global config account, used to check the admin" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } } ], "args": [ { "name": "nav_data", "type": { "vec": { "defined": { "name": "NavData" } } } }, { "name": "price_difference_bps", "type": "u16" }, { "name": "asset_type", "type": { "defined": { "name": "AssetType" } } } ] }, { "name": "update_global_config", "discriminator": [ 164, 84, 130, 189, 111, 58, 250, 200 ], "accounts": [ { "name": "global_config", "docs": [ "the global config account, will be updated" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } }, { "name": "admin", "docs": [ "the multi liquid admin" ], "signer": true, "relations": [ "global_config" ] } ], "args": [ { "name": "fee_wallet", "type": { "option": "pubkey" } }, { "name": "paused", "type": { "option": "bool" } }, { "name": "protocol_fees_bps", "type": { "option": "u16" } } ] }, { "name": "update_pair", "discriminator": [ 176, 62, 36, 215, 255, 206, 35, 12 ], "accounts": [ { "name": "liquidity_provider", "docs": [ "the liquidity provider, must be the liquidity provider of the pair" ], "signer": true, "relations": [ "pair" ] }, { "name": "pair", "docs": [ "the pair account, will be updated" ], "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ 112, 97, 105, 114 ] }, { "kind": "account", "path": "liquidity_provider" }, { "kind": "account", "path": "stable_coin_mint_address" }, { "kind": "account", "path": "asset_token_mint_address" } ] } }, { "name": "stable_coin_mint_address", "docs": [ "the mint address of the stable coin (used in seeds)" ] }, { "name": "asset_token_mint_address", "docs": [ "the mint address of the asset token (used in seeds)" ] }, { "name": "global_config", "docs": [ "the global config account, used to check if the program is paused" ], "pda": { "seeds": [ { "kind": "const", "value": [ 103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103 ] } ] } } ], "args": [ { "name": "redemption_fee_bps", "type": "u16" }, { "name": "discount_rate_bps", "type": "u16" }, { "name": "paused", "type": "bool" } ] } ], "accounts": [ { "name": "AssetConfig", "discriminator": [ 57, 112, 247, 166, 247, 64, 140, 23 ] }, { "name": "GlobalConfig", "discriminator": [ 149, 8, 156, 202, 160, 252, 176, 217 ] }, { "name": "LpStableConfig", "discriminator": [ 178, 48, 23, 140, 152, 86, 22, 226 ] }, { "name": "Pair", "discriminator": [ 85, 72, 49, 176, 182, 228, 141, 82 ] }, { "name": "UserVaultInfo", "discriminator": [ 100, 30, 4, 32, 178, 226, 105, 0 ] } ], "events": [ { "name": "SwapExecuted", "discriminator": [ 150, 166, 26, 225, 28, 89, 38, 79 ] } ], "errors": [ { "code": 6000, "name": "InvalidNav", "msg": "Invalid NAV" }, { "code": 6001, "name": "ConfidenceTooLow", "msg": "Confidence too low" }, { "code": 6002, "name": "OutOfRange", "msg": "Out of range" }, { "code": 6003, "name": "MustProvideAtLeastOneNavData", "msg": "Must provide at least one NAV data" }, { "code": 6004, "name": "MustProvideHardcodedPrice", "msg": "Must provide hardcoded price" }, { "code": 6005, "name": "ProgramPaused", "msg": "Program paused" }, { "code": 6006, "name": "AmountMustBePositive", "msg": "Amount must be positive" }, { "code": 6007, "name": "InsufficientLiquidity", "msg": "Insufficient liquidity" }, { "code": 6008, "name": "PairPaused", "msg": "Pair paused" }, { "code": 6009, "name": "RwaPaused", "msg": "RWA paused" }, { "code": 6010, "name": "StablePaused", "msg": "Stable paused" }, { "code": 6011, "name": "Max5NavData", "msg": "Max 5 NAV data" }, { "code": 6012, "name": "ProtocolFeesMustBePositive", "msg": "Protocol fees must be positive" }, { "code": 6013, "name": "DiscountMustBePositive", "msg": "Discount must be positive" }, { "code": 6014, "name": "AmountInMustBePositive", "msg": "Amount in must be positive" }, { "code": 6015, "name": "AmountOutMustBePositive", "msg": "Amount out must be positive" }, { "code": 6016, "name": "PriceDecimalsTooLarge", "msg": "Price decimals too large" }, { "code": 6017, "name": "Unauthorized", "msg": "Unauthorized" }, { "code": 6018, "name": "MathOverflow", "msg": "Math Overflow" }, { "code": 6019, "name": "MathUnderflow", "msg": "Math Underflow" }, { "code": 6020, "name": "InvalidAssetType", "msg": "Invalid asset type" }, { "code": 6021, "name": "NavMustBePositive", "msg": "Nav must be positive" }, { "code": 6022, "name": "FeesOutOfRange", "msg": "Fees out of range" }, { "code": 6023, "name": "AssetConfigInUse", "msg": "Asset config in use" }, { "code": 6024, "name": "InvalidNewAdmin", "msg": "Invalid new admin" }, { "code": 6025, "name": "InvalidAccountPublicKey", "msg": "Invalid account public key" }, { "code": 6026, "name": "AmountInTooHigh", "msg": "Amount in too high" }, { "code": 6027, "name": "AmountOutTooLow", "msg": "Amount out too low" }, { "code": 6028, "name": "InvalidFeedId", "msg": "Invalid pyth feed id" }, { "code": 6029, "name": "InvalidMaxAge", "msg": "Pyth max age must be > 0" }, { "code": 6030, "name": "MaxAgeTooLarge", "msg": "Pyth max age too high" }, { "code": 6031, "name": "ConfBpsTooLarge", "msg": "Pyth max conf too high" }, { "code": 6032, "name": "MissingPythAccount", "msg": "Pyth feed missing from remaining accounts" }, { "code": 6033, "name": "FeedIdMismatch", "msg": "Pyth feed mismatch" } ], "types": [ { "name": "AssetConfig", "docs": [ "State that tracks configurations related to a specific asset (RWA or stable).", "PDA seeds [b\"asset\", asset_mint_address]" ], "type": { "kind": "struct", "fields": [ { "name": "mint_address", "docs": [ "asset mint address" ], "type": "pubkey" }, { "name": "nav_data", "docs": [ "NAV data" ], "type": { "vec": { "defined": { "name": "NavData" } } } }, { "name": "price_difference_bps", "docs": [ "the max and min prices fetched with the vec above should not have", "a bigger difference than this BPS" ], "type": "u16" }, { "name": "paused", "docs": [ "whether this asset is paused at a program level" ], "type": "bool" }, { "name": "version", "docs": [ "version of the NavData struct" ], "type": "u8" }, { "name": "asset_type", "docs": [ "type of the asset" ], "type": { "defined": { "name": "AssetType" } } }, { "name": "used_in_pairs_count", "docs": [ "how many times this asset is set in a pair" ], "type": "u16" }, { "name": "bump", "docs": [ "bump" ], "type": "u8" }, { "name": "padding", "docs": [ "padding in case we need to add more fields in the future (128 bytes for each NavData)" ], "type": { "vec": { "array": [ "u8", 128 ] } } } ] } }, { "name": "AssetType", "type": { "kind": "enum", "variants": [ { "name": "Rwa" }, { "name": "Stable" } ] } }, { "name": "GlobalConfig", "docs": [ "Multiliquid swap program global configration", "PDA seeds [b\"global_config\"]" ], "type": { "kind": "struct", "fields": [ { "name": "admin", "docs": [ "MultiLiquid admin" ], "type": "pubkey" }, { "name": "fee_wallet", "docs": [ "Wallet address that has the ability to withdraw the fees" ], "type": "pubkey" }, { "name": "protocol_fees_bps", "docs": [ "Protocol fee in basis points (0-10000)" ], "type": "u16" }, { "name": "paused", "docs": [ "whether the whole app is paused" ], "type": "bool" }, { "name": "pending_new_admin", "docs": [ "the new admin that is pending to be set" ], "type": { "option": "pubkey" } }, { "name": "bump", "docs": [ "bump" ], "type": "u8" }, { "name": "padding", "docs": [ "add some padding" ], "type": { "array": [ "u8", 128 ] } } ] } }, { "name": "LpStableConfig", "docs": [ "State that tracks configurations related to a specific LP Stable.", "PDA seeds [b\"lp_stable_config\", stable_coin, liquidity_provider]" ], "type": { "kind": "struct", "fields": [ { "name": "stable_coin_mint_address", "docs": [ "mint address of the stable coin" ], "type": "pubkey" }, { "name": "paused", "docs": [ "whether this LP Stable is paused (for all pairs)" ], "type": "bool" }, { "name": "liquidity_provider", "docs": [ "liquidity provider address" ], "type": "pubkey" }, { "name": "bump", "docs": [ "bump" ], "type": "u8" }, { "name": "padding", "docs": [ "add some padding" ], "type": { "array": [ "u8", 128 ] } } ] } }, { "name": "NavData", "docs": [ "NavData is used to store info about the methods to retrieve the NAV data for a specific asset", "Currently we support 3 methods:", "1. U64FixedAddress: read the price from an onchain account with a fixed address", "in this case we need the account address and the offset of the price in the account data", "2. HARDCODED: read the price from a hardcoded value", "3. PYTH_PUSH: read the price from a pyth push account" ], "type": { "kind": "enum", "variants": [ { "name": "U64FixedAddress", "fields": [ { "name": "nav_account_address", "docs": [ "fixed account address" ], "type": "pubkey" }, { "name": "nav_price_offset", "docs": [ "Byte offset of the NAV" ], "type": "u16" }, { "name": "price_decimals", "docs": [ "price decimals" ], "type": "u8" } ] }, { "name": "Hardcoded", "fields": [ { "name": "hardcoded_price", "docs": [ "hardcoded price" ], "type": "u64" }, { "name": "price_decimals", "docs": [ "price decimals" ], "type": "u8" } ] }, { "name": "PythPush", "fields": [ { "name": "pyth_push_account_address", "docs": [ "Pyth push account address" ], "type": "pubkey" }, { "name": "feed_id", "docs": [ "Pyth feed id" ], "type": { "array": [ "u8", 32 ] } }, { "name": "max_age_secs", "docs": [ "Maximum accepted age for the price update" ], "type": "u64" }, { "name": "max_conf_bps", "docs": [ "Maximum accepted confidence ratio in basis points" ], "type": "u16" } ] } ] } }, { "name": "Pair", "docs": [ "Account that maintains state for a given stable coin and asset pairing.", "PDA seeds [b\"pair\", liquidity_provider, stable_coin_mint_address, asset_token_mint_address]" ], "type": { "kind": "struct", "fields": [ { "name": "redemption_fee_bps", "docs": [ "Fee applied when swapping stable to asset" ], "type": "u16" }, { "name": "discount_rate_bps", "docs": [ "Fee applied when swapping asset to stable" ], "type": "u16" }, { "name": "stable_coin_mint_address", "docs": [ "Mint address of the stable coin that prices the asset" ], "type": "pubkey" }, { "name": "asset_token_mint_address", "docs": [ "Mint address of the RWA asset that can be swapped for stable coin" ], "type": "pubkey" }, { "name": "liquidity_provider", "docs": [ "liquidiy ptovider (pair owner)" ], "type": "pubkey" }, { "name": "paused", "docs": [ "if this pair is paused" ], "type": "bool" }, { "name": "bump", "docs": [ "bump" ], "type": "u8" }, { "name": "padding", "docs": [ "add some padding" ], "type": { "array": [ "u8", 128 ] } } ] } }, { "name": "SwapDirection", "type": { "kind": "enum", "variants": [ { "name": "StableToAsset" }, { "name": "AssetToStable" } ] } }, { "name": "SwapExecuted", "type": { "kind": "struct", "fields": [ { "name": "requestor", "docs": [ "the trader" ], "type": "pubkey" }, { "name": "amount_in", "docs": [ "the amount in" ], "type": "u64" }, { "name": "protocol_fee_amount", "docs": [ "the protocol fee amount" ], "type": "u64" }, { "name": "discount_bps", "docs": [ "the discount bps" ], "type": "u16" }, { "name": "amount_out", "docs": [ "the amount out" ], "type": "u64" }, { "name": "pair", "docs": [ "the pair pubkey" ], "type": "pubkey" }, { "name": "swap_direction", "type": { "defined": { "name": "SwapDirection" } } }, { "name": "swap_type", "type": { "defined": { "name": "SwapType" } } } ] } }, { "name": "SwapType", "type": { "kind": "enum", "variants": [ { "name": "ExactIn" }, { "name": "ExactOut" } ] } }, { "name": "UserVaultInfo", "docs": [ "Vaults are shared between multiple pairs", "We use this state to track how many pairs include a specific vault", "PDA seeds [b\"user_vault_info\", asset_mint_address, liquidity_provider]" ], "type": { "kind": "struct", "fields": [ { "name": "user", "docs": [ "LP wallet address" ], "type": "pubkey" }, { "name": "mint_address", "docs": [ "Mint address of the vault's token" ], "type": "pubkey" }, { "name": "used", "docs": [ "Counter for how many pairs include this vault (for the same LP)" ], "type": "u16" }, { "name": "bump", "docs": [ "bump" ], "type": "u8" }, { "name": "padding", "docs": [ "add some padding" ], "type": { "array": [ "u8", 128 ] } } ] } } ], "constants": [ { "name": "GLOBAL_CONFIG_PREFIX", "type": "bytes", "value": "[103, 108, 111, 98, 97, 108, 95, 99, 111, 110, 102, 105, 103]" } ] } ``` ## Usage ### Fetch from On-Chain The IDL is published on-chain and can be fetched directly: ```bash theme={null} anchor idl fetch HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6 --provider.cluster mainnet ``` ### Load from File ```typescript theme={null} import { Program, AnchorProvider } from "@coral-xyz/anchor"; import idl from "./swap_program.json"; const provider = AnchorProvider.env(); const program = new Program(idl as any, provider); ``` ### Fetch Programmatically ```typescript theme={null} import { Program, AnchorProvider } from "@coral-xyz/anchor"; const provider = new AnchorProvider(connection, wallet, {}); const idl = await Program.fetchIdl( "HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6", provider ); const program = new Program(idl, provider); ``` ## Deriving PDAs All program accounts are Program-Derived Addresses. Use the program ID and the appropriate seeds to derive any account address: ```typescript theme={null} import { PublicKey } from "@solana/web3.js"; import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; const PROGRAM_ID = new PublicKey("HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6"); function deriveGlobalConfig(): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("global_config")], PROGRAM_ID )[0]; } function deriveAssetConfig(mint: PublicKey): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("asset"), mint.toBuffer()], PROGRAM_ID )[0]; } function derivePair( lp: PublicKey, stableMint: PublicKey, assetMint: PublicKey ): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("pair"), lp.toBuffer(), stableMint.toBuffer(), assetMint.toBuffer()], PROGRAM_ID )[0]; } function deriveAssociatedTokenAccount( mint: PublicKey, owner: PublicKey, tokenProgram = TOKEN_PROGRAM_ID ): PublicKey { return PublicKey.findProgramAddressSync( [owner.toBuffer(), tokenProgram.toBuffer(), mint.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID )[0]; } function deriveVaultAuthority(lp: PublicKey): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("vault_authority"), lp.toBuffer()], PROGRAM_ID )[0]; } function deriveVault(mint: PublicKey, lp: PublicKey): PublicKey { return deriveAssociatedTokenAccount(mint, deriveVaultAuthority(lp)); } function deriveFeeVault(mint: PublicKey): PublicKey { return deriveAssociatedTokenAccount(mint, deriveProgramAuthority()); } function deriveLpStableConfig( stableMint: PublicKey, lp: PublicKey ): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("lp_stable_config"), stableMint.toBuffer(), lp.toBuffer()], PROGRAM_ID )[0]; } function deriveProgramAuthority(): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("program_authority")], PROGRAM_ID )[0]; } ``` For program and account addresses, see the [Deployments](/svm/deployments) page. # Asset Configuration Source: https://docs.multiliquid.xyz/svm/instructions/asset-config Per-token NAV pricing configuration and asset management ## Overview Asset Configuration accounts store the NAV (Net Asset Value) pricing sources and settings for each token in the protocol. Every RWA and stablecoin must have an AssetConfig account before it can be used in trading pairs. ## Account Structure ```rust theme={null} pub struct AssetConfig { pub mint_address: Pubkey, // Token mint address pub nav_data: Vec, // Up to 5 NAV pricing sources pub price_difference_bps: u16, // Maximum price divergence tolerance pub paused: bool, // Asset-level pause flag pub version: u8, // Configuration version pub asset_type: AssetType, // Rwa or Stable pub used_in_pairs_count: u16, // Number of pairs using this asset pub bump: u8, // PDA bump seed } ``` **PDA Seeds**: `["asset", mint_address]` ### Asset Type ```rust theme={null} pub enum AssetType { Rwa, // Real World Asset token Stable, // Stablecoin token } ``` ## Instructions ### init\_asset\_config\_account Register a new token for use in the protocol. ```rust theme={null} pub fn init_asset_config_account( ctx: Context, nav_data: Vec, price_difference_bps: u16, asset_type: AssetType, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | ---------------------- | -------------- | --------------------------------------------- | | `nav_data` | `Vec` | 1-5 NAV pricing sources | | `price_difference_bps` | `u16` | Maximum allowed price divergence (0-9900 BPS) | | `asset_type` | `AssetType` | Whether token is RWA or Stablecoin | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct InitAssetConfigAccount<'info> { #[account( init, payer = admin, space = 8 + AssetConfig::INIT_SPACE, seeds = [ASSET_CONFIG_PREFIX, mint_address.key().as_ref()], bump, )] pub asset_config: Account<'info, AssetConfig>, pub mint_address: InterfaceAccount<'info, Mint>, #[account(mut)] pub admin: Signer<'info>, // Fee vault ATA owned by the global program authority #[account( init_if_needed, payer = admin, associated_token::mint = mint_address, associated_token::authority = program_authority, associated_token::token_program = token_program, )] pub fee_vault_token_account: InterfaceAccount<'info, TokenAccount>, /// CHECK: Global PDA used as the protocol fee-vault authority #[account( seeds = [PROGRAM_AUTHORITY_PREFIX], bump, )] pub program_authority: UncheckedAccount<'info>, #[account( seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, has_one = admin )] pub global_config: Account<'info, GlobalConfig>, pub system_program: Program<'info, System>, pub token_program: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, } ``` #### Behavior * Creates AssetConfig PDA for the token mint * Creates the fee-vault ATA owned by `program_authority` for fee collection * Validates NAV data (1-5 sources, decimals ≤ 9) and `price_difference_bps <= 9900` * Sets `paused` to `false` by default * Sets `used_in_pairs_count` to 0 #### Access Control **Access**: Admin only #### Example ```typescript theme={null} import { BN } from "@coral-xyz/anchor"; // NAV source: Read price from a fixed account address const navData = [{ u64FixedAddress: { navAccountAddress: priceOracleAccount, navPriceOffset: 0, priceDecimals: 6, } }]; await program.methods .initAssetConfigAccount( navData, 100, // 1% max price divergence { rwa: {} } // Asset type ) .accounts({ assetConfig, mintAddress: rwaMint, admin: adminWallet.publicKey, feeVaultTokenAccount, programAuthority, globalConfig, systemProgram: SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, }) .rpc(); ``` *** ### update\_asset\_config\_account Update NAV sources for an existing asset. ```rust theme={null} pub fn update_asset_config_account( ctx: Context, nav_data: Vec, price_difference_bps: u16, asset_type: AssetType, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | ---------------------- | -------------- | -------------------------------------------------- | | `nav_data` | `Vec` | Updated NAV pricing sources (1-5) | | `price_difference_bps` | `u16` | Updated price divergence tolerance (0-9900 BPS) | | `asset_type` | `AssetType` | New asset type (change blocked if asset is in use) | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct UpdateAssetConfigAccount<'info> { #[account( mut, seeds = [ASSET_CONFIG_PREFIX, mint_address.key().as_ref()], bump = asset_config.bump, )] pub asset_config: Account<'info, AssetConfig>, pub mint_address: InterfaceAccount<'info, Mint>, pub admin: Signer<'info>, #[account( seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, has_one = admin )] pub global_config: Account<'info, GlobalConfig>, } ``` #### Behavior * Updates NAV sources and price divergence threshold after validating `price_difference_bps <= 9900` * Can update `asset_type`, but change is blocked if `used_in_pairs_count > 0` * Does not affect pause state or usage count #### Access Control **Access**: Admin only #### Example ```typescript theme={null} // Add a second NAV source (Pyth oracle) const updatedNavData = [ { u64FixedAddress: { navAccountAddress: primaryOracle, navPriceOffset: 0, priceDecimals: 6, } }, { pythPush: { pythPushAccountAddress: pythAccount, feedId: Array.from(Buffer.from("40ac3329933a6b5b65cf31496018c5764ac0567316146f7d0de00095886b480d", "hex")), maxAgeSecs: new BN(86_400), maxConfBps: 100, } } ]; await program.methods .updateAssetConfigAccount( updatedNavData, 50, // 0.5% max divergence { rwa: {} } // Asset type ) .accounts({ assetConfig, admin: adminWallet.publicKey, mintAddress: rwaMint, globalConfig, }) .rpc(); ``` *** ### set\_paused\_for\_asset Set the pause state for a specific asset. ```rust theme={null} pub fn set_paused_for_asset( ctx: Context, paused: bool, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | --------- | ------ | --------------- | | `paused` | `bool` | New pause state | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct SetPausedForAsset<'info> { #[account( seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, has_one = admin, )] pub global_config: Account<'info, GlobalConfig>, #[account( mut, seeds = [ASSET_CONFIG_PREFIX, mint_address.key().as_ref()], bump = asset_config.bump, )] pub asset_config: Account<'info, AssetConfig>, pub mint_address: UncheckedAccount<'info>, #[account(mut)] pub admin: Signer<'info>, } ``` #### Behavior * Updates asset's pause state * When paused, all swaps involving this asset are blocked * Does not affect other assets or global state #### Access Control **Access**: Admin only #### Example ```typescript theme={null} // Pause an asset await program.methods .setPausedForAsset(true) .accounts({ globalConfig, assetConfig, mintAddress: rwaMint, admin: adminWallet.publicKey, }) .rpc(); ``` *** ## NAV Data Types The program supports three types of NAV pricing sources: ### U64FixedAddress Read price from a fixed byte offset in an on-chain account. ```rust theme={null} NavData::U64FixedAddress { nav_account_address: Pubkey, // Account containing price data nav_price_offset: u16, // Byte offset to read price price_decimals: u8, // Decimal places (0-9) } ``` **Use Cases**: * Custom price oracle accounts * RWA issuer-published NAV accounts * Any account with a u64 price at known offset ### Hardcoded Static price value for stable-value assets. ```rust theme={null} NavData::Hardcoded { hardcoded_price: u64, // Fixed price value price_decimals: u8, // Decimal places (0-9) } ``` **Use Cases**: * Dollar-pegged stablecoins (price = 1.0) * Assets with contractually fixed prices * Testing and development ### PythPush Pyth Network oracle integration. ```rust theme={null} NavData::PythPush { pyth_push_account_address: Pubkey, // Pyth receiver price update account feed_id: [u8; 32], // Expected Pyth feed id max_age_secs: u64, // Maximum accepted price age max_conf_bps: u16, // Maximum accepted confidence ratio } ``` **Use Cases**: * Market-priced assets * Assets with Pyth price feeds * High-frequency price updates Complete documentation on NAV pricing configuration and validation *** ## Price Aggregation When multiple NAV sources are configured: 1. **Read All Sources**: Each source returns a price 2. **Normalize**: All prices converted to 9 decimal places 3. **Validate Divergence**: Check all prices within `price_difference_bps` 4. **Average**: Return average of all valid prices If any source diverges beyond threshold, the function returns 0, blocking swaps. *** ## Error Codes | Error | Description | | ------------------------------------------------------------------------ | ---------------------------------------------------------------------- | | `Unauthorized` | Caller is not admin | | `MustProvideAtLeastOneNavData` | No NAV sources provided | | `Max5NavData` | More than 5 NAV sources | | `PriceDecimalsTooLarge` | Decimals exceed 9 | | `MustProvideHardcodedPrice` | Hardcoded price is zero | | `NavMustBePositive` | NAV source returned a non-positive price | | `InvalidAccountPublicKey` | Required NAV source account was not passed | | `InvalidFeedId` / `InvalidMaxAge` / `MaxAgeTooLarge` / `ConfBpsTooLarge` | Pyth configuration failed scalar validation | | `MissingPythAccount` / `FeedIdMismatch` | Pyth receiver account was missing or did not match the configured feed | | `OutOfRange` | `price_difference_bps` exceeds 9900 | *** Learn about trading pair creation and configuration # Global Configuration Source: https://docs.multiliquid.xyz/svm/instructions/global-config Program-wide configuration management for admin, fees, and pause control ## Overview The Global Configuration manages program-wide settings including the admin address, fee wallet, protocol fees, and global pause state. These instructions are primarily admin-only operations. ## Account Structure ```rust theme={null} pub struct GlobalConfig { pub admin: Pubkey, // Program administrator pub fee_wallet: Pubkey, // Protocol fee collection wallet pub protocol_fees_bps: u16, // Protocol fees (0-9900 BPS) pub paused: bool, // Program-wide pause flag pub pending_new_admin: Option, // Two-step admin transfer pub bump: u8, // PDA bump seed } ``` **PDA Seeds**: `["global_config"]` ## Instructions ### init\_global\_config Initialize the program's global configuration. This is a one-time operation called immediately after deployment. ```rust theme={null} pub fn init_global_config( ctx: Context, fee_wallet: Pubkey, protocol_fees_bps: u16, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | ------------------- | -------- | --------------------------------------- | | `fee_wallet` | `Pubkey` | Wallet to receive claimed protocol fees | | `protocol_fees_bps` | `u16` | Protocol fee in basis points (0-9900) | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct InitGlobalConfig<'info> { #[account( init, payer = admin, space = 8 + GlobalConfig::INIT_SPACE, seeds = [GLOBAL_CONFIG_PREFIX], bump, )] pub global_config: Account<'info, GlobalConfig>, #[account(mut)] pub admin: Signer<'info>, pub system_program: Program<'info, System>, } ``` #### Behavior * Creates the GlobalConfig PDA account * Sets caller as admin * Sets program to **paused by default** * Validates `protocol_fees_bps` is within range #### Access Control **Access**: Permissionless (one-time only) The first caller becomes the admin. Subsequent calls will fail as the account already exists. #### Example ```typescript theme={null} await program.methods .initGlobalConfig( feeWallet, 100 // 1% protocol fee (100 BPS) ) .accounts({ globalConfig, admin: wallet.publicKey, systemProgram: SystemProgram.programId, }) .rpc(); ``` *** ### update\_global\_config Update program-wide configuration settings. ```rust theme={null} pub fn update_global_config( ctx: Context, fee_wallet: Option, paused: Option, protocol_fees_bps: Option, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | ------------------- | ---------------- | --------------------------------------- | | `fee_wallet` | `Option` | New fee wallet (None to keep current) | | `paused` | `Option` | New pause state (None to keep current) | | `protocol_fees_bps` | `Option` | New protocol fee (None to keep current) | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct UpdateGlobalConfig<'info> { #[account( mut, seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, has_one = admin, )] pub global_config: Account<'info, GlobalConfig>, pub admin: Signer<'info>, } ``` #### Behavior * Updates only the fields provided (non-None values) * Validates `protocol_fees_bps` if provided #### Access Control **Access**: Admin only #### Example ```typescript theme={null} // Unpause the program and update fees await program.methods .updateGlobalConfig( null, // Keep current fee wallet false, // Unpause 50 // 0.5% protocol fee (50 BPS) ) .accounts({ globalConfig, admin: adminWallet.publicKey, }) .rpc(); ``` *** ### set\_new\_admin Propose a new admin address (first step of two-step transfer). ```rust theme={null} pub fn set_new_admin( ctx: Context, new_admin: Pubkey, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | ----------- | -------- | -------------------------- | | `new_admin` | `Pubkey` | Proposed new admin address | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct SetNewAdmin<'info> { #[account( mut, seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, has_one = admin, )] pub global_config: Account<'info, GlobalConfig>, pub admin: Signer<'info>, } ``` #### Behavior * Sets `pending_new_admin` to proposed address * Current admin remains in control * Can be called multiple times to change proposed admin #### Access Control **Access**: Current admin only #### Example ```typescript theme={null} await program.methods .setNewAdmin(newAdminPubkey) .accounts({ globalConfig, admin: currentAdmin.publicKey, }) .rpc(); ``` *** ### confirm\_new\_admin Accept the admin role (second step of two-step transfer). ```rust theme={null} pub fn confirm_new_admin( ctx: Context, ) -> Result<()> ``` #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct ConfirmNewAdmin<'info> { #[account( mut, seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, )] pub global_config: Account<'info, GlobalConfig>, pub new_admin: Signer<'info>, } ``` #### Behavior * Verifies caller matches `pending_new_admin` * Updates `admin` to new address * Clears `pending_new_admin` #### Access Control **Access**: Pending new admin only #### Example ```typescript theme={null} await program.methods .confirmNewAdmin() .accounts({ globalConfig, newAdmin: newAdminWallet.publicKey, }) .rpc(); ``` *** ### claim\_fees Claim accumulated protocol fees from a fee vault. ```rust theme={null} pub fn claim_fees( ctx: Context, fee_wallet: Pubkey, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | ------------ | -------- | ------------------------------------------------- | | `fee_wallet` | `Pubkey` | The fee wallet address configured in GlobalConfig | #### Required Accounts ```rust theme={null} #[derive(Accounts)] #[instruction(fee_wallet: Pubkey)] pub struct ClaimFees<'info> { pub stable_coin_mint_address: InterfaceAccount<'info, Mint>, #[account( mut, token::mint = stable_coin_mint_address, token::authority = fee_wallet, token::token_program = token_program, )] pub fee_wallet_token_account: InterfaceAccount<'info, TokenAccount>, /// CHECK: checked by seeds #[account(seeds = [PROGRAM_AUTHORITY_PREFIX], bump)] pub program_authority: UncheckedAccount<'info>, #[account( mut, associated_token::mint = stable_coin_mint_address, associated_token::authority = program_authority, associated_token::token_program = token_program, )] pub fee_token_account: InterfaceAccount<'info, TokenAccount>, #[account( seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, has_one = fee_wallet, )] pub global_config: Account<'info, GlobalConfig>, pub token_program: Interface<'info, TokenInterface>, } ``` #### Behavior * Requires program to be unpaused * Transfers all tokens from the `program_authority`-owned fee-vault ATA to the fee wallet * Fee vault remains open for future fee collection * Anyone can call, but fees always go to configured `fee_wallet` #### Access Control **Access**: Permissionless While anyone can trigger fee claims, the fees are always sent to the `fee_wallet` configured in GlobalConfig, not to the caller. #### Example ```typescript theme={null} await program.methods .claimFees(feeWallet) .accounts({ stableCoinMintAddress: stableMint, feeWalletTokenAccount, programAuthority, feeTokenAccount, globalConfig, tokenProgram: TOKEN_PROGRAM_ID, }) .rpc(); ``` *** ## Error Codes | Error | Description | | ----------------- | ------------------------------------------- | | `Unauthorized` | Caller is not admin or pending admin | | `OutOfRange` | Protocol fees exceed 9900 BPS | | `ProgramPaused` | Operation blocked by global pause | | `InvalidNewAdmin` | Proposed admin is already the current admin | *** Learn about per-token NAV configuration and asset management # Pair Management Source: https://docs.multiliquid.xyz/svm/instructions/pair Trading pair creation, configuration, and liquidity management ## Overview Pair accounts link RWA tokens to stablecoins, enabling trading between them. Each pair is owned by a specific Liquidity Provider (LP) who controls its configuration, fees, and liquidity. ## Account Structures ### Pair Account ```rust theme={null} pub struct Pair { pub redemption_fee_bps: u16, // Fee for Stable → RWA swaps pub discount_rate_bps: u16, // Fee for RWA → Stable swaps pub stable_coin_mint_address: Pubkey, // Stablecoin token mint pub asset_token_mint_address: Pubkey, // RWA token mint pub liquidity_provider: Pubkey, // LP owner address pub paused: bool, // Pair-level pause flag pub bump: u8, // PDA bump seed } ``` **PDA Seeds**: `["pair", liquidity_provider, stable_mint, asset_mint]` ### LpStableConfig Account ```rust theme={null} pub struct LpStableConfig { pub stable_coin_mint_address: Pubkey, // Stablecoin mint pub paused: bool, // Pause all pairs with this config pub liquidity_provider: Pubkey, // LP address pub bump: u8, // PDA bump seed } ``` **PDA Seeds**: `["lp_stable_config", stable_mint, liquidity_provider]` ### UserVaultInfo Account ```rust theme={null} pub struct UserVaultInfo { pub user: Pubkey, // LP wallet pub mint_address: Pubkey, // Token mint pub used: u16, // Number of pairs using this vault pub bump: u8, // PDA bump seed } ``` **PDA Seeds**: `["user_vault_info", mint_address, liquidity_provider]` ### VaultAuthority PDA **PDA Seeds**: `["vault_authority", liquidity_provider]` For permissioned mints (tokens), the issuer must separately whitelist (allowlist) both the `liquidity_provider` address and the derived `vault_authority` PDA. The LP address owns the source token account; the PDA owns the destination vault ATA. Approval of one address does not apply to the other. The corresponding LP-owned token account and PDA-owned vault ATA must also be thawed before a transfer can succeed. Creating the pair or vault ATA does not perform issuer whitelisting or thawing. The `vault_authority` PDA is LP-specific and owns that LP's vault token accounts. The vault token accounts themselves are associated token accounts for `(mint, vault_authority)`, so they are keyed by LP and mint without using the global program authority. ## Pair Instructions ### init\_pair Create a new trading pair. The LP signs and pays for all account creation; the configured global-config admin is referenced as a non-signing account and checked by `has_one`. ```rust theme={null} pub fn init_pair( ctx: Context, redemption_fee_bps: u16, discount_rate_bps: u16, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | -------------------- | ----- | --------------------------------- | | `redemption_fee_bps` | `u16` | Fee for Stable → RWA (0-9900 BPS) | | `discount_rate_bps` | `u16` | Fee for RWA → Stable (0-9900 BPS) | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct InitPair<'info> { /// CHECK: checked by has_one constraint on global_config pub admin: UncheckedAccount<'info>, #[account(mut)] pub liquidity_provider: Signer<'info>, #[account( init, payer = liquidity_provider, space = 8 + Pair::INIT_SPACE, seeds = [PAIR_PREFIX, liquidity_provider.key().as_ref(), stable_coin_mint_address.key().as_ref(), asset_token_mint_address.key().as_ref()], bump, )] pub pair: Account<'info, Pair>, pub stable_coin_mint_address: InterfaceAccount<'info, Mint>, pub asset_token_mint_address: InterfaceAccount<'info, Mint>, #[account( seeds = [VAULT_AUTHORITY_PREFIX, liquidity_provider.key().as_ref()], bump, )] pub lp_vault_authority: UncheckedAccount<'info>, #[account( init_if_needed, payer = liquidity_provider, associated_token::mint = stable_coin_mint_address, associated_token::authority = lp_vault_authority, associated_token::token_program = token_program_stable, )] pub stable_coin_vault_token_account: InterfaceAccount<'info, TokenAccount>, #[account( init_if_needed, payer = liquidity_provider, associated_token::mint = asset_token_mint_address, associated_token::authority = lp_vault_authority, associated_token::token_program = token_program_asset, )] pub asset_token_vault_token_account: InterfaceAccount<'info, TokenAccount>, #[account( init_if_needed, payer = liquidity_provider, space = 8 + UserVaultInfo::INIT_SPACE, seeds = [USER_VAULT_PREFIX, stable_coin_mint_address.key().as_ref(), liquidity_provider.key().as_ref()], bump, )] pub lp_vault_stable_pda: Account<'info, UserVaultInfo>, #[account( init_if_needed, payer = liquidity_provider, space = 8 + UserVaultInfo::INIT_SPACE, seeds = [USER_VAULT_PREFIX, asset_token_mint_address.key().as_ref(), liquidity_provider.key().as_ref()], bump, )] pub lp_vault_asset_pda: Account<'info, UserVaultInfo>, #[account( seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, has_one = admin )] pub global_config: Account<'info, GlobalConfig>, #[account( init_if_needed, payer = liquidity_provider, space = 8 + LpStableConfig::INIT_SPACE, seeds = [LP_STABLE_CONFIG_PREFIX, stable_coin_mint_address.key().as_ref(), liquidity_provider.key().as_ref()], bump, )] pub lp_stable_config: Account<'info, LpStableConfig>, #[account( mut, seeds = [ASSET_CONFIG_PREFIX, stable_coin_mint_address.key().as_ref()], bump = asset_config_stable.bump, )] pub asset_config_stable: Account<'info, AssetConfig>, #[account( mut, seeds = [ASSET_CONFIG_PREFIX, asset_token_mint_address.key().as_ref()], bump = asset_config_rwa.bump, )] pub asset_config_rwa: Account<'info, AssetConfig>, pub token_program_stable: Interface<'info, TokenInterface>, pub token_program_asset: Interface<'info, TokenInterface>, pub associated_token_program: Program<'info, AssociatedToken>, pub system_program: Program<'info, System>, } ``` #### Behavior * Creates Pair PDA account, paid for by the LP * Validates that `asset_config_stable.asset_type == Stable` and `asset_config_rwa.asset_type == Rwa` * Creates or updates LpStableConfig for LP/stablecoin * Creates vault ATAs owned by the LP's `vault_authority` PDA if they don't exist * Creates UserVaultInfo accounts to track vault usage * Increments `used_in_pairs_count` on both asset configs * Initializes `redemption_fee_bps` and `discount_rate_bps` from the instruction arguments * Requires program to be unpaused * Pair pause state defaults to **unpaused** unless the LP later pauses it with `update_pair` #### Access Control **Access**: LP (signs as `liquidity_provider`). The `admin` account must be provided and is verified against `global_config.admin` via `has_one`, but admin does not sign. Swaps still require every pause gate to be open: global config, both asset configs, the LP stable config, and the pair itself. #### Example ```typescript theme={null} await program.methods .initPair( 50, // 0.5% redemption fee 25 // 0.25% discount rate ) .accounts({ admin: globalConfigAdmin, liquidityProvider: lpWallet.publicKey, pair, stableCoinMintAddress: stableMint, assetTokenMintAddress: assetMint, lpVaultAuthority, stableCoinVaultTokenAccount: stableVault, assetTokenVaultTokenAccount: rwaVault, lpVaultStablePda: stableUserVaultInfo, lpVaultAssetPda: rwaUserVaultInfo, globalConfig, lpStableConfig, assetConfigStable: stableAssetConfig, assetConfigRwa: rwaAssetConfig, tokenProgramStable: TOKEN_PROGRAM_ID, tokenProgramAsset: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, systemProgram: SystemProgram.programId, }) .signers([lpWallet]) .rpc(); ``` *** ### update\_pair Update pair configuration (fees and pause state). ```rust theme={null} pub fn update_pair( ctx: Context, redemption_fee_bps: u16, discount_rate_bps: u16, paused: bool, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | -------------------- | ------ | --------------------------------------------- | | `redemption_fee_bps` | `u16` | New redemption fee (Stable → RWA, 0-9900 BPS) | | `discount_rate_bps` | `u16` | New discount rate (RWA → Stable, 0-9900 BPS) | | `paused` | `bool` | New pause state | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct UpdatePair<'info> { pub liquidity_provider: Signer<'info>, #[account( mut, seeds = [PAIR_PREFIX, liquidity_provider.key().as_ref(), stable_coin_mint_address.key().as_ref(), asset_token_mint_address.key().as_ref()], bump = pair.bump, has_one = liquidity_provider, )] pub pair: Account<'info, Pair>, pub stable_coin_mint_address: InterfaceAccount<'info, Mint>, pub asset_token_mint_address: InterfaceAccount<'info, Mint>, pub global_config: Account<'info, GlobalConfig>, } ``` #### Behavior * Updates fee configuration * Updates pause state * Requires program to be unpaused * Validates both fee values are `<= 9900` #### Access Control **Access**: LP (pair owner) only #### Example ```typescript theme={null} // Set final fees and ensure the pair is unpaused await program.methods .updatePair( 50, // 0.5% redemption fee 25, // 0.25% discount rate false // Unpause ) .accounts({ liquidityProvider: lpWallet.publicKey, pair, stableCoinMintAddress: stableMint, assetTokenMintAddress: assetMint, globalConfig, }) .rpc(); ``` *** ### close\_pair Permanently close a trading pair. The LP signs and receives the reclaimed rent for the pair account and any closed vault accounts; the configured global-config admin is referenced as a non-signing account and checked by `has_one`. ```rust theme={null} pub fn close_pair( ctx: Context, ) -> Result<()> ``` #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct ClosePair<'info> { /// CHECK: checked by has_one constraint on global_config #[account(mut)] pub admin: UncheckedAccount<'info>, #[account(mut)] pub liquidity_provider: Signer<'info>, #[account( mut, close = liquidity_provider, seeds = [PAIR_PREFIX, liquidity_provider.key().as_ref(), stable_coin_mint_address.key().as_ref(), asset_token_mint_address.key().as_ref()], bump = pair.bump, )] pub pair: Account<'info, Pair>, pub stable_coin_mint_address: InterfaceAccount<'info, Mint>, pub asset_token_mint_address: InterfaceAccount<'info, Mint>, #[account( seeds = [VAULT_AUTHORITY_PREFIX, liquidity_provider.key().as_ref()], bump, )] pub lp_vault_authority: UncheckedAccount<'info>, #[account( mut, associated_token::mint = stable_coin_mint_address, associated_token::authority = lp_vault_authority, associated_token::token_program = token_program_stable, )] pub stable_coin_vault_token_account: InterfaceAccount<'info, TokenAccount>, #[account( mut, associated_token::mint = asset_token_mint_address, associated_token::authority = lp_vault_authority, associated_token::token_program = token_program_asset, )] pub asset_token_vault_token_account: InterfaceAccount<'info, TokenAccount>, #[account( mut, token::mint = stable_coin_mint_address, token::authority = liquidity_provider, token::token_program = token_program_stable, )] pub lp_stable_token_account: InterfaceAccount<'info, TokenAccount>, #[account( mut, token::mint = asset_token_mint_address, token::authority = liquidity_provider, token::token_program = token_program_asset, )] pub lp_asset_token_account: InterfaceAccount<'info, TokenAccount>, #[account( mut, seeds = [USER_VAULT_PREFIX, stable_coin_mint_address.key().as_ref(), liquidity_provider.key().as_ref()], bump = lp_vault_stable_pda.bump, )] pub lp_vault_stable_pda: Account<'info, UserVaultInfo>, #[account( mut, seeds = [USER_VAULT_PREFIX, asset_token_mint_address.key().as_ref(), liquidity_provider.key().as_ref()], bump = lp_vault_asset_pda.bump, )] pub lp_vault_asset_pda: Account<'info, UserVaultInfo>, #[account( mut, seeds = [ASSET_CONFIG_PREFIX, stable_coin_mint_address.key().as_ref()], bump = asset_config_stable.bump, )] pub asset_config_stable: Account<'info, AssetConfig>, #[account( mut, seeds = [ASSET_CONFIG_PREFIX, asset_token_mint_address.key().as_ref()], bump = asset_config_rwa.bump, )] pub asset_config_rwa: Account<'info, AssetConfig>, #[account( seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, has_one = admin )] pub global_config: Account<'info, GlobalConfig>, pub token_program_stable: Interface<'info, TokenInterface>, pub token_program_asset: Interface<'info, TokenInterface>, } ``` #### Behavior * Closes Pair account * Requires program to be unpaused * Decrements `used_in_pairs_count` on both asset configs * Returns remaining vault tokens to the LP's recipient token accounts * Closes vault ATAs if no longer used by other pairs * Closes the corresponding UserVaultInfo PDAs when their `used` count reaches 0 * Returns reclaimed rent (Pair PDA, closed vault ATAs, and closed UserVaultInfo PDAs) to the LP Closing a pair reclaims rent and clears the pair's configuration (fees, pause state). The LP can re-initialize a pair for the same (stable, asset) combination later via `init_pair`, but it will start with fresh configuration — prior fee settings are not restored. #### Access Control **Access**: LP (pair owner). The `admin` account must be provided and is verified against `global_config.admin` via `has_one`, but admin does not sign. *** ### set\_paused\_for\_lp\_stable\_config Set pause state for all pairs of an LP/stablecoin combination. ```rust theme={null} pub fn set_paused_for_lp_stable_config( ctx: Context, paused: bool, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | --------- | ------ | --------------- | | `paused` | `bool` | New pause state | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct SetPausedForLpStableConfig<'info> { pub global_config: Account<'info, GlobalConfig>, #[account( mut, seeds = [LP_STABLE_CONFIG_PREFIX, mint_address.key().as_ref(), liquidity_provider.key().as_ref()], bump = lp_stable_config.bump, )] pub lp_stable_config: Account<'info, LpStableConfig>, /// CHECK: checked by seeds pub mint_address: UncheckedAccount<'info>, /// CHECK: checked by seeds pub liquidity_provider: UncheckedAccount<'info>, // Either admin OR liquidity_provider can call pub signer: Signer<'info>, } ``` #### Behavior * Updates pause state on LpStableConfig * Affects ALL pairs using this LP/stablecoin combination * Requires program to be unpaused #### Access Control **Access**: Admin OR LP (config owner) *** ## Liquidity Instructions ### add\_liquidity Deposit tokens into a vault. ```rust theme={null} pub fn add_liquidity( ctx: Context, amount: u64, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | --------- | ----- | --------------------------- | | `amount` | `u64` | Amount of tokens to deposit | #### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct AddLiquidity<'info> { pub liquidity_provider: Signer<'info>, pub mint_address: InterfaceAccount<'info, Mint>, #[account( mut, token::mint = mint_address, token::authority = liquidity_provider, token::token_program = token_program, )] pub lp_token_account: InterfaceAccount<'info, TokenAccount>, #[account( mut, associated_token::mint = mint_address, associated_token::authority = lp_vault_authority, associated_token::token_program = token_program, )] pub vault_token_account: InterfaceAccount<'info, TokenAccount>, #[account( seeds = [VAULT_AUTHORITY_PREFIX, liquidity_provider.key().as_ref()], bump, )] pub lp_vault_authority: UncheckedAccount<'info>, #[account( seeds = [GLOBAL_CONFIG_PREFIX], bump = global_config.bump, )] pub global_config: Account<'info, GlobalConfig>, pub token_program: Interface<'info, TokenInterface>, } ``` #### Behavior * Transfers tokens from LP's account to vault * Vault is the mint's ATA owned by the LP's `vault_authority` PDA * Requires program to be unpaused * Amount must be greater than 0 #### Access Control **Access**: LP (vault owner) only #### Example ```typescript theme={null} await program.methods .addLiquidity(new BN(1000_000000)) // 1000 tokens .accounts({ liquidityProvider: lpWallet.publicKey, mintAddress: stableMint, lpTokenAccount: lpStableTokenAccount, vaultTokenAccount: stableVault, lpVaultAuthority, globalConfig, tokenProgram: TOKEN_PROGRAM_ID, }) .rpc(); ``` *** ### remove\_liquidity Withdraw tokens from a vault. ```rust theme={null} pub fn remove_liquidity( ctx: Context, amount: u64, ) -> Result<()> ``` #### Parameters | Parameter | Type | Description | | --------- | ----- | ---------------------------- | | `amount` | `u64` | Amount of tokens to withdraw | #### Required Accounts Same as `add_liquidity`. #### Behavior * Transfers tokens from vault to LP's account * Vault is the mint's ATA owned by the LP's `vault_authority` PDA * Requires program to be unpaused * Amount must be greater than 0 * Vault must have sufficient balance #### Access Control **Access**: LP (vault owner) only *** ## Vault Architecture ### Shared Vault Model Vaults are shared across pairs for the same LP/token combination. Each LP has a `vault_authority` PDA, and each vault is the associated token account for `(mint, vault_authority)`: ``` LP Alice → vault_authority PDA ["vault_authority", Alice] USDC mint + Alice vault_authority → One USDC vault ATA ULTRA mint + Alice vault_authority → One ULTRA vault ATA Pair 1: Alice's USDC ↔ ULTRA (uses both vaults) Pair 2: Alice's USDC ↔ USTB (uses USDC vault + new USTB vault) ``` ### UserVaultInfo Tracking The `UserVaultInfo` account tracks how many pairs use each vault: * Incremented when pair is created * Decremented when pair is closed * Vault closed only when `used` reaches 0 This ensures vaults aren't closed while still in use by other pairs. *** ## Error Codes | Error | Description | | ----------------------- | -------------------------------------------------------- | | `Unauthorized` | Caller is not admin or LP owner | | `ProgramPaused` | Program is paused | | `PairPaused` | Pair is paused | | `AmountMustBePositive` | Amount is zero | | `InsufficientLiquidity` | Vault has insufficient balance | | `InvalidAssetType` | Asset type mismatch (RWA vs Stable) | | `OutOfRange` | Pair fee BPS exceeds 9900 during pair creation or update | | `AssetConfigInUse` | Cannot change asset type while in use | *** Learn about NAV pricing sources and oracle integration # Price Sources Source: https://docs.multiliquid.xyz/svm/instructions/price-sources NAV pricing sources and oracle integration for asset valuation ## Overview Price sources (NavData) provide USD-denominated pricing for all assets in the Multiliquid Program. Each asset can have 1-5 pricing sources, which are aggregated and validated during swap execution. ## NAV Data Types The program supports three types of NAV pricing sources: ### U64FixedAddress Read price from a fixed byte offset in an on-chain account. ```rust theme={null} NavData::U64FixedAddress { nav_account_address: Pubkey, // Account containing price data nav_price_offset: u16, // Byte offset to read price (as u64) price_decimals: u8, // Decimal places (0-9) } ``` **How It Works**: 1. Read account data at `nav_account_address` 2. Read u64 at byte offset `nav_price_offset` 3. Interpret value with `price_decimals` decimal places **Use Cases**: * Custom price oracle accounts * RWA issuer-published NAV accounts * Any account storing price as u64 at known offset **Example Configuration**: ```typescript theme={null} // Read price from offset 0 in a custom oracle account const navData = { u64FixedAddress: { navAccountAddress: oracleAccount, navPriceOffset: 0, // Read from start of account data priceDecimals: 6, // Price has 6 decimal places } }; // Example: Price of 1050000 with 6 decimals = $1.05 ``` *** ### Hardcoded Static price value for stable-value assets. ```rust theme={null} NavData::Hardcoded { hardcoded_price: u64, // Fixed price value price_decimals: u8, // Decimal places (0-9) } ``` **How It Works**: 1. Return `hardcoded_price` directly 2. Interpret with `price_decimals` decimal places **Use Cases**: * Dollar-pegged stablecoins (USDC, USDT) * Assets with contractually fixed prices * Testing and development environments **Example Configuration**: ```typescript theme={null} // Stablecoin pegged to $1.00 const navData = { hardcoded: { hardcodedPrice: new BN(1_000000), // 1.000000 priceDecimals: 6, } }; // Example: Price of 1000000 with 6 decimals = $1.00 ``` Hardcoded prices are ideal for dollar-pegged stablecoins that maintain a 1:1 USD value. They have zero external dependencies and minimal gas costs. *** ### PythPush Pyth Network oracle integration for market prices. ```rust theme={null} NavData::PythPush { pyth_push_account_address: Pubkey, // Pyth receiver price update account feed_id: [u8; 32], // Expected Pyth feed id max_age_secs: u64, // Maximum accepted price age max_conf_bps: u16, // Maximum accepted confidence ratio } ``` **How It Works**: 1. Deserialize the configured Pyth receiver `PriceUpdateV2` account 2. Verify the account's feed id matches `feed_id` 3. Require the update to be no older than `max_age_secs` 4. Require confidence to be within `max_conf_bps` 5. Normalize the Pyth price and exponent into the program's 9-decimal NAV format **Use Cases**: * Market-priced assets with Pyth feeds * Real-time price updates * Cross-chain price consistency **Example Configuration**: ```typescript theme={null} // Pyth oracle for an RWA with market price const navData = { pythPush: { pythPushAccountAddress: pythPriceAccount, feedId: Array.from(Buffer.from("40ac3329933a6b5b65cf31496018c5764ac0567316146f7d0de00095886b480d", "hex")), maxAgeSecs: new BN(86_400), maxConfBps: 100, } }; ``` Pyth oracle integration requires the correct receiver account and feed id. Stale updates, excessive confidence ratios, or feed-id mismatches will reject the source. *** ## Price Aggregation When multiple NAV sources are configured for an asset, the program performs price aggregation: ### Aggregation Process ``` ┌─────────────────────────────────────────────────────────┐ │ Price Aggregation │ ├─────────────────────────────────────────────────────────┤ │ 1. Read all NAV sources │ │ Source 1: $1.0500 (6 decimals) │ │ Source 2: $1.0510 (8 decimals) │ │ Source 3: $1.0495 (6 decimals) │ │ │ │ 2. Normalize to 9 decimals │ │ Source 1: 1_050_000_000 │ │ Source 2: 1_051_000_000 │ │ Source 3: 1_049_500_000 │ │ │ │ 3. Validate divergence (e.g., 100 BPS = 1%) │ │ Max: 1_051_000_000 │ │ Min: 1_049_500_000 │ │ Diff: 0.14% ✓ (within threshold) │ │ │ │ 4. Calculate average │ │ Average: 1_050_166_666 (9 decimals) │ │ = $1.050166666 │ └─────────────────────────────────────────────────────────┘ ``` ### Divergence Validation The `price_difference_bps` setting controls maximum allowed price divergence: ```rust theme={null} // In AssetConfig pub price_difference_bps: u16, // Maximum divergence in basis points ``` **Calculation**: ``` divergence_bps = (max_price - min_price) / max_price × 10000 ``` **Behavior**: * If `divergence_bps > price_difference_bps`: Return 0 (block swaps) * If within threshold: Return average price **Example**: ``` price_difference_bps = 100 (1%) Source 1: $1.05 Source 2: $1.06 Divergence: 0.95% ✓ Allowed Source 1: $1.05 Source 2: $1.10 Divergence: 4.55% ✗ Blocked (returns 0) ``` *** ## Decimal Normalization All prices are normalized to 9 decimal places internally: | Source Decimals | Raw Value | Normalized (9 decimals) | USD Value | | --------------- | ------------ | ----------------------- | --------- | | 6 | 1\_050000 | 1\_050\_000\_000 | \$1.05 | | 8 | 105\_000000 | 1\_050\_000\_000 | \$1.05 | | 9 | 1\_050000000 | 1\_050\_000\_000 | \$1.05 | **Normalization Formula**: ``` normalized = raw_value × 10^(9 - source_decimals) ``` *** ## Configuration Examples ### Single Source: Dollar-Pegged Stablecoin ```typescript theme={null} // USDC with fixed $1.00 price const usdcNavData = [{ hardcoded: { hardcodedPrice: new BN(1_000000), priceDecimals: 6, } }]; await program.methods .initAssetConfigAccount( usdcNavData, 0, // No divergence check needed (single source) { stable: {} } ) .accounts({ /* ... */ }) .rpc(); ``` ### Single Source: RWA with Custom Oracle ```typescript theme={null} // RWA with price published to custom account const rwaNavData = [{ u64FixedAddress: { navAccountAddress: issuerOracleAccount, navPriceOffset: 0, priceDecimals: 6, } }]; await program.methods .initAssetConfigAccount( rwaNavData, 0, // Single source { rwa: {} } ) .accounts({ /* ... */ }) .rpc(); ``` ### Multiple Sources: RWA with Redundancy ```typescript theme={null} // RWA with primary oracle + Pyth backup const rwaNavData = [ { u64FixedAddress: { navAccountAddress: primaryOracle, navPriceOffset: 0, priceDecimals: 6, } }, { pythPush: { pythPushAccountAddress: pythAccount, feedId: Array.from(Buffer.from("40ac3329933a6b5b65cf31496018c5764ac0567316146f7d0de00095886b480d", "hex")), maxAgeSecs: new BN(86_400), maxConfBps: 100, } } ]; await program.methods .initAssetConfigAccount( rwaNavData, 100, // Allow 1% divergence between sources { rwa: {} } ) .accounts({ /* ... */ }) .rpc(); ``` *** ## Reading NAV Accounts in Swaps When executing swaps, NAV source accounts must be passed as remaining accounts: ```typescript theme={null} // Collect all NAV source accounts for both assets const navAccounts = [ // RWA NAV sources { pubkey: rwaOracleAccount, isWritable: false, isSigner: false }, // Stablecoin NAV sources (if not hardcoded) // ... additional accounts as needed ]; await program.methods .swap(amount, minOut, null, { stableToAsset: {} }, { exactIn: {} }) .accounts({ /* ... */ }) .remainingAccounts(navAccounts) .rpc(); ``` Hardcoded NAV sources don't require external accounts. Only `U64FixedAddress` and `PythPush` sources need their accounts passed. *** ## Best Practices ### Choosing NAV Sources | Asset Type | Recommended Source | Rationale | | ------------------------ | ------------------ | ---------------------------------- | | Dollar-pegged stablecoin | Hardcoded | No oracle dependency, fixed \$1.00 | | NAV-accruing RWA | U64FixedAddress | Issuer publishes official NAV | | Market-priced asset | PythPush | Real-time market data | | Critical assets | Multiple sources | Redundancy and validation | ### Divergence Thresholds | Scenario | Recommended BPS | Notes | | --------------------------- | --------------- | -------------------------- | | Single source | 0 | No divergence possible | | Similar sources (same feed) | 10-50 | Account for timing | | Different feeds | 100-200 | Allow for feed differences | | Volatile assets | 200-500 | Wider tolerance needed | ### Monitoring * Track NAV source health and availability * Alert on divergence events (swaps blocked) * Monitor Pyth oracle staleness * Verify issuer oracle updates regularly *** ## Error Handling | Error | Cause | Solution | | --------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------- | | `InvalidFeedId` | Pyth feed id is zeroed | Provide the expected 32-byte feed id | | `InvalidMaxAge` / `MaxAgeTooLarge` | Pyth max age is invalid | Use a value from 1 second through 24 hours | | `ConfBpsTooLarge` | Pyth max confidence setting exceeds the ceiling | Use 1000 bps or lower | | `MissingPythAccount` / `FeedIdMismatch` | Pyth receiver account is missing or for the wrong feed | Pass the configured Pyth account in remaining accounts | | `ConfidenceTooLow` | Pyth confidence ratio exceeds `max_conf_bps` | Investigate oracle health or widen the configured threshold | | Price returns 0 | Divergence exceeded | Check `price_difference_bps` | | `InvalidAccountPublicKey` | Required NAV source account was not passed | Include each non-hardcoded source account in remaining accounts | *** Explore the program's modular design and account structure # Swap Instruction Source: https://docs.multiliquid.xyz/svm/instructions/swap Core swap instruction for executing trades between RWAs and stablecoins ## Overview The `swap` instruction is the core trading operation of the Multiliquid Program, enabling atomic swaps between Real World Asset (RWA) tokens and stablecoins. It supports both swap directions and both exact-in and exact-out modes. ### Key Responsibilities * Execute bidirectional swaps between RWAs and stablecoins * Calculate swap amounts using NAV-based pricing * Apply protocol fees and LP fees * Validate all pause states before execution * Emit comprehensive swap events ## Swap Instruction ### Function Signature ```rust theme={null} pub fn swap( ctx: Context, amount: u64, min_amount_out: Option, max_amount_in: Option, swap_direction: SwapDirection, swap_type: SwapType, ) -> Result<()> ``` ### Parameters | Parameter | Type | Description | | ---------------- | --------------- | ------------------------------------------------------- | | `amount` | `u64` | Primary amount (input for ExactIn, output for ExactOut) | | `min_amount_out` | `Option` | Minimum output (slippage protection for ExactIn) | | `max_amount_in` | `Option` | Maximum input (slippage protection for ExactOut) | | `swap_direction` | `SwapDirection` | Direction of the swap | | `swap_type` | `SwapType` | ExactIn or ExactOut mode | ### Swap Direction ```rust theme={null} pub enum SwapDirection { StableToAsset, // Stablecoin → RWA AssetToStable, // RWA → Stablecoin } ``` ### Swap Type ```rust theme={null} pub enum SwapType { ExactIn, // User specifies exact input amount ExactOut, // User specifies exact output amount } ``` ### Required Accounts ```rust theme={null} #[derive(Accounts)] pub struct Swap<'info> { // Signer pub user: Signer<'info>, // Pair account (PDA seeded by liquidity_provider, stable mint, asset mint) pub pair: Account<'info, Pair>, // Program authority PDA (owns the stablecoin fee vault) pub program_authority: UncheckedAccount<'info>, // Liquidity provider wallet (referenced in PDA seeds) pub liquidity_provider: UncheckedAccount<'info>, // LP-specific vault authority PDA (signs LP vault transfers) pub lp_vault_authority: UncheckedAccount<'info>, // Token mints pub stable_coin_mint_address: InterfaceAccount<'info, Mint>, pub asset_token_mint_address: InterfaceAccount<'info, Mint>, // User token accounts pub asset_token_user_token_account: InterfaceAccount<'info, TokenAccount>, pub stable_coin_user_token_account: InterfaceAccount<'info, TokenAccount>, // LP vault token accounts (ATAs owned by lp_vault_authority) pub stable_coin_vault_token_account: InterfaceAccount<'info, TokenAccount>, pub asset_token_vault_token_account: InterfaceAccount<'info, TokenAccount>, // Configuration accounts pub global_config: Account<'info, GlobalConfig>, pub fee_token_account: InterfaceAccount<'info, TokenAccount>, pub rwa_config: Account<'info, AssetConfig>, pub stable_config: Account<'info, AssetConfig>, pub lp_stable_config: Account<'info, LpStableConfig>, // Token programs (one per mint, supporting Token and Token-2022) pub token_program_stable: Interface<'info, TokenInterface>, pub token_program_asset: Interface<'info, TokenInterface>, // NAV accounts passed via remaining_accounts // ... up to 5 NAV source accounts per asset } ``` ### Validation Checks Before executing the swap, the instruction validates: 1. **Pause States**: Program, both assets, pair, and LP config must be unpaused 2. **Asset Types**: RWA must be `AssetType::Rwa`, stablecoin must be `AssetType::Stable` 3. **Amount**: Must be greater than zero 4. **NAV Prices**: Both assets must return valid, non-zero NAV prices 5. **Slippage**: Calculated output/input must satisfy min/max constraints 6. **Combined StableToAsset Fees**: `protocol_fees_bps + redemption_fee_bps` must not exceed 10000 ### Events ```rust theme={null} #[event] pub struct SwapExecuted { pub requestor: Pubkey, // User wallet pub amount_in: u64, // Actual input amount pub protocol_fee_amount: u64, // Protocol fees collected pub discount_bps: u16, // LP fee applied (redemption or discount) pub amount_out: u64, // Actual output amount pub pair: Pubkey, // Pair account address pub swap_direction: SwapDirection, pub swap_type: SwapType, } ``` ## Swap Calculation ### NAV-Based Pricing **Deterministic Pricing**: The Multiliquid Program uses NAV-based pricing, not AMM-style liquidity curves. Prices are determined by asset NAV values, not by liquidity depth. There is no price impact from trade size. The swap amount is calculated using the exchange rate between asset NAVs: ``` output_amount = input_amount × input_nav / output_nav ``` ### Fee Application Fees are applied in a specific order depending on swap direction: #### StableToAsset (Stablecoin → RWA) 1. **Combined Fees**: Protocol fees and redemption fee are calculated simultaneously from the input amount 2. **Exchange Rate**: Apply NAV ratio to the post-fee amount to calculate RWA output ``` total_fees_bps = protocol_fees_bps + redemption_fee_bps protocol_fees = ceil(amount × protocol_fees_bps / 10000) total_fees = ceil(amount × total_fees_bps / 10000) amount_after_fees = amount - total_fees rwa_output = amount_after_fees × stable_nav / rwa_nav ``` #### AssetToStable (RWA → Stablecoin) 1. **Exchange Rate**: Apply NAV ratio to calculate stablecoin equivalent 2. **Discount Rate**: Apply LP discount fee 3. **Protocol Fees**: Deducted from discounted amount ``` stable_equivalent = rwa_amount × rwa_nav / stable_nav after_discount = stable_equivalent - (stable_equivalent × discount_rate_bps / 10000) stable_output = after_discount - protocol_fee ``` ### Decimal Normalization All calculations use 128-bit arithmetic with proper decimal handling: * NAV prices normalized to 9 decimals * Intermediate calculations use combined decimal precision * Final amounts rounded appropriately (ceil for fees, floor for outputs) ## Usage Examples ### ExactIn: Stable → RWA Swap exactly 1000 USDC for RWA tokens with minimum output protection: ```typescript theme={null} import { BN } from "@coral-xyz/anchor"; const amount = new BN(1000_000000); // 1000 USDC (6 decimals) const minAmountOut = new BN(990_000000); // Minimum 990 RWA tokens await program.methods .swap( amount, minAmountOut, // min_amount_out null, // max_amount_in (not used for ExactIn) { stableToAsset: {} }, { exactIn: {} } ) .accounts({ user: wallet.publicKey, pair, programAuthority, liquidityProvider, lpVaultAuthority, stableCoinMintAddress, assetTokenMintAddress, assetTokenUserTokenAccount, stableCoinUserTokenAccount, stableCoinVaultTokenAccount, assetTokenVaultTokenAccount, globalConfig, feeTokenAccount, rwaConfig, stableConfig, lpStableConfig, tokenProgramStable: TOKEN_PROGRAM_ID, tokenProgramAsset: TOKEN_PROGRAM_ID, }) .remainingAccounts(navAccounts) // NAV source accounts .rpc(); ``` ### ExactOut: Stable → RWA Receive exactly 1000 RWA tokens, spending at most a specified USDC amount: ```typescript theme={null} const amount = new BN(1000_000000); // Exact 1000 RWA tokens out const maxAmountIn = new BN(1010_000000); // Maximum 1010 USDC in await program.methods .swap( amount, null, // min_amount_out (not used for ExactOut) maxAmountIn, // max_amount_in { stableToAsset: {} }, { exactOut: {} } ) .accounts({ // ... same accounts as above }) .remainingAccounts(navAccounts) .rpc(); ``` ### ExactIn: RWA → Stable Swap exactly 1000 RWA tokens for stablecoins: ```typescript theme={null} const amount = new BN(1000_000000); // 1000 RWA tokens const minAmountOut = new BN(995_000000); // Minimum 995 USDC await program.methods .swap( amount, minAmountOut, null, { assetToStable: {} }, { exactIn: {} } ) .accounts({ // ... accounts }) .remainingAccounts(navAccounts) .rpc(); ``` ### ExactOut: RWA → Stable Receive exactly 1000 USDC, spending at most a specified RWA amount: ```typescript theme={null} const amount = new BN(1000_000000); // Exact 1000 USDC out const maxAmountIn = new BN(1005_000000); // Maximum 1005 RWA in await program.methods .swap( amount, null, maxAmountIn, { assetToStable: {} }, { exactOut: {} } ) .accounts({ // ... accounts }) .remainingAccounts(navAccounts) .rpc(); ``` ## Error Codes | Error | Description | | ---------------------------- | ------------------------------------------------------------- | | `ProgramPaused` | Program is paused | | `RwaPaused` | RWA asset is paused | | `StablePaused` | Stablecoin or LP stable config is paused | | `PairPaused` | Trading pair is paused | | `AmountMustBePositive` | Amount is zero | | `InvalidAssetType` | Asset types don't match expected (RWA vs Stable) | | `AmountOutTooLow` | Output less than minimum | | `AmountInTooHigh` | Input more than maximum | | `FeesOutOfRange` | StableToAsset protocol plus redemption fees exceed 10000 BPS | | `ProtocolFeesMustBePositive` | Nonzero configured protocol fee rounded to zero for the trade | | `DiscountMustBePositive` | Nonzero configured LP fee rounded to zero for the trade | | `AmountInMustBePositive` | Calculated vault input is zero | | `AmountOutMustBePositive` | Calculated output is zero | | `InvalidNav` | NAV sources returned invalid or divergent data | | `MathOverflow` | Calculation overflow | ## Access Control **Access**: Permissionless Any wallet can execute swaps provided: * All pause states are inactive * User has sufficient token balance * User has approved token transfers * Valid NAV prices are available *** Learn about program-wide configuration management # Program Architecture Source: https://docs.multiliquid.xyz/svm/overview/architecture Comprehensive technical overview of the Multiliquid Program's modular design and account infrastructure on Solana ## System Overview The Multiliquid Program is built on a modular, account-based architecture that enables atomic swaps between multiple permissioned Real World Asset (RWA) tokens and multiple stablecoins on Solana. The system is designed for institutional-grade operations with comprehensive access controls, transparent fee structures, and flexible pricing mechanisms. ### Design Principles 1. **Account-Based State**: All state stored in Program-Derived Address (PDA) accounts 2. **Anchor Framework**: Type-safe program development with automatic serialization 3. **Atomicity**: All operations either fully succeed or fully revert within a transaction 4. **Transparency**: All state changes emit comprehensive events via Anchor's event system 5. **Extensibility**: New assets can be integrated without program upgrades ## Core Account System The program consists of five primary account types that work together to enable secure, atomic swaps between RWAs and stablecoins. ### How the Accounts Work Together At the heart of the system, **GlobalConfig** acts as the central configuration holding program-wide settings including the admin, fee wallet, and protocol fees. When an admin registers a new token, an **AssetConfig** account is created to store the token's NAV pricing sources and configuration. When a trading pair is created, a **Pair** account links an RWA to a stablecoin with LP-specific settings, while **LpStableConfig** tracks the LP's stablecoin-specific pause state. Token custody is managed through LP vault token accounts owned by an LP-specific **VaultAuthority** PDA, with protocol fees collected in fee vault token accounts owned by the global **ProgramAuthority** PDA. This separation of concerns ensures each account focuses on its specialized responsibility while maintaining atomic execution. ### 1. GlobalConfig Account **PDA Seeds**: `["global_config"]` The central configuration account managing program-wide settings. ```rust theme={null} pub struct GlobalConfig { pub admin: Pubkey, // Program administrator pub fee_wallet: Pubkey, // Protocol fee collection wallet pub protocol_fees_bps: u16, // Protocol fees (0-9900 basis points) pub paused: bool, // Program-wide pause flag pub pending_new_admin: Option, // Two-step admin transfer pub bump: u8, // PDA bump seed } ``` #### Core Responsibilities * **Admin Management**: Track current admin and pending admin transfer * **Fee Configuration**: Store protocol fee percentage * **Global Pause**: Control program-wide pause state * **Fee Wallet**: Destination for claimed protocol fees Complete instruction details for global configuration management ### 2. AssetConfig Account **PDA Seeds**: `["asset", mint_address]` Per-token configuration storing NAV pricing sources and asset-level controls. ```rust theme={null} pub struct AssetConfig { pub mint_address: Pubkey, // Token mint address pub nav_data: Vec, // Up to 5 NAV pricing sources pub price_difference_bps: u16, // Maximum price divergence tolerance pub paused: bool, // Asset-level pause flag pub version: u8, // Configuration version pub asset_type: AssetType, // Rwa or Stable pub used_in_pairs_count: u16, // Number of pairs using this asset pub bump: u8, // PDA bump seed } ``` #### Asset Types ```rust theme={null} pub enum AssetType { Rwa, // Real World Asset token Stable, // Stablecoin token } ``` #### Core Responsibilities * **NAV Pricing**: Store and manage up to 5 pricing sources per asset * **Price Validation**: Enforce maximum price divergence between sources * **Asset Pause**: Independent pause control per asset * **Usage Tracking**: Track how many pairs reference this asset Complete instruction details for asset configuration ### 3. Pair Account **PDA Seeds**: `["pair", liquidity_provider, stable_mint, asset_mint]` Trading pair state linking an RWA to a stablecoin with LP-specific configuration. ```rust theme={null} pub struct Pair { pub redemption_fee_bps: u16, // Fee for Stable → RWA swaps pub discount_rate_bps: u16, // Fee for RWA → Stable swaps pub stable_coin_mint_address: Pubkey, // Stablecoin token mint pub asset_token_mint_address: Pubkey, // RWA token mint pub liquidity_provider: Pubkey, // LP owner address pub paused: bool, // Pair-level pause flag pub bump: u8, // PDA bump seed } ``` #### Core Responsibilities * **Fee Configuration**: Store LP-specific redemption and discount fees * **Pair Identity**: Link specific RWA and stablecoin tokens * **LP Ownership**: Track the liquidity provider controlling this pair * **Pair Pause**: Independent pause control per pair Complete instruction details for pair management ### 4. LpStableConfig Account **PDA Seeds**: `["lp_stable_config", stable_mint, liquidity_provider]` Per-LP, per-stablecoin configuration for controlling multiple pairs at once. ```rust theme={null} pub struct LpStableConfig { pub stable_coin_mint_address: Pubkey, // Stablecoin mint pub paused: bool, // Pause all pairs with this config pub liquidity_provider: Pubkey, // LP address pub bump: u8, // PDA bump seed } ``` #### Core Responsibilities * **Batch Pause Control**: Pause all pairs for a specific LP/stablecoin combination * **LP Identification**: Link configuration to specific liquidity provider * **Stablecoin Association**: Scope configuration to specific stablecoin ### 5. Vault Accounts **VaultAuthority PDA Seeds**: `["vault_authority", liquidity_provider]` **LP Vault Token Account**: Associated token account for `(mint_address, vault_authority)` **ProgramAuthority PDA Seeds**: `["program_authority"]` **Fee Vault Token Account**: Associated token account for `(mint_address, program_authority)` Token accounts for custody and fee collection. For permissioned mints, both the listed **Liquidity Provider** address and its listed **Vault Authority (PDA)** must be separately whitelisted (allowlisted) by the token issuer. Whitelisting the liquidity provider does not whitelist its PDA. The LP-owned token account and PDA-owned vault ATA must also be thawed before liquidity or swap transfers can succeed. #### Vault (Liquidity Custody) * Holds liquidity for a specific LP/token combination * Owned by the LP-specific `vault_authority` PDA, not by the global program authority * Derived as the associated token account for the token mint and `vault_authority` * Shared across multiple pairs using the same LP and token * Tracked by `UserVaultInfo` account for usage counting #### Fee Vault (Protocol Fees) * Created per asset when asset config is initialized * Owned by the global `program_authority` PDA * Protocol fees accumulate only in stablecoin fee vaults, since fees are always denominated in stablecoins * Claimed via `claim_fees` instruction to fee wallet ### 6. UserVaultInfo Account **PDA Seeds**: `["user_vault_info", mint_address, liquidity_provider]` Tracks vault usage across pairs. ```rust theme={null} pub struct UserVaultInfo { pub user: Pubkey, // LP wallet pub mint_address: Pubkey, // Token mint pub used: u16, // Number of pairs using this vault pub bump: u8, // PDA bump seed } ``` ### 7. Vault and Program Authorities #### VaultAuthority **PDA Seeds**: `["vault_authority", liquidity_provider]` The vault authority is an LP-specific signer PDA used to own and authorize outbound transfers from LP liquidity vaults. Every vault for the same LP is an associated token account owned by this PDA. #### ProgramAuthority **PDA Seeds**: `["program_authority"]` The program authority is a global signer PDA used for protocol fee vaults. It authorizes outbound transfers from fee vaults during `claim_fees`. It does not own LP liquidity vaults. ## Operational Flow ### Swap Execution Flow ```mermaid theme={null} sequenceDiagram participant User participant Program participant GlobalConfig participant AssetConfigs participant Pair participant LpStableConfig participant Vaults participant FeeVault User->>Program: swap instruction Note over Program: 1. Validate pause states Program->>GlobalConfig: Check not paused Program->>AssetConfigs: Check RWA not paused Program->>AssetConfigs: Check Stablecoin not paused Program->>Pair: Check pair not paused Program->>LpStableConfig: Check LP config not paused Note over Program: 2. Get NAV prices Program->>AssetConfigs: Read RWA NAV price Program->>AssetConfigs: Read Stablecoin NAV price Note over Program: 3. Calculate amounts Program->>Program: Apply protocol fees Program->>Program: Apply LP fees (discount/redemption) Program->>Program: Calculate exchange rate Note over Program: 4. Execute transfers Program->>Vaults: Move input tokens into LP vault Program->>FeeVault: Move protocol fees into fee vault Program->>User: Move output tokens from LP vault Note over Program: 5. Emit event Program->>Program: Emit SwapExecuted event ``` ## NAV Pricing System The program supports three types of NAV (Net Asset Value) pricing sources: ### NavData Types ```rust theme={null} pub enum NavData { /// Read price from a fixed on-chain account address U64FixedAddress { nav_account_address: Pubkey, // Account containing price nav_price_offset: u16, // Byte offset in account data price_decimals: u8, // Price decimals (0-9) }, /// Static hardcoded price value Hardcoded { hardcoded_price: u64, // Fixed price value price_decimals: u8, // Price decimals (0-9) }, /// Pyth oracle push-based pricing PythPush { pyth_push_account_address: Pubkey, // Pyth receiver price update account feed_id: [u8; 32], // Expected Pyth feed id max_age_secs: u64, // Maximum accepted price age max_conf_bps: u16, // Maximum accepted confidence ratio }, } ``` ### Price Aggregation When multiple NAV sources are configured: 1. All prices are normalized to 9 decimal places 2. Price divergence is validated against `price_difference_bps` as `(max_nav - min_nav) / max_nav` 3. If divergence exceeds threshold, the function returns 0 (blocking swaps) 4. Average price is returned if all sources agree within tolerance Complete documentation on NAV pricing configuration ## Fee Structure ### Protocol Fees Configured in `GlobalConfig.protocol_fees_bps`: * Range: 0-9900 basis points (0% - 99%) * Applied to all swaps regardless of direction * Collected in stablecoin fee vaults * Claimed to `fee_wallet` via `claim_fees` instruction ### LP Fees Configured per pair by the liquidity provider: | Fee Type | Direction | Description | | -------------------- | ------------ | --------------------------------------------- | | `redemption_fee_bps` | Stable → RWA | Fee charged when redeeming stablecoin for RWA | | `discount_rate_bps` | RWA → Stable | Fee charged when swapping RWA for stablecoin | ### Fee Calculation Order **Stable → RWA (StableToAsset)**: 1. Calculate protocol fees and redemption fee simultaneously from input 2. Deduct both fees from input amount 3. Apply NAV exchange rate to post-fee amount **RWA → Stable (AssetToStable)**: 1. Apply NAV exchange rate 2. Apply discount rate to output 3. Deduct protocol fees from discounted amount ## Permission Model The program implements a comprehensive permission model: ### Admin Role **Controlled by**: `GlobalConfig.admin` **Permissions**: * Initialize and update global configuration * Set and confirm new admin (two-step transfer) * Initialize asset configurations * Update asset configurations * Initialize pairs on behalf of LPs * Set pause state for assets and LP configs ### Liquidity Provider Role **Identified by**: Pair and vault ownership **Permissions**: * Update pair configuration (fees, pause state) * Close pairs they own * Add and remove liquidity from their vaults * Set pause state for their LP stable configs ### User Role (Permissionless) **Anyone can**: * Execute swaps on active pairs * Claim protocol fees (sent to fee\_wallet) ### Permission Hierarchy ```mermaid theme={null} graph TD ADMIN[Admin
Global Configuration] LP[Liquidity Providers
Pair Management] USER[Users
Swap Execution] ADMIN -->|Initializes| ASSETS[Asset Configs] ADMIN -->|Creates| PAIRS[Trading Pairs] ADMIN -->|Controls| GLOBAL_PAUSE[Global Pause] ADMIN -->|Controls| ASSET_PAUSE[Asset Pause] LP -->|Configures| PAIRS LP -->|Manages| LIQUIDITY[Vaults] LP -->|Controls| PAIR_PAUSE[Pair Pause] LP -->|Controls| LP_PAUSE[LP Config Pause] USER -->|Executes| SWAPS[Swaps] USER -->|Triggers| CLAIM_FEES[Fee Claims] ``` ## Event System The program emits events for all significant state changes using Anchor's event system: ### Swap Events ```rust theme={null} #[event] pub struct SwapExecuted { pub requestor: Pubkey, // User executing swap pub amount_in: u64, // Input amount pub protocol_fee_amount: u64, // Protocol fees collected pub discount_bps: u16, // LP fee applied pub amount_out: u64, // Output amount pub pair: Pubkey, // Pair account address pub swap_direction: SwapDirection, pub swap_type: SwapType, } ``` ### Administrative Events Events are emitted for configuration changes, pair updates, liquidity operations, and pause state changes. ## Extensibility The program's modular architecture enables straightforward extension: ### Adding a New Token 1. **Admin Action**: Call `init_asset_config_account` with NAV sources 2. **Fee Vault Created**: Automatic fee vault creation for the asset 3. **Configure NAV**: Set appropriate pricing sources (Pyth, hardcoded, etc.) 4. **Test Integration**: Validate NAV reads correctly ### Adding a New Pair 1. **Admin Action**: Call `init_pair` specifying LP, RWA, and stablecoin 2. **Vaults Created**: Automatic vault creation if needed 3. **LP Configuration**: LP calls `update_pair` to adjust fees or pause state as needed 4. **Add Liquidity**: LP adds tokens to vaults 5. **Enable Trading**: Pair is ready for swaps *** Review the comprehensive security measures, access controls, and pause mechanisms # Multiliquid Protocol Source: https://docs.multiliquid.xyz/svm/overview/index Decentralized infrastructure for atomic swaps between permissioned Real World Assets and stablecoins ## Overview Multiliquid is a high-performance program that enables institutional-grade atomic swaps between multiple permissioned Real World Asset (RWA) tokens and multiple stablecoins. Built with the Anchor framework, the program provides secure, transparent, and efficient infrastructure for digital asset exchange on the Solana Virtual Machine (SVM). Explore the program's modular design and account structure Review security measures and access control mechanisms Learn how to execute swaps on the Multiliquid Program ## Key Features ### Atomic Swap Execution The program ensures complete transaction atomicity—either all conditions are fulfilled or none are. This eliminates partial executions and guarantees consistent state across all operations within a single Solana transaction. ### Multi-Asset Support * **Any RWA Token**: Support for multiple permissioned Real World Asset tokens * **Any Stablecoin**: Integration with multiple Liquidity Providers * **Token Standards**: Native support for both SPL Token and Token-2022 programs * **Extensible Design**: Modular architecture enables seamless onboarding of new assets ### Institutional-Grade Security * **Program-Derived Addresses (PDAs)**: All accounts derived deterministically for security * **Role-Based Access Control**: Granular permissions for admin and liquidity provider operations * **Multi-Level Pause Controls**: Emergency pause capabilities at program, asset, pair, and LP levels * **Two-Step Admin Transfer**: Secure admin handover requiring confirmation from new admin ### Transparent Fee Structure * **Protocol Fees**: Configurable protocol-level fees in basis points * **LP Fees**: Liquidity providers set their own discount rates and redemption fees * **On-Chain Fee Collection**: All fees accumulated transparently in dedicated fee vault accounts * **Per-Stablecoin Fee Vaults**: Separate fee collection per stablecoin for clear accounting ## How It Works The program operates through a coordinated system of Solana accounts and instructions: 1. **Global Configuration**: Central program settings managing admin, fee wallet, protocol fees, and global pause state 2. **Asset Configurations**: Per-token accounts storing NAV (Net Asset Value) pricing sources and asset-level controls 3. **Pairs**: Trading pair accounts linking RWAs to stablecoins with LP-specific fee configuration 4. **LP Vaults**: Token custody ATAs owned by each LP's `vault_authority` PDA for each LP/token combination 5. **Fee Vaults**: Protocol fee collection ATAs owned by the global `program_authority` PDA per stablecoin ### Swap Process ```mermaid theme={null} sequenceDiagram participant User participant Program as Multiliquid Program participant AssetConfig as Asset Configs participant Pair participant Vault as LP Vaults participant FeeVault as Fee Vault User->>Program: Swap Instruction Program->>Pair: Validate pair state & fees Program->>AssetConfig: Get RWA NAV Price Program->>AssetConfig: Get Stablecoin NAV Price Program->>Program: Calculate swap amounts & fees User->>Vault: Transfer tokens in Vault->>User: Transfer tokens out Program->>FeeVault: Collect protocol fees Program->>Program: Emit SwapExecuted event ``` ## Supported Assets The program supports a growing ecosystem of institutional-grade digital assets on Solana. ### Real World Assets (RWAs) | Asset | Description | NAV Oracle | | :---- | :------------------------------------------ | :--------------- | | ACRED | Securitize Credit Fund | Pyth Oracle | | BENJI | Franklin OnChain US Gov Money Fund | Hardcoded \$1.00 | | USCC | Superstate Crypto Carry Fund | Pyth Oracle | | USTB | Superstate Short Duration US Gov Securities | Pyth Oracle | | VBILL | VanEck Treasury Fund | Hardcoded \$1.00 | | WTGXX | WisdomTree US Dollar Digital Fund | Hardcoded \$1.00 | ### Stablecoins | Asset | Description | | :---- | :-------------------------- | | USDC | Circle USD Coin (SPL Token) | The program supports both SPL Token and Token-2022 standards, enabling seamless onboarding of new assets as they launch on Solana. See all deployed program and token addresses on Solana Mainnet ## Program Governance The program employs a multi-tiered permission model: * **Program Admin**: Manages global configuration, asset acceptance, and pair creation * **Liquidity Providers**: Control their pair configurations, fees, and liquidity * **Permissionless Swaps**: Any user can execute swaps on active, unpaused pairs ## Technology Stack Built on industry-standard frameworks: * **Anchor 0.32.1**: Modern Solana program framework with safety features * **Rust**: Memory-safe systems programming language * **SPL Token**: Standard Solana token program support * **Token-2022**: Extended token program with additional features * **PDAs**: Program-Derived Addresses for deterministic account derivation ## Integration Options ### Current: **Multiliquid UI**: The Multiliquid web interface (coming soon) provides institutional users with a secure, intuitive environment for executing swaps. The UI handles wallet integration, transaction signing, and real-time status updates. **Direct Program Integration**: Multiliquid allows for direct swap integration on Solana using the Anchor client or @solana/web3.js. ### Future: REST API A comprehensive REST API is under development to enable programmatic access for institutional trading systems, portfolio management platforms, and automated treasury operations. For developer integration details and technical specifications, contact the Multiliquid team directly. ## Next Steps Deep dive into the program's account structure, instruction flow, and swap mechanics Examine security measures, access controls, and pause mechanisms Browse detailed instruction documentation and parameter specifications *** The Multiliquid Program handles significant value and operates in regulated markets. All integrations should undergo thorough testing and security review. Contact the Multiliquid team before production deployment. # Security & Access Control Source: https://docs.multiliquid.xyz/svm/overview/security Comprehensive security measures, access controls, and operational safeguards in the Multiliquid Program on Solana ## Security Overview The Multiliquid Program is built with institutional-grade security as a core requirement. Every instruction, account validation, and state transition has been designed with security-first principles, incorporating multiple layers of protection and comprehensive access controls. Anchor framework and secure coding practices Multi-tiered permission model Multi-level emergency pause capabilities Secure admin handover mechanism ## Program Security ### Anchor Framework The program is built exclusively using the Anchor framework, providing: **Type Safety**: * Automatic account serialization and deserialization * Compile-time account constraint validation * Strong typing for all instruction parameters **Account Validation**: * Automatic owner checks on all accounts * PDA derivation verification via seeds constraints * `has_one` constraints for relationship validation **Security Features**: * Built-in discriminator checks to prevent account confusion * Automatic rent-exemption verification * Safe arithmetic operations with checked math ### Core Security Mechanisms #### 1. Program-Derived Addresses (PDAs) All program accounts are derived deterministically using PDAs: ```rust theme={null} // Example: Pair account derivation #[account( seeds = [ PAIR_PREFIX, liquidity_provider.key().as_ref(), stable_mint.key().as_ref(), asset_mint.key().as_ref() ], bump = pair.bump, )] pub pair: Account<'info, Pair>, ``` **Security Benefits**: * Accounts cannot be spoofed or substituted * Deterministic derivation enables verification * No external account addresses stored unnecessarily LP liquidity vaults use an additional authority separation: ```rust theme={null} // LP vault authority derivation #[account( seeds = [VAULT_AUTHORITY_PREFIX, liquidity_provider.key().as_ref()], bump, )] pub lp_vault_authority: UncheckedAccount<'info>, ``` Vault token accounts are associated token accounts owned by this LP-specific `vault_authority` PDA. The global `program_authority` PDA is reserved for protocol fee vaults, so one LP's vault authority cannot sign for another LP's liquidity. #### 2. Signer Validation All privileged operations require appropriate signatures: ```rust theme={null} // Admin-only operation #[account( has_one = admin, )] pub global_config: Account<'info, GlobalConfig>, pub admin: Signer<'info>, // LP-only operation #[account( has_one = liquidity_provider, )] pub pair: Account<'info, Pair>, pub liquidity_provider: Signer<'info>, ``` #### 3. Integer Overflow Protection All arithmetic operations use checked math: ```rust theme={null} // Safe multiplication with overflow check let result = amount .checked_mul(price) .ok_or(ErrorCode::MathOverflow)?; // Safe division with zero check let output = numerator .checked_div(denominator) .ok_or(ErrorCode::MathOverflow)?; ``` #### 4. Input Validation Every instruction validates inputs before execution: * Amounts must be greater than zero * Basis points must be within valid range (0-9900) * StableToAsset combined protocol and redemption fees must not exceed 10000 BPS * NAV sources must have valid decimals (0-9) * Asset types must match expected values ### Token Security #### SPL Token Integration The program uses Anchor's SPL token helpers for safe token operations: ```rust theme={null} // Safe token transfer using CPI (checked variant) transfer_checked( CpiContext::new( token_program.to_account_info(), TransferChecked { from: user_token_account.to_account_info(), to: vault.to_account_info(), authority: user.to_account_info(), mint: mint.to_account_info(), }, ), amount, decimals, )?; ``` **Security Features**: * Automatic account ownership validation * Mint address verification * Sufficient balance checks #### Token-2022 Support The program supports both SPL Token and Token-2022: * Automatic program detection based on mint owner * Compatible with Token-2022 extensions * Safe handling of both token standards ## Access Control System The program implements a sophisticated multi-tier permission model: ### Admin Role **Holder**: `GlobalConfig.admin` **Permissions**: | Operation | Description | | --------------------------------- | ------------------------------------------- | | `init_global_config` | Initialize program configuration (one-time) | | `update_global_config` | Update fees, fee wallet, pause state | | `set_new_admin` | Propose new admin address | | `confirm_new_admin` | Accept admin role (new admin) | | `init_asset_config_account` | Register new tokens | | `update_asset_config_account` | Update token NAV sources | | `init_pair` | Create trading pairs | | `set_paused_for_asset` | Pause/unpause assets | | `set_paused_for_lp_stable_config` | Pause/unpause LP configs | **Security Considerations**: * Admin should be a multi-signature wallet * Two-step transfer prevents accidental handover * Program starts paused by default ### Liquidity Provider Role **Holder**: Address specified as `liquidity_provider` in pair creation **Permissions**: | Operation | Description | | --------------------------------- | ------------------------------ | | `update_pair` | Configure fees and pause state | | `close_pair` | Permanently close a pair | | `add_liquidity` | Deposit tokens to vault | | `remove_liquidity` | Withdraw tokens from vault | | `set_paused_for_lp_stable_config` | Pause own LP config | **Security Considerations**: * Each LP controls only their own pairs * Cannot affect other LPs' configurations * Pair pause state defaults to unpaused at creation, and LPs can pause or unpause their own pairs with `update_pair` ### User Role (Permissionless) **Holder**: Any wallet **Permissions**: | Operation | Description | | ------------ | -------------------------------------- | | `swap` | Execute token swaps | | `claim_fees` | Trigger fee distribution to fee wallet | **Security Considerations**: * Swaps validated against multiple pause states * Slippage protection via min/max amount parameters * Fees always sent to configured fee wallet (not caller) ## Pause Control System The program implements a comprehensive four-level pause system: ### Pause Hierarchy ```mermaid theme={null} graph TD GLOBAL[Global Pause
Program-wide halt] ASSET[Asset Pause
Per-token halt] LP_CONFIG[LP Config Pause
Per-LP/stablecoin halt] PAIR[Pair Pause
Per-pair halt] GLOBAL -->|Blocks| ALL[All Operations] ASSET -->|Blocks| ASSET_SWAPS[Swaps with this asset] LP_CONFIG -->|Blocks| LP_SWAPS[LP's stablecoin pairs] PAIR -->|Blocks| PAIR_SWAPS[This specific pair] ``` ### Level 1: Global Pause **Controlled by**: Admin via `update_global_config` **Effects**: * Blocks ALL swap operations * Blocks liquidity operations * Blocks fee claims * Admin operations still accessible **Use Cases**: * Emergency halt during security incident * Program maintenance or upgrade preparation * Regulatory requirement ### Level 2: Asset Pause **Controlled by**: Admin via `set_paused_for_asset` **Effects**: * Blocks swaps involving this specific asset * Other assets remain operational * Affects all pairs using this asset **Use Cases**: * Token-specific security issue * NAV oracle malfunction * Compliance requirement for specific asset ### Level 3: LP Stable Config Pause **Controlled by**: Admin OR LP via `set_paused_for_lp_stable_config` **Effects**: * Blocks all pairs for this LP/stablecoin combination * Other LPs unaffected * Other stablecoins for same LP unaffected **Use Cases**: * LP-specific operational issue * Stablecoin-specific concern for one LP * LP maintenance period ### Level 4: Pair Pause **Controlled by**: LP via `update_pair` **Effects**: * Blocks only this specific trading pair * All other pairs remain operational **Use Cases**: * Liquidity rebalancing * Fee adjustment period * Individual pair maintenance ### Swap Validation Every swap validates ALL four pause levels: ```rust theme={null} // Pause checks in swap instruction require!(!global_config.paused, ErrorCode::ProgramPaused); require!(!pair.paused, ErrorCode::PairPaused); require!(!rwa_config.paused, ErrorCode::RwaPaused); require!(!stable_config.paused, ErrorCode::StablePaused); require!(!lp_stable_config.paused, ErrorCode::StablePaused); ``` If ANY level is paused, the swap is rejected. ## Two-Step Admin Transfer The program implements a secure two-step admin transfer mechanism to prevent accidental or malicious admin handover: ### Step 1: Propose New Admin Current admin calls `set_new_admin`: ```rust theme={null} pub fn set_new_admin( ctx: Context, new_admin: Pubkey, ) -> Result<()> ``` **Effects**: * Sets `pending_new_admin` to proposed address * Current admin remains in control * No immediate transfer occurs ### Step 2: Confirm New Admin New admin calls `confirm_new_admin`: ```rust theme={null} pub fn confirm_new_admin( ctx: Context, ) -> Result<()> ``` **Requirements**: * Caller must match `pending_new_admin` * Caller must sign the transaction **Effects**: * `admin` updated to new address * `pending_new_admin` cleared * Transfer complete ### Security Benefits * **No Accidental Transfer**: Typo in address won't transfer control * **Recipient Verification**: New admin must actively accept * **Revocable**: Current admin can propose different address before confirmation ## Error Handling The program defines comprehensive error codes for security validation: ```rust theme={null} #[error_code] pub enum ErrorCode { #[msg("Unauthorized")] Unauthorized, #[msg("Program paused")] ProgramPaused, #[msg("Pair paused")] PairPaused, #[msg("RWA paused")] RwaPaused, #[msg("Stable paused")] StablePaused, #[msg("Invalid NAV")] InvalidNav, #[msg("Math Overflow")] MathOverflow, #[msg("Math Underflow")] MathUnderflow, #[msg("Insufficient liquidity")] InsufficientLiquidity, #[msg("Amount in too high")] AmountInTooHigh, #[msg("Amount out too low")] AmountOutTooLow, #[msg("Fees out of range")] FeesOutOfRange, #[msg("Price decimals too large")] PriceDecimalsTooLarge, // ... additional validation and operational error codes } ``` ## Operational Security Best Practices For institutions integrating with the program: ### Key Management * **Use Hardware Wallets**: For all signing operations * **Multi-Signature**: Admin should be a multi-sig wallet * **Key Rotation**: Periodic admin transfer to fresh keys * **Backup Procedures**: Secure backup of all signing keys ### Transaction Security * **Simulation**: Use Solana's transaction simulation before signing * **Verification**: Double-check all account addresses * **Priority Fees**: Set appropriate priority fees for time-sensitive operations * **Recent Blockhash**: Use recent blockhashes to prevent replay ### Monitoring * **Event Tracking**: Monitor program events for all state changes * **Balance Monitoring**: Track LP vault ATAs and protocol fee vault balances separately * **Pause State**: Alert on any pause state changes * **Admin Changes**: Alert on admin transfer initiation ## Known Limitations and Assumptions ### Token Compatibility The program assumes: * Tokens follow SPL Token or Token-2022 standards * Tokens do not have transfer fees that break atomicity * NAV oracles remain available and accurate * Token decimals are correctly configured ### Price Oracle Assumptions * NAV sources return valid prices within configured decimals * Price divergence threshold is appropriately set * At least one NAV source is always available * Pyth oracle accounts are correctly configured ### Operational Assumptions * Admin wallet is secure and properly managed * LPs manage their liquidity appropriately * Fee vault balances are claimed periodically * Pairs are closed properly before decommissioning *** ## Security Contacts For security vulnerabilities or concerns: **DO NOT** disclose security vulnerabilities publicly. Contact the Multiliquid team directly through secure channels. * **Website**: [https://www.multiliquid.xyz/](https://www.multiliquid.xyz/) * **Public Repository**: [https://github.com/uniformlabs](https://github.com/uniformlabs) *** This security documentation is a living document and will be updated as the program evolves and new security measures are implemented. *** Explore the main swap instruction managing all trading operations