Skip to main content

RouterStatic Overview

Interface: IPRouterStatic

info

RouterStatic is designed for off-chain simulation only and should not be used for fund-sensitive or on-chain transactions. If you need to call any of these functions on-chain, please contact the Pendle team.

Recommended: Use the Pendle API

For most integrations, the Pendle Hosted SDK / API is the better choice. It handles off-chain approximation, limit-order filling, and multi-DEX zap routing automatically — all things RouterStatic cannot do. Use RouterStatic directly only when you need fully on-chain quote generation without any external API dependency.

Overview

RouterStatic is a read-only contract that mirrors every swap, liquidity, mint/redeem, and user-info operation exposed by the live PendleRouter. Its purpose is pre-flight simulation: call a *Static function off-chain to get expected outputs, price impact, and exchange rates, then use those results to set minOut / ApproxParams before submitting the real transaction.

RouterStatic is a multi-facet proxy (ERC-2535 Diamond Standard). Use the ABI of IPRouterStatic to interact with it — the proxy resolves each call to the correct facet automatically.

Deployment addresses are listed on the Deployments page.

Interface Facets

InterfacePurpose
IPActionInfoStaticUser & position info
IPActionMarketAuxStaticMarket rates, state, price impact, ApproxParams helpers
IPActionMarketCoreStaticSwap + liquidity simulations
IPActionMintRedeemStaticMint/redeem simulations + pyIndex views
IPActionStorageStaticDefault ApproxParams storage
IPActionVePendleStaticvePENDLE-related views

Return Types

TokenAmount

struct TokenAmount {
address token;
uint256 amount;
}

A generic token–amount pair used throughout info functions.

UserSYInfo

struct UserSYInfo {
TokenAmount syBalance;
TokenAmount[] unclaimedRewards;
}

Returned by getUserSYInfo. Contains the user's SY balance and any unclaimed reward tokens.

UserPYInfo

struct UserPYInfo {
TokenAmount ptBalance;
TokenAmount ytBalance;
TokenAmount unclaimedInterest;
TokenAmount[] unclaimedRewards;
}

Returned by getUserPYInfo. Contains PT/YT balances, accrued interest, and unclaimed rewards.

UserMarketInfo

struct UserMarketInfo {
TokenAmount lpBalance;
TokenAmount ptBalance;
TokenAmount syBalance;
TokenAmount[] unclaimedRewards;
}

Returned by getUserMarketInfo. Contains LP balance, the underlying PT and SY value of that LP, and unclaimed LP rewards.

MarketState

MarketState is the low-level AMM state struct returned by getMarketState and readMarketState. See PendleMarket for field-level documentation.

ApproxParams

ApproxParams is used by the live Router for binary-search approximation. The swapExact*StaticAndGenerateApproxParams helpers construct a ready-to-use ApproxParams struct. See Types for field documentation.


Integration Examples

RouterStatic always follows the same two-step pattern:

  1. Call a *Static function (off-chain, eth_call) to get expected amounts and parameters.
  2. Pass those results into the corresponding Router function as minOut and/or ApproxParams.

The examples below use ethers.js v6. Addresses and amounts are illustrative.

Setup

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);

// Use IPRouterStatic ABI for RouterStatic — the Diamond proxy resolves all facets
const routerStatic = new ethers.Contract(ROUTER_STATIC_ADDRESS, IPRouterStaticABI, provider);

// Use IPAllActionV3 ABI for the live Router
const router = new ethers.Contract(ROUTER_ADDRESS, IPAllActionV3ABI, signer);

const SLIPPAGE = ethers.parseEther("0.005"); // 0.5%

function applySlippage(amount: bigint): bigint {
return (amount * 995n) / 1000n; // 0.5% slippage
}

Buy PT with Token

swapExactTokenForPtStaticAndGenerateApproxParams is the recommended way to preflight this trade — it returns ApproxParams ready to pass to the Router, which eliminates the need to tune the binary-search parameters manually.

const tokenIn   = WSTETH_ADDRESS;
const amountIn = ethers.parseEther("1");

// 1. Simulate and get ApproxParams
const [netPtOut, , , priceImpact, , approxParams] =
await routerStatic.swapExactTokenForPtStaticAndGenerateApproxParams(
MARKET_ADDRESS,
tokenIn,
amountIn,
SLIPPAGE
);

console.log(`Expected PT out: ${ethers.formatEther(netPtOut)}`);
console.log(`Price impact: ${ethers.formatEther(priceImpact * 100n)}%`);

