From 2069a2c405f5929eb1317697b67cc21841371185 Mon Sep 17 00:00:00 2001 From: chad-js Date: Wed, 29 Oct 2025 10:51:30 -0400 Subject: [PATCH 1/7] feat: add pendle swap integration --- abis/DutchAuctionRebalancer.ts | 22 +- abis/PendleRouter.ts | 842 ++++++++++++++++++++++++ abis/PendleStaticRouter.ts | 47 ++ abis/PreLiquidationRebalancer.ts | 23 +- src/constants/contracts.ts | 2 + src/services/preLiquidationRebalance.ts | 3 + src/services/routing/fluid.ts | 20 +- src/services/routing/getSwapParams.ts | 26 +- src/services/routing/lifi.ts | 4 +- src/services/routing/pendle.ts | 317 +++++++++ src/services/routing/uniswapV2.ts | 5 +- src/services/routing/uniswapV3.ts | 8 +- src/subscribers/auctionCreated.ts | 7 +- src/types/index.ts | 32 + src/utils/contractHelpers.ts | 18 + src/utils/tokens.ts | 51 ++ 16 files changed, 1401 insertions(+), 26 deletions(-) create mode 100644 abis/PendleRouter.ts create mode 100644 abis/PendleStaticRouter.ts create mode 100644 src/services/routing/pendle.ts create mode 100644 src/utils/tokens.ts diff --git a/abis/DutchAuctionRebalancer.ts b/abis/DutchAuctionRebalancer.ts index 159c761..bbee6fa 100644 --- a/abis/DutchAuctionRebalancer.ts +++ b/abis/DutchAuctionRebalancer.ts @@ -1,4 +1,4 @@ -export const DutchAuctionRebalancerAbi = [ +const DutchAuctionRebalancerBaseAbi = [ { "type": "constructor", "inputs": [ @@ -321,5 +321,25 @@ export const DutchAuctionRebalancerAbi = [ "type": "error", "name": "Unauthorized", "inputs": [] + }, +] as const; + +export const DutchAuctionRebalancerAbi = [ + ...DutchAuctionRebalancerBaseAbi, + { + "type": "error", + "name": "AuctionNotValid", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidLeverageTokenStateAfterRebalance", + "inputs": [ + { + "name": "leverageToken", + "type": "address", + "internalType": "address" + }, + ] } ] as const; diff --git a/abis/PendleRouter.ts b/abis/PendleRouter.ts new file mode 100644 index 0000000..76323c2 --- /dev/null +++ b/abis/PendleRouter.ts @@ -0,0 +1,842 @@ +const PendleRouterAbi = [ + { + inputs: [{ internalType: "int256", name: "exchangeRate", type: "int256" }], + name: "MarketExchangeRateBelowOne", + type: "error", + }, + { inputs: [], name: "MarketExpired", type: "error" }, + { inputs: [], name: "MarketProportionMustNotEqualOne", type: "error" }, + { + inputs: [ + { internalType: "int256", name: "proportion", type: "int256" }, + { internalType: "int256", name: "maxProportion", type: "int256" }, + ], + name: "MarketProportionTooHigh", + type: "error", + }, + { + inputs: [{ internalType: "int256", name: "rateScalar", type: "int256" }], + name: "MarketRateScalarBelowZero", + type: "error", + }, + { + inputs: [ + { internalType: "int256", name: "totalPt", type: "int256" }, + { internalType: "int256", name: "totalAsset", type: "int256" }, + ], + name: "MarketZeroTotalPtOrTotalAsset", + type: "error", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netSyUsed", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPtUsed", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netLpOut", type: "uint256" }, + ], + name: "AddLiquidityDualSyAndPt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "tokenIn", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netTokenUsed", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPtUsed", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netLpOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "AddLiquidityDualTokenAndPt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netPtIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netLpOut", type: "uint256" }, + ], + name: "AddLiquiditySinglePt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netSyIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netLpOut", type: "uint256" }, + ], + name: "AddLiquiditySingleSy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netSyIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyMintPy", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netLpOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netYtOut", type: "uint256" }, + ], + name: "AddLiquiditySingleSyKeepYt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netTokenIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netLpOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "AddLiquiditySingleToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netTokenIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netLpOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netYtOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyMintPy", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "AddLiquiditySingleTokenKeepYt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpIn", type: "uint256" }, + { + components: [ + { internalType: "uint256", name: "netPtFromRemove", type: "uint256" }, + { internalType: "uint256", name: "netSyFromRemove", type: "uint256" }, + { internalType: "uint256", name: "netPtRedeem", type: "uint256" }, + { internalType: "uint256", name: "netSyFromRedeem", type: "uint256" }, + { internalType: "uint256", name: "totalSyOut", type: "uint256" }, + ], + indexed: false, + internalType: "struct ExitPostExpReturnParams", + name: "params", + type: "tuple", + }, + ], + name: "ExitPostExpToSy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "totalTokenOut", type: "uint256" }, + { + components: [ + { internalType: "uint256", name: "netPtFromRemove", type: "uint256" }, + { internalType: "uint256", name: "netSyFromRemove", type: "uint256" }, + { internalType: "uint256", name: "netPtRedeem", type: "uint256" }, + { internalType: "uint256", name: "netSyFromRedeem", type: "uint256" }, + { internalType: "uint256", name: "totalSyOut", type: "uint256" }, + ], + indexed: false, + internalType: "struct ExitPostExpReturnParams", + name: "params", + type: "tuple", + }, + ], + name: "ExitPostExpToToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpIn", type: "uint256" }, + { + components: [ + { internalType: "uint256", name: "netPtFromRemove", type: "uint256" }, + { internalType: "uint256", name: "netSyFromRemove", type: "uint256" }, + { internalType: "uint256", name: "netPyRedeem", type: "uint256" }, + { internalType: "uint256", name: "netSyFromRedeem", type: "uint256" }, + { internalType: "uint256", name: "netPtSwap", type: "uint256" }, + { internalType: "uint256", name: "netYtSwap", type: "uint256" }, + { internalType: "uint256", name: "netSyFromSwap", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + { internalType: "uint256", name: "totalSyOut", type: "uint256" }, + ], + indexed: false, + internalType: "struct ExitPreExpReturnParams", + name: "params", + type: "tuple", + }, + ], + name: "ExitPreExpToSy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "totalTokenOut", type: "uint256" }, + { + components: [ + { internalType: "uint256", name: "netPtFromRemove", type: "uint256" }, + { internalType: "uint256", name: "netSyFromRemove", type: "uint256" }, + { internalType: "uint256", name: "netPyRedeem", type: "uint256" }, + { internalType: "uint256", name: "netSyFromRedeem", type: "uint256" }, + { internalType: "uint256", name: "netPtSwap", type: "uint256" }, + { internalType: "uint256", name: "netYtSwap", type: "uint256" }, + { internalType: "uint256", name: "netSyFromSwap", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + { internalType: "uint256", name: "totalSyOut", type: "uint256" }, + ], + indexed: false, + internalType: "struct ExitPreExpReturnParams", + name: "params", + type: "tuple", + }, + ], + name: "ExitPreExpToToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: true, internalType: "address", name: "YT", type: "address" }, + { indexed: false, internalType: "uint256", name: "netSyIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPyOut", type: "uint256" }, + ], + name: "MintPyFromSy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "tokenIn", type: "address" }, + { indexed: true, internalType: "address", name: "YT", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netTokenIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPyOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "MintPyFromToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "tokenIn", type: "address" }, + { indexed: true, internalType: "address", name: "SY", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netTokenIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyOut", type: "uint256" }, + ], + name: "MintSyFromToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "previousOwner", type: "address" }, + { indexed: true, internalType: "address", name: "newOwner", type: "address" }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: true, internalType: "address", name: "YT", type: "address" }, + { indexed: false, internalType: "uint256", name: "netPyIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyOut", type: "uint256" }, + ], + name: "RedeemPyToSy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "tokenOut", type: "address" }, + { indexed: true, internalType: "address", name: "YT", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netPyIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netTokenOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "RedeemPyToToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "tokenOut", type: "address" }, + { indexed: true, internalType: "address", name: "SY", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netSyIn", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netTokenOut", type: "uint256" }, + ], + name: "RedeemSyToToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpToRemove", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPtOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyOut", type: "uint256" }, + ], + name: "RemoveLiquidityDualSyAndPt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "tokenOut", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpToRemove", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPtOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netTokenOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "RemoveLiquidityDualTokenAndPt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpToRemove", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPtOut", type: "uint256" }, + ], + name: "RemoveLiquiditySinglePt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpToRemove", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyOut", type: "uint256" }, + ], + name: "RemoveLiquiditySingleSy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpToRemove", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netTokenOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "RemoveLiquiditySingleToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes4", name: "selector", type: "bytes4" }, + { indexed: true, internalType: "address", name: "facet", type: "address" }, + ], + name: "SelectorToFacetSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "int256", name: "netPtToAccount", type: "int256" }, + { indexed: false, internalType: "int256", name: "netSyToAccount", type: "int256" }, + ], + name: "SwapPtAndSy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "int256", name: "netPtToAccount", type: "int256" }, + { indexed: false, internalType: "int256", name: "netTokenToAccount", type: "int256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "SwapPtAndToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "int256", name: "netYtToAccount", type: "int256" }, + { indexed: false, internalType: "int256", name: "netSyToAccount", type: "int256" }, + ], + name: "SwapYtAndSy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "market", type: "address" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "int256", name: "netYtToAccount", type: "int256" }, + { indexed: false, internalType: "int256", name: "netTokenToAccount", type: "int256" }, + { indexed: false, internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + name: "SwapYtAndToken", + type: "event", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "address", name: "market", type: "address" }, + { internalType: "uint256", name: "exactPtIn", type: "uint256" }, + { internalType: "uint256", name: "minSyOut", type: "uint256" }, + { + components: [ + { internalType: "address", name: "limitRouter", type: "address" }, + { internalType: "uint256", name: "epsSkipMarket", type: "uint256" }, + { + components: [ + { + components: [ + { internalType: "uint256", name: "salt", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "enum IPLimitOrderType.OrderType", name: "orderType", type: "uint8" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "YT", type: "address" }, + { internalType: "address", name: "maker", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + { internalType: "uint256", name: "lnImpliedRate", type: "uint256" }, + { internalType: "uint256", name: "failSafeRate", type: "uint256" }, + { internalType: "bytes", name: "permit", type: "bytes" }, + ], + internalType: "struct Order", + name: "order", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + ], + internalType: "struct FillOrderParams[]", + name: "normalFills", + type: "tuple[]", + }, + { + components: [ + { + components: [ + { internalType: "uint256", name: "salt", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "enum IPLimitOrderType.OrderType", name: "orderType", type: "uint8" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "YT", type: "address" }, + { internalType: "address", name: "maker", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + { internalType: "uint256", name: "lnImpliedRate", type: "uint256" }, + { internalType: "uint256", name: "failSafeRate", type: "uint256" }, + { internalType: "bytes", name: "permit", type: "bytes" }, + ], + internalType: "struct Order", + name: "order", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + ], + internalType: "struct FillOrderParams[]", + name: "flashFills", + type: "tuple[]", + }, + { internalType: "bytes", name: "optData", type: "bytes" }, + ], + internalType: "struct LimitOrderData", + name: "limit", + type: "tuple", + }, + ], + name: "swapExactPtForSy", + outputs: [ + { internalType: "uint256", name: "netSyOut", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "address", name: "market", type: "address" }, + { internalType: "uint256", name: "exactPtIn", type: "uint256" }, + { + components: [ + { internalType: "address", name: "tokenOut", type: "address" }, + { internalType: "uint256", name: "minTokenOut", type: "uint256" }, + { internalType: "address", name: "tokenRedeemSy", type: "address" }, + { internalType: "address", name: "pendleSwap", type: "address" }, + { + components: [ + { internalType: "enum SwapType", name: "swapType", type: "uint8" }, + { internalType: "address", name: "extRouter", type: "address" }, + { internalType: "bytes", name: "extCalldata", type: "bytes" }, + { internalType: "bool", name: "needScale", type: "bool" }, + ], + internalType: "struct SwapData", + name: "swapData", + type: "tuple", + }, + ], + internalType: "struct TokenOutput", + name: "output", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "limitRouter", type: "address" }, + { internalType: "uint256", name: "epsSkipMarket", type: "uint256" }, + { + components: [ + { + components: [ + { internalType: "uint256", name: "salt", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "enum IPLimitOrderType.OrderType", name: "orderType", type: "uint8" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "YT", type: "address" }, + { internalType: "address", name: "maker", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + { internalType: "uint256", name: "lnImpliedRate", type: "uint256" }, + { internalType: "uint256", name: "failSafeRate", type: "uint256" }, + { internalType: "bytes", name: "permit", type: "bytes" }, + ], + internalType: "struct Order", + name: "order", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + ], + internalType: "struct FillOrderParams[]", + name: "normalFills", + type: "tuple[]", + }, + { + components: [ + { + components: [ + { internalType: "uint256", name: "salt", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "enum IPLimitOrderType.OrderType", name: "orderType", type: "uint8" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "YT", type: "address" }, + { internalType: "address", name: "maker", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + { internalType: "uint256", name: "lnImpliedRate", type: "uint256" }, + { internalType: "uint256", name: "failSafeRate", type: "uint256" }, + { internalType: "bytes", name: "permit", type: "bytes" }, + ], + internalType: "struct Order", + name: "order", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + ], + internalType: "struct FillOrderParams[]", + name: "flashFills", + type: "tuple[]", + }, + { internalType: "bytes", name: "optData", type: "bytes" }, + ], + internalType: "struct LimitOrderData", + name: "limit", + type: "tuple", + }, + ], + name: "swapExactPtForToken", + outputs: [ + { internalType: "uint256", name: "netTokenOut", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + { internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "address", name: "market", type: "address" }, + { internalType: "uint256", name: "exactSyIn", type: "uint256" }, + { internalType: "uint256", name: "minPtOut", type: "uint256" }, + { + components: [ + { internalType: "uint256", name: "guessMin", type: "uint256" }, + { internalType: "uint256", name: "guessMax", type: "uint256" }, + { internalType: "uint256", name: "guessOffchain", type: "uint256" }, + { internalType: "uint256", name: "maxIteration", type: "uint256" }, + { internalType: "uint256", name: "eps", type: "uint256" }, + ], + internalType: "struct ApproxParams", + name: "guessPtOut", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "limitRouter", type: "address" }, + { internalType: "uint256", name: "epsSkipMarket", type: "uint256" }, + { + components: [ + { + components: [ + { internalType: "uint256", name: "salt", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "enum IPLimitOrderType.OrderType", name: "orderType", type: "uint8" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "YT", type: "address" }, + { internalType: "address", name: "maker", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + { internalType: "uint256", name: "lnImpliedRate", type: "uint256" }, + { internalType: "uint256", name: "failSafeRate", type: "uint256" }, + { internalType: "bytes", name: "permit", type: "bytes" }, + ], + internalType: "struct Order", + name: "order", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + ], + internalType: "struct FillOrderParams[]", + name: "normalFills", + type: "tuple[]", + }, + { + components: [ + { + components: [ + { internalType: "uint256", name: "salt", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "enum IPLimitOrderType.OrderType", name: "orderType", type: "uint8" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "YT", type: "address" }, + { internalType: "address", name: "maker", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + { internalType: "uint256", name: "lnImpliedRate", type: "uint256" }, + { internalType: "uint256", name: "failSafeRate", type: "uint256" }, + { internalType: "bytes", name: "permit", type: "bytes" }, + ], + internalType: "struct Order", + name: "order", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + ], + internalType: "struct FillOrderParams[]", + name: "flashFills", + type: "tuple[]", + }, + { internalType: "bytes", name: "optData", type: "bytes" }, + ], + internalType: "struct LimitOrderData", + name: "limit", + type: "tuple", + }, + ], + name: "swapExactSyForPt", + outputs: [ + { internalType: "uint256", name: "netPtOut", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "address", name: "market", type: "address" }, + { internalType: "uint256", name: "minPtOut", type: "uint256" }, + { + components: [ + { internalType: "uint256", name: "guessMin", type: "uint256" }, + { internalType: "uint256", name: "guessMax", type: "uint256" }, + { internalType: "uint256", name: "guessOffchain", type: "uint256" }, + { internalType: "uint256", name: "maxIteration", type: "uint256" }, + { internalType: "uint256", name: "eps", type: "uint256" }, + ], + internalType: "struct ApproxParams", + name: "guessPtOut", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "tokenIn", type: "address" }, + { internalType: "uint256", name: "netTokenIn", type: "uint256" }, + { internalType: "address", name: "tokenMintSy", type: "address" }, + { internalType: "address", name: "pendleSwap", type: "address" }, + { + components: [ + { internalType: "enum SwapType", name: "swapType", type: "uint8" }, + { internalType: "address", name: "extRouter", type: "address" }, + { internalType: "bytes", name: "extCalldata", type: "bytes" }, + { internalType: "bool", name: "needScale", type: "bool" }, + ], + internalType: "struct SwapData", + name: "swapData", + type: "tuple", + }, + ], + internalType: "struct TokenInput", + name: "input", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "limitRouter", type: "address" }, + { internalType: "uint256", name: "epsSkipMarket", type: "uint256" }, + { + components: [ + { + components: [ + { internalType: "uint256", name: "salt", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "enum IPLimitOrderType.OrderType", name: "orderType", type: "uint8" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "YT", type: "address" }, + { internalType: "address", name: "maker", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + { internalType: "uint256", name: "lnImpliedRate", type: "uint256" }, + { internalType: "uint256", name: "failSafeRate", type: "uint256" }, + { internalType: "bytes", name: "permit", type: "bytes" }, + ], + internalType: "struct Order", + name: "order", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + ], + internalType: "struct FillOrderParams[]", + name: "normalFills", + type: "tuple[]", + }, + { + components: [ + { + components: [ + { internalType: "uint256", name: "salt", type: "uint256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "enum IPLimitOrderType.OrderType", name: "orderType", type: "uint8" }, + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "YT", type: "address" }, + { internalType: "address", name: "maker", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + { internalType: "uint256", name: "lnImpliedRate", type: "uint256" }, + { internalType: "uint256", name: "failSafeRate", type: "uint256" }, + { internalType: "bytes", name: "permit", type: "bytes" }, + ], + internalType: "struct Order", + name: "order", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "uint256", name: "makingAmount", type: "uint256" }, + ], + internalType: "struct FillOrderParams[]", + name: "flashFills", + type: "tuple[]", + }, + { internalType: "bytes", name: "optData", type: "bytes" }, + ], + internalType: "struct LimitOrderData", + name: "limit", + type: "tuple", + }, + ], + name: "swapExactTokenForPt", + outputs: [ + { internalType: "uint256", name: "netPtOut", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + { internalType: "uint256", name: "netSyInterm", type: "uint256" }, + ], + stateMutability: "payable", + type: "function", + }, +] as const; + +export default PendleRouterAbi; diff --git a/abis/PendleStaticRouter.ts b/abis/PendleStaticRouter.ts new file mode 100644 index 0000000..be9d05f --- /dev/null +++ b/abis/PendleStaticRouter.ts @@ -0,0 +1,47 @@ +const PendleStaticRouterAbi = [ + { + inputs: [ + { internalType: "address", name: "market", type: "address" }, + { internalType: "address", name: "tokenIn", type: "address" }, + { internalType: "uint256", name: "amountTokenIn", type: "uint256" }, + { internalType: "uint256", name: "slippage", type: "uint256" }, + ], + outputs: [ + { internalType: "uint256", name: "netPtOut", type: "uint256" }, + { internalType: "uint256", name: "netSyMinted", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + { internalType: "uint256", name: "priceImpact", type: "uint256" }, + { internalType: "uint256", name: "exchangeRateAfter", type: "uint256" }, + { + components: [ + { internalType: "uint256", name: "guessMin", type: "uint256" }, + { internalType: "uint256", name: "guessMax", type: "uint256" }, + { internalType: "uint256", name: "guessOffchain", type: "uint256" }, + { internalType: "uint256", name: "maxIteration", type: "uint256" }, + { internalType: "uint256", name: "eps", type: "uint256" }, + ], + internalType: "struct ApproxParams", + name: "guessPtOut", + type: "tuple", + }, + ], + stateMutability: "view", + name: "swapExactTokenForPtStaticAndGenerateApproxParams", + type: "function", + }, + { + type: "function", + stateMutability: "view", + name: "getYieldTokenAndPtRate", + inputs: [ + { internalType: "address", name: "market", type: "address" }, + ], + outputs: [ + { internalType: "address", name: "yieldToken", type: "address" }, + { internalType: "uint256", name: "netPtOut", type: "uint256" }, + { internalType: "uint256", name: "netYieldTokenOut", type: "uint256" }, + ], + } +] as const; + +export default PendleStaticRouterAbi; diff --git a/abis/PreLiquidationRebalancer.ts b/abis/PreLiquidationRebalancer.ts index e62f1c5..def16cd 100644 --- a/abis/PreLiquidationRebalancer.ts +++ b/abis/PreLiquidationRebalancer.ts @@ -1,4 +1,4 @@ -export const PreLiquidationRebalancerAbi = [ +const PreLiquidationRebalancerBaseAbi = [ { "type": "constructor", "inputs": [ @@ -284,3 +284,24 @@ export const PreLiquidationRebalancerAbi = [ "inputs": [] } ] as const; + +export const PreLiquidationRebalancerAbi = [ + ...PreLiquidationRebalancerBaseAbi, + { + "type": "error", + "name": "AuctionNotValid", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidLeverageTokenStateAfterRebalance", + "inputs": [ + { + "name": "leverageToken", + "type": "address", + "internalType": "address" + }, + ] + } +] as const; + diff --git a/src/constants/contracts.ts b/src/constants/contracts.ts index 8a608da..61c6c20 100644 --- a/src/constants/contracts.ts +++ b/src/constants/contracts.ts @@ -15,6 +15,8 @@ export const CONTRACT_ADDRESSES: Record = { FLUID_DEX_RESERVES_RESOLVER: "0xC93876C0EEd99645DD53937b25433e311881A27C", LEVERAGE_MANAGER: "0x5C37EB148D4a261ACD101e2B997A0F163Fb3E351", MULTICALL_EXECUTOR: "0x16D02Ebd89988cAd1Ce945807b963aB7A9Fd22E1", + PENDLE_ROUTER: "0x888888888889758F76e7103c6CbF23ABbF58F946", + PENDLE_ROUTER_STATIC: "0x263833d47eA3fA4a30f269323aba6a107f9eB14C", PRE_LIQUIDATION_REBALANCER: process.env.PRE_LIQUIDATION_REBALANCER_ADDRESS as Address || zeroAddress, WSTETH: "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", UNISWAP_SWAP_ROUTER_02: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", diff --git a/src/services/preLiquidationRebalance.ts b/src/services/preLiquidationRebalance.ts index 942e5f6..c9b7c4e 100644 --- a/src/services/preLiquidationRebalance.ts +++ b/src/services/preLiquidationRebalance.ts @@ -133,10 +133,13 @@ const executePreLiquidationRebalance = async ( const swapParams = await getRebalanceSwapParams({ leverageToken, stakeType: StakeType.NONE, + receiver: CONTRACT_ADDRESSES[CHAIN_ID].PRE_LIQUIDATION_REBALANCER, assetIn, assetOut, takeAmount, requiredAmountIn, + collateralAsset, + debtAsset, }); if (!swapParams.isProfitable) { diff --git a/src/services/routing/fluid.ts b/src/services/routing/fluid.ts index f310a92..c8d4ac0 100644 --- a/src/services/routing/fluid.ts +++ b/src/services/routing/fluid.ts @@ -1,4 +1,4 @@ -import { Address, encodeFunctionData, erc20Abi, getAddress } from "viem"; +import { Address, encodeFunctionData, erc20Abi, getAddress, isAddressEqual, zeroAddress } from "viem"; import FluidDexReservesResolverAbi from "../../../abis/FluidDexReservesResolver"; import { CONTRACT_ADDRESSES } from "../../constants/contracts"; import { CHAIN_ID } from "../../constants/chain"; @@ -7,6 +7,8 @@ import FluidDexT1Abi from "../../../abis/FluidDexT1"; import { Call } from "../../types"; import { ComponentLogger } from "../../utils/logger"; +const FLUID_DEX_SLIPPAGE = 1000000000000000n; // 0.1% + export class FluidDex { // Maps from token pair `tokenA-tokenB` to an array of pool addresses that serve the pair private tokenPairToPoolsCache = new Map(); @@ -35,7 +37,7 @@ export class FluidDex { address: CONTRACT_ADDRESSES[CHAIN_ID].FLUID_DEX_RESERVES_RESOLVER as Address, abi: FluidDexReservesResolverAbi, functionName: "estimateSwapIn", - args: [pool, this.poolsToTokenPairCache.get(pool)?.[0] === fromToken, fromAmount, 0n], + args: [pool, isAddressEqual(this.poolsToTokenPairCache.get(pool)?.[0] || zeroAddress, fromToken), fromAmount, 0n], })), }); @@ -45,8 +47,12 @@ export class FluidDex { if (estimate.status === "success") { const result = estimate.result as bigint; - if (result > bestEstimate) { - bestEstimate = result; + // We apply a default slippage to the estimate to avoid the risk of on-chain execution reverts due to state change + // between the simulation and the actual execution. + const resultWithSlippage = result * (10n ** 18n - FLUID_DEX_SLIPPAGE) / 10n ** 18n; + + if (resultWithSlippage > bestEstimate) { + bestEstimate = resultWithSlippage; bestPool = pools[index]; } } @@ -65,12 +71,12 @@ export class FluidDex { } } - prepareSwapCalldata(pool: Address | null, fromToken: Address, fromAmount: bigint): Call[] { + prepareSwapCalldata(receiver: Address, pool: Address | null, fromToken: Address, fromAmount: bigint): Call[] { if (!pool) { return []; } - const swap0to1 = this.poolsToTokenPairCache.get(pool)?.[0] === fromToken; + const swap0to1 = isAddressEqual(this.poolsToTokenPairCache.get(pool)?.[0] || zeroAddress, fromToken); const approveCalldata = encodeFunctionData({ abi: erc20Abi, @@ -89,7 +95,7 @@ export class FluidDex { data: encodeFunctionData({ abi: FluidDexT1Abi, functionName: "swapIn", - args: [swap0to1, fromAmount, 0n, CONTRACT_ADDRESSES[CHAIN_ID].DUTCH_AUCTION_REBALANCER], // Recipient of the swap is the rebalancer contract + args: [swap0to1, fromAmount, 0n, receiver], }), value: 0n, }, diff --git a/src/services/routing/getSwapParams.ts b/src/services/routing/getSwapParams.ts index b80f544..3fb9a24 100644 --- a/src/services/routing/getSwapParams.ts +++ b/src/services/routing/getSwapParams.ts @@ -10,6 +10,7 @@ import { import { getLidoEthStakeQuote, prepareLidoEthStakeCalldata } from "./lido"; import { FLUID_DEX } from "./fluid"; import { createComponentLogger } from "../../utils/logger"; +import { getPendleSwapQuote } from "./pendle"; const logger = createComponentLogger('getRebalanceSwapParams'); @@ -48,11 +49,11 @@ export const getDexSwapParams = async ( input: GetRebalanceSwapParamsInput, requiredAmountIn: bigint ): Promise => { - const { leverageToken, assetIn, assetOut, takeAmount } = input; + const { leverageToken, receiver, assetIn, assetOut, takeAmount, collateralAsset, debtAsset } = input; - // Fetch routes and quotes from DEXes directly - const [lifiQuote, amountOutUniV2, uniswapV3Route, fluidDexRoute] = await Promise.all([ + const [lifiQuote, amountOutUniV2, uniswapV3Route, fluidDexRoute, pendleQuote] = await Promise.all([ getLIFIQuote({ + receiver, fromToken: assetOut, toToken: assetIn, fromAmount: takeAmount, @@ -63,19 +64,30 @@ export const getDexSwapParams = async ( amountInRaw: takeAmount.toString(), }, logger), getRouteUniswapV3ExactInput({ + receiver, tokenInAddress: assetOut, tokenOutAddress: assetIn, amountInRaw: takeAmount.toString(), }, logger), FLUID_DEX.getEstimateSwapIn(assetOut, assetIn, takeAmount, logger), + getPendleSwapQuote({ + leverageToken, + receiver, + collateralAsset, + debtAsset, + fromAsset: assetOut, + toAsset: assetIn, + fromAmount: takeAmount, + }, logger), ]); // Find the best route by comparing all three options const routes = [ { amountOut: lifiQuote?.amountOut || 0n, prepareCalldata: () => prepareLIFISwapCalldata(lifiQuote!, assetOut, takeAmount) }, - { amountOut: amountOutUniV2, prepareCalldata: () => prepareUniswapV2SwapCalldata(assetOut, assetIn, takeAmount, requiredAmountIn) }, - { amountOut: BigInt((uniswapV3Route?.rawQuote || "0").toString()), prepareCalldata: () => prepareUniswapV3SwapCalldata(assetOut, uniswapV3Route!, takeAmount, requiredAmountIn) }, - { amountOut: fluidDexRoute.amountOut, prepareCalldata: () => FLUID_DEX.prepareSwapCalldata(fluidDexRoute.pool, assetOut, takeAmount) } + { amountOut: amountOutUniV2, prepareCalldata: () => prepareUniswapV2SwapCalldata(receiver, assetOut, assetIn, takeAmount, requiredAmountIn) }, + { amountOut: BigInt((uniswapV3Route?.rawQuote || "0").toString()), prepareCalldata: () => prepareUniswapV3SwapCalldata(receiver, assetOut, uniswapV3Route!, takeAmount, requiredAmountIn) }, + { amountOut: fluidDexRoute.amountOut, prepareCalldata: () => FLUID_DEX.prepareSwapCalldata(receiver, fluidDexRoute.pool, assetOut, takeAmount) }, + { amountOut: pendleQuote?.amountOut || 0n, prepareCalldata: () => pendleQuote ? pendleQuote.prepareCalldata(pendleQuote) : [] }, ]; logger.debug({ @@ -83,10 +95,12 @@ export const getDexSwapParams = async ( assetIn, assetOut, fromAmount: takeAmount, + requiredAmountOut: requiredAmountIn, lifi: routes[0].amountOut, uniswapV2: routes[1].amountOut, uniswapV3: routes[2].amountOut, fluid: routes[3].amountOut, + pendle: routes[4].amountOut, }, 'DEX swap quotes'); // Sort by amountOut in descending order and pick the best one diff --git a/src/services/routing/lifi.ts b/src/services/routing/lifi.ts index ccf4f2b..27ccd11 100644 --- a/src/services/routing/lifi.ts +++ b/src/services/routing/lifi.ts @@ -11,7 +11,7 @@ export const getLIFIQuote = async (args: GetLIFIQuoteInput, logger: ComponentLog return null; } - const { fromToken, toToken, fromAmount } = args; + const { receiver, fromToken, toToken, fromAmount } = args; try { // Addresses must be checksummed addresses @@ -23,7 +23,7 @@ export const getLIFIQuote = async (args: GetLIFIQuoteInput, logger: ComponentLog toToken: getAddress(toToken), fromAmount, fromAddress: getAddress(CONTRACT_ADDRESSES[CHAIN_ID].MULTICALL_EXECUTOR), - toAddress: getAddress(CONTRACT_ADDRESSES[CHAIN_ID].DUTCH_AUCTION_REBALANCER), + toAddress: getAddress(receiver), allowBridges: "none", slippage: LIFI_SLIPPAGE }, diff --git a/src/services/routing/pendle.ts b/src/services/routing/pendle.ts new file mode 100644 index 0000000..c48b5db --- /dev/null +++ b/src/services/routing/pendle.ts @@ -0,0 +1,317 @@ +import { ComponentLogger } from "../../utils/logger"; +import { CHAIN_ID } from "../../constants/chain"; +import { Address, encodeFunctionData, erc20Abi, isAddressEqual, getAddress, zeroAddress } from "viem"; +import { CONTRACT_ADDRESSES } from "../../constants/contracts"; +import { Call, GetRebalanceSwapParamsOutput, GetPendleSwapQuoteInput, GetPendleSwapQuoteOutput, StakeType } from "../../types"; +import { getPendleStaticRouterContract } from "../../utils/contractHelpers"; +import { getDexSwapParams } from "./getSwapParams"; +import PendleRouterAbi from "../../../abis/PendleRouter"; +import { getTokenDecimals } from "../../utils/tokens"; + +const PENDLE_SLIPPAGE = 1000000000000000n; // 0.1% + +// Maps from PT address to the Pendle market address to query for quotes +const PT_TO_PENDLE_MARKET = new Map([ + [ + // PT-RLP-4DEC2025 + getAddress("0x3A70F0C696dcB3A4aB3833cD9726397dD61AC85e"), + "0x9942a74e6E75cEa2DB5D068c1E75C9ac687bcA06", + ], +]); + +const PENDLE_MARKET_TO_YIELD_TOKEN = new Map([ + [getAddress("0x9942a74e6E75cEa2DB5D068c1E75C9ac687bcA06"), "0x4956b52ae2ff65d74ca2d61207523288e4528f96"], +]); + +export const getPendleSwapQuote = async ( + args: GetPendleSwapQuoteInput, + logger: ComponentLogger +): Promise => { + if (PT_TO_PENDLE_MARKET.has(getAddress(args.fromAsset))) { + return getPendleSwapExactPtForTokenQuote(args, logger); + } + if (PT_TO_PENDLE_MARKET.has(getAddress(args.toAsset))) { + return getPendleSwapExactTokenForPtQuote(args, logger); + } + + return null; +}; + +const getPendleSwapExactPtForTokenQuote = async ( + args: GetPendleSwapQuoteInput, + logger: ComponentLogger +): Promise => { + const { leverageToken, receiver, collateralAsset, debtAsset, fromAsset, toAsset, fromAmount } = args; + + const pt = getAddress(fromAsset); + + try { + const pendleRouterAddress = CONTRACT_ADDRESSES[CHAIN_ID].PENDLE_ROUTER; + if (!pendleRouterAddress) { + return null; + } + + const market = PT_TO_PENDLE_MARKET.get(pt); + if (!market) { + logger.dexQuoteError({ pt }, "No Pendle market found for PT"); + return null; + } + + const yieldToken = PENDLE_MARKET_TO_YIELD_TOKEN.get(market); + if (!yieldToken) { + logger.error({ pt, market }, "No underlying yield token found for Pendle PT"); + return null; + } + + const isYieldTokenToToken = isAddressEqual(toAsset, yieldToken); + + const staticRouter = getPendleStaticRouterContract(); + const result = await staticRouter.read.getYieldTokenAndPtRate([market]); + const [, , yieldTokenRate] = result; + + const ptDecimals = (await getTokenDecimals([pt]))[pt]; + // We apply a default slippage to the estimate to avoid the risk of on-chain execution reverts due to state change + // between the simulation and the actual execution. + const yieldTokenAmountOutWithSlippage = (yieldTokenRate * fromAmount) / 10n ** BigInt(ptDecimals) * (10n ** 18n - PENDLE_SLIPPAGE) / 10n ** 18n; + + const tokenOutput = createTokenOutputSimple(yieldToken, yieldTokenAmountOutWithSlippage); + const emptyLimit = createEmptyLimitOrderData(); + const calldata = encodeFunctionData({ + abi: PendleRouterAbi, + functionName: "swapExactPtForToken", + args: [ + isYieldTokenToToken ? receiver : CONTRACT_ADDRESSES[CHAIN_ID].MULTICALL_EXECUTOR, + market, + fromAmount, + tokenOutput, + emptyLimit, + ], + }); + + // Swap calldata for underlying yield token of PT -> toToken + let underlyingSwapData: GetRebalanceSwapParamsOutput | null = null; + if (!isYieldTokenToToken) { + underlyingSwapData = await getDexSwapParams( + { + leverageToken, + receiver, + stakeType: StakeType.NONE, + assetIn: toAsset, + assetOut: yieldToken, + takeAmount: yieldTokenAmountOutWithSlippage, + requiredAmountIn: 0n, + collateralAsset, + debtAsset, + }, + 0n + ); + } + + return { + amountOut: underlyingSwapData?.amountOut || 0n, + pendleSwapData: { + amountIn: fromAmount, + assetIn: pt, + data: calldata, + to: pendleRouterAddress, + value: 0n, + }, + underlyingSwapData, + to: pendleRouterAddress, + value: 0n, + prepareCalldata: (quote: GetPendleSwapQuoteOutput) => preparePendleSwapExactPtForTokenCalldata(quote), + }; + } catch (error) { + logger.dexQuoteError({ error }, "Error getting Pendle swap quote"); + return null; + } +}; + +const getPendleSwapExactTokenForPtQuote = async ( + args: GetPendleSwapQuoteInput, + logger: ComponentLogger +): Promise => { + const { leverageToken, receiver, collateralAsset, debtAsset, fromAsset, toAsset, fromAmount } = args; + + const pt = getAddress(toAsset); + + try { + const pendleRouterAddress = CONTRACT_ADDRESSES[CHAIN_ID].PENDLE_ROUTER; + if (!pendleRouterAddress) { + logger.dexQuoteError({ pendleRouterAddress }, "Pendle router address is not set"); + return null; + } + + const market = PT_TO_PENDLE_MARKET.get(pt); + if (!market) { + logger.dexQuoteError({ pt }, "No Pendle market found for PT"); + return null; + } + + const yieldToken = PENDLE_MARKET_TO_YIELD_TOKEN.get(getAddress(market)); + if (!yieldToken) { + logger.error({ pt, market }, "No underlying yield token found for Pendle PT"); + return null; + } + + const isFromTokenYieldToken = isAddressEqual(getAddress(fromAsset), getAddress(yieldToken)); + + // Swap calldata for from token -> underlying token of PT + let underlyingSwapData: GetRebalanceSwapParamsOutput | null = null; + let underlyingAmountInForPt = fromAmount; + if (!isFromTokenYieldToken) { + underlyingSwapData = await getDexSwapParams( + { + leverageToken, + // Set to the multicall executor because after the swap, the underlying token of the PT is used to mint PTs + receiver: CONTRACT_ADDRESSES[CHAIN_ID].MULTICALL_EXECUTOR, + stakeType: StakeType.NONE, + assetIn: yieldToken, + assetOut: fromAsset, + takeAmount: fromAmount, + requiredAmountIn: 0n, + collateralAsset, + debtAsset, + }, + 0n + ); + + underlyingAmountInForPt = underlyingSwapData.amountOut; + } + + const staticRouter = getPendleStaticRouterContract(); + const result = await staticRouter.read.swapExactTokenForPtStaticAndGenerateApproxParams([ + market, + yieldToken, + underlyingAmountInForPt, + PENDLE_SLIPPAGE, + ]); + const guessPtOut = result[5]; + + const approxParams = guessPtOut; + const tokenInput = createTokenInputSimple(yieldToken, underlyingAmountInForPt); + const emptyLimit = createEmptyLimitOrderData(); + const calldata = encodeFunctionData({ + abi: PendleRouterAbi, + functionName: "swapExactTokenForPt", + args: [ + receiver, + market, + 0n, + approxParams, + tokenInput, + emptyLimit, + ], + }); + + return { + amountOut: guessPtOut.guessOffchain, + pendleSwapData: { + amountIn: underlyingAmountInForPt, + assetIn: yieldToken, + data: calldata, + to: pendleRouterAddress, + value: 0n, + }, + underlyingSwapData, + to: pendleRouterAddress, + value: 0n, + prepareCalldata: (quote: GetPendleSwapQuoteOutput) => preparePendleSwapExactTokenForPtCalldata(quote), + }; + } catch (error) { + logger.dexQuoteError({ error }, "Error getting Pendle swap quote"); + return null; + } +}; + +export const preparePendleSwapExactPtForTokenCalldata = (pendleQuote: { + pendleSwapData: { amountIn: bigint; assetIn: Address; data: `0x${string}`; to: Address; value: bigint }; + underlyingSwapData: GetRebalanceSwapParamsOutput | null; +}): Call[] => { + const pendleRouterApproveCalldata = encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [pendleQuote.pendleSwapData.to, pendleQuote.pendleSwapData.amountIn], + }); + + return [ + { + target: pendleQuote.pendleSwapData.assetIn, + data: pendleRouterApproveCalldata, + value: 0n, + }, + { + target: pendleQuote.pendleSwapData.to, + data: pendleQuote.pendleSwapData.data, + value: 0n, + }, + ...(pendleQuote.underlyingSwapData?.swapCalls || []), + ]; +}; + +export const preparePendleSwapExactTokenForPtCalldata = (pendleQuote: { + pendleSwapData: { amountIn: bigint; assetIn: Address; data: `0x${string}`; to: Address; value: bigint }; + underlyingSwapData: GetRebalanceSwapParamsOutput | null; +}): Call[] => { + const pendleRouterApproveCalldata = encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [pendleQuote.pendleSwapData.to, pendleQuote.pendleSwapData.amountIn], + }); + + const calldata = [ + ...(pendleQuote.underlyingSwapData?.swapCalls || []), + { + target: pendleQuote.pendleSwapData.assetIn, + data: pendleRouterApproveCalldata, + value: 0n, + }, + { + target: pendleQuote.pendleSwapData.to, + data: pendleQuote.pendleSwapData.data, + value: 0n, + }, + ]; + + return calldata; +}; + +const createTokenInputSimple = (tokenIn: Address, netTokenIn: bigint) => { + return { + tokenIn, + netTokenIn, + tokenMintSy: tokenIn, + pendleSwap: zeroAddress, + swapData: { + swapType: 0, + extRouter: zeroAddress, + extCalldata: "" as `0x${string}`, + needScale: false, + }, + }; +}; + +const createTokenOutputSimple = (tokenOut: Address, minTokenOut: bigint) => { + return { + tokenOut, + minTokenOut, + tokenRedeemSy: tokenOut, + pendleSwap: zeroAddress, + swapData: { + swapType: 0, + extRouter: zeroAddress, + extCalldata: "" as `0x${string}`, + needScale: false, + }, + }; +}; + +const createEmptyLimitOrderData = () => { + return { + limitRouter: zeroAddress, + epsSkipMarket: 0n, + normalFills: [], + flashFills: [], + optData: "" as `0x${string}`, + }; +}; diff --git a/src/services/routing/uniswapV2.ts b/src/services/routing/uniswapV2.ts index fb774d1..f6e41c9 100644 --- a/src/services/routing/uniswapV2.ts +++ b/src/services/routing/uniswapV2.ts @@ -35,8 +35,7 @@ export const getAmountsOutUniswapV2 = async (args: UniswapV2GetAmountsOutArgs, l } }; - -export const prepareUniswapV2SwapCalldata = (assetIn: Address, assetOut: Address, inputAmount: bigint, outputAmountMin: bigint): Call[] => { +export const prepareUniswapV2SwapCalldata = (receiver: Address, assetIn: Address, assetOut: Address, inputAmount: bigint, outputAmountMin: bigint): Call[] => { const uniswapV2RouterAbi = UniswapV2Router02Abi; // Approve the router to spend the input amount @@ -53,7 +52,7 @@ export const prepareUniswapV2SwapCalldata = (assetIn: Address, assetOut: Address inputAmount, outputAmountMin, [assetIn, assetOut], - CONTRACT_ADDRESSES[CHAIN_ID].DUTCH_AUCTION_REBALANCER, // Recipient of the swap is the rebalancer contract + receiver, BigInt(Math.floor(Date.now() / 1000) + 60) // Deadline is set to 60 seconds from now ], }); diff --git a/src/services/routing/uniswapV3.ts b/src/services/routing/uniswapV3.ts index 387d19d..34174cd 100644 --- a/src/services/routing/uniswapV3.ts +++ b/src/services/routing/uniswapV3.ts @@ -39,7 +39,7 @@ export const getRouteUniswapV3ExactInput = async ( logger: ComponentLogger ): Promise => { try { - const { tokenInAddress, tokenOutAddress, amountInRaw } = args; + const { receiver, tokenInAddress, tokenOutAddress, amountInRaw } = args; const { tokenInDecimals, tokenOutDecimals } = await getTokensDecimals(tokenInAddress, tokenOutAddress); const tokenIn = new Token(CHAIN_ID, tokenInAddress, tokenInDecimals); @@ -58,7 +58,7 @@ export const getRouteUniswapV3ExactInput = async ( const amountIn = CurrencyAmount.fromRawAmount(tokenIn, amountInRaw); const options: SwapOptions = { - recipient: CONTRACT_ADDRESSES[CHAIN_ID].DUTCH_AUCTION_REBALANCER, + recipient: receiver, slippageTolerance: new Percent(100), deadline: Number.MAX_SAFE_INTEGER, type: SwapType.SWAP_ROUTER_02, @@ -84,7 +84,7 @@ export const getRouteUniswapV3ExactInput = async ( } }; -export const prepareUniswapV3SwapCalldata = (assetIn: Address, route: RouteWithValidQuote, inputAmount: bigint, outputAmountMin: bigint): Call[] => { +export const prepareUniswapV3SwapCalldata = (receiver: Address, assetIn: Address, route: RouteWithValidQuote, inputAmount: bigint, outputAmountMin: bigint): Call[] => { const uniswapV3RouterAbi = UniswapSwapRouter02Abi; const approveCalldata = encodeFunctionData({ @@ -100,7 +100,7 @@ export const prepareUniswapV3SwapCalldata = (assetIn: Address, route: RouteWithV functionName: 'exactInput', args: [{ path: encodedPath, - recipient: CONTRACT_ADDRESSES[CHAIN_ID].DUTCH_AUCTION_REBALANCER, // Recipient of the swap is the rebalancer contract + recipient: receiver, amountIn: inputAmount, amountOutMinimum: outputAmountMin, }], diff --git a/src/subscribers/auctionCreated.ts b/src/subscribers/auctionCreated.ts index 14b218f..f69ca79 100644 --- a/src/subscribers/auctionCreated.ts +++ b/src/subscribers/auctionCreated.ts @@ -247,10 +247,13 @@ export const handleAuctionCreatedEvent = async ( const swapParams = await getRebalanceSwapParams({ leverageToken, stakeType, + receiver: CONTRACT_ADDRESSES[CHAIN_ID].DUTCH_AUCTION_REBALANCER, assetIn, assetOut, takeAmount, - requiredAmountIn + requiredAmountIn, + collateralAsset, + debtAsset, }); if (!swapParams.isProfitable) { @@ -354,7 +357,7 @@ export const handleAuctionCreatedEvent = async ( if (receipt.status === "reverted") { const errorString = `Transaction to take auction for LeverageToken ${leverageToken} reverted. takeAmount: ${takeAmount}. Transaction hash: ${tx}`; await sendAlert(`*Error submitting takeAuction transaction*\n${errorString}`, LogLevel.ERROR); - handleAuctionLogger.error({ leverageToken, transactionHash: tx, takeAmount: takeAmount }, "Error submitting takeAuction transaction"); + handleAuctionLogger.error({ leverageToken, transactionHash: tx, takeAmount }, "Error submitting takeAuction transaction"); // We continue trying to take the auction with the next step, since it's likely that the transaction reverted // due to the max take amount decreasing during on-chain execution because of borrow interest or redemptions diff --git a/src/types/index.ts b/src/types/index.ts index 589314c..146fe0f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -40,6 +40,8 @@ export interface ContractAddresses { FLUID_DEX_RESERVES_RESOLVER?: Address; LEVERAGE_MANAGER: Address; MULTICALL_EXECUTOR: Address; + PENDLE_ROUTER?: Address; + PENDLE_ROUTER_STATIC?: Address; PRE_LIQUIDATION_REBALANCER: Address; WSTETH?: Address; UNISWAP_SWAP_ROUTER_02: Address; @@ -63,6 +65,7 @@ export interface UniswapV2GetAmountsOutArgs { } export interface UniswapV3QuoteExactInputArgs { + receiver: Address; tokenInAddress: Address; tokenOutAddress: Address; amountInRaw: string; @@ -85,10 +88,13 @@ export interface StakeContext { export interface GetRebalanceSwapParamsInput { leverageToken: Address; stakeType: StakeType; + receiver: Address; assetIn: Address; assetOut: Address; takeAmount: bigint; requiredAmountIn: bigint; + collateralAsset: Address; + debtAsset: Address; } export interface Call { @@ -104,6 +110,7 @@ export interface GetRebalanceSwapParamsOutput { } export interface GetLIFIQuoteInput { + receiver: Address; fromToken: Address; toToken: Address; fromAmount: bigint; @@ -122,3 +129,28 @@ export interface LeverageTokenRebalanceData { equity: bigint; targetRatio: bigint; } + +export interface GetPendleSwapQuoteInput { + leverageToken: Address; + receiver: Address; + collateralAsset: Address; + debtAsset: Address; + fromAsset: Address; + toAsset: Address; + fromAmount: bigint; +} + +export interface GetPendleSwapQuoteOutput { + amountOut: bigint; + pendleSwapData: { + amountIn: bigint; + assetIn: Address; + data: `0x${string}`; + to: Address; + value: bigint; + }; + underlyingSwapData: GetRebalanceSwapParamsOutput | null; + to: Address; + value: bigint; + prepareCalldata: (quote: GetPendleSwapQuoteOutput) => Call[]; +} diff --git a/src/utils/contractHelpers.ts b/src/utils/contractHelpers.ts index cec2d38..e9b1936 100644 --- a/src/utils/contractHelpers.ts +++ b/src/utils/contractHelpers.ts @@ -13,6 +13,8 @@ import { PreLiquidationRebalancerAbi } from "../../abis/PreLiquidationRebalancer import rebalanceAdapterAbi from "../../abis/RebalanceAdapter"; import { DutchAuctionRebalancerAbi } from "../../abis/DutchAuctionRebalancer"; import uniswapV2Router02Abi from "../../abis/UniswapV2Router02"; +import PendleStaticRouterAbi from "../../abis/PendleStaticRouter"; +import PendleRouterAbi from "../../abis/PendleRouter"; export const getHistoricalLogs = async ({ contractAddress, @@ -179,3 +181,19 @@ export const getPreLiquidationRebalancerContract = () => { client: walletClient, }); }; + +export const getPendleStaticRouterContract = () => { + return getContract({ + address: CONTRACT_ADDRESSES[CHAIN_ID].PENDLE_ROUTER_STATIC as Address, + abi: PendleStaticRouterAbi, + client: walletClient, + }); +}; + +export const getPendleRouterContract = () => { + return getContract({ + address: CONTRACT_ADDRESSES[CHAIN_ID].PENDLE_ROUTER as Address, + abi: PendleRouterAbi, + client: walletClient, + }); +}; diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts new file mode 100644 index 0000000..5feeab3 --- /dev/null +++ b/src/utils/tokens.ts @@ -0,0 +1,51 @@ +import { Address, erc20Abi, getAddress } from "viem"; +import { publicClient } from "./transactionHelpers"; + +const TOKEN_DECIMALS = new Map(); + +export const getTokenDecimals = async (tokenAddresses: Address[]): Promise> => { + + // Find which tokens need to be fetched + const tokensToFetch: Address[] = []; + const result: Record = {}; + + for (const address of tokenAddresses) { + const checksumAddress = getAddress(address); + const cachedDecimals = TOKEN_DECIMALS.get(checksumAddress); + if (cachedDecimals !== undefined) { + result[checksumAddress] = cachedDecimals; + } else { + tokensToFetch.push(checksumAddress); + } + } + + // If all tokens are cached, return early + if (tokensToFetch.length === 0) { + return result; + } + + // Fetch decimals for tokens not in cache using multicall + const multicallResults = await publicClient.multicall({ + contracts: tokensToFetch.map((address) => ({ + address, + abi: erc20Abi, + functionName: "decimals", + })), + }); + + // Process results and update cache + for (let i = 0; i < tokensToFetch.length; i++) { + const multicallResult = multicallResults[i]; + + const decimals = multicallResult.result; + const address = tokensToFetch[i]; + + // Update cache + TOKEN_DECIMALS.set(address, decimals as number); + + // Add to result + result[address] = decimals as number; + } + + return result; +}; \ No newline at end of file From 9b9ba6ac4a26837ec1e6a34fe127885935597156 Mon Sep 17 00:00:00 2001 From: chad-js Date: Fri, 31 Oct 2025 10:56:48 -0400 Subject: [PATCH 2/7] fetch yield token --- abis/PendleMarketV3.ts | 453 +++++++++++++++++++++++++++++++++ abis/PendleSy.ts | 406 +++++++++++++++++++++++++++++ src/services/routing/pendle.ts | 33 ++- 3 files changed, 883 insertions(+), 9 deletions(-) create mode 100644 abis/PendleMarketV3.ts create mode 100644 abis/PendleSy.ts diff --git a/abis/PendleMarketV3.ts b/abis/PendleMarketV3.ts new file mode 100644 index 0000000..d325dbe --- /dev/null +++ b/abis/PendleMarketV3.ts @@ -0,0 +1,453 @@ +const PendleMarketV3Abi = [ + { + inputs: [ + { internalType: "address", name: "_PT", type: "address" }, + { internalType: "int256", name: "_scalarRoot", type: "int256" }, + { internalType: "int256", name: "_initialAnchor", type: "int256" }, + { internalType: "uint80", name: "_lnFeeRateRoot", type: "uint80" }, + { internalType: "address", name: "_vePendle", type: "address" }, + { internalType: "address", name: "_gaugeController", type: "address" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [{ internalType: "int256", name: "exchangeRate", type: "int256" }], + name: "MarketExchangeRateBelowOne", + type: "error", + }, + { inputs: [], name: "MarketExpired", type: "error" }, + { + inputs: [ + { internalType: "int256", name: "currentAmount", type: "int256" }, + { internalType: "int256", name: "requiredAmount", type: "int256" }, + ], + name: "MarketInsufficientPtForTrade", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "actualBalance", type: "uint256" }, + { internalType: "uint256", name: "requiredBalance", type: "uint256" }, + ], + name: "MarketInsufficientPtReceived", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "actualBalance", type: "uint256" }, + { internalType: "uint256", name: "requiredBalance", type: "uint256" }, + ], + name: "MarketInsufficientSyReceived", + type: "error", + }, + { inputs: [], name: "MarketProportionMustNotEqualOne", type: "error" }, + { + inputs: [ + { internalType: "int256", name: "proportion", type: "int256" }, + { internalType: "int256", name: "maxProportion", type: "int256" }, + ], + name: "MarketProportionTooHigh", + type: "error", + }, + { + inputs: [{ internalType: "int256", name: "rateScalar", type: "int256" }], + name: "MarketRateScalarBelowZero", + type: "error", + }, + { + inputs: [{ internalType: "int256", name: "scalarRoot", type: "int256" }], + name: "MarketScalarRootBelowZero", + type: "error", + }, + { inputs: [], name: "MarketZeroAmountsInput", type: "error" }, + { inputs: [], name: "MarketZeroAmountsOutput", type: "error" }, + { inputs: [], name: "MarketZeroLnImpliedRate", type: "error" }, + { + inputs: [ + { internalType: "int256", name: "totalPt", type: "int256" }, + { internalType: "int256", name: "totalAsset", type: "int256" }, + ], + name: "MarketZeroTotalPtOrTotalAsset", + type: "error", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "spender", type: "address" }, + { indexed: false, internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "receiverSy", type: "address" }, + { indexed: true, internalType: "address", name: "receiverPt", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpBurned", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyOut", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPtOut", type: "uint256" }, + ], + name: "Burn", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "uint16", name: "observationCardinalityNextOld", type: "uint16" }, + { indexed: false, internalType: "uint16", name: "observationCardinalityNextNew", type: "uint16" }, + ], + name: "IncreaseObservationCardinalityNext", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "uint256", name: "netLpMinted", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyUsed", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netPtUsed", type: "uint256" }, + ], + name: "Mint", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "user", type: "address" }, + { indexed: false, internalType: "uint256[]", name: "rewardsOut", type: "uint256[]" }, + ], + name: "RedeemRewards", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: false, internalType: "int256", name: "netPtOut", type: "int256" }, + { indexed: false, internalType: "int256", name: "netSyOut", type: "int256" }, + { indexed: false, internalType: "uint256", name: "netSyFee", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "netSyToReserve", type: "uint256" }, + ], + name: "Swap", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: false, internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "uint256", name: "timestamp", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "lnLastImpliedRate", type: "uint256" }, + ], + name: "UpdateImpliedRate", + type: "event", + }, + { + inputs: [], + name: "_storage", + outputs: [ + { internalType: "int128", name: "totalPt", type: "int128" }, + { internalType: "int128", name: "totalSy", type: "int128" }, + { internalType: "uint96", name: "lastLnImpliedRate", type: "uint96" }, + { internalType: "uint16", name: "observationIndex", type: "uint16" }, + { internalType: "uint16", name: "observationCardinality", type: "uint16" }, + { internalType: "uint16", name: "observationCardinalityNext", type: "uint16" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "activeBalance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + ], + name: "allowance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiverSy", type: "address" }, + { internalType: "address", name: "receiverPt", type: "address" }, + { internalType: "uint256", name: "netLpToBurn", type: "uint256" }, + ], + name: "burn", + outputs: [ + { internalType: "uint256", name: "netSyOut", type: "uint256" }, + { internalType: "uint256", name: "netPtOut", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "expiry", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "factory", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getNonOverrideLnFeeRateRoot", + outputs: [{ internalType: "uint80", name: "", type: "uint80" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getRewardTokens", + outputs: [{ internalType: "address[]", name: "", type: "address[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint16", name: "cardinalityNext", type: "uint16" }], + name: "increaseObservationsCardinalityNext", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "isExpired", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastRewardBlock", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "netSyDesired", type: "uint256" }, + { internalType: "uint256", name: "netPtDesired", type: "uint256" }, + ], + name: "mint", + outputs: [ + { internalType: "uint256", name: "netLpOut", type: "uint256" }, + { internalType: "uint256", name: "netSyUsed", type: "uint256" }, + { internalType: "uint256", name: "netPtUsed", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "", type: "uint256" }], + name: "observations", + outputs: [ + { internalType: "uint32", name: "blockTimestamp", type: "uint32" }, + { internalType: "uint216", name: "lnImpliedRateCumulative", type: "uint216" }, + { internalType: "bool", name: "initialized", type: "bool" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint32[]", name: "secondsAgos", type: "uint32[]" }], + name: "observe", + outputs: [{ internalType: "uint216[]", name: "lnImpliedRateCumulative", type: "uint216[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "router", type: "address" }], + name: "readState", + outputs: [ + { + components: [ + { internalType: "int256", name: "totalPt", type: "int256" }, + { internalType: "int256", name: "totalSy", type: "int256" }, + { internalType: "int256", name: "totalLp", type: "int256" }, + { internalType: "address", name: "treasury", type: "address" }, + { internalType: "int256", name: "scalarRoot", type: "int256" }, + { internalType: "uint256", name: "expiry", type: "uint256" }, + { internalType: "uint256", name: "lnFeeRateRoot", type: "uint256" }, + { internalType: "uint256", name: "reserveFeePercent", type: "uint256" }, + { internalType: "uint256", name: "lastLnImpliedRate", type: "uint256" }, + ], + internalType: "struct MarketState", + name: "market", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "readTokens", + outputs: [ + { internalType: "contract IStandardizedYield", name: "_SY", type: "address" }, + { internalType: "contract IPPrincipalToken", name: "_PT", type: "address" }, + { internalType: "contract IPYieldToken", name: "_YT", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "redeemRewards", + outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "rewardState", + outputs: [ + { internalType: "uint128", name: "index", type: "uint128" }, + { internalType: "uint128", name: "lastBalance", type: "uint128" }, + ], + stateMutability: "view", + type: "function", + }, + { inputs: [], name: "skim", outputs: [], stateMutability: "nonpayable", type: "function" }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "exactPtIn", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "swapExactPtForSy", + outputs: [ + { internalType: "uint256", name: "netSyOut", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "exactPtOut", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "swapSyForExactPt", + outputs: [ + { internalType: "uint256", name: "netSyIn", type: "uint256" }, + { internalType: "uint256", name: "netSyFee", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalActiveSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "address", name: "", type: "address" }, + ], + name: "userReward", + outputs: [ + { internalType: "uint128", name: "index", type: "uint128" }, + { internalType: "uint128", name: "accrued", type: "uint128" }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +export default PendleMarketV3Abi; \ No newline at end of file diff --git a/abis/PendleSy.ts b/abis/PendleSy.ts new file mode 100644 index 0000000..76f9578 --- /dev/null +++ b/abis/PendleSy.ts @@ -0,0 +1,406 @@ +const PendleSyAbi = [ + { inputs: [], stateMutability: "nonpayable", type: "constructor" }, + { + inputs: [ + { internalType: "uint256", name: "actualSharesOut", type: "uint256" }, + { internalType: "uint256", name: "requiredSharesOut", type: "uint256" }, + ], + name: "SYInsufficientSharesOut", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "actualTokenOut", type: "uint256" }, + { internalType: "uint256", name: "requiredTokenOut", type: "uint256" }, + ], + name: "SYInsufficientTokenOut", + type: "error", + }, + { inputs: [{ internalType: "address", name: "token", type: "address" }], name: "SYInvalidTokenIn", type: "error" }, + { inputs: [{ internalType: "address", name: "token", type: "address" }], name: "SYInvalidTokenOut", type: "error" }, + { inputs: [], name: "SYZeroDeposit", type: "error" }, + { inputs: [], name: "SYZeroRedeem", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "spender", type: "address" }, + { indexed: false, internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "user", type: "address" }, + { indexed: false, internalType: "address[]", name: "rewardTokens", type: "address[]" }, + { indexed: false, internalType: "uint256[]", name: "rewardAmounts", type: "uint256[]" }, + ], + name: "ClaimRewards", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: true, internalType: "address", name: "tokenIn", type: "address" }, + { indexed: false, internalType: "uint256", name: "amountDeposited", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "amountSyOut", type: "uint256" }, + ], + name: "Deposit", + type: "event", + }, + { anonymous: false, inputs: [], name: "EIP712DomainChanged", type: "event" }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "uint8", name: "version", type: "uint8" }], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "previousOwner", type: "address" }, + { indexed: true, internalType: "address", name: "newOwner", type: "address" }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "address", name: "account", type: "address" }], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "caller", type: "address" }, + { indexed: true, internalType: "address", name: "receiver", type: "address" }, + { indexed: true, internalType: "address", name: "tokenOut", type: "address" }, + { indexed: false, internalType: "uint256", name: "amountSyToRedeem", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "amountTokenOut", type: "uint256" }, + ], + name: "Redeem", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: false, internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "address", name: "account", type: "address" }], + name: "Unpaused", + type: "event", + }, + { + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "accruedRewards", + outputs: [{ internalType: "uint256[]", name: "rewardAmounts", type: "uint256[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + ], + name: "allowance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "assetInfo", + outputs: [ + { internalType: "enum IStandardizedYield.AssetType", name: "assetType", type: "uint8" }, + { internalType: "address", name: "assetAddress", type: "address" }, + { internalType: "uint8", name: "assetDecimals", type: "uint8" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { inputs: [], name: "claimOwnership", outputs: [], stateMutability: "nonpayable", type: "function" }, + { + inputs: [{ internalType: "address", name: "", type: "address" }], + name: "claimRewards", + outputs: [{ internalType: "uint256[]", name: "rewardAmounts", type: "uint256[]" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "address", name: "tokenIn", type: "address" }, + { internalType: "uint256", name: "amountTokenToDeposit", type: "uint256" }, + { internalType: "uint256", name: "minSharesOut", type: "uint256" }, + ], + name: "deposit", + outputs: [{ internalType: "uint256", name: "amountSharesOut", type: "uint256" }], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "eip712Domain", + outputs: [ + { internalType: "bytes1", name: "fields", type: "bytes1" }, + { internalType: "string", name: "name", type: "string" }, + { internalType: "string", name: "version", type: "string" }, + { internalType: "uint256", name: "chainId", type: "uint256" }, + { internalType: "address", name: "verifyingContract", type: "address" }, + { internalType: "bytes32", name: "salt", type: "bytes32" }, + { internalType: "uint256[]", name: "extensions", type: "uint256[]" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "exchangeRate", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getRewardTokens", + outputs: [{ internalType: "address[]", name: "rewardTokens", type: "address[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTokensIn", + outputs: [{ internalType: "address[]", name: "res", type: "address[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTokensOut", + outputs: [{ internalType: "address[]", name: "res", type: "address[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "string", name: "_name", type: "string" }, + { internalType: "string", name: "_symbol", type: "string" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "isValidTokenIn", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "isValidTokenOut", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "owner", type: "address" }], + name: "nonces", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { inputs: [], name: "pause", outputs: [], stateMutability: "nonpayable", type: "function" }, + { + inputs: [], + name: "paused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pendingOwner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { internalType: "uint8", name: "v", type: "uint8" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + name: "permit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "tokenIn", type: "address" }, + { internalType: "uint256", name: "amountTokenToDeposit", type: "uint256" }, + ], + name: "previewDeposit", + outputs: [{ internalType: "uint256", name: "amountSharesOut", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "tokenOut", type: "address" }, + { internalType: "uint256", name: "amountSharesToRedeem", type: "uint256" }, + ], + name: "previewRedeem", + outputs: [{ internalType: "uint256", name: "amountTokenOut", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "amountSharesToRedeem", type: "uint256" }, + { internalType: "address", name: "tokenOut", type: "address" }, + { internalType: "uint256", name: "minTokenOut", type: "uint256" }, + { internalType: "bool", name: "burnFromInternalBalance", type: "bool" }, + ], + name: "redeem", + outputs: [{ internalType: "uint256", name: "amountTokenOut", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "rewardIndexesCurrent", + outputs: [{ internalType: "uint256[]", name: "indexes", type: "uint256[]" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "rewardIndexesStored", + outputs: [{ internalType: "uint256[]", name: "indexes", type: "uint256[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newOwner", type: "address" }, + { internalType: "bool", name: "direct", type: "bool" }, + { internalType: "bool", name: "renounce", type: "bool" }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { inputs: [], name: "unpause", outputs: [], stateMutability: "nonpayable", type: "function" }, + { + inputs: [], + name: "yieldToken", + outputs: [{ internalType: "address", name: "yieldToken", type: "address" }], + stateMutability: "view", + type: "function", + }, + { stateMutability: "payable", type: "receive" }, +]; + +export default PendleSyAbi; \ No newline at end of file diff --git a/src/services/routing/pendle.ts b/src/services/routing/pendle.ts index c48b5db..5316591 100644 --- a/src/services/routing/pendle.ts +++ b/src/services/routing/pendle.ts @@ -6,7 +6,10 @@ import { Call, GetRebalanceSwapParamsOutput, GetPendleSwapQuoteInput, GetPendleS import { getPendleStaticRouterContract } from "../../utils/contractHelpers"; import { getDexSwapParams } from "./getSwapParams"; import PendleRouterAbi from "../../../abis/PendleRouter"; +import PendleMarketV3Abi from "../../../abis/PendleMarketV3"; +import PendleSyAbi from "../../../abis/PendleSy"; import { getTokenDecimals } from "../../utils/tokens"; +import { publicClient } from "@/utils/transactionHelpers"; const PENDLE_SLIPPAGE = 1000000000000000n; // 0.1% @@ -19,9 +22,7 @@ const PT_TO_PENDLE_MARKET = new Map([ ], ]); -const PENDLE_MARKET_TO_YIELD_TOKEN = new Map([ - [getAddress("0x9942a74e6E75cEa2DB5D068c1E75C9ac687bcA06"), "0x4956b52ae2ff65d74ca2d61207523288e4528f96"], -]); +const PENDLE_MARKET_TO_YIELD_TOKEN = new Map(); export const getPendleSwapQuote = async ( args: GetPendleSwapQuoteInput, @@ -57,13 +58,27 @@ const getPendleSwapExactPtForTokenQuote = async ( return null; } - const yieldToken = PENDLE_MARKET_TO_YIELD_TOKEN.get(market); + let yieldToken = PENDLE_MARKET_TO_YIELD_TOKEN.get(market); if (!yieldToken) { - logger.error({ pt, market }, "No underlying yield token found for Pendle PT"); - return null; + try { + const [syToken,,] = await publicClient.readContract({ + address: market, + abi: PendleMarketV3Abi, + functionName: "readTokens", + }); + yieldToken = (await publicClient.readContract({ + address: syToken, + abi: PendleSyAbi, + functionName: "yieldToken", + })) as Address; + PENDLE_MARKET_TO_YIELD_TOKEN.set(market, yieldToken); + } catch (error) { + logger.error({ error, pt, market }, "Error getting Pendle yield token"); + throw error; + } } - const isYieldTokenToToken = isAddressEqual(toAsset, yieldToken); + const isToTokenYieldToken = isAddressEqual(toAsset, yieldToken); const staticRouter = getPendleStaticRouterContract(); const result = await staticRouter.read.getYieldTokenAndPtRate([market]); @@ -80,7 +95,7 @@ const getPendleSwapExactPtForTokenQuote = async ( abi: PendleRouterAbi, functionName: "swapExactPtForToken", args: [ - isYieldTokenToToken ? receiver : CONTRACT_ADDRESSES[CHAIN_ID].MULTICALL_EXECUTOR, + isToTokenYieldToken ? receiver : CONTRACT_ADDRESSES[CHAIN_ID].MULTICALL_EXECUTOR, market, fromAmount, tokenOutput, @@ -90,7 +105,7 @@ const getPendleSwapExactPtForTokenQuote = async ( // Swap calldata for underlying yield token of PT -> toToken let underlyingSwapData: GetRebalanceSwapParamsOutput | null = null; - if (!isYieldTokenToToken) { + if (!isToTokenYieldToken) { underlyingSwapData = await getDexSwapParams( { leverageToken, From 7fd4edcf5b464b5351f03c6706e276a7c81b3335 Mon Sep 17 00:00:00 2001 From: chad-js Date: Fri, 31 Oct 2025 11:18:34 -0400 Subject: [PATCH 3/7] fetch yield token --- src/services/routing/pendle.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/services/routing/pendle.ts b/src/services/routing/pendle.ts index 5316591..c919897 100644 --- a/src/services/routing/pendle.ts +++ b/src/services/routing/pendle.ts @@ -9,7 +9,7 @@ import PendleRouterAbi from "../../../abis/PendleRouter"; import PendleMarketV3Abi from "../../../abis/PendleMarketV3"; import PendleSyAbi from "../../../abis/PendleSy"; import { getTokenDecimals } from "../../utils/tokens"; -import { publicClient } from "@/utils/transactionHelpers"; +import { publicClient } from "../../utils/transactionHelpers"; const PENDLE_SLIPPAGE = 1000000000000000n; // 0.1% @@ -163,10 +163,24 @@ const getPendleSwapExactTokenForPtQuote = async ( return null; } - const yieldToken = PENDLE_MARKET_TO_YIELD_TOKEN.get(getAddress(market)); + let yieldToken = PENDLE_MARKET_TO_YIELD_TOKEN.get(getAddress(market)); if (!yieldToken) { - logger.error({ pt, market }, "No underlying yield token found for Pendle PT"); - return null; + try { + const [syToken,,] = await publicClient.readContract({ + address: market, + abi: PendleMarketV3Abi, + functionName: "readTokens", + }); + yieldToken = (await publicClient.readContract({ + address: syToken, + abi: PendleSyAbi, + functionName: "yieldToken", + })) as Address; + PENDLE_MARKET_TO_YIELD_TOKEN.set(market, yieldToken); + } catch (error) { + logger.error({ error, pt, market }, "Error getting Pendle yield token"); + throw error; + } } const isFromTokenYieldToken = isAddressEqual(getAddress(fromAsset), getAddress(yieldToken)); From 8af1f83404e9bfa8208a1f685d447e500360237e Mon Sep 17 00:00:00 2001 From: chad-js Date: Mon, 3 Nov 2025 18:03:37 -0500 Subject: [PATCH 4/7] Merge main --- .env.example | 9 +-- package-lock.json | 110 +++++++++++++++++++++++--- package.json | 6 +- src/constants/chain.ts | 1 + src/constants/values.ts | 3 +- src/services/routing/balmy.ts | 103 ++++++++++++++++++++++++ src/services/routing/getSwapParams.ts | 42 ++++++---- src/services/routing/lifi.ts | 66 ---------------- 8 files changed, 238 insertions(+), 102 deletions(-) create mode 100644 src/services/routing/balmy.ts delete mode 100644 src/services/routing/lifi.ts diff --git a/.env.example b/.env.example index 81e09ef..f976da3 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ PRIMARY_RPC_URL_WS= FALLBACK_RPC_URL= ETHEREUM_MAINNET_RPC_URL= ETHEREUM_MAINNET_FALLBACK_RPC_URL= +ALCHEMY_API_KEY= # Comma separated list of leverage tokens to add to backfill BACKFILL_LEVERAGE_TOKENS= @@ -70,10 +71,8 @@ SLACK_AUTH_TOKEN= SLACK_REBALANCE_BOT_NAME= # ============== -# ==== LiFi ==== +# ==== Balmy ==== # ============== -LIFI_API_KEY= - -# Slippage parameter used for LiFi API quote -LIFI_SLIPPAGE= \ No newline at end of file +BALMY_SLIPPAGE_PERCENTAGE= +LIFI_API_KEY= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d33d4c9..3570ede 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@balmy/sdk": "github:seamless-protocol/balmy-sdk#main", "@slack/web-api": "^7.9.1", "@uniswap/sdk-core": "^7.7.2", "@uniswap/smart-order-router": "^4.20.10", @@ -41,6 +42,67 @@ "version": "1.11.0", "license": "MIT" }, + "node_modules/@balmy/sdk": { + "name": "@seamless-protocol/balmy-sdk", + "version": "0.9.0", + "resolved": "git+ssh://git@github.com/seamless-protocol/balmy-sdk.git#864933e0d366d49fe86bb87be8c6c953627484a4", + "dependencies": { + "cross-fetch": "3.1.5", + "crypto-js": "4.2.0", + "deepmerge": "4.3.1", + "lru-cache": "9.0.3", + "ms": "3.0.0-canary.1", + "qs": "6.11.2", + "viem": "2.28.0" + } + }, + "node_modules/@balmy/sdk/node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "license": "MIT", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/@balmy/sdk/node_modules/lru-cache": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.0.3.tgz", + "integrity": "sha512-cyjNRew29d4kbgnz1sjDqxg7qg8NW4s+HQzCGjeon7DV5T2yDije16W9HaUFV1dhVEMh+SjrOcK0TomBmf3Egg==", + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@balmy/sdk/node_modules/ms": { + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz", + "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==", + "license": "MIT", + "engines": { + "node": ">=12.13" + } + }, + "node_modules/@balmy/sdk/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.2", "cpu": [ @@ -1445,10 +1507,12 @@ } }, "node_modules/@noble/curves": { - "version": "1.8.1", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", + "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.7.1" + "@noble/hashes": "1.7.2" }, "engines": { "node": "^14.21.3 || >=16" @@ -1458,7 +1522,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.7.1", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", + "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -3235,7 +3301,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3627,6 +3692,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "dev": true, @@ -6665,7 +6739,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7200,6 +7273,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -7649,7 +7737,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7667,7 +7754,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7682,7 +7768,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7699,7 +7784,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8477,7 +8561,9 @@ } }, "node_modules/viem": { - "version": "2.25.0", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.28.0.tgz", + "integrity": "sha512-Z4W5O1pe+6pirYTFm451FcZmfGAUxUWt2L/eWC+YfTF28j/8rd7q6MBAi05lMN4KhLJjhN0s5YGIPB+kf1L20g==", "funding": [ { "type": "github", @@ -8486,8 +8572,8 @@ ], "license": "MIT", "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", + "@noble/curves": "1.8.2", + "@noble/hashes": "1.7.2", "@scure/bip32": "1.6.2", "@scure/bip39": "1.5.4", "abitype": "1.0.8", diff --git a/package.json b/package.json index b94ef30..30394f5 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,14 @@ "scripts": { "dev": "npm run type-check && tsx src/index.ts", "dev:watch": "tsx watch src/index.ts", - "build": "npm run clean:dist && tsc -p tsconfig.build.json && npm run copy:leverage-tokens", + "build": "npm run clean:dist && tsc -p tsconfig.build.json", "start": "node dist/src/index.js", "clean:dist": "rimraf dist", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "format": "prettier --check .", "format:fix": "prettier --write .", - "type-check": "tsc --noEmit", - "copy:leverage-tokens": "mkdir -p dist/src/data && [ -f src/data/leverageTokens.json ] && cp src/data/leverageTokens.json dist/src/data/ || true" + "type-check": "tsc --noEmit" }, "devDependencies": { "@types/node": "^22.14.0", @@ -36,6 +35,7 @@ "typescript": "^5.8.2" }, "dependencies": { + "@balmy/sdk": "github:seamless-protocol/balmy-sdk#main", "@slack/web-api": "^7.9.1", "@uniswap/sdk-core": "^7.7.2", "@uniswap/smart-order-router": "^4.20.10", diff --git a/src/constants/chain.ts b/src/constants/chain.ts index 041afa4..4eb3fc5 100644 --- a/src/constants/chain.ts +++ b/src/constants/chain.ts @@ -14,6 +14,7 @@ export const REBALANCE_ELIGIBILITY_POLL_INTERVAL = parseInt( export const PRIMARY_RPC_URL = process.env.PRIMARY_RPC_URL || ""; export const PRIMARY_RPC_URL_WS = process.env.PRIMARY_RPC_URL_WS || ""; export const FALLBACK_RPC_URL = process.env.FALLBACK_RPC_URL || ""; +export const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY || ""; export const MAINNET_RPC_URL = process.env.ETHEREUM_MAINNET_RPC_URL || ""; export const MAINNET_FALLBACK_RPC_URL = process.env.ETHEREUM_MAINNET_FALLBACK_RPC_URL || ""; export const VIEM_CHAIN = extractChain({ chains: [mainnet, base], id: CHAIN_ID as any }); diff --git a/src/constants/values.ts b/src/constants/values.ts index 11b4c2e..aae3acf 100644 --- a/src/constants/values.ts +++ b/src/constants/values.ts @@ -40,9 +40,8 @@ export const MAX_TAKE_AMOUNT_SCALING = BigInt(String(process.env.MAX_TAKE_AMOUNT export const ETHERFI_L2_MODE_SYNC_POOL_ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as Address; -export const LIFI_API_URL = "https://li.quest/v1/quote"; export const LIFI_API_KEY = process.env.LIFI_API_KEY || null; -export const LIFI_SLIPPAGE = Number(process.env.LIFI_SLIPPAGE) || 0.01; +export const BALMY_SLIPPAGE_PERCENTAGE = Number(process.env.BALMY_SLIPPAGE_PERCENTAGE) || 1; // 1% by default export const CHECK_PROFITABILITY_WITH_GAS_FEE = Boolean(process.env.CHECK_PROFITABILITY_WITH_GAS_FEE && process.env.CHECK_PROFITABILITY_WITH_GAS_FEE.toLowerCase() === "true"); diff --git a/src/services/routing/balmy.ts b/src/services/routing/balmy.ts new file mode 100644 index 0000000..4c9e856 --- /dev/null +++ b/src/services/routing/balmy.ts @@ -0,0 +1,103 @@ +import { ComponentLogger } from "../../utils/logger"; +import { encodeFunctionData, erc20Abi, zeroAddress } from "viem"; +import { buildSDK, QuoteRequest, QuoteResponse } from "@balmy/sdk"; +import { Call } from "../../types"; +import { publicClient } from "../../utils/transactionHelpers"; +import { ALCHEMY_API_KEY } from "../../constants/chain"; +import { LIFI_API_KEY } from "../../constants/values"; + +const sdk = buildSDK({ + quotes: { + defaultConfig: { + global: { + referrer: { + address: zeroAddress, + name: zeroAddress, + } + }, + custom: { + "li-fi": { + apiKey: LIFI_API_KEY || undefined, + } + } + }, + sourceList: { type: "local" } + }, + providers: { + source: { + type: "custom", + instance: publicClient + } + }, + prices: { + source: { + type: "prioritized", + sources: [ + ...(ALCHEMY_API_KEY !== "" ? [{ + type: 'alchemy', + apiKey: ALCHEMY_API_KEY, + }] : []), + { + type: 'coingecko', + }, + { + type: 'defi-llama', + } + ] + } + } +}); + +export const getBalmyQuote = async (args: QuoteRequest, logger: ComponentLogger): Promise => { + try { + const quote = await sdk.quoteService.getBestQuote({ + request: { + ...args, + sourceConfig: { global: { disableValidation: true } }, + }, + config: { choose: { by: "most-swapped", using: "max sell/min buy amounts" } } + }); + + logger.debug({ quote }, 'Balmy quote'); + + return quote; + } catch (error) { + logger.dexQuoteError({ error }, 'Error getting Balmy quote'); + return null; + } +}; + +export const prepareBalmySwapCalldata = async (quote: QuoteResponse, logger: ComponentLogger): Promise => { + try { + const txs = sdk.quoteService.buildTxs({ + quotes: { + [quote.source.id]: quote + }, + sourceConfig: { global: { disableValidation: true } } + }); + + const tx = await txs[quote.source.id]; + + const approveCalldata = encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [quote.source.allowanceTarget as `0x${string}`, quote.sellAmount.amount], + }); + + return [ + { + target: quote.sellToken.address as `0x${string}`, + data: approveCalldata, + value: 0n, + }, + { + target: tx.to as `0x${string}`, + data: tx.data as `0x${string}`, + value: 0n, + } + ]; + } catch (error) { + logger.dexQuoteError({ error }, 'Error preparing Balmy swap calldata'); + throw error; + } +}; \ No newline at end of file diff --git a/src/services/routing/getSwapParams.ts b/src/services/routing/getSwapParams.ts index 3fb9a24..b874998 100644 --- a/src/services/routing/getSwapParams.ts +++ b/src/services/routing/getSwapParams.ts @@ -1,5 +1,5 @@ +import { getAddress } from "viem"; import {getEtherFiEthStakeQuote, prepareEtherFiEthStakeCalldata} from "./etherFi"; -import { getLIFIQuote, prepareLIFISwapCalldata } from "./lifi"; import { getAmountsOutUniswapV2, prepareUniswapV2SwapCalldata } from "./uniswapV2"; import { getRouteUniswapV3ExactInput, prepareUniswapV3SwapCalldata } from "./uniswapV3"; import { @@ -8,9 +8,13 @@ import { StakeType, } from "../../types"; import { getLidoEthStakeQuote, prepareLidoEthStakeCalldata } from "./lido"; +import { BALMY_SLIPPAGE_PERCENTAGE } from "../../constants/values"; import { FLUID_DEX } from "./fluid"; import { createComponentLogger } from "../../utils/logger"; import { getPendleSwapQuote } from "./pendle"; +import { getBalmyQuote, prepareBalmySwapCalldata } from "./balmy"; +import { CHAIN_ID } from "../../constants/chain"; +import { CONTRACT_ADDRESSES } from "../../constants/contracts"; const logger = createComponentLogger('getRebalanceSwapParams'); @@ -51,12 +55,18 @@ export const getDexSwapParams = async ( ): Promise => { const { leverageToken, receiver, assetIn, assetOut, takeAmount, collateralAsset, debtAsset } = input; - const [lifiQuote, amountOutUniV2, uniswapV3Route, fluidDexRoute, pendleQuote] = await Promise.all([ - getLIFIQuote({ - receiver, - fromToken: assetOut, - toToken: assetIn, - fromAmount: takeAmount, + const [balmyQuote, amountOutUniV2, uniswapV3Route, fluidDexRoute, pendleQuote] = await Promise.all([ + getBalmyQuote({ + chainId: CHAIN_ID, + sellToken: assetOut as `0x${string}`, + buyToken: assetIn as `0x${string}`, + order: { + type: "sell", + sellAmount: takeAmount, + }, + slippagePercentage: BALMY_SLIPPAGE_PERCENTAGE, + takerAddress: getAddress(CONTRACT_ADDRESSES[CHAIN_ID].MULTICALL_EXECUTOR), + recipient: getAddress(receiver), }, logger), getAmountsOutUniswapV2({ inputTokenAddress: assetOut, @@ -81,13 +91,17 @@ export const getDexSwapParams = async ( }, logger), ]); + if (!balmyQuote && !amountOutUniV2 && !uniswapV3Route && !fluidDexRoute) { + throw new Error('No quotes found'); + } + // Find the best route by comparing all three options const routes = [ - { amountOut: lifiQuote?.amountOut || 0n, prepareCalldata: () => prepareLIFISwapCalldata(lifiQuote!, assetOut, takeAmount) }, - { amountOut: amountOutUniV2, prepareCalldata: () => prepareUniswapV2SwapCalldata(receiver, assetOut, assetIn, takeAmount, requiredAmountIn) }, - { amountOut: BigInt((uniswapV3Route?.rawQuote || "0").toString()), prepareCalldata: () => prepareUniswapV3SwapCalldata(receiver, assetOut, uniswapV3Route!, takeAmount, requiredAmountIn) }, - { amountOut: fluidDexRoute.amountOut, prepareCalldata: () => FLUID_DEX.prepareSwapCalldata(receiver, fluidDexRoute.pool, assetOut, takeAmount) }, - { amountOut: pendleQuote?.amountOut || 0n, prepareCalldata: () => pendleQuote ? pendleQuote.prepareCalldata(pendleQuote) : [] }, + { amountOut: balmyQuote?.buyAmount.amount || 0n, prepareCalldata: async () => prepareBalmySwapCalldata(balmyQuote!, logger) }, + { amountOut: amountOutUniV2, prepareCalldata: async () => prepareUniswapV2SwapCalldata(receiver, assetOut, assetIn, takeAmount, requiredAmountIn) }, + { amountOut: BigInt((uniswapV3Route?.rawQuote || "0").toString()), prepareCalldata: async () => prepareUniswapV3SwapCalldata(receiver, assetOut, uniswapV3Route!, takeAmount, requiredAmountIn) }, + { amountOut: fluidDexRoute.amountOut, prepareCalldata: async () => FLUID_DEX.prepareSwapCalldata(receiver, fluidDexRoute.pool, assetOut, takeAmount) }, + { amountOut: pendleQuote?.amountOut || 0n, prepareCalldata: async () => pendleQuote ? pendleQuote.prepareCalldata(pendleQuote) : [] }, ]; logger.debug({ @@ -96,7 +110,7 @@ export const getDexSwapParams = async ( assetOut, fromAmount: takeAmount, requiredAmountOut: requiredAmountIn, - lifi: routes[0].amountOut, + balmy: routes[0].amountOut, uniswapV2: routes[1].amountOut, uniswapV3: routes[2].amountOut, fluid: routes[3].amountOut, @@ -119,6 +133,6 @@ export const getDexSwapParams = async ( return { isProfitable: true, amountOut: bestRoute.amountOut, - swapCalls: bestRoute.prepareCalldata(), + swapCalls: await bestRoute.prepareCalldata() || [], }; }; \ No newline at end of file diff --git a/src/services/routing/lifi.ts b/src/services/routing/lifi.ts deleted file mode 100644 index 27ccd11..0000000 --- a/src/services/routing/lifi.ts +++ /dev/null @@ -1,66 +0,0 @@ -import axios from "axios"; -import { Address, encodeFunctionData, erc20Abi, getAddress } from "viem"; -import { LIFI_API_KEY, LIFI_API_URL, LIFI_SLIPPAGE } from "../../constants/values"; -import { CHAIN_ID } from "../../constants/chain"; -import { CONTRACT_ADDRESSES } from "../../constants/contracts"; -import { Call, GetLIFIQuoteInput, GetLIFIQuoteOutput } from "../../types"; -import { ComponentLogger } from "../../utils/logger"; - -export const getLIFIQuote = async (args: GetLIFIQuoteInput, logger: ComponentLogger): Promise => { - if (!LIFI_API_KEY) { - return null; - } - - const { receiver, fromToken, toToken, fromAmount } = args; - - try { - // Addresses must be checksummed addresses - const result = await axios.get(LIFI_API_URL, { - params: { - fromChain: CHAIN_ID, - toChain: CHAIN_ID, - fromToken: getAddress(fromToken), - toToken: getAddress(toToken), - fromAmount, - fromAddress: getAddress(CONTRACT_ADDRESSES[CHAIN_ID].MULTICALL_EXECUTOR), - toAddress: getAddress(receiver), - allowBridges: "none", - slippage: LIFI_SLIPPAGE - }, - headers: { - "x-lifi-api-key": LIFI_API_KEY, - }, - }); - - return { - amountOut: BigInt(result.data.estimate.toAmount), - to: result.data.transactionRequest.to, - data: result.data.transactionRequest.data, - value: result.data.transactionRequest.value, - }; - } catch (error) { - logger.dexQuoteError({ error }, 'Error getting LIFI quote'); - return null; - } -}; - -export const prepareLIFISwapCalldata = (lifiQuote: GetLIFIQuoteOutput, assetIn: Address, inputAmount: bigint): Call[] => { - const approveCalldata = encodeFunctionData({ - abi: erc20Abi, - functionName: 'approve', - args: [lifiQuote.to, inputAmount], - }); - - return [ - { - target: assetIn, - data: approveCalldata, - value: 0n, - }, - { - target: lifiQuote.to, - data: lifiQuote.data, - value: 0n, - } - ]; -}; \ No newline at end of file From cf13f5c756d86c52b2204b4fda1c4fd251c07fa8 Mon Sep 17 00:00:00 2001 From: chad-js Date: Tue, 4 Nov 2025 13:48:59 -0500 Subject: [PATCH 5/7] handle max uint response for pendle rate --- src/services/routing/pendle.ts | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/services/routing/pendle.ts b/src/services/routing/pendle.ts index c919897..22e2c73 100644 --- a/src/services/routing/pendle.ts +++ b/src/services/routing/pendle.ts @@ -1,6 +1,6 @@ import { ComponentLogger } from "../../utils/logger"; import { CHAIN_ID } from "../../constants/chain"; -import { Address, encodeFunctionData, erc20Abi, isAddressEqual, getAddress, zeroAddress } from "viem"; +import { Address, encodeFunctionData, erc20Abi, isAddressEqual, getAddress, maxUint256, zeroAddress } from "viem"; import { CONTRACT_ADDRESSES } from "../../constants/contracts"; import { Call, GetRebalanceSwapParamsOutput, GetPendleSwapQuoteInput, GetPendleSwapQuoteOutput, StakeType } from "../../types"; import { getPendleStaticRouterContract } from "../../utils/contractHelpers"; @@ -58,31 +58,17 @@ const getPendleSwapExactPtForTokenQuote = async ( return null; } - let yieldToken = PENDLE_MARKET_TO_YIELD_TOKEN.get(market); - if (!yieldToken) { - try { - const [syToken,,] = await publicClient.readContract({ - address: market, - abi: PendleMarketV3Abi, - functionName: "readTokens", - }); - yieldToken = (await publicClient.readContract({ - address: syToken, - abi: PendleSyAbi, - functionName: "yieldToken", - })) as Address; - PENDLE_MARKET_TO_YIELD_TOKEN.set(market, yieldToken); - } catch (error) { - logger.error({ error, pt, market }, "Error getting Pendle yield token"); - throw error; - } + const staticRouter = getPendleStaticRouterContract(); + const [yieldToken, , yieldTokenRate] = await staticRouter.read.getYieldTokenAndPtRate([market]); + + if (yieldTokenRate === maxUint256) { + logger.dexQuoteError({ pt, yieldToken, market, yieldTokenRate }, "Yield token rate is max uint256 for PT -> yield token swap, swap cannot be executed"); + return null; } - const isToTokenYieldToken = isAddressEqual(toAsset, yieldToken); + PENDLE_MARKET_TO_YIELD_TOKEN.set(market, yieldToken); - const staticRouter = getPendleStaticRouterContract(); - const result = await staticRouter.read.getYieldTokenAndPtRate([market]); - const [, , yieldTokenRate] = result; + const isToTokenYieldToken = isAddressEqual(toAsset, yieldToken); const ptDecimals = (await getTokenDecimals([pt]))[pt]; // We apply a default slippage to the estimate to avoid the risk of on-chain execution reverts due to state change From 660d5b210f87e0c40c285757ae04f77a3361f06f Mon Sep 17 00:00:00 2001 From: chad-js Date: Tue, 4 Nov 2025 16:35:23 -0500 Subject: [PATCH 6/7] add min amount out to dex params output --- .env.example | 12 +++++++++--- src/constants/values.ts | 1 + src/services/routing/fluid.ts | 28 ++++++++++----------------- src/services/routing/getSwapParams.ts | 23 ++++++++++++---------- src/services/routing/pendle.ts | 26 ++++++++++++++----------- src/services/routing/uniswapV2.ts | 12 ++++++++---- src/services/routing/uniswapV3.ts | 12 ++++++++---- src/types/index.ts | 10 +++++++++- src/utils/math.ts | 9 +++++++++ 9 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 src/utils/math.ts diff --git a/.env.example b/.env.example index f976da3..a27d308 100644 --- a/.env.example +++ b/.env.example @@ -70,9 +70,15 @@ SLACK_AUTH_TOKEN= # Descriptive name for this bot, for logging purposes SLACK_REBALANCE_BOT_NAME= -# ============== +# =============== # ==== Balmy ==== -# ============== +# =============== BALMY_SLIPPAGE_PERCENTAGE= -LIFI_API_KEY= \ No newline at end of file +LIFI_API_KEY= + +# ============= +# ==== DEX ==== +# ============= + +DEX_SLIPPAGE_BPS= \ No newline at end of file diff --git a/src/constants/values.ts b/src/constants/values.ts index aae3acf..e8ba1c1 100644 --- a/src/constants/values.ts +++ b/src/constants/values.ts @@ -42,6 +42,7 @@ export const ETHERFI_L2_MODE_SYNC_POOL_ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEE export const LIFI_API_KEY = process.env.LIFI_API_KEY || null; export const BALMY_SLIPPAGE_PERCENTAGE = Number(process.env.BALMY_SLIPPAGE_PERCENTAGE) || 1; // 1% by default +export const DEX_SLIPPAGE_BPS = BigInt(String(process.env.DEX_SLIPPAGE_BPS || "10")); // 0.1% by default export const CHECK_PROFITABILITY_WITH_GAS_FEE = Boolean(process.env.CHECK_PROFITABILITY_WITH_GAS_FEE && process.env.CHECK_PROFITABILITY_WITH_GAS_FEE.toLowerCase() === "true"); diff --git a/src/services/routing/fluid.ts b/src/services/routing/fluid.ts index c8d4ac0..2ad7354 100644 --- a/src/services/routing/fluid.ts +++ b/src/services/routing/fluid.ts @@ -6,8 +6,7 @@ import { publicClient } from "../../utils/transactionHelpers"; import FluidDexT1Abi from "../../../abis/FluidDexT1"; import { Call } from "../../types"; import { ComponentLogger } from "../../utils/logger"; - -const FLUID_DEX_SLIPPAGE = 1000000000000000n; // 0.1% +import { getDexSlippageAdjustedAmount } from "../../utils/math"; export class FluidDex { // Maps from token pair `tokenA-tokenB` to an array of pool addresses that serve the pair @@ -21,15 +20,12 @@ export class FluidDex { toToken: Address, fromAmount: bigint, logger: ComponentLogger - ): Promise<{ amountOut: bigint; pool: Address | null }> { + ): Promise<{ amountOut: bigint; minAmountOut: bigint; pool: Address } | null> { try { const pools = await this.getPools(fromToken, toToken); if (!pools || pools.length === 0) { - return { - amountOut: 0n, - pool: null, - }; + return null; } const estimates = await publicClient.multicall({ @@ -47,27 +43,23 @@ export class FluidDex { if (estimate.status === "success") { const result = estimate.result as bigint; - // We apply a default slippage to the estimate to avoid the risk of on-chain execution reverts due to state change - // between the simulation and the actual execution. - const resultWithSlippage = result * (10n ** 18n - FLUID_DEX_SLIPPAGE) / 10n ** 18n; - - if (resultWithSlippage > bestEstimate) { - bestEstimate = resultWithSlippage; + if (result > bestEstimate) { + bestEstimate = result; bestPool = pools[index]; } } }); + const bestEstimateWithSlippage = getDexSlippageAdjustedAmount(bestEstimate); + return { amountOut: bestEstimate, - pool: bestPool, + minAmountOut: bestEstimateWithSlippage, + pool: bestPool!, }; } catch (error) { logger.dexQuoteError({ error }, 'Error estimating swap in with Fluid DEX'); - return { - amountOut: 0n, - pool: null, - }; + return null; } } diff --git a/src/services/routing/getSwapParams.ts b/src/services/routing/getSwapParams.ts index b874998..1584045 100644 --- a/src/services/routing/getSwapParams.ts +++ b/src/services/routing/getSwapParams.ts @@ -3,6 +3,7 @@ import {getEtherFiEthStakeQuote, prepareEtherFiEthStakeCalldata} from "./etherFi import { getAmountsOutUniswapV2, prepareUniswapV2SwapCalldata } from "./uniswapV2"; import { getRouteUniswapV3ExactInput, prepareUniswapV3SwapCalldata } from "./uniswapV3"; import { + GetDexSwapParamsOutput, GetRebalanceSwapParamsInput, GetRebalanceSwapParamsOutput, StakeType, @@ -46,16 +47,16 @@ export const getRebalanceSwapParams = async ( } } - return getDexSwapParams(input, requiredAmountIn); + return await getDexSwapParams(input, requiredAmountIn); }; export const getDexSwapParams = async ( input: GetRebalanceSwapParamsInput, - requiredAmountIn: bigint -): Promise => { + requiredAmountIn: bigint, +): Promise => { const { leverageToken, receiver, assetIn, assetOut, takeAmount, collateralAsset, debtAsset } = input; - const [balmyQuote, amountOutUniV2, uniswapV3Route, fluidDexRoute, pendleQuote] = await Promise.all([ + const [balmyQuote, uniswapV2Quote, uniswapV3Route, fluidDexRoute, pendleQuote] = await Promise.all([ getBalmyQuote({ chainId: CHAIN_ID, sellToken: assetOut as `0x${string}`, @@ -91,17 +92,17 @@ export const getDexSwapParams = async ( }, logger), ]); - if (!balmyQuote && !amountOutUniV2 && !uniswapV3Route && !fluidDexRoute) { + if (!balmyQuote && !uniswapV2Quote && !uniswapV3Route && !fluidDexRoute && !pendleQuote) { throw new Error('No quotes found'); } // Find the best route by comparing all three options const routes = [ - { amountOut: balmyQuote?.buyAmount.amount || 0n, prepareCalldata: async () => prepareBalmySwapCalldata(balmyQuote!, logger) }, - { amountOut: amountOutUniV2, prepareCalldata: async () => prepareUniswapV2SwapCalldata(receiver, assetOut, assetIn, takeAmount, requiredAmountIn) }, - { amountOut: BigInt((uniswapV3Route?.rawQuote || "0").toString()), prepareCalldata: async () => prepareUniswapV3SwapCalldata(receiver, assetOut, uniswapV3Route!, takeAmount, requiredAmountIn) }, - { amountOut: fluidDexRoute.amountOut, prepareCalldata: async () => FLUID_DEX.prepareSwapCalldata(receiver, fluidDexRoute.pool, assetOut, takeAmount) }, - { amountOut: pendleQuote?.amountOut || 0n, prepareCalldata: async () => pendleQuote ? pendleQuote.prepareCalldata(pendleQuote) : [] }, + { amountOut: balmyQuote?.buyAmount.amount || 0n, minAmountOut: balmyQuote?.minBuyAmount.amount || 0n, prepareCalldata: async () => prepareBalmySwapCalldata(balmyQuote!, logger) }, + { amountOut: uniswapV2Quote?.amountOut || 0n, minAmountOut: uniswapV2Quote?.minAmountOut || 0n, prepareCalldata: async () => prepareUniswapV2SwapCalldata(receiver, assetOut, assetIn, takeAmount, requiredAmountIn) }, + { amountOut: BigInt((uniswapV3Route?.route.rawQuote || "0").toString()), minAmountOut: uniswapV3Route?.minAmountOut || 0n, prepareCalldata: async () => prepareUniswapV3SwapCalldata(receiver, assetOut, uniswapV3Route!.route, takeAmount, requiredAmountIn) }, + { amountOut: fluidDexRoute?.amountOut || 0n, minAmountOut: fluidDexRoute?.minAmountOut || 0n, prepareCalldata: async () => FLUID_DEX.prepareSwapCalldata(receiver, fluidDexRoute!.pool, assetOut, takeAmount) }, + { amountOut: pendleQuote?.amountOut || 0n, minAmountOut: pendleQuote?.minAmountOut || 0n, prepareCalldata: async () => pendleQuote ? pendleQuote.prepareCalldata(pendleQuote) : [] }, ]; logger.debug({ @@ -126,6 +127,7 @@ export const getDexSwapParams = async ( return { isProfitable: false, amountOut: bestRoute.amountOut, + minAmountOut: bestRoute.minAmountOut, swapCalls: [], } } @@ -133,6 +135,7 @@ export const getDexSwapParams = async ( return { isProfitable: true, amountOut: bestRoute.amountOut, + minAmountOut: bestRoute.minAmountOut, swapCalls: await bestRoute.prepareCalldata() || [], }; }; \ No newline at end of file diff --git a/src/services/routing/pendle.ts b/src/services/routing/pendle.ts index 22e2c73..c1a1e7f 100644 --- a/src/services/routing/pendle.ts +++ b/src/services/routing/pendle.ts @@ -2,7 +2,7 @@ import { ComponentLogger } from "../../utils/logger"; import { CHAIN_ID } from "../../constants/chain"; import { Address, encodeFunctionData, erc20Abi, isAddressEqual, getAddress, maxUint256, zeroAddress } from "viem"; import { CONTRACT_ADDRESSES } from "../../constants/contracts"; -import { Call, GetRebalanceSwapParamsOutput, GetPendleSwapQuoteInput, GetPendleSwapQuoteOutput, StakeType } from "../../types"; +import { Call, GetPendleSwapQuoteInput, GetPendleSwapQuoteOutput, StakeType, GetDexSwapParamsOutput } from "../../types"; import { getPendleStaticRouterContract } from "../../utils/contractHelpers"; import { getDexSwapParams } from "./getSwapParams"; import PendleRouterAbi from "../../../abis/PendleRouter"; @@ -10,8 +10,8 @@ import PendleMarketV3Abi from "../../../abis/PendleMarketV3"; import PendleSyAbi from "../../../abis/PendleSy"; import { getTokenDecimals } from "../../utils/tokens"; import { publicClient } from "../../utils/transactionHelpers"; - -const PENDLE_SLIPPAGE = 1000000000000000n; // 0.1% +import { getDexSlippageAdjustedAmount } from "../../utils/math"; +import { DEX_SLIPPAGE_BPS } from "../../constants/values"; // Maps from PT address to the Pendle market address to query for quotes const PT_TO_PENDLE_MARKET = new Map([ @@ -73,7 +73,7 @@ const getPendleSwapExactPtForTokenQuote = async ( const ptDecimals = (await getTokenDecimals([pt]))[pt]; // We apply a default slippage to the estimate to avoid the risk of on-chain execution reverts due to state change // between the simulation and the actual execution. - const yieldTokenAmountOutWithSlippage = (yieldTokenRate * fromAmount) / 10n ** BigInt(ptDecimals) * (10n ** 18n - PENDLE_SLIPPAGE) / 10n ** 18n; + const yieldTokenAmountOutWithSlippage = getDexSlippageAdjustedAmount((yieldTokenRate * fromAmount) / 10n ** BigInt(ptDecimals)); const tokenOutput = createTokenOutputSimple(yieldToken, yieldTokenAmountOutWithSlippage); const emptyLimit = createEmptyLimitOrderData(); @@ -90,7 +90,7 @@ const getPendleSwapExactPtForTokenQuote = async ( }); // Swap calldata for underlying yield token of PT -> toToken - let underlyingSwapData: GetRebalanceSwapParamsOutput | null = null; + let underlyingSwapData: GetDexSwapParamsOutput | null = null; if (!isToTokenYieldToken) { underlyingSwapData = await getDexSwapParams( { @@ -110,6 +110,7 @@ const getPendleSwapExactPtForTokenQuote = async ( return { amountOut: underlyingSwapData?.amountOut || 0n, + minAmountOut: underlyingSwapData?.minAmountOut || 0n, pendleSwapData: { amountIn: fromAmount, assetIn: pt, @@ -172,7 +173,7 @@ const getPendleSwapExactTokenForPtQuote = async ( const isFromTokenYieldToken = isAddressEqual(getAddress(fromAsset), getAddress(yieldToken)); // Swap calldata for from token -> underlying token of PT - let underlyingSwapData: GetRebalanceSwapParamsOutput | null = null; + let underlyingSwapData: GetDexSwapParamsOutput | null = null; let underlyingAmountInForPt = fromAmount; if (!isFromTokenYieldToken) { underlyingSwapData = await getDexSwapParams( @@ -188,18 +189,20 @@ const getPendleSwapExactTokenForPtQuote = async ( collateralAsset, debtAsset, }, - 0n + 0n, ); - underlyingAmountInForPt = underlyingSwapData.amountOut; + // We use the min amount out for the intermediate swap to avoid reverts + underlyingAmountInForPt = underlyingSwapData.minAmountOut; } const staticRouter = getPendleStaticRouterContract(); + const pendleSlippage = DEX_SLIPPAGE_BPS * 100000000000000n; const result = await staticRouter.read.swapExactTokenForPtStaticAndGenerateApproxParams([ market, yieldToken, underlyingAmountInForPt, - PENDLE_SLIPPAGE, + pendleSlippage, ]); const guessPtOut = result[5]; @@ -221,6 +224,7 @@ const getPendleSwapExactTokenForPtQuote = async ( return { amountOut: guessPtOut.guessOffchain, + minAmountOut: guessPtOut.guessMin, pendleSwapData: { amountIn: underlyingAmountInForPt, assetIn: yieldToken, @@ -241,7 +245,7 @@ const getPendleSwapExactTokenForPtQuote = async ( export const preparePendleSwapExactPtForTokenCalldata = (pendleQuote: { pendleSwapData: { amountIn: bigint; assetIn: Address; data: `0x${string}`; to: Address; value: bigint }; - underlyingSwapData: GetRebalanceSwapParamsOutput | null; + underlyingSwapData: GetDexSwapParamsOutput | null; }): Call[] => { const pendleRouterApproveCalldata = encodeFunctionData({ abi: erc20Abi, @@ -266,7 +270,7 @@ export const preparePendleSwapExactPtForTokenCalldata = (pendleQuote: { export const preparePendleSwapExactTokenForPtCalldata = (pendleQuote: { pendleSwapData: { amountIn: bigint; assetIn: Address; data: `0x${string}`; to: Address; value: bigint }; - underlyingSwapData: GetRebalanceSwapParamsOutput | null; + underlyingSwapData: GetDexSwapParamsOutput | null; }): Call[] => { const pendleRouterApproveCalldata = encodeFunctionData({ abi: erc20Abi, diff --git a/src/services/routing/uniswapV2.ts b/src/services/routing/uniswapV2.ts index f6e41c9..77c1166 100644 --- a/src/services/routing/uniswapV2.ts +++ b/src/services/routing/uniswapV2.ts @@ -5,15 +5,16 @@ import UniswapV2Router02Abi from "../../../abis/UniswapV2Router02"; import { CONTRACT_ADDRESSES } from "../../constants/contracts"; import { CHAIN_ID } from "../../constants/chain"; import { ComponentLogger } from "../../utils/logger"; +import { getDexSlippageAdjustedAmount } from "../../utils/math"; -export const getAmountsOutUniswapV2 = async (args: UniswapV2GetAmountsOutArgs, logger: ComponentLogger) => { +export const getAmountsOutUniswapV2 = async (args: UniswapV2GetAmountsOutArgs, logger: ComponentLogger): Promise<{ amountOut: bigint; minAmountOut: bigint } | null> => { try { const router = getUniswapV2Router02Contract(); const { inputTokenAddress, outputTokenAddress, amountInRaw } = args; if (amountInRaw === "0") { - return 0n; + return null; } // Convert the input string to BigInt (this is the raw base-units value) @@ -28,10 +29,13 @@ export const getAmountsOutUniswapV2 = async (args: UniswapV2GetAmountsOutArgs, l // For a 2-token path, amountsOut = [amountIn, amountOut] const outputAmountRaw = amountsOut[1]; - return outputAmountRaw; + return { + amountOut: outputAmountRaw, + minAmountOut: getDexSlippageAdjustedAmount(outputAmountRaw), + } } catch (error) { logger.dexQuoteError({ error }, 'Error calling Uniswap V2 Router02 getAmountsOut'); - return 0n; + return null; } }; diff --git a/src/services/routing/uniswapV3.ts b/src/services/routing/uniswapV3.ts index 34174cd..bc510ad 100644 --- a/src/services/routing/uniswapV3.ts +++ b/src/services/routing/uniswapV3.ts @@ -5,11 +5,12 @@ import { CurrencyAmount, Percent, Token, TradeType } from "@uniswap/sdk-core"; import { encodeRouteToPath } from "@uniswap/v3-sdk"; import { CONTRACT_ADDRESSES } from "../../constants/contracts"; import { Call, UniswapV3QuoteExactInputArgs } from "../../types"; -import { IS_USING_FORK } from "../../constants/values"; +import { DEX_SLIPPAGE_BPS, IS_USING_FORK } from "../../constants/values"; import { primaryEthersProvider, publicClient } from "../../utils/transactionHelpers"; import UniswapSwapRouter02Abi from "../../../abis/UniswapSwapRouter02"; import { CHAIN_ID } from "../../constants/chain"; import { ComponentLogger } from "../../utils/logger"; +import { getDexSlippageAdjustedAmount } from "../../utils/math"; class StaticGasPriceProvider implements IGasPriceProvider { constructor(private gasPriceWei: BigNumber) {} @@ -37,7 +38,7 @@ const getTokensDecimals = async (tokenInAddress: Address, tokenOutAddress: Addre export const getRouteUniswapV3ExactInput = async ( args: UniswapV3QuoteExactInputArgs, logger: ComponentLogger -): Promise => { +): Promise<{ route: RouteWithValidQuote; minAmountOut: bigint } | null> => { try { const { receiver, tokenInAddress, tokenOutAddress, amountInRaw } = args; const { tokenInDecimals, tokenOutDecimals } = await getTokensDecimals(tokenInAddress, tokenOutAddress); @@ -59,7 +60,7 @@ export const getRouteUniswapV3ExactInput = async ( const amountIn = CurrencyAmount.fromRawAmount(tokenIn, amountInRaw); const options: SwapOptions = { recipient: receiver, - slippageTolerance: new Percent(100), + slippageTolerance: new Percent(Number(DEX_SLIPPAGE_BPS), 10_000), deadline: Number.MAX_SAFE_INTEGER, type: SwapType.SWAP_ROUTER_02, }; @@ -77,7 +78,10 @@ export const getRouteUniswapV3ExactInput = async ( return null; } - return v3Route; + return { + route: v3Route, + minAmountOut: getDexSlippageAdjustedAmount(BigInt((v3Route?.rawQuote || "0").toString())), + }; } catch (error) { logger.dexQuoteError({ error }, 'Error getting Uniswap V3 route'); return null; diff --git a/src/types/index.ts b/src/types/index.ts index 146fe0f..27dc5f3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -103,6 +103,13 @@ export interface Call { value: bigint; } +export interface GetDexSwapParamsOutput { + isProfitable: boolean; + amountOut: bigint; + minAmountOut: bigint; + swapCalls: Call[]; +} + export interface GetRebalanceSwapParamsOutput { isProfitable: boolean; amountOut: bigint; @@ -142,6 +149,7 @@ export interface GetPendleSwapQuoteInput { export interface GetPendleSwapQuoteOutput { amountOut: bigint; + minAmountOut: bigint; pendleSwapData: { amountIn: bigint; assetIn: Address; @@ -149,7 +157,7 @@ export interface GetPendleSwapQuoteOutput { to: Address; value: bigint; }; - underlyingSwapData: GetRebalanceSwapParamsOutput | null; + underlyingSwapData: GetDexSwapParamsOutput | null; to: Address; value: bigint; prepareCalldata: (quote: GetPendleSwapQuoteOutput) => Call[]; diff --git a/src/utils/math.ts b/src/utils/math.ts new file mode 100644 index 0000000..c9fbbc5 --- /dev/null +++ b/src/utils/math.ts @@ -0,0 +1,9 @@ +import { DEX_SLIPPAGE_BPS } from "../constants/values"; + +export const getSlippageAdjustedAmount = (amount: bigint, slippageBps: bigint) => { + return amount * (10000n - slippageBps) / 10000n; +}; + +export const getDexSlippageAdjustedAmount = (amount: bigint) => { + return getSlippageAdjustedAmount(amount, DEX_SLIPPAGE_BPS); +}; \ No newline at end of file From 07460127a1d0eb4a7c31396f2a68e9aa99117d80 Mon Sep 17 00:00:00 2001 From: chad-js Date: Tue, 4 Nov 2025 16:37:07 -0500 Subject: [PATCH 7/7] lint --- src/services/routing/getSwapParams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/routing/getSwapParams.ts b/src/services/routing/getSwapParams.ts index 1584045..0fa7419 100644 --- a/src/services/routing/getSwapParams.ts +++ b/src/services/routing/getSwapParams.ts @@ -47,7 +47,7 @@ export const getRebalanceSwapParams = async ( } } - return await getDexSwapParams(input, requiredAmountIn); + return getDexSwapParams(input, requiredAmountIn); }; export const getDexSwapParams = async (