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

# Integration Guide

> Integrate with Multiliquid on EVM chains using the v0.2.0 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.

Version `0.2.0` 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.

<CardGroup cols={2}>
  <Card title="Route Model" icon="route" href="#route-model">
    Understand route IDs, exact-in, exact-out, and delegate selection
  </Card>

  <Card title="Quotes and Simulation" icon="calculator" href="#quoting">
    Price hypothetical swaps or test complete settlement
  </Card>

  <Card title="Delegated Authorization" icon="signature" href="#delegated-authorization">
    Use standing allowances or one-time EIP-712 permits
  </Card>

  <Card title="Auto-Sweep" icon="bolt" href="/evm/guides/auto-sweep">
    LP prefund, sweep, and JIT liquidity configuration workflows
  </Card>

  <Card title="Asset Queries" icon="magnifying-glass" href="#querying-assets">
    Read assets, prices, fees, eligibility, and protocol status
  </Card>

  <Card title="LP Administration" icon="shield-check" href="#lp-admin-operations">
    Manage delegate configuration directly or through a multisig
  </Card>
</CardGroup>

## Installation

```bash theme={null}
npm install @uniformlabs/multiliquid-evm-sdk@0.2.0 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;
```

<Warning>
  Asset IDs are not derived from token addresses. Always use the IDs in the
  selected deployment config or the [Deployments](/evm/deployments) page.
</Warning>

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

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

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.

<Card title="Auto-Sweep Configuration" icon="bolt" href="/evm/guides/auto-sweep">
  Complete LP enablement, allowance, preview, JIT funding, and disablement
  workflow
</Card>

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

Version `0.2.0` 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)