// 2. Execute
await IERC20(tokenIn).approve(ROUTER_ADDRESS, amountIn);
await router.swapExactTokenForPt(
signer.address, // receiver
MARKET_ADDRESS,
applySlippage(netPtOut), // minPtOut
approxParams, // from RouterStatic
createTokenInputStruct(tokenIn, amountIn),
emptyLimitOrderData
);

Sell PT for Token

No ApproxParams needed — selling PT is an exact-out AMM operation.

const tokenOut = WSTETH_ADDRESS;
const ptAmount = ethers.parseEther("1");

// 1. Simulate
const [netTokenOut, , , priceImpact] =
await routerStatic.swapExactPtForTokenStatic(
MARKET_ADDRESS,
ptAmount,
tokenOut
);

console.log(`Expected token out: ${ethers.formatEther(netTokenOut)}`);

// 2. Execute
await IERC20(PT_ADDRESS).approve(ROUTER_ADDRESS, ptAmount);
await router.swapExactPtForToken(
signer.address,
MARKET_ADDRESS,
ptAmount,
createTokenOutputStruct(tokenOut, applySlippage(netTokenOut)), // minTokenOut baked in
emptyLimitOrderData
);

Buy YT with Token

YT buying also requires ApproxParams. Use swapExactSyForPtStaticAndGenerateApproxParams (after minting SY first) or simply use swapExactSyForYtStatic to check the trade, then supply defaultApproxParams to the Router.

const tokenIn  = WSTETH_ADDRESS;
const amountIn = ethers.parseEther("0.1");

// 1. Simulate YT out (no ApproxParams helper for YT — use defaultApprox on Router)
const [netYtOut, , priceImpact] =
await routerStatic.swapExactTokenForYtStatic(
MARKET_ADDRESS,
tokenIn,
amountIn
);

console.log(`Expected YT out: ${ethers.formatEther(netYtOut)}`);
console.log(`Price impact: ${ethers.formatEther(priceImpact * 100n)}%`);

// Reject if price impact is too high
if (priceImpact > ethers.parseEther("0.02")) { // > 2%
throw new Error("Price impact too high");
}

// 2. Execute
await IERC20(tokenIn).approve(ROUTER_ADDRESS, amountIn);
await router.swapExactTokenForYt(
signer.address,
MARKET_ADDRESS,
applySlippage(netYtOut), // minYtOut
defaultApproxParams,
createTokenInputStruct(tokenIn, amountIn),
emptyLimitOrderData
);

Sell YT for Token

const tokenOut = WSTETH_ADDRESS;
const ytAmount = ethers.parseEther("10");

// 1. Simulate
const [netTokenOut, , priceImpact] =
await routerStatic.swapExactYtForTokenStatic(
MARKET_ADDRESS,
ytAmount,
tokenOut
);

console.log(`Expected token out: ${ethers.formatEther(netTokenOut)}`);

// 2. Execute
await IERC20(YT_ADDRESS).approve(ROUTER_ADDRESS, ytAmount);
await router.swapExactYtForToken(
signer.address,
MARKET_ADDRESS,
ytAmount,
createTokenOutputStruct(tokenOut, applySlippage(netTokenOut)),
emptyLimitOrderData
);

Add Liquidity (Single Token)

const tokenIn  = WSTETH_ADDRESS;
const amountIn = ethers.parseEther("1");

// 1. Simulate
const [netLpOut, , , priceImpact] =
await routerStatic.addLiquiditySingleTokenStatic(
MARKET_ADDRESS,
tokenIn,
amountIn
);

console.log(`Expected LP out: ${ethers.formatEther(netLpOut)}`);

// 2. Execute
await IERC20(tokenIn).approve(ROUTER_ADDRESS, amountIn);
await router.addLiquiditySingleToken(
signer.address,
MARKET_ADDRESS,
applySlippage(netLpOut), // minLpOut
defaultApproxParams,
createTokenInputStruct(tokenIn, amountIn),
emptyLimitOrderData
);

Add Liquidity and Keep YT

This operation splits your token into LP + YT. RouterStatic lets you preview both outputs.

const tokenIn  = WSTETH_ADDRESS;
const amountIn = ethers.parseEther("1");

// 1. Simulate
const [netLpOut, netYtOut, netSyMinted, netSyToPY] =
await routerStatic.addLiquiditySingleTokenKeepYtStatic(
MARKET_ADDRESS,
tokenIn,
amountIn
);

console.log(`Expected LP out: ${ethers.formatEther(netLpOut)}`);
console.log(`Expected YT out: ${ethers.formatEther(netYtOut)}`);

// 2. Execute
await IERC20(tokenIn).approve(ROUTER_ADDRESS, amountIn);
await router.addLiquiditySingleTokenKeepYt(
signer.address,
MARKET_ADDRESS,
applySlippage(netLpOut), // minLpOut
applySlippage(netYtOut), // minYtOut
createTokenInputStruct(tokenIn, amountIn)
// note: no limit order parameter for KeepYt variant
);

Remove Liquidity (Single Token)

const tokenOut = WSTETH_ADDRESS;
const lpAmount = ethers.parseEther("0.5");

// 1. Simulate
const [netTokenOut, , priceImpact] =
await routerStatic.removeLiquiditySingleTokenStatic(
MARKET_ADDRESS,
lpAmount,
tokenOut
);

console.log(`Expected token out: ${ethers.formatEther(netTokenOut)}`);

// 2. Execute
await IERC20(MARKET_ADDRESS).approve(ROUTER_ADDRESS, lpAmount);
await router.removeLiquiditySingleToken(
signer.address,
MARKET_ADDRESS,
lpAmount,
createTokenOutputStruct(tokenOut, applySlippage(netTokenOut)),
emptyLimitOrderData
);

Mint PT & YT from Token

const tokenIn  = WSTETH_ADDRESS;
const amountIn = ethers.parseEther("1");

// 1. Simulate
const netPyOut = await routerStatic.mintPyFromTokenStatic(
YT_ADDRESS,
tokenIn,
amountIn
);

console.log(`Expected PT+YT out: ${ethers.formatEther(netPyOut)} each`);

// 2. Execute
await IERC20(tokenIn).approve(ROUTER_ADDRESS, amountIn);
await router.mintPyFromToken(
signer.address,
YT_ADDRESS,
applySlippage(netPyOut), // minPyOut
createTokenInputStruct(tokenIn, amountIn)
);

Redeem PT & YT to Token

const tokenOut = WSTETH_ADDRESS;
const pyAmount = ethers.parseEther("1");

// 1. Simulate
const netTokenOut = await routerStatic.redeemPyToTokenStatic(
YT_ADDRESS,
pyAmount,
tokenOut
);

console.log(`Expected token out: ${ethers.formatEther(netTokenOut)}`);

// 2. Execute
await IERC20(PT_ADDRESS).approve(ROUTER_ADDRESS, pyAmount);
await IERC20(YT_ADDRESS).approve(ROUTER_ADDRESS, pyAmount);
await router.redeemPyToToken(
signer.address,
YT_ADDRESS,
pyAmount,
createTokenOutputStruct(tokenOut, applySlippage(netTokenOut))
);

Price Impact Guard

Use RouterStatic to reject trades that exceed a price impact threshold before ever building a transaction.

const MAX_PRICE_IMPACT = ethers.parseEther("0.01"); // 1%

async function safeBuyPt(market: string, tokenIn: string, amountIn: bigint) {
const [netPtOut, , , priceImpact, , approxParams] =
await routerStatic.swapExactTokenForPtStaticAndGenerateApproxParams(
market, tokenIn, amountIn, SLIPPAGE
);

if (priceImpact > MAX_PRICE_IMPACT) {
throw new Error(`Price impact ${ethers.formatEther(priceImpact * 100n)}% exceeds limit`);
}

await IERC20(tokenIn).approve(ROUTER_ADDRESS, amountIn);
return router.swapExactTokenForPt(
signer.address,
market,
applySlippage(netPtOut),
approxParams,
createTokenInputStruct(tokenIn, amountIn),
emptyLimitOrderData
);
}

Check Pending Rewards Before Claiming

// 1. Preview rewards without writing state
const marketInfo = await routerStatic.getUserMarketInfo(MARKET_ADDRESS, userAddress);

for (const reward of marketInfo.unclaimedRewards) {
console.log(`Reward token: ${reward.token}, amount: ${ethers.formatEther(reward.amount)}`);
}

// Only claim if there's something worth the gas
const totalRewardValue = await estimateRewardValue(marketInfo.unclaimedRewards);
if (totalRewardValue > MIN_CLAIM_THRESHOLD) {
await router.redeemDueInterestAndRewards(
userAddress,
[], // SY addresses
[], // YT addresses
[MARKET_ADDRESS]
);
}

Note: RouterStatic functions must be called with eth_call (read-only). Never submit them as state-changing transactions.