Skip to main content

Integration Overview

The Multiliquid Protocol on Ethereum can be integrated via the official TypeScript SDK, built on viem. The SDK provides type-safe access to all protocol operations including quoting, swap execution, asset queries, event monitoring, and yield management.

Installation

npm install @uniformlabs/multiliquid-evm-sdk viem
The SDK requires viem as a peer dependency. Node.js 18+ is required.

Client Initialization

The SDK exports a createMultiliquidClient factory along with pre-configured chain deployments:
import { createPublicClient, createWalletClient, http } from "viem";
import { mainnet as viemMainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { createMultiliquidClient, mainnet } from "@uniformlabs/multiliquid-evm-sdk";

// Setup viem clients
const publicClient = createPublicClient({
  chain: viemMainnet,
  transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});

const account = privateKeyToAccount("0x...");
const walletClient = createWalletClient({
  chain: viemMainnet,
  transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
  account,
});

// Create Multiliquid client
const ml = createMultiliquidClient({
  deployment: mainnet,    // or sepolia for testnet
  publicClient,
  walletClient,           // optional — required only for swap execution
});
The client exposes seven modules: ml.quote, ml.swap, ml.assets, ml.delegates, ml.yield, ml.events, and ml.multicall.

Asset IDs

Assets in the protocol are identified by bytes32 IDs (not token addresses). The SDK includes all asset IDs for each deployment:
import { mainnet } from "@uniformlabs/multiliquid-evm-sdk";

// RWA Asset IDs
mainnet.assetIds.rwa.ULTRA
mainnet.assetIds.rwa.JTRSY
mainnet.assetIds.rwa.WTGXX
mainnet.assetIds.rwa.BENJI
mainnet.assetIds.rwa.USTB
mainnet.assetIds.rwa.VBILL

// Stablecoin Asset IDs
mainnet.assetIds.stablecoin.USDC
mainnet.assetIds.stablecoin.TSY_YIELD
Asset IDs are NOT derived from token addresses. They are chosen on deployment using keccak256 of a specific string. Always use the IDs from the SDK’s deployment config or the Deployments page.
The Multiliquid Protocol uses deterministic NAV-based pricing for all swaps. Unlike AMM-style DEXs (Uniswap, Curve, etc.), there is no price slippage based on liquidity depth or market impact.Use the exact amounts returned by the quote functions without applying slippage buffers. Pricing is deterministic and based on real-time NAV values.

Quoting

The ml.quote module provides quote functions for all swap types. Each returns the exact amounts, fees, and an optional simulation result.

Stablecoin to RWA (ExactIn)

const quote = await ml.quote.swapIntoRWA({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,   // 1 USDC (6 decimals)
  user: account.address,
  simulate: true,                  // optional: verify swap would succeed
});

console.log("RWA output:", quote.rwaAmount);
console.log("Protocol fee:", quote.protocolFee);
console.log("Redemption fee:", quote.redemptionFee);
console.log("Gas estimate:", quote.simulation?.gasEstimate);

Stablecoin to RWA (ExactOut)

const quote = await ml.quote.swapIntoRWAExactOut({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaAmount: 1_000_000_000_000_000_000n,  // 1 ULTRA (18 decimals)
  user: account.address,
});

console.log("Stablecoin required:", quote.stablecoinAmount);

RWA to Stablecoin (ExactIn)

const quote = await ml.quote.swapIntoStablecoin({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaID: mainnet.assetIds.rwa.ULTRA,
  rwaAmount: 1_000_000_000_000_000_000n,  // 1 ULTRA
});

console.log("Stablecoin output:", quote.stablecoinAmount);
console.log("Protocol fee:", quote.protocolFee);

RWA to Stablecoin (ExactOut)

const quote = await ml.quote.swapIntoStablecoinExactOut({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinAmount: 1_000_000n,   // exact 1 USDC out
  user: account.address,
});

console.log("RWA required:", quote.rwaAmount);

RWA to RWA

Swap one RWA directly for another. The swap is priced through an intermediate stablecoin issuer.
const quote = await ml.quote.swapRWAToRWA({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaInID: mainnet.assetIds.rwa.ULTRA,
  rwaOutID: mainnet.assetIds.rwa.USTB,
  exactOut: false,
  rwaInAmount: 1_000_000_000_000_000_000n,   // 1 ULTRA in
  rwaOutAmount: 0n,
});

console.log("RWA output:", quote.rwaOutAmount);
console.log("Protocol fee:", quote.protocolFee);
console.log("Redemption fee:", quote.redemptionFee);

Stablecoin to Stablecoin

const quote = await ml.quote.swapStablecoinToStablecoin({
  stablecoinInID: mainnet.assetIds.stablecoin.USDC,
  stablecoinOutID: mainnet.assetIds.stablecoin.TSY_YIELD,
  exactOut: false,
  useDelegateForStablecoinOut: true,
  stablecoinInAmount: 1_000_000n,   // 1 USDC in
  stablecoinOutAmount: 0n,
  user: account.address,
});

console.log("Stablecoin output:", quote.stablecoinOutAmount);
The useDelegateForStablecoinOut parameter determines which stablecoin’s delegate handles the swap:
  • true: Use the output stablecoin’s delegate
  • false: Use the input stablecoin’s delegate
One stablecoin must have a delegate (full protocol integration), while the other can be “swap-only”. The delegate handles token movements, fee collection, and compliance checks.

Executing Swaps

The ml.swap module executes swaps on-chain. All methods require a walletClient with an attached account and return a transaction hash.
ERC-20 token approvals must be granted to the MultiliquidSwap contract before executing swaps. The SDK does not handle approvals automatically.

Stablecoin to RWA

// Get quote first
const quote = await ml.quote.swapIntoRWA({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,
  user: account.address,
});

// Execute swap using quoted amounts
const txHash = await ml.swap.swapIntoRWA({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,
  minRwaAmount: quote.rwaAmount,
});

const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

RWA to Stablecoin

const quote = await ml.quote.swapIntoStablecoin({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaID: mainnet.assetIds.rwa.ULTRA,
  rwaAmount: 1_000_000_000_000_000_000n,
});

const txHash = await ml.swap.swapIntoStablecoin({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaID: mainnet.assetIds.rwa.ULTRA,
  rwaAmount: 1_000_000_000_000_000_000n,
  minStablecoinAmount: quote.stablecoinAmount,
});

RWA to RWA

const quote = await ml.quote.swapRWAToRWA({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaInID: mainnet.assetIds.rwa.ULTRA,
  rwaOutID: mainnet.assetIds.rwa.USTB,
  exactOut: false,
  rwaInAmount: 1_000_000_000_000_000_000n,
  rwaOutAmount: 0n,
});

const txHash = await ml.swap.swapRWAToRWA({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaInID: mainnet.assetIds.rwa.ULTRA,
  rwaOutID: mainnet.assetIds.rwa.USTB,
  exactOut: false,
  rwaInAmount: 1_000_000_000_000_000_000n,
  rwaOutAmount: quote.rwaOutAmount,
});

Stablecoin to Stablecoin

const quote = await ml.quote.swapStablecoinToStablecoin({
  stablecoinInID: mainnet.assetIds.stablecoin.USDC,
  stablecoinOutID: mainnet.assetIds.stablecoin.TSY_YIELD,
  exactOut: false,
  useDelegateForStablecoinOut: true,
  stablecoinInAmount: 1_000_000n,
  stablecoinOutAmount: 0n,
  user: account.address,
});

const txHash = await ml.swap.swapStablecoinToStablecoin({
  exactOut: false,
  useDelegateForStablecoinOut: true,
  stablecoinInID: mainnet.assetIds.stablecoin.USDC,
  stablecoinInAmount: 1_000_000n,
  stablecoinOutID: mainnet.assetIds.stablecoin.TSY_YIELD,
  stablecoinOutAmount: quote.stablecoinOutAmount,
});

Building Calldata Without Sending

For smart account batching or offline signing, generate raw calldata without submitting a transaction:
const calldata = ml.swap.buildSwapCalldata("swapIntoRWA", {
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,
  minRwaAmount: quote.rwaAmount,
});
// Returns: Hex-encoded calldata

Querying Assets

The ml.assets module provides read-only access to asset information, prices, and fee configuration.

Asset Information

// Individual asset queries
const ultraInfo = await ml.assets.getRWAInfo(mainnet.assetIds.rwa.ULTRA);
console.log("Accepted:", ultraInfo.accepted);
console.log("Decimals:", ultraInfo.decimals);
console.log("Token address:", ultraInfo.assetAddress);

const usdcInfo = await ml.assets.getStablecoinInfo(mainnet.assetIds.stablecoin.USDC);

// Bulk queries (multicalled into single RPC call)
const allRWAs = await ml.assets.getAllRWAInfo();
const allStablecoins = await ml.assets.getAllStablecoinInfo();

Prices

// Individual prices (WAD format: 1e18 = $1.00)
const ultraPrice = await ml.assets.getRWAPrice(mainnet.assetIds.rwa.ULTRA);
const usdcValue = await ml.assets.getStablecoinUSDValue(mainnet.assetIds.stablecoin.USDC);

// All prices in one call
const prices = await ml.assets.getAllPrices();
for (const p of prices) {
  console.log(p.assetId, p.price, p.source);
}

Fee Configuration

// Volume-based fee tiers
const feeTiers = await ml.assets.getFeeTiers();
for (const tier of feeTiers) {
  console.log("Max volume:", tier.maxVolume, "Fee:", tier.fee);
}

// Per-pair fees
const discount = await ml.assets.getDiscountRate(
  mainnet.assetIds.stablecoin.USDC,
  mainnet.assetIds.rwa.ULTRA
);
const redemptionFee = await ml.assets.getRedemptionFee(
  mainnet.assetIds.stablecoin.USDC,
  mainnet.assetIds.rwa.ULTRA
);

Access Control Checks

const paused = await ml.assets.isPaused();
const blacklisted = await ml.assets.isBlacklisted(userAddress);
const whitelisted = await ml.assets.isWhitelistedForRWA(
  mainnet.assetIds.rwa.ULTRA,
  fromAddress,
  toAddress,
  amount
);

Event Monitoring

The ml.events module provides historical event queries and real-time monitoring.

Query Historical Events

const events = await ml.events.getEvents({
  user: account.address,
  fromBlock: 19_000_000n,
  toBlock: "latest",
});

for (const event of events) {
  switch (event.type) {
    case "SwapIntoRWA":
      console.log("Bought RWA:", event.rwaAmount, "for", event.stablecoinAmount);
      break;
    case "SwapIntoStablecoin":
      console.log("Sold RWA:", event.rwaAmount, "for", event.stablecoinAmount);
      break;
    case "RWAToRWASwap":
      console.log("Swapped RWA:", event.rwaInAmount, "->", event.rwaOutAmount);
      break;
    case "StablecoinToStablecoinSwap":
      console.log("Swapped stable:", event.stablecoinInAmount, "->", event.stablecoinOutAmount);
      break;
  }
}

Watch Events in Real-Time

const unsubscribe = ml.events.watchEvents(
  { user: account.address },
  (event) => {
    console.log("New swap event:", event.type);
  }
);

// Later: stop watching
unsubscribe();

Error Handling

The SDK provides typed error classes for all protocol errors:
import {
  UserBlacklistedError,
  UserNotWhitelistedError,
  ContractPausedError,
  InsufficientRWAOutputError,
  InsufficientStablecoinOutputError,
  StablecoinPriceOutsideBandError,
  decodeMultiliquidError,
} from "@uniformlabs/multiliquid-evm-sdk";

try {
  const txHash = await ml.swap.swapIntoRWA({ ... });
} catch (error) {
  if (error instanceof ContractPausedError) {
    console.log("Protocol is paused");
  } else if (error instanceof UserBlacklistedError) {
    console.log("User is blacklisted:", error.args.user);
  } else if (error instanceof UserNotWhitelistedError) {
    console.log("Not whitelisted for RWA:", error.args.rwaID);
  } else if (error instanceof InsufficientRWAOutputError) {
    console.log("Output below minimum — re-quote and retry");
  } else if (error instanceof StablecoinPriceOutsideBandError) {
    console.log("Stablecoin price outside band:", error.args.oraclePrice);
  }
}
Use the simulate: true option on quote functions to catch errors before submitting a transaction:
const quote = await ml.quote.swapIntoRWA({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,
  user: account.address,
  simulate: true,
});

if (!quote.simulation?.success) {
  console.log("Swap would fail:", quote.simulation?.error);
}

Delegate Queries

Query stablecoin delegate information for custody and compliance details:
const delegateInfo = await ml.delegates.getDelegateInfo(
  mainnet.assetIds.stablecoin.USDC
);

console.log("Delegate type:", delegateInfo.type);
console.log("RWA custody:", delegateInfo.rwaCustodyAddress);
console.log("Stablecoin custody:", delegateInfo.stablecoinCustodyAddress);

Yield Operations

For yield-bearing stablecoins, the ml.yield module provides interest accrual and rate queries:
const TSY = mainnet.assetIds.stablecoin.TSY_YIELD;

// Query current day and rates
const currentDay = await ml.yield.getCurrentDay(TSY);
const rates = await ml.yield.getDailyRates(TSY, currentDay - 7n, currentDay);

// Calculate yield for a user
const yieldAmount = await ml.yield.getYieldAmount({
  stablecoinID: TSY,
  user: account.address,
  redeemValue: 1_000_000_000_000_000_000n,
  stablecoinWithdrawal: false,
});

// Accrue interest
const txHash = await ml.yield.accrueInterest(TSY, account.address);

Integration Best Practices

Security

  1. Use Hardware Wallets: Never store private keys in application code
  2. Simulate Before Executing: Use simulate: true on quotes to verify swaps before submitting
  3. Test on Sepolia: Thoroughly test all swap flows on testnet before mainnet
  4. Monitor Events: Use ml.events.watchEvents() for real-time swap tracking

Performance

  1. Batch Approvals: Approve large amounts to reduce approval transactions
  2. Use Multicall: ml.assets.getAllPrices() and ml.assets.getAllRWAInfo() batch reads into single RPC calls
  3. Reuse Client: Create one client instance and reuse across operations
  4. Cache Asset IDs: Asset IDs are static per deployment — reference them from the SDK config

Compliance

  1. Check Whitelist Status: Call ml.assets.isWhitelistedForRWA() before presenting swap UI
  2. Check Blacklist Status: Call ml.assets.isBlacklisted() to verify user eligibility
  3. Metadata Fields: Pass compliance data in the optional metadata parameters
  4. Audit Trails: Use ml.events.getEvents() to build regulatory reports

Testing

Sepolia Testnet

import { createMultiliquidClient, sepolia } from "@uniformlabs/multiliquid-evm-sdk";
import { sepolia as viemSepolia } from "viem/chains";

const publicClient = createPublicClient({
  chain: viemSepolia,
  transport: http(),
});

const ml = createMultiliquidClient({
  deployment: sepolia,
  publicClient,
  walletClient,
});

// Sepolia asset IDs
sepolia.assetIds.rwa.MOCK_RWA_WHITELIST
sepolia.assetIds.stablecoin.USDC
Faucets:

Support and Resources