diff --git a/packages/deploy/src/config.ts b/packages/deploy/src/config.ts index 9e13814a..0f5410ca 100644 --- a/packages/deploy/src/config.ts +++ b/packages/deploy/src/config.ts @@ -482,7 +482,8 @@ export interface PendleCurveRouterAdapterPair { curveRoute: string[]; curveSwapParams: number[][]; curvePools: string[]; - curveSlippage: number; + curveDxAdjustPtToToken: number; + curveDxAdjustTokenToPt: number; } export interface PendlePtToAssetAdapterPair { diff --git a/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/config.json b/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/config.json index 762072e7..31863fa5 100644 --- a/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/config.json +++ b/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/config.json @@ -1,6 +1,6 @@ { "systemContextDefaults": { - "ethNodeUri": "https://eth-mainnet.g.alchemy.com/v2/4wZHNnzMpPD4FZQ-4DOIGbYoZ7M_Hwn6" + "ethNodeUri": "https://ethereum-rpc.publicnode.com" }, "connection": { "assertChainId": 1, @@ -14,40 +14,70 @@ "assertDecimals": 18 }, { - "id": "weeth", - "address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", - "assertSymbol": "weETH", + "id": "susde", + "address": "0x9d39a5de30e57443bff2a8307a4256c8797a3497", + "assertSymbol": "sUSDe", "assertDecimals": 18 }, { - "id": "pt-weeth-26jun2025", - "address": "0xef6122835a2bbf575d0117d394fda24ab7d09d4e", - "assertSymbol": "PT-weETH-26JUN2025", + "id": "usde", + "address": "0x4c9edd5852cd905f086c759e8383e09bff1e68b3", + "assertSymbol": "USDe", "assertDecimals": 18 }, { - "id": "pt-susde-31jul2025", - "address": "0x3b3fb9c57858ef816833dc91565efcd85d96f634", - "assertSymbol": "PT-sUSDE-31JUL2025", + "id": "wsteth", + "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "assertSymbol": "wstETH", "assertDecimals": 18 }, { - "id": "pt-susde-29may2025", - "address": "0xb7de5dfcb74d25c2f21841fbd6230355c50d9308", - "assertSymbol": "PT-sUSDE-29MAY2025", + "id": "usds", + "address": "0xdc035d45d973e3ec169d2276ddab16f1e407384f", + "assertSymbol": "USDS", "assertDecimals": 18 }, { - "id": "susde", - "address": "0x9d39a5de30e57443bff2a8307a4256c8797a3497", - "assertSymbol": "sUSDe", + "id": "usr", + "address": "0x66a1E37c9b0eAddca17d3662D6c05F4DECf3e110", + "assertSymbol": "USR", + "assertDecimals": 18 + }, + { + "id": "wstusr", + "address": "0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055", + "assertSymbol": "wstUSR", + "assertDecimals": 18 + }, + { + "id": "pt-susde-25sep2025", + "address": "0x9f56094c450763769ba0ea9fe2876070c0fd5f77", + "assertSymbol": "PT-sUSDE-25SEP2025", + "assertDecimals": 18 + }, + { + "id": "pt-wstusr-25sep2025", + "address": "0x23e60d1488525bf4685f53b3aa8e676c30321066", + "assertSymbol": "PT-wstUSR-25SEP2025", + "assertDecimals": 18 + }, + { + "id": "pt-steth-25dec2025", + "address": "0xf99985822fb361117fcf3768d34a6353e6022f5f", + "assertSymbol": "PT-stETH-25DEC2025", + "assertDecimals": 18 + }, + { + "id": "pt-usde-31jul2025", + "address": "0x917459337caac939d41d7493b3999f571d20d667", + "assertSymbol": "PT-USDe-31JUL2025", "assertDecimals": 18 }, { - "id": "usdc", - "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "assertSymbol": "USDC", - "assertDecimals": 6 + "id": "pt-usds-14aug2025", + "address": "0xffec096c087c13cc268497b89a613cace4df9a48", + "assertSymbol": "PT-USDS-14AUG2025", + "assertDecimals": 18 } ], "prices": [], @@ -58,25 +88,105 @@ "pendlePtLpOracle": "0x66a1096C6366b2529274dF4f5D8247827fe4CEA8", "settings": [ { - "quoteTokenId": "weeth", - "baseTokenId": "pt-weeth-26jun2025", - "pendleMarket": "0xf4cf59259d007a96c641b41621ab52c93b9691b1", + "quoteTokenId": "susde", + "baseTokenId": "pt-susde-25sep2025", + "pendleMarket": "0xa36b60a14a1a5247912584768c6e53e1a269a9f7", "secondsAgo": "30 min", "secondsAgoLiquidation": "5 sec" }, { - "quoteTokenId": "susde", - "baseTokenId": "pt-susde-29may2025", - "pendleMarket": "0xb162b764044697cf03617c2efbcb1f42e31e4766", + "quoteTokenId": "wstusr", + "baseTokenId": "pt-wstusr-25sep2025", + "pendleMarket": "0x09fa04aac9c6d1c6131352ee950cd67ecc6d4fb9", "secondsAgo": "30 min", "secondsAgoLiquidation": "5 sec" }, { - "quoteTokenId": "susde", - "baseTokenId": "pt-susde-31jul2025", - "pendleMarket": "0x4339ffe2b7592dc783ed13cce310531ab366deac", + "quoteTokenId": "usde", + "baseTokenId": "pt-usde-31jul2025", + "pendleMarket": "0x9df192d13d61609d1852461c4850595e1f56e714", + "secondsAgo": "30 min", + "secondsAgoLiquidation": "5 sec" + }, + { + "quoteTokenId": "wsteth", + "baseTokenId": "pt-steth-25dec2025", + "pendleMarket": "0xc374f7ec85f8c7de3207a10bb1978ba104bda3b2", "secondsAgo": "30 min", "secondsAgoLiquidation": "5 sec" + }, + { + "quoteTokenId": "usds", + "baseTokenId": "pt-usds-14aug2025", + "pendleMarket": "0xdace1121e10500e9e29d071f01593fd76b000f08", + "secondsAgo": "30 min", + "secondsAgoLiquidation": "5 sec" + } + ] + }, + { + "id": "chainlinkOracle", + "type": "chainlink", + "sequencerFeed": "0x0000000000000000000000000000000000000000", + "settings": [ + { + "type": "double", + "quoteTokenId": "susde", + "intermediateTokenId": "usd", + "baseTokenId": "usde", + "quoteAggregatorV3": "0xFF3BC18cCBd5999CE63E788A1c250a88626aD099", + "baseAggregatorV3": "0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961", + "maxPriceAge": "1500 min" + }, + { + "type": "double", + "quoteTokenId": "susde", + "intermediateTokenId": "usd", + "baseTokenId": "usds", + "quoteAggregatorV3": "0xFF3BC18cCBd5999CE63E788A1c250a88626aD099", + "baseAggregatorV3": "0xfF30586cD0F29eD462364C7e81375FC0C71219b1", + "maxPriceAge": "1500 min" + } + ] + }, + { + "id": "pythOracle", + "type": "pyth", + "pyth": "0x4305FB66699C3B2702D4d05CF36551390A4c69C6", + "settings": [ + { + "type": "single", + "quoteTokenId": "usr", + "baseTokenId": "wstusr", + "pythPriceId": "0xb74c2bc175c2dab850ce5a5451608501c293fe8410cb4aba7449dd1c355ab706", + "maxPriceAge": "1440 min" + } + ] + }, + { + "id": "compositeOracle", + "type": "composite", + "settings": [ + { + "quoteTokenId": "usr", + "intermediateTokenId": "wstusr", + "baseTokenId": "pt-wstusr-25sep2025", + "quoteIntermediateOracleId": "pythOracle", + "intermediateBaseOracleId": "pendleMarketOracle" + }, + { + "quoteTokenId": "susde", + "intermediateTokenId": "usde", + "baseTokenId": "pt-usde-31jul2025", + "quoteIntermediateOracleId": "chainlinkOracle", + "intermediateBaseOracleId": "pendleMarketOracle" + }, + { + "quoteTokenId": "susde", + "intermediateTokenId": "usds", + "baseTokenId": "pt-usds-14aug2025", + "quoteIntermediateOracleId": "chainlinkOracle", + "intermediateBaseOracleId": "pendleMarketOracle" } ] } @@ -89,8 +199,8 @@ }, "marginlyPools": [ { - "id": "pt-susde-31jul2025-susde", - "baseTokenId": "pt-susde-31jul2025", + "id": "pt-susde-25sep2025-susde", + "baseTokenId": "pt-susde-25sep2025", "quoteTokenId": "susde", "priceOracleId": "pendleMarketOracle", "defaultSwapCallData": "20447233", @@ -99,23 +209,23 @@ "maxLeverage": "20", "swapFee": "0%", "fee": "0%", - "mcSlippage": "0.5%", + "mcSlippage": "0.1%", "positionMinAmount": "5", - "quoteLimit": "2000000" + "quoteLimit": "500000" } }, { - "id": "pt-susde-29may2025-susde", - "baseTokenId": "pt-susde-29may2025", - "quoteTokenId": "susde", - "priceOracleId": "pendleMarketOracle", - "defaultSwapCallData": "20447233", + "id": "pt-wstusr-25sep2025-usr", + "baseTokenId": "pt-wstusr-25sep2025", + "quoteTokenId": "usr", + "priceOracleId": "compositeOracle", + "defaultSwapCallData": "35127297", "params": { "interestRate": "0.6%", "maxLeverage": "20", "swapFee": "0%", "fee": "0%", - "mcSlippage": "0.5%", + "mcSlippage": "0.1%", "positionMinAmount": "5", "quoteLimit": "2000000" } @@ -123,34 +233,26 @@ ], "adapters": [ { - "dexId": 30, - "adapterName": "PendleCurveNgAdapter", + "dexId": 19, + "adapterName": "PendleMarketAdapter", "pools": [ { - "pendleMarket": "0xf4cf59259d007a96c641b41621ab52c93b9691b1", + "poolAddress": "0xa36b60a14a1a5247912584768c6e53e1a269a9f7", "slippage": "45", - "curveSlippage": "50", - "curvePool": "0xdb74dfdd3bb46be8ce6c33dc9d82777bcfc3ded5", - "ibTokenId": "weeth", - "quoteTokenId": "weth" + "tokenAId": "pt-susde-25sep2025", + "tokenBId": "susde" } ] }, { - "dexId": 19, - "adapterName": "PendleMarketAdapter", + "dexId": 33, + "adapterName": "PendlePtToAssetAdapter", "pools": [ { - "poolAddress": "0xb162b764044697cf03617c2efbcb1f42e31e4766", - "slippage": "45", - "tokenAId": "pt-susde-29may2025", - "tokenBId": "susde" - }, - { - "poolAddress": "0x4339ffe2b7592dc783ed13cce310531ab366deac", - "slippage": "45", - "tokenAId": "pt-susde-31jul2025", - "tokenBId": "susde" + "pendleMarket": "0x09fa04aac9c6d1c6131352ee950cd67ecc6d4fb9", + "slippage": 35, + "tokenAId": "pt-wstusr-25sep2025", + "tokenBId": "usr" } ] } diff --git a/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/config_30_05_2025.json b/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/config_30_05_2025.json new file mode 100644 index 00000000..e09f4b6e --- /dev/null +++ b/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/config_30_05_2025.json @@ -0,0 +1,378 @@ +{ + "systemContextDefaults": { + "ethNodeUri": "https://ethereum-rpc.publicnode.com" + }, + "connection": { + "assertChainId": 1, + "ethOptions": {} + }, + "tokens": [ + { + "id": "weth", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "assertSymbol": "WETH", + "assertDecimals": 18 + }, + { + "id": "susde", + "address": "0x9d39a5de30e57443bff2a8307a4256c8797a3497", + "assertSymbol": "sUSDe", + "assertDecimals": 18 + }, + { + "id": "usde", + "address": "0x4c9edd5852cd905f086c759e8383e09bff1e68b3", + "assertSymbol": "USDe", + "assertDecimals": 18 + }, + { + "id": "wsteth", + "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "assertSymbol": "wstETH", + "assertDecimals": 18 + }, + { + "id": "usds", + "address": "0xdc035d45d973e3ec169d2276ddab16f1e407384f", + "assertSymbol": "USDS", + "assertDecimals": 18 + }, + { + "id": "usr", + "address": "0x66a1E37c9b0eAddca17d3662D6c05F4DECf3e110", + "assertSymbol": "USR", + "assertDecimals": 18 + }, + { + "id": "wstusr", + "address": "0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055", + "assertSymbol": "wstUSR", + "assertDecimals": 18 + }, + { + "id": "pt-susde-25sep2025", + "address": "0x9f56094c450763769ba0ea9fe2876070c0fd5f77", + "assertSymbol": "PT-sUSDE-25SEP2025", + "assertDecimals": 18 + }, + { + "id": "pt-wstusr-25sep2025", + "address": "0x23e60d1488525bf4685f53b3aa8e676c30321066", + "assertSymbol": "PT-wstUSR-25SEP2025", + "assertDecimals": 18 + }, + { + "id": "pt-steth-25dec2025", + "address": "0xf99985822fb361117fcf3768d34a6353e6022f5f", + "assertSymbol": "PT-stETH-25DEC2025", + "assertDecimals": 18 + }, + { + "id": "pt-usde-31jul2025", + "address": "0x917459337caac939d41d7493b3999f571d20d667", + "assertSymbol": "PT-USDe-31JUL2025", + "assertDecimals": 18 + }, + { + "id": "pt-usds-14aug2025", + "address": "0xffec096c087c13cc268497b89a613cace4df9a48", + "assertSymbol": "PT-USDS-14AUG2025", + "assertDecimals": 18 + } + ], + "prices": [], + "priceOracles": [ + { + "id": "pendleMarketOracle", + "type": "pendleMarket", + "pendlePtLpOracle": "0x66a1096C6366b2529274dF4f5D8247827fe4CEA8", + "settings": [ + { + "quoteTokenId": "susde", + "baseTokenId": "pt-susde-25sep2025", + "pendleMarket": "0xa36b60a14a1a5247912584768c6e53e1a269a9f7", + "secondsAgo": "30 min", + "secondsAgoLiquidation": "5 sec" + }, + { + "quoteTokenId": "wstusr", + "baseTokenId": "pt-wstusr-25sep2025", + "pendleMarket": "0x09fa04aac9c6d1c6131352ee950cd67ecc6d4fb9", + "secondsAgo": "30 min", + "secondsAgoLiquidation": "5 sec" + }, + { + "quoteTokenId": "usde", + "baseTokenId": "pt-usde-31jul2025", + "pendleMarket": "0x9df192d13d61609d1852461c4850595e1f56e714", + "secondsAgo": "30 min", + "secondsAgoLiquidation": "5 sec" + }, + { + "quoteTokenId": "wsteth", + "baseTokenId": "pt-steth-25dec2025", + "pendleMarket": "0xc374f7ec85f8c7de3207a10bb1978ba104bda3b2", + "secondsAgo": "30 min", + "secondsAgoLiquidation": "5 sec" + }, + { + "quoteTokenId": "usds", + "baseTokenId": "pt-usds-14aug2025", + "pendleMarket": "0xdace1121e10500e9e29d071f01593fd76b000f08", + "secondsAgo": "30 min", + "secondsAgoLiquidation": "5 sec" + } + ] + }, + { + "id": "chainlinkOracle", + "type": "chainlink", + "sequencerFeed": "0x0000000000000000000000000000000000000000", + "settings": [ + { + "type": "double", + "quoteTokenId": "susde", + "intermediateTokenId": "usd", + "baseTokenId": "usde", + "quoteAggregatorV3": "0xFF3BC18cCBd5999CE63E788A1c250a88626aD099", + "baseAggregatorV3": "0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961", + "maxPriceAge": "1500 min" + }, + { + "type": "double", + "quoteTokenId": "susde", + "intermediateTokenId": "usd", + "baseTokenId": "usds", + "quoteAggregatorV3": "0xFF3BC18cCBd5999CE63E788A1c250a88626aD099", + "baseAggregatorV3": "0xfF30586cD0F29eD462364C7e81375FC0C71219b1", + "maxPriceAge": "1500 min" + } + ] + }, + { + "id": "pythOracle", + "type": "pyth", + "pyth": "0x4305FB66699C3B2702D4d05CF36551390A4c69C6", + "settings": [ + { + "type": "single", + "quoteTokenId": "usr", + "baseTokenId": "wstusr", + "pythPriceId": "0xb74c2bc175c2dab850ce5a5451608501c293fe8410cb4aba7449dd1c355ab706", + "maxPriceAge": "1440 min" + } + ] + }, + { + "id": "compositeOracle", + "type": "composite", + "settings": [ + { + "quoteTokenId": "usr", + "intermediateTokenId": "wstusr", + "baseTokenId": "pt-wstusr-25sep2025", + "quoteIntermediateOracleId": "pythOracle", + "intermediateBaseOracleId": "pendleMarketOracle" + }, + { + "quoteTokenId": "susde", + "intermediateTokenId": "usde", + "baseTokenId": "pt-usde-31jul2025", + "quoteIntermediateOracleId": "chainlinkOracle", + "intermediateBaseOracleId": "pendleMarketOracle" + }, + { + "quoteTokenId": "susde", + "intermediateTokenId": "usds", + "baseTokenId": "pt-usds-14aug2025", + "quoteIntermediateOracleId": "chainlinkOracle", + "intermediateBaseOracleId": "pendleMarketOracle" + } + ] + } + ], + "marginlyFactory": { + "feeHolder": "0x601A564628f9467ea76945fdDC6F9C7604eE1C1E", + "techPositionOwner": "0xd48658962b93aa404fD56baa7FD07977a0EB05a9", + "wethTokenId": "weth", + "timelockOwner": "0x8cDAf202eBe2f38488074DcFCa08c0B0cB7B8Aa5" + }, + "marginlyPools": [ + { + "id": "pt-susde-25sep2025-susde", + "baseTokenId": "pt-susde-25sep2025", + "quoteTokenId": "susde", + "priceOracleId": "pendleMarketOracle", + "defaultSwapCallData": "20447233", + "params": { + "interestRate": "0.6%", + "maxLeverage": "20", + "swapFee": "0%", + "fee": "0%", + "mcSlippage": "0.1%", + "positionMinAmount": "5", + "quoteLimit": "500000" + } + }, + { + "id": "pt-wstusr-25sep2025-usr", + "baseTokenId": "pt-wstusr-25sep2025", + "quoteTokenId": "usr", + "priceOracleId": "compositeOracle", + "defaultSwapCallData": "35127297", + "params": { + "interestRate": "0.6%", + "maxLeverage": "20", + "swapFee": "0%", + "fee": "0%", + "mcSlippage": "0.1%", + "positionMinAmount": "5", + "quoteLimit": "2000000" + } + } + ], + "adapters": [ + { + "dexId": 19, + "adapterName": "PendleMarketAdapter", + "pools": [ + { + "poolAddress": "0xa36b60a14a1a5247912584768c6e53e1a269a9f7", + "slippage": "45", + "tokenAId": "pt-susde-25sep2025", + "tokenBId": "susde" + } + ] + }, + { + "dexId": 33, + "adapterName": "PendlePtToAssetAdapter", + "pools": [ + { + "pendleMarket": "0x09fa04aac9c6d1c6131352ee950cd67ecc6d4fb9", + "slippage": 35, + "tokenAId": "pt-wstusr-25sep2025", + "tokenBId": "usr" + } + ] + }, + { + "dexId": 31, + "adapterName": "PendleCurveRouterNgAdapter", + "curveRouter": "0x45312ea0eFf7E09C83CBE249fa1d7598c4C8cd4e", + "pools": [ + { + "description": "sUSDE/PT-USDS", + "pendleMarket": "0xdace1121e10500e9e29d071f01593fd76b000f08", + "slippage": 35, + "curveDxAdjustTokenToPt": -103100, + "curveDxAdjustPtToToken": 116000, + "curveRoute": [ + "0xdc035d45d973e3ec169d2276ddab16f1e407384f", + "0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD", + "0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD", + "0x3CEf1AFC0E8324b57293a6E7cE663781bbEFBB79", + "0x9d39a5de30e57443bff2a8307a4256c8797a3497", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ], + "curveSwapParams": [ + [0, 1, 9, 0, 0], + [1, 0, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ], + "curvePools": [ + "0x0000000000000000000000000000000000000000", + "0x3CEf1AFC0E8324b57293a6E7cE663781bbEFBB79", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ] + }, + { + "description": "sUSDE/PT-USDE", + "pendleMarket": "0x9df192d13d61609d1852461c4850595e1f56e714", + "slippage": 35, + "curveDxAdjustTokenToPt": -145000, + "curveDxAdjustPtToToken": 175000, + "curveRoute": [ + "0x4c9edd5852cd905f086c759e8383e09bff1e68b3", + "0x5dc1BF6f1e983C0b21EfB003c105133736fA0743", + "0x853d955aCEf822Db058eb8505911ED77F175b99e", + "0xcE6431D21E3fb1036CE9973a3312368ED96F5CE7", + "0x83F20F44975D03b1b09e64809B757c47f942BEeA", + "0x167478921b907422F8E88B43C4Af2B8BEa278d3A", + "0x9d39a5de30e57443bff2a8307a4256c8797a3497", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ], + "curveSwapParams": [ + [1, 0, 1, 1, 2], + [0, 1, 1, 1, 2], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ], + "curvePools": [ + "0x5dc1BF6f1e983C0b21EfB003c105133736fA0743", + "0xcE6431D21E3fb1036CE9973a3312368ED96F5CE7", + "0x167478921b907422F8E88B43C4Af2B8BEa278d3A", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ] + }, + { + "description": "WETH / PT-stETH", + "pendleMarket": "0xc374f7ec85f8c7de3207a10bb1978ba104bda3b2", + "slippage": 35, + "curveDxAdjustTokenToPt": -900, + "curveDxAdjustPtToToken": 1000, + "curveRoute": [ + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022", + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ], + "curveSwapParams": [ + [1, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [1, 0, 8, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0] + ], + "curvePools": [ + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "0x5dc1BF6f1e983C0b21EfB003c105133736fA0743", + "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000" + ] + } + ] + } + ], + "marginlyKeeper": { + "aaveKeeper": { + "aavePoolAddressesProvider": "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e" + }, + "uniswapKeeper": true, + "balancerKeeper": { + "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + } + } +} diff --git a/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/deployment.json b/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/deployment.json index 572f616e..637f8b33 100644 --- a/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/deployment.json +++ b/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/deployment.json @@ -104,6 +104,10 @@ "id": "pt-rsweth-26dec2024-rsweth", "address": "0x6b79E799Ebdb9e867A1fe268A0703cbAA532C60d" }, + { + "id": "pt-susde-25sep2025-susde", + "address": "0x849A31f96b240a58a2A70168e904d7C70B654c18" + }, { "id": "pt-susde-29may2025-susde", "address": "0x6C651b633470Fe9CDe7C7b71865Cc7790c164c27" @@ -168,6 +172,10 @@ "id": "pt-wstusr-1750896030-wstusr", "address": "0xfb90799F0966728Ac79bec97F46B317d80605bA0" }, + { + "id": "pt-wstusr-25sep2025-usr", + "address": "0x11CCf4a4Db276eeAa4b13eDac4F945ce82bF2dA8" + }, { "id": "pt-wstusr-27mar2025-usr", "address": "0x97332917C5ABccDdaAB6597c638aA18a35f077f6" diff --git a/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/states/2025-02-28.json b/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/states/2025-02-28.json index 869f3907..55c8f033 100644 --- a/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/states/2025-02-28.json +++ b/packages/deploy/src/data/deploy/ethereum-v1.5-pendle/states/2025-02-28.json @@ -251,6 +251,18 @@ "marginlyPool_pt-susde-29may2025-susde": { "address": "0x6C651b633470Fe9CDe7C7b71865Cc7790c164c27", "txHash": "0x666cbdad7fbe0db9a03e273f7575ebf92662e68c9c5722d8e734e83033662377" + }, + "PendleCurveRouterNgAdapter_31": { + "address": "0xd6fE5fb548A6d8B6eD04B3cab3979f6fE98bAAeD", + "txHash": "0x0c3dee6f3055ba9c4e0931b57c27fa109abeb92cb116b93c474c0069bb81a25a" + }, + "marginlyPool_pt-susde-25sep2025-susde": { + "address": "0x849A31f96b240a58a2A70168e904d7C70B654c18", + "txHash": "0x6f497c2f30601083eee0d6ed497f24f758be76e4c5f30cbb55ee6eae39b3e483" + }, + "marginlyPool_pt-wstusr-25sep2025-usr": { + "address": "0x11CCf4a4Db276eeAa4b13eDac4F945ce82bF2dA8", + "txHash": "0x23b3a534ba69783c64b7b216c365c753e8233b6b317b18637f5213412e76193e" } } } \ No newline at end of file diff --git a/packages/deploy/src/deployer/MarginlyRouterDeployer.ts b/packages/deploy/src/deployer/MarginlyRouterDeployer.ts index 26b3e358..0db6908d 100644 --- a/packages/deploy/src/deployer/MarginlyRouterDeployer.ts +++ b/packages/deploy/src/deployer/MarginlyRouterDeployer.ts @@ -1,5 +1,5 @@ import { EthAddress } from '@marginly/common'; -import { BigNumber, Signer } from 'ethers'; +import { BigNumber, ethers, Signer } from 'ethers'; import { StateStore, readMarginlyAdapterContract, readMarginlyRouterContract } from '../common'; import { ITokenRepository, DeployResult } from '../common/interfaces'; import { BaseDeployer } from './BaseDeployer'; @@ -21,10 +21,13 @@ import { } from './configs'; import { EthOptions } from '../config'; import { Logger } from '../logger'; +import { createTimelockWhitelistContract } from './contract-reader'; export class MarginlyRouterDeployer extends BaseDeployer { + private readonly readTimeLockContract; public constructor(signer: Signer, ethArgs: EthOptions, stateStore: StateStore, logger: Logger) { super(signer, ethArgs, stateStore, logger); + this.readTimeLockContract = createTimelockWhitelistContract(); } public async deployMarginlyAdapter( @@ -102,7 +105,8 @@ export class MarginlyRouterDeployer extends BaseDeployer { return [ locConfig.pendleMarket.toString(), locConfig.slippage, - locConfig.curveSlippage, + locConfig.curveDxAdjustPtToToken, + locConfig.curveDxAdjustTokenToPt, locConfig.curveRoute.map((y) => y.toString()), locConfig.curveSwapParams, locConfig.curvePools.map((y) => y.toString()), @@ -158,7 +162,10 @@ export class MarginlyRouterDeployer extends BaseDeployer { console.log('\n\n'); } - public async deployMarginlyRouter(adapters: { dexId: BigNumber; adapter: EthAddress }[]): Promise { + public async deployMarginlyRouter( + adapters: { dexId: BigNumber; adapter: EthAddress }[], + timelockOwner?: EthAddress + ): Promise { const args = [adapters.map((x) => [x.dexId.toNumber(), x.adapter.toString()])]; const deployResult = await this.deploy('MarginlyRouter', args, 'MarginlyRouter', readMarginlyRouterContract); @@ -175,7 +182,29 @@ export class MarginlyRouterDeployer extends BaseDeployer { if (adaptersToAdd.length > 0) { const adaptersToAddArgs = adaptersToAdd.map((x) => [x.dexId.toNumber(), x.adapter.toString()]); - await router.connect(this.signer).addDexAdapters(adaptersToAddArgs); + if (timelockOwner) { + const timelockDescription = this.readTimeLockContract('TimelockWhitelist'); + + const addAdaptersCallData = router.interface.encodeFunctionData('addDexAdapters', [adaptersToAddArgs]); + const timelockContract = new ethers.Contract(timelockOwner.toString(), timelockDescription.abi, this.signer); + + const minDelay = await timelockContract.getMinDelay(); + const addAdapterTx = await timelockContract.schedule( + router.address, //target + 0, //value + addAdaptersCallData, //calldata + ethers.constants.HashZero, //predecessor + ethers.constants.HashZero, //salt + minDelay, + this.ethArgs //overrides + ); + + await addAdapterTx.wait(); + + this.logger.log(`Scheduled add ${adaptersToAdd.length} adapters to MarginlyRouter`); + } else { + await router.connect(this.signer).addDexAdapters(adaptersToAddArgs); + } this.logger.log(`Added ${adaptersToAdd.length} adapters to MarginlyRouter`); } diff --git a/packages/deploy/src/deployer/PriceOracleDeployer.ts b/packages/deploy/src/deployer/PriceOracleDeployer.ts index f742e717..a244b5b2 100644 --- a/packages/deploy/src/deployer/PriceOracleDeployer.ts +++ b/packages/deploy/src/deployer/PriceOracleDeployer.ts @@ -279,7 +279,7 @@ export class PriceOracleDeployer extends BaseDeployer { const pair = await priceOracle.getParams(quoteToken.toString(), baseToken.toString()); - if (BigNumber.from(pair.maxPriceAge) !== setting.maxPriceAge.toSeconds()) { + if (!BigNumber.from(pair.maxPriceAge).eq(setting.maxPriceAge.toSeconds())) { // oracle already initialized const tx = await priceOracle.setPair( diff --git a/packages/deploy/src/deployer/configs.ts b/packages/deploy/src/deployer/configs.ts index 389f3a57..38f244b9 100644 --- a/packages/deploy/src/deployer/configs.ts +++ b/packages/deploy/src/deployer/configs.ts @@ -212,7 +212,8 @@ export interface PendleCurveRouterAdapterParam { type: 'pendleCurveRouter'; pendleMarket: EthAddress; slippage: number; - curveSlippage: number; + curveDxAdjustTokenToPt: number; + curveDxAdjustPtToToken: number; curveRoute: EthAddress[]; // array of fixed length 11 curveSwapParams: number[][]; // array of fixed length 5 x 5 curvePools: EthAddress[]; // array of fixed length 5 @@ -753,7 +754,7 @@ export class StrictMarginlyDeployConfig { return this.createPendlePtToAssetAdapterParam(pair, tokens, dexId); } else if (adapterName === 'PendleCurveNgAdapter') { return this.createPendleCurveNgAdapterConfig(pair, tokens, dexId); - } else if (adapterName === 'PendleCurveRouterNg') { + } else if (adapterName === 'PendleCurveRouterNgAdapter') { return this.createPendleCurveRouterAdapterConfig(pair, tokens, dexId); } else if (adapterName === 'SpectraAdapter') { return this.createSpectraAdapterConfig(pair, tokens, dexId); @@ -917,7 +918,8 @@ export class StrictMarginlyDeployConfig { type: 'pendleCurveRouter', pendleMarket: EthAddress.parse(pairConfig.pendleMarket), slippage: pairConfig.slippage, - curveSlippage: pairConfig.curveSlippage, + curveDxAdjustPtToToken: pairConfig.curveDxAdjustPtToToken, + curveDxAdjustTokenToPt: pairConfig.curveDxAdjustTokenToPt, curveRoute: pairConfig.curveRoute.map(EthAddress.parse), curveSwapParams: pairConfig.curveSwapParams, curvePools: pairConfig.curvePools.map(EthAddress.parse), diff --git a/packages/deploy/src/index.ts b/packages/deploy/src/index.ts index cdc46613..76d2f201 100644 --- a/packages/deploy/src/index.ts +++ b/packages/deploy/src/index.ts @@ -172,7 +172,10 @@ async function processMarginlyRouter( }); } - const marginlyRouterDeployResult = await marginlyRouterDeployer.deployMarginlyRouter(adapterDeployResults); + const marginlyRouterDeployResult = await marginlyRouterDeployer.deployMarginlyRouter( + adapterDeployResults, + config.marginlyFactory.timelockOwner + ); printDeployState('Marginly router', marginlyRouterDeployResult, logger); return marginlyRouterDeployResult; @@ -407,5 +410,7 @@ export async function deployMarginly( const ethSpent = balanceBefore.sub(balanceAfter); logger.log(`ETH spent: ${ethers.utils.formatEther(ethSpent)}`); + const gasPrice = await provider.getGasPrice(); + logger.log(`gas price: ${ethers.utils.formatUnits(gasPrice, 'gwei')} gwei`); } } diff --git a/packages/router/contracts/adapters/PendleCurveRouterNgAdapter.sol b/packages/router/contracts/adapters/PendleCurveRouterNgAdapter.sol index 67f5a634..417d72ec 100644 --- a/packages/router/contracts/adapters/PendleCurveRouterNgAdapter.sol +++ b/packages/router/contracts/adapters/PendleCurveRouterNgAdapter.sol @@ -1,601 +1,591 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.19; -import '@openzeppelin/contracts/utils/math/Math.sol'; -import '@openzeppelin/contracts/access/Ownable2Step.sol'; -import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/utils/math/SignedMath.sol"; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import '@pendle/core-v2/contracts/router/base/MarketApproxLib.sol'; -import '@pendle/core-v2/contracts/interfaces/IPMarket.sol'; -import '@pendle/core-v2/contracts/core/StandardizedYield/PYIndex.sol'; +import "@pendle/core-v2/contracts/router/base/MarketApproxLib.sol"; +import "@pendle/core-v2/contracts/interfaces/IPMarket.sol"; +import "@pendle/core-v2/contracts/core/StandardizedYield/PYIndex.sol"; -import '../interfaces/IMarginlyAdapter.sol'; -import '../interfaces/IMarginlyRouter.sol'; -import './interfaces/ICurveRouterNg.sol'; +import "../interfaces/IMarginlyAdapter.sol"; +import "../interfaces/IMarginlyRouter.sol"; +import "./interfaces/ICurveRouterNg.sol"; /// @dev This adapter is using for swaps PT token (Principal token) to IB token (Interest bearing) in Pendle Market without trading pools contract PendleCurveRouterNgAdapter is IMarginlyAdapter, Ownable2Step { - using PYIndexLib for IPYieldToken; - - struct RouteData { - IPMarket pendleMarket; - IStandardizedYield sy; - IPPrincipalToken pt; - IPYieldToken yt; - uint8 slippage; - uint32 curveSlippage; - address ib; // interest bearing token - address[11] curveRoute; - uint256[5][5] curveSwapParams; - address[5] curvePools; - } - - struct RouteInput { - // address of pendle market, that holds info about pt, ib, sy tokens - address pendleMarket; - // slippage using in pendle approx swap - uint8 slippage; - uint32 curveSlippage; // by default 0.001%, 0.00001 - // Docs from https://github.com/curvefi/curve-router-ng/blob/master/contracts/Router.vy - // Array of [initial token, pool or zap, token, pool or zap, token, ...] - // The array is iterated until a pool address of 0x00, then the last - // given token is transferred to `_receiver` - address[11] curveRoute; - // Multidimensional array of [i, j, swap_type, pool_type, n_coins] where - // i is the index of input token - // j is the index of output token - - // The swap_type should be: - // 1. for `exchange`, - // 2. for `exchange_underlying`, - // 3. for underlying exchange via zap: factory stable metapools with lending base pool `exchange_underlying` - // and factory crypto-meta pools underlying exchange (`exchange` method in zap) - // 4. for coin -> LP token "exchange" (actually `add_liquidity`), - // 5. for lending pool underlying coin -> LP token "exchange" (actually `add_liquidity`), - // 6. for LP token -> coin "exchange" (actually `remove_liquidity_one_coin`) - // 7. for LP token -> lending or fake pool underlying coin "exchange" (actually `remove_liquidity_one_coin`) - // 8. for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH, USDe -> sUSDe - - // pool_type: 1 - stable, 2 - twocrypto, 3 - tricrypto, 4 - llamma - // 10 - stable-ng, 20 - twocrypto-ng, 30 - tricrypto-ng - - // n_coins is the number of coins in pool - uint256[5][5] curveSwapParams; - // Array of pools for swaps via zap contracts. This parameter is only needed for swap_type = 3. - address[5] curvePools; - } - - struct CallbackData { - address tokenIn; - address tokenOut; - address router; - bool isExactOutput; - bytes adapterCallbackData; - } - - uint256 private constant PENDLE_ONE = 1e18; - uint256 private constant EPSILON = 1e15; - uint256 private constant PENDLE_SLIPPAGE_ONE = 100; - uint256 private constant MAX_ITERATIONS = 10; - uint256 private constant CURVE_SLIPPAGE_ONE = 1e6; - - address public curveRouter; - - mapping(address => mapping(address => RouteData)) public getRouteData; - uint256 private callbackAmountIn; - - event NewPair(address indexed ptToken, address indexed quoteToken, address pendleMarket, uint8 slippage); - - error ApproximationFailed(); - error UnknownPair(); - error WrongInput(); - error ZeroAddress(); - - constructor(address _curveRouter, RouteInput[] memory routes) { - if (_curveRouter == address(0)) revert ZeroAddress(); - - curveRouter = _curveRouter; - _addPairs(routes); - } - - function addPairs(RouteInput[] calldata routes) external onlyOwner { - _addPairs(routes); - } - - /// @dev During swap Pt to exact SY before maturity a little amount of SY might stay at the adapter contract - function redeemDust(address token, address recipient) external onlyOwner { - SafeERC20.safeTransfer(IERC20(token), recipient, IERC20(token).balanceOf(address(this))); - } - - function swapExactInput( - address recipient, - address tokenIn, - address tokenOut, - uint256 amountIn, - uint256 minAmountOut, - bytes calldata data - ) external returns (uint256 amountOut) { - RouteData memory routeData = _getRouteDataSafe(tokenIn, tokenOut); - - if (routeData.pt.isExpired()) { - amountOut = _swapExactInputPostMaturity(routeData, recipient, tokenIn, amountIn, minAmountOut, data); - } else { - amountOut = _swapExactInputPreMaturity(routeData, recipient, tokenIn, tokenOut, amountIn, minAmountOut, data); + using PYIndexLib for IPYieldToken; + using SignedMath for int32; + + struct RouteData { + IPMarket pendleMarket; + IStandardizedYield sy; + IPPrincipalToken pt; + IPYieldToken yt; + uint8 slippage; + int32 curveDxAdjust; + address ib; // interest bearing token + address[11] curveRoute; + uint256[5][5] curveSwapParams; + address[5] curvePools; } - if (amountOut < minAmountOut) revert InsufficientAmount(); - } - - function swapExactOutput( - address recipient, - address tokenIn, - address tokenOut, - uint256 maxAmountIn, - uint256 amountOut, - bytes calldata data - ) external returns (uint256 amountIn) { - RouteData memory routeData = _getRouteDataSafe(tokenIn, tokenOut); - - if (routeData.yt.isExpired()) { - amountIn = _swapExactOutputPostMaturity(routeData, recipient, tokenIn, tokenOut, amountOut, data); - } else { - amountIn = _swapExactOutputPreMaturity(routeData, recipient, tokenIn, tokenOut, maxAmountIn, amountOut, data); + struct RouteInput { + // address of pendle market, that holds info about pt, ib, sy tokens + address pendleMarket; + // slippage using in pendle approx swap + uint8 slippage; + int32 curveDxAdjustPtToToken; // by default 0.001%, 0.00001 + int32 curveDxAdjustTokenToPt; + // Docs from https://github.com/curvefi/curve-router-ng/blob/master/contracts/Router.vy + // Array of [initial token, pool or zap, token, pool or zap, token, ...] + // The array is iterated until a pool address of 0x00, then the last + // given token is transferred to `_receiver` + address[11] curveRoute; + // Multidimensional array of [i, j, swap_type, pool_type, n_coins] where + // i is the index of input token + // j is the index of output token + + // The swap_type should be: + // 1. for `exchange`, + // 2. for `exchange_underlying`, + // 3. for underlying exchange via zap: factory stable metapools with lending base pool `exchange_underlying` + // and factory crypto-meta pools underlying exchange (`exchange` method in zap) + // 4. for coin -> LP token "exchange" (actually `add_liquidity`), + // 5. for lending pool underlying coin -> LP token "exchange" (actually `add_liquidity`), + // 6. for LP token -> coin "exchange" (actually `remove_liquidity_one_coin`) + // 7. for LP token -> lending or fake pool underlying coin "exchange" (actually `remove_liquidity_one_coin`) + // 8. for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH, USDe -> sUSDe + // 9. for ERC4626 asset <-> share + // + // pool_type: 1 - stable, 2 - twocrypto, 3 - tricrypto, 4 - llamma + // 10 - stable-ng, 20 - twocrypto-ng, 30 - tricrypto-ng + + // n_coins is the number of coins in pool + uint256[5][5] curveSwapParams; + // Array of pools for swaps via zap contracts. This parameter is only needed for swap_type = 3. + address[5] curvePools; } - if (amountIn > maxAmountIn) revert TooMuchRequested(); - } - - /// @dev Triggered by PendleMarket - function swapCallback(int256 ptToAccount, int256 syToAccount, bytes calldata _data) external { - require(ptToAccount > 0 || syToAccount > 0); - - CallbackData memory data = abi.decode(_data, (CallbackData)); - RouteData memory routeData = _getRouteDataSafe(data.tokenIn, data.tokenOut); - require(msg.sender == address(routeData.pendleMarket)); - - if (syToAccount > 0) { - // this clause is realized in case of both exactInput and exactOutput with pt tokens as input - // we need to send pt tokens from router-call initiator to finalize the swap - IMarginlyRouter(data.router).adapterCallback(msg.sender, uint256(-ptToAccount), data.adapterCallbackData); - } else { - // pt token is output token - // 1) swapExactInput - // quote token -> ib token in curve - // approx swap exact sy to pt in pendle - // callback: mint sy amount into pendle market - // - // 2) swapExactOutput - // swap sy for exact pt in pendle - // callback: in callback we know exact amount sy/ib and make swap quote -> ib in curve - // callback: mint sy amount into pendle market - uint256 ibAmount = uint256(-syToAccount); - if (data.isExactOutput) { - // estimate amount of quoteToken to get uint256(-syToAccount) - address _curveRouter = curveRouter; - uint256 estimatedQuoteAmount = ICurveRouterNg(_curveRouter).get_dx( - routeData.curveRoute, - routeData.curveSwapParams, - ibAmount, - routeData.curvePools - ); - estimatedQuoteAmount = - (estimatedQuoteAmount * (routeData.curveSlippage + CURVE_SLIPPAGE_ONE)) / - CURVE_SLIPPAGE_ONE; - - callbackAmountIn = estimatedQuoteAmount; - - IMarginlyRouter(data.router).adapterCallback(address(this), estimatedQuoteAmount, data.adapterCallbackData); - - SafeERC20.forceApprove(IERC20(data.tokenIn), _curveRouter, estimatedQuoteAmount); - ICurveRouterNg(_curveRouter).exchange( - routeData.curveRoute, - routeData.curveSwapParams, - estimatedQuoteAmount, - ibAmount, // min output ibAmount - routeData.curvePools, - address(this) - ); - } - _pendleMintSy(routeData, msg.sender, ibAmount); + struct CallbackData { + address tokenIn; + address tokenOut; + address router; + bool isExactOutput; + bytes adapterCallbackData; + } + + uint256 private constant PENDLE_ONE = 1e18; + uint256 private constant EPSILON = 1e15; + uint256 private constant PENDLE_SLIPPAGE_ONE = 100; + uint256 private constant MAX_ITERATIONS = 10; + int256 private constant CURVE_DX_ADJUST_ONE = 1e6; + + address public curveRouter; + + mapping(address => mapping(address => RouteData)) public getRouteData; + uint256 private callbackAmountIn; + + event NewPair(address indexed ptToken, address indexed quoteToken, address pendleMarket, uint8 slippage); + + error ApproximationFailed(); + error UnknownPair(); + error WrongInput(); + error ZeroAddress(); + + constructor(address _curveRouter, RouteInput[] memory routes) { + if (_curveRouter == address(0)) revert ZeroAddress(); + + curveRouter = _curveRouter; + _addPairs(routes); } - } - - function _getRouteDataSafe(address tokenA, address tokenB) private view returns (RouteData memory routeData) { - routeData = getRouteData[tokenA][tokenB]; - if (address(routeData.pendleMarket) == address(0)) revert UnknownPair(); - } - - function _swapExactInputPreMaturity( - RouteData memory routeData, - address recipient, - address tokenIn, - address tokenOut, - uint256 amountIn, - uint256 minAmountOut, - bytes calldata data - ) private returns (uint256 amountOut) { - address _curveRouter = curveRouter; - if (tokenIn == address(routeData.pt)) { - // swap pt -> sy in pendle - IMarginlyRouter(msg.sender).adapterCallback(address(routeData.pendleMarket), amountIn, data); - (uint256 syAmountIn, ) = routeData.pendleMarket.swapExactPtForSy(address(this), amountIn, new bytes(0)); - - // unwrap sy to ib to address(this) - uint256 ibAmountIn = _pendleRedeemSy(routeData, address(this), syAmountIn); - - // approve router to spend ib from adapter - SafeERC20.forceApprove(IERC20(routeData.ib), _curveRouter, ibAmountIn); - - // swap ib -> quote token in curveRouter - amountOut = ICurveRouterNg(_curveRouter).exchange( - routeData.curveRoute, - routeData.curveSwapParams, - ibAmountIn, - minAmountOut, - routeData.curvePools, - recipient - ); - } else { - // transfer quote token into adapter - IMarginlyRouter(msg.sender).adapterCallback(address(this), amountIn, data); - SafeERC20.forceApprove(IERC20(tokenIn), _curveRouter, amountIn); - // swap quote token -> ib - uint256 ibAmount = ICurveRouterNg(_curveRouter).exchange( - routeData.curveRoute, - routeData.curveSwapParams, - amountIn, // quoteToken amount In - 0, // unknown minAmountOut of ib - routeData.curvePools, - address(this) - ); - // wrap ib to sy (in swapCallback from pendle) - // swap sy to pt in pendle, pt to recipient - - // tokenIn ib to sy wrap (in swap callback) -> sy to pendle -> pt to recipient - CallbackData memory swapCallbackData = CallbackData({ - tokenIn: tokenIn, - tokenOut: tokenOut, - router: msg.sender, - isExactOutput: false, - adapterCallbackData: data - }); - amountOut = _pendleApproxSwapExactSyForPt( - routeData, - recipient, - ibAmount, - minAmountOut, - abi.encode(swapCallbackData) - ); + + function addPairs(RouteInput[] calldata routes) external onlyOwner { + _addPairs(routes); + } + + /// @dev During swap Pt to exact SY before maturity a little amount of SY might stay at the adapter contract + function redeemDust(address token, address recipient) external onlyOwner { + SafeERC20.safeTransfer(IERC20(token), recipient, IERC20(token).balanceOf(address(this))); + } + + function swapExactInput( + address recipient, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + bytes calldata data + ) external returns (uint256 amountOut) { + RouteData memory routeData = _getRouteDataSafe(tokenIn, tokenOut); + + if (routeData.pt.isExpired()) { + amountOut = _swapExactInputPostMaturity(routeData, recipient, tokenIn, amountIn, minAmountOut, data); + } else { + amountOut = + _swapExactInputPreMaturity(routeData, recipient, tokenIn, tokenOut, amountIn, minAmountOut, data); + } + + if (amountOut < minAmountOut) revert InsufficientAmount(); + } + + function swapExactOutput( + address recipient, + address tokenIn, + address tokenOut, + uint256 maxAmountIn, + uint256 amountOut, + bytes calldata data + ) external returns (uint256 amountIn) { + RouteData memory routeData = _getRouteDataSafe(tokenIn, tokenOut); + + if (routeData.yt.isExpired()) { + amountIn = _swapExactOutputPostMaturity(routeData, recipient, tokenIn, tokenOut, amountOut, data); + } else { + amountIn = + _swapExactOutputPreMaturity(routeData, recipient, tokenIn, tokenOut, maxAmountIn, amountOut, data); + } + + if (amountIn > maxAmountIn) revert TooMuchRequested(); + } + + /// @dev Triggered by PendleMarket + function swapCallback(int256 ptToAccount, int256 syToAccount, bytes calldata _data) external { + require(ptToAccount > 0 || syToAccount > 0); + + CallbackData memory data = abi.decode(_data, (CallbackData)); + RouteData memory routeData = _getRouteDataSafe(data.tokenIn, data.tokenOut); + require(msg.sender == address(routeData.pendleMarket)); + + if (syToAccount > 0) { + // this clause is realized in case of both exactInput and exactOutput with pt tokens as input + // we need to send pt tokens from router-call initiator to finalize the swap + IMarginlyRouter(data.router).adapterCallback(msg.sender, uint256(-ptToAccount), data.adapterCallbackData); + } else { + // pt token is output token + // 1) swapExactInput + // quote token -> ib token in curve + // approx swap exact sy to pt in pendle + // callback: mint sy amount into pendle market + // + // 2) swapExactOutput + // swap sy for exact pt in pendle + // callback: in callback we know exact amount sy/ib and make swap quote -> ib in curve + // callback: mint sy amount into pendle market + uint256 ibAmount = uint256(-syToAccount); + if (data.isExactOutput) { + // estimate amount of quoteToken to get uint256(-syToAccount) + address _curveRouter = curveRouter; + uint256 estimatedQuoteAmount = ICurveRouterNg(_curveRouter).get_dx( + routeData.curveRoute, routeData.curveSwapParams, ibAmount, routeData.curvePools + ); + estimatedQuoteAmount = (estimatedQuoteAmount * uint256((CURVE_DX_ADJUST_ONE + routeData.curveDxAdjust))) + / uint256(CURVE_DX_ADJUST_ONE); + + callbackAmountIn = estimatedQuoteAmount; + + IMarginlyRouter(data.router).adapterCallback( + address(this), estimatedQuoteAmount, data.adapterCallbackData + ); + + SafeERC20.forceApprove(IERC20(data.tokenIn), _curveRouter, estimatedQuoteAmount); + ICurveRouterNg(_curveRouter).exchange( + routeData.curveRoute, + routeData.curveSwapParams, + estimatedQuoteAmount, + ibAmount, // min output ibAmount + routeData.curvePools, + address(this) + ); + } + _pendleMintSy(routeData, msg.sender, ibAmount); + } + } + + function _getRouteDataSafe(address tokenA, address tokenB) private view returns (RouteData memory routeData) { + routeData = getRouteData[tokenA][tokenB]; + if (address(routeData.pendleMarket) == address(0)) revert UnknownPair(); } - } - - function _swapExactOutputPreMaturity( - RouteData memory routeData, - address recipient, - address tokenIn, - address tokenOut, - uint256 maxAmountIn, - uint256 amountOut, - bytes calldata data - ) private returns (uint256 amountIn) { - CallbackData memory swapCallbackData = CallbackData({ - tokenIn: tokenIn, - tokenOut: tokenOut, - router: msg.sender, - isExactOutput: true, - adapterCallbackData: data - }); - address _curveRouter = curveRouter; - - if (tokenIn == address(routeData.pt)) { - // estimate ibIn to get quoteToken amountOut in curve - uint256 estimatedIbAmount = ICurveRouterNg(_curveRouter).get_dx( - routeData.curveRoute, - routeData.curveSwapParams, - amountOut, //quoteToken amount - routeData.curvePools - ); - estimatedIbAmount = (estimatedIbAmount * (routeData.curveSlippage + CURVE_SLIPPAGE_ONE)) / CURVE_SLIPPAGE_ONE; - - // approx Pt to Sy -> in callback send Pt to PendleMarket - // then unwrap Sy to Ib and send to recipient - (, uint256 ptAmountIn) = _pendleApproxSwapPtForExactSy( - routeData, - address(this), - estimatedIbAmount, - maxAmountIn, - abi.encode(swapCallbackData) - ); - amountIn = ptAmountIn; - - // use amountOut here, because actualSyAmountOut a little bit more than amountOut - uint256 ibRedeemed = _pendleRedeemSy(routeData, address(this), estimatedIbAmount); - - SafeERC20.forceApprove(IERC20(routeData.ib), _curveRouter, ibRedeemed); - - // swap ib to quote token - ICurveRouterNg(_curveRouter).exchange( - routeData.curveRoute, - routeData.curveSwapParams, - ibRedeemed, - amountOut, - routeData.curvePools, - address(this) - ); - - // transfer amountOut to recipient, delta = actualAmountOut - amountOut stays in adapter contract balance - SafeERC20.safeTransfer(IERC20(tokenOut), recipient, amountOut); - } else { - // Sy to Pt -> in callback unwrap Sy to Ib and send to pendle market - // in callback: - // estimate amount of quote to get ib in curve - // exchange quote -> ib in curve - routeData.pendleMarket.swapSyForExactPt(recipient, amountOut, abi.encode(swapCallbackData)); - amountIn = _getAmountIn(); + + function _swapExactInputPreMaturity( + RouteData memory routeData, + address recipient, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + bytes calldata data + ) private returns (uint256 amountOut) { + address _curveRouter = curveRouter; + if (tokenIn == address(routeData.pt)) { + // swap pt -> sy in pendle + IMarginlyRouter(msg.sender).adapterCallback(address(routeData.pendleMarket), amountIn, data); + (uint256 syAmountIn,) = routeData.pendleMarket.swapExactPtForSy(address(this), amountIn, new bytes(0)); + + // unwrap sy to ib to address(this) + uint256 ibAmountIn = _pendleRedeemSy(routeData, address(this), syAmountIn); + + // approve router to spend ib from adapter + SafeERC20.forceApprove(IERC20(routeData.ib), _curveRouter, ibAmountIn); + + // swap ib -> quote token in curveRouter + amountOut = ICurveRouterNg(_curveRouter).exchange( + routeData.curveRoute, + routeData.curveSwapParams, + ibAmountIn, + minAmountOut, + routeData.curvePools, + recipient + ); + } else { + // transfer quote token into adapter + IMarginlyRouter(msg.sender).adapterCallback(address(this), amountIn, data); + SafeERC20.forceApprove(IERC20(tokenIn), _curveRouter, amountIn); + // swap quote token -> ib + uint256 ibAmount = ICurveRouterNg(_curveRouter).exchange( + routeData.curveRoute, + routeData.curveSwapParams, + amountIn, // quoteToken amount In + 0, // unknown minAmountOut of ib + routeData.curvePools, + address(this) + ); + // wrap ib to sy (in swapCallback from pendle) + // swap sy to pt in pendle, pt to recipient + + // tokenIn ib to sy wrap (in swap callback) -> sy to pendle -> pt to recipient + CallbackData memory swapCallbackData = CallbackData({ + tokenIn: tokenIn, + tokenOut: tokenOut, + router: msg.sender, + isExactOutput: false, + adapterCallbackData: data + }); + amountOut = _pendleApproxSwapExactSyForPt( + routeData, recipient, ibAmount, minAmountOut, abi.encode(swapCallbackData) + ); + } } - } - - function _swapExactInputPostMaturity( - RouteData memory routeData, - address recipient, - address tokenIn, - uint256 amountIn, - uint256 minAmountOut, - bytes calldata data - ) private returns (uint256 amountOut) { - if (tokenIn == address(routeData.pt)) { - // pt redeem -> sy -> unwrap sy to ib - uint256 syRedeemed = _redeemPY(routeData.yt, msg.sender, amountIn, data); - uint256 ibAmount = _pendleRedeemSy(routeData, address(this), syRedeemed); - - address _curveRouter = curveRouter; - - // approve to curve router - SafeERC20.forceApprove(IERC20(routeData.ib), _curveRouter, ibAmount); - - // ib to quote in curve - amountOut = ICurveRouterNg(_curveRouter).exchange( - routeData.curveRoute, - routeData.curveSwapParams, - ibAmount, - minAmountOut, - routeData.curvePools, - recipient - ); - } else { - // quote to pt swap is not possible after maturity - revert NotSupported(); + + function _swapExactOutputPreMaturity( + RouteData memory routeData, + address recipient, + address tokenIn, + address tokenOut, + uint256 maxAmountIn, + uint256 amountOut, + bytes calldata data + ) private returns (uint256 amountIn) { + CallbackData memory swapCallbackData = CallbackData({ + tokenIn: tokenIn, + tokenOut: tokenOut, + router: msg.sender, + isExactOutput: true, + adapterCallbackData: data + }); + address _curveRouter = curveRouter; + + if (tokenIn == address(routeData.pt)) { + // estimate ibIn to get quoteToken amountOut in curve + uint256 estimatedIbAmount = ICurveRouterNg(_curveRouter).get_dx( + routeData.curveRoute, + routeData.curveSwapParams, + amountOut, //quoteToken amount + routeData.curvePools + ); + estimatedIbAmount = (estimatedIbAmount * uint256((CURVE_DX_ADJUST_ONE + routeData.curveDxAdjust))) + / uint256(CURVE_DX_ADJUST_ONE); + + // approx Pt to Sy -> in callback send Pt to PendleMarket + // then unwrap Sy to Ib and send to recipient + (, uint256 ptAmountIn) = _pendleApproxSwapPtForExactSy( + routeData, address(this), estimatedIbAmount, maxAmountIn, abi.encode(swapCallbackData) + ); + amountIn = ptAmountIn; + + // use amountOut here, because actualSyAmountOut a little bit more than amountOut + uint256 ibRedeemed = _pendleRedeemSy(routeData, address(this), estimatedIbAmount); + + SafeERC20.forceApprove(IERC20(routeData.ib), _curveRouter, ibRedeemed); + + // swap ib to quote token + ICurveRouterNg(_curveRouter).exchange( + routeData.curveRoute, + routeData.curveSwapParams, + ibRedeemed, + amountOut, + routeData.curvePools, + address(this) + ); + + // transfer amountOut to recipient, delta = actualAmountOut - amountOut stays in adapter contract balance + SafeERC20.safeTransfer(IERC20(tokenOut), recipient, amountOut); + } else { + // Sy to Pt -> in callback unwrap Sy to Ib and send to pendle market + // in callback: + // estimate amount of quote to get ib in curve + // exchange quote -> ib in curve + routeData.pendleMarket.swapSyForExactPt(recipient, amountOut, abi.encode(swapCallbackData)); + amountIn = _getAmountIn(); + } } - } - - function _swapExactOutputPostMaturity( - RouteData memory routeData, - address recipient, - address tokenIn, - address tokenOut, - uint256 amountOut, - bytes calldata data - ) private returns (uint256 amountIn) { - if (tokenIn == address(routeData.pt)) { - address _curveRouter = curveRouter; - - // estimate on curve ibAmount to get amountOut - uint256 estimatedIbAmount = ICurveRouterNg(_curveRouter).get_dx( - routeData.curveRoute, - routeData.curveSwapParams, - amountOut, // quoteAmountOut - routeData.curvePools - ); - - // here we use a little more than estimationValue - estimatedIbAmount = (estimatedIbAmount * (CURVE_SLIPPAGE_ONE + routeData.curveSlippage)) / CURVE_SLIPPAGE_ONE; - - // calculate pt amountIn - // https://github.com/pendle-finance/pendle-core-v2-public/blob/bc27b10c33ac16d6e1936a9ddd24d536b00c96a4/contracts/core/YieldContractsV2/PendleYieldTokenV2.sol#L301 - amountIn = Math.mulDiv(estimatedIbAmount, routeData.yt.pyIndexCurrent(), PENDLE_ONE, Math.Rounding.Up); - - uint256 ibRedeemed = _pendleRedeemSy( - routeData, - address(this), - _redeemPY(routeData.yt, msg.sender, amountIn, data) // syRedeemed - ); - - SafeERC20.forceApprove(IERC20(routeData.ib), _curveRouter, ibRedeemed); - - // exchange ib to quoteToken in curve - ICurveRouterNg(_curveRouter).exchange( - routeData.curveRoute, - routeData.curveSwapParams, - ibRedeemed, - amountOut, - routeData.curvePools, - address(this) - ); - // delta actualAmountOut - amountOut stays in adapter contract, because router has strict check of amountOut - // transfer to recipient exact amountOut - SafeERC20.safeTransfer(IERC20(tokenOut), recipient, amountOut); - } else { - // sy to pt swap is not possible after maturity - revert NotSupported(); + + function _swapExactInputPostMaturity( + RouteData memory routeData, + address recipient, + address tokenIn, + uint256 amountIn, + uint256 minAmountOut, + bytes calldata data + ) private returns (uint256 amountOut) { + if (tokenIn == address(routeData.pt)) { + // pt redeem -> sy -> unwrap sy to ib + uint256 syRedeemed = _redeemPY(routeData.yt, msg.sender, amountIn, data); + uint256 ibAmount = _pendleRedeemSy(routeData, address(this), syRedeemed); + + address _curveRouter = curveRouter; + + // approve to curve router + SafeERC20.forceApprove(IERC20(routeData.ib), _curveRouter, ibAmount); + + // ib to quote in curve + amountOut = ICurveRouterNg(_curveRouter).exchange( + routeData.curveRoute, routeData.curveSwapParams, ibAmount, minAmountOut, routeData.curvePools, recipient + ); + } else { + // quote to pt swap is not possible after maturity + revert NotSupported(); + } } - } - - function _pendleApproxSwapExactSyForPt( - RouteData memory routeData, - address recipient, - uint256 syAmountIn, - uint256 minPtAmountOut, - bytes memory data - ) private returns (uint256 ptAmountOut) { - uint8 slippage = routeData.slippage; - ApproxParams memory approx = ApproxParams({ - guessMin: minPtAmountOut, - guessMax: (minPtAmountOut * (PENDLE_SLIPPAGE_ONE + slippage)) / PENDLE_SLIPPAGE_ONE, - guessOffchain: 0, - maxIteration: MAX_ITERATIONS, - eps: EPSILON - }); - - (ptAmountOut, ) = MarketApproxPtOutLib.approxSwapExactSyForPt( - routeData.pendleMarket.readState(address(this)), - routeData.yt.newIndex(), - syAmountIn, - block.timestamp, - approx - ); - (uint256 actualSyAmountIn, ) = routeData.pendleMarket.swapSyForExactPt(recipient, ptAmountOut, data); - if (actualSyAmountIn > syAmountIn) revert ApproximationFailed(); - } - - function _pendleApproxSwapPtForExactSy( - RouteData memory routeData, - address recipient, - uint256 syAmountOut, - uint256 maxPtAmountIn, - bytes memory data - ) private returns (uint256 actualSyAmountOut, uint256 actualPtAmountIn) { - uint8 slippage = routeData.slippage; - ApproxParams memory approx = ApproxParams({ - guessMin: (maxPtAmountIn * (PENDLE_SLIPPAGE_ONE - slippage)) / PENDLE_SLIPPAGE_ONE, - guessMax: maxPtAmountIn, - guessOffchain: 0, - maxIteration: MAX_ITERATIONS, - eps: EPSILON - }); - - (actualPtAmountIn, , ) = MarketApproxPtInLib.approxSwapPtForExactSy( - routeData.pendleMarket.readState(address(this)), - routeData.yt.newIndex(), - syAmountOut, - block.timestamp, - approx - ); - if (actualPtAmountIn > maxPtAmountIn) revert ApproximationFailed(); - - (actualSyAmountOut, ) = routeData.pendleMarket.swapExactPtForSy(recipient, actualPtAmountIn, data); - if (actualSyAmountOut < syAmountOut) revert ApproximationFailed(); - } - - function _pendleMintSy( - RouteData memory routeData, - address recipient, - uint256 ibIn - ) private returns (uint256 syMinted) { - SafeERC20.forceApprove(IERC20(routeData.ib), address(routeData.sy), ibIn); - // setting `minSyOut` value as ibIn (1:1 swap) - syMinted = routeData.sy.deposit(recipient, routeData.ib, ibIn, ibIn); - } - - function _pendleRedeemSy( - RouteData memory routeData, - address recipient, - uint256 syIn - ) private returns (uint256 ibRedeemed) { - // setting `minTokenOut` value as syIn (1:1 swap) - ibRedeemed = routeData.sy.redeem(recipient, syIn, routeData.ib, syIn, false); - } - - function _redeemPY( - IPYieldToken yt, - address router, - uint256 ptAmount, - bytes memory adapterCallbackData - ) private returns (uint256 syRedeemed) { - IMarginlyRouter(router).adapterCallback(address(yt), ptAmount, adapterCallbackData); - syRedeemed = yt.redeemPY(address(this)); - } - - function _getAmountIn() private returns (uint256 amountIn) { - amountIn = callbackAmountIn; - delete callbackAmountIn; - } - - function _addPairs(RouteInput[] memory routes) private { - RouteInput memory input; - uint256 length = routes.length; - for (uint256 i; i < length; ) { - input = routes[i]; - - if (input.pendleMarket == address(0)) revert WrongInput(); - if (input.slippage >= PENDLE_ONE) revert WrongInput(); - if (input.curveSlippage >= CURVE_SLIPPAGE_ONE) revert WrongInput(); - - address ibToken = input.curveRoute[0]; - - // prepare inverted route for swap quoteToken -> ..curve.. -> ibToken -> ptToken - address[11] memory invertedCurveRoute; - uint256 index = 0; - for (uint256 j = 10; ; --j) { - if (input.curveRoute[j] == address(0)) continue; - - invertedCurveRoute[index] = input.curveRoute[j]; - - ++index; - if (j == 0) break; - } - - address quoteToken = invertedCurveRoute[0]; - - // prepare inverted curveSwapParams and invertedCurvePools - uint256[5][5] memory invertedCurveSwapParams; - address[5] memory invertedCurvePools; - index = 0; - for (uint256 j = 4; ; --j) { - if (input.curveSwapParams[j][0] == 0 && input.curveSwapParams[j][1] == 0) { - continue; // empty element + + function _swapExactOutputPostMaturity( + RouteData memory routeData, + address recipient, + address tokenIn, + address tokenOut, + uint256 amountOut, + bytes calldata data + ) private returns (uint256 amountIn) { + if (tokenIn == address(routeData.pt)) { + address _curveRouter = curveRouter; + + // estimate on curve ibAmount to get amountOut + uint256 estimatedIbAmount = ICurveRouterNg(_curveRouter).get_dx( + routeData.curveRoute, + routeData.curveSwapParams, + amountOut, // quoteAmountOut + routeData.curvePools + ); + + // here we use a little more than estimationValue + estimatedIbAmount = (estimatedIbAmount * uint256((CURVE_DX_ADJUST_ONE + routeData.curveDxAdjust))) + / uint256(CURVE_DX_ADJUST_ONE); + + // calculate pt amountIn + // https://github.com/pendle-finance/pendle-core-v2-public/blob/bc27b10c33ac16d6e1936a9ddd24d536b00c96a4/contracts/core/YieldContractsV2/PendleYieldTokenV2.sol#L301 + amountIn = Math.mulDiv(estimatedIbAmount, routeData.yt.pyIndexCurrent(), PENDLE_ONE, Math.Rounding.Up); + + uint256 ibRedeemed = _pendleRedeemSy( + routeData, + address(this), + _redeemPY(routeData.yt, msg.sender, amountIn, data) // syRedeemed + ); + + SafeERC20.forceApprove(IERC20(routeData.ib), _curveRouter, ibRedeemed); + + // exchange ib to quoteToken in curve + ICurveRouterNg(_curveRouter).exchange( + routeData.curveRoute, + routeData.curveSwapParams, + ibRedeemed, + amountOut, + routeData.curvePools, + address(this) + ); + // delta actualAmountOut - amountOut stays in adapter contract, because router has strict check of amountOut + // transfer to recipient exact amountOut + SafeERC20.safeTransfer(IERC20(tokenOut), recipient, amountOut); + } else { + // sy to pt swap is not possible after maturity + revert NotSupported(); } + } - invertedCurveSwapParams[index][0] = input.curveSwapParams[j][1]; - invertedCurveSwapParams[index][1] = input.curveSwapParams[j][0]; - invertedCurveSwapParams[index][2] = input.curveSwapParams[j][2]; - invertedCurveSwapParams[index][3] = input.curveSwapParams[j][3]; - invertedCurveSwapParams[index][4] = input.curveSwapParams[j][4]; - - invertedCurvePools[index] = input.curvePools[j]; - - ++index; - if (j == 0) break; - } - - (IStandardizedYield sy, IPPrincipalToken pt, IPYieldToken yt) = IPMarket(input.pendleMarket).readTokens(); - if (!sy.isValidTokenIn(ibToken)) revert WrongInput(); - if (!sy.isValidTokenOut(ibToken)) revert WrongInput(); - - { - RouteData memory ptToQuoteSwapRoute = RouteData({ - pendleMarket: IPMarket(input.pendleMarket), - ib: ibToken, - sy: sy, - pt: pt, - yt: yt, - slippage: input.slippage, - curveSlippage: input.curveSlippage, - curveRoute: input.curveRoute, - curveSwapParams: input.curveSwapParams, - curvePools: input.curvePools + function _pendleApproxSwapExactSyForPt( + RouteData memory routeData, + address recipient, + uint256 syAmountIn, + uint256 minPtAmountOut, + bytes memory data + ) private returns (uint256 ptAmountOut) { + uint8 slippage = routeData.slippage; + ApproxParams memory approx = ApproxParams({ + guessMin: minPtAmountOut, + guessMax: (minPtAmountOut * (PENDLE_SLIPPAGE_ONE + slippage)) / PENDLE_SLIPPAGE_ONE, + guessOffchain: 0, + maxIteration: MAX_ITERATIONS, + eps: EPSILON }); - RouteData memory quoteToPtSwapRoute = RouteData({ - pendleMarket: IPMarket(input.pendleMarket), - ib: ibToken, - sy: sy, - pt: pt, - yt: yt, - slippage: input.slippage, - curveSlippage: input.curveSlippage, - curveRoute: invertedCurveRoute, - curveSwapParams: invertedCurveSwapParams, - curvePools: invertedCurvePools + + (ptAmountOut,) = MarketApproxPtOutLib.approxSwapExactSyForPt( + routeData.pendleMarket.readState(address(this)), + routeData.yt.newIndex(), + syAmountIn, + block.timestamp, + approx + ); + (uint256 actualSyAmountIn,) = routeData.pendleMarket.swapSyForExactPt(recipient, ptAmountOut, data); + if (actualSyAmountIn > syAmountIn) revert ApproximationFailed(); + } + + function _pendleApproxSwapPtForExactSy( + RouteData memory routeData, + address recipient, + uint256 syAmountOut, + uint256 maxPtAmountIn, + bytes memory data + ) private returns (uint256 actualSyAmountOut, uint256 actualPtAmountIn) { + uint8 slippage = routeData.slippage; + ApproxParams memory approx = ApproxParams({ + guessMin: (maxPtAmountIn * (PENDLE_SLIPPAGE_ONE - slippage)) / PENDLE_SLIPPAGE_ONE, + guessMax: maxPtAmountIn, + guessOffchain: 0, + maxIteration: MAX_ITERATIONS, + eps: EPSILON }); - getRouteData[address(pt)][quoteToken] = ptToQuoteSwapRoute; - getRouteData[quoteToken][address(pt)] = quoteToPtSwapRoute; - } + (actualPtAmountIn,,) = MarketApproxPtInLib.approxSwapPtForExactSy( + routeData.pendleMarket.readState(address(this)), + routeData.yt.newIndex(), + syAmountOut, + block.timestamp, + approx + ); + if (actualPtAmountIn > maxPtAmountIn) revert ApproximationFailed(); - emit NewPair(address(pt), quoteToken, input.pendleMarket, input.slippage); + (actualSyAmountOut,) = routeData.pendleMarket.swapExactPtForSy(recipient, actualPtAmountIn, data); + if (actualSyAmountOut < syAmountOut) revert ApproximationFailed(); + } - unchecked { - ++i; - } + function _pendleMintSy(RouteData memory routeData, address recipient, uint256 ibIn) + private + returns (uint256 syMinted) + { + SafeERC20.forceApprove(IERC20(routeData.ib), address(routeData.sy), ibIn); + // setting `minSyOut` value as ibIn (1:1 swap) + syMinted = routeData.sy.deposit(recipient, routeData.ib, ibIn, ibIn); + } + + function _pendleRedeemSy(RouteData memory routeData, address recipient, uint256 syIn) + private + returns (uint256 ibRedeemed) + { + // setting `minTokenOut` value as syIn (1:1 swap) + ibRedeemed = routeData.sy.redeem(recipient, syIn, routeData.ib, syIn, false); + } + + function _redeemPY(IPYieldToken yt, address router, uint256 ptAmount, bytes memory adapterCallbackData) + private + returns (uint256 syRedeemed) + { + IMarginlyRouter(router).adapterCallback(address(yt), ptAmount, adapterCallbackData); + syRedeemed = yt.redeemPY(address(this)); + } + + function _getAmountIn() private returns (uint256 amountIn) { + amountIn = callbackAmountIn; + delete callbackAmountIn; + } + + function _addPairs(RouteInput[] memory routes) private { + RouteInput memory input; + uint256 length = routes.length; + for (uint256 i; i < length;) { + input = routes[i]; + + if (input.pendleMarket == address(0)) revert WrongInput(); + if (input.slippage >= PENDLE_ONE) revert WrongInput(); + if (input.curveDxAdjustPtToToken.abs() >= uint256(CURVE_DX_ADJUST_ONE)) revert WrongInput(); + if (input.curveDxAdjustTokenToPt.abs() >= uint256(CURVE_DX_ADJUST_ONE)) revert WrongInput(); + + address ibToken = input.curveRoute[0]; + + // prepare inverted route for swap quoteToken -> ..curve.. -> ibToken -> ptToken + address[11] memory invertedCurveRoute; + uint256 index = 0; + for (uint256 j = 10;; --j) { + if (input.curveRoute[j] == address(0)) continue; + + invertedCurveRoute[index] = input.curveRoute[j]; + + ++index; + if (j == 0) break; + } + + address quoteToken = invertedCurveRoute[0]; + + // prepare inverted curveSwapParams and invertedCurvePools + uint256[5][5] memory invertedCurveSwapParams; + address[5] memory invertedCurvePools; + index = 0; + for (uint256 j = 4;; --j) { + if (input.curveSwapParams[j][0] == 0 && input.curveSwapParams[j][1] == 0) { + continue; // empty element + } + + invertedCurveSwapParams[index][0] = input.curveSwapParams[j][1]; + invertedCurveSwapParams[index][1] = input.curveSwapParams[j][0]; + invertedCurveSwapParams[index][2] = input.curveSwapParams[j][2]; + invertedCurveSwapParams[index][3] = input.curveSwapParams[j][3]; + invertedCurveSwapParams[index][4] = input.curveSwapParams[j][4]; + + invertedCurvePools[index] = input.curvePools[j]; + + ++index; + if (j == 0) break; + } + + (IStandardizedYield sy, IPPrincipalToken pt, IPYieldToken yt) = IPMarket(input.pendleMarket).readTokens(); + if (!sy.isValidTokenIn(ibToken)) revert WrongInput(); + if (!sy.isValidTokenOut(ibToken)) revert WrongInput(); + + { + RouteData memory ptToQuoteSwapRoute = RouteData({ + pendleMarket: IPMarket(input.pendleMarket), + ib: ibToken, + sy: sy, + pt: pt, + yt: yt, + slippage: input.slippage, + curveDxAdjust: input.curveDxAdjustPtToToken, + curveRoute: input.curveRoute, + curveSwapParams: input.curveSwapParams, + curvePools: input.curvePools + }); + RouteData memory quoteToPtSwapRoute = RouteData({ + pendleMarket: IPMarket(input.pendleMarket), + ib: ibToken, + sy: sy, + pt: pt, + yt: yt, + slippage: input.slippage, + curveDxAdjust: input.curveDxAdjustTokenToPt, + curveRoute: invertedCurveRoute, + curveSwapParams: invertedCurveSwapParams, + curvePools: invertedCurvePools + }); + + getRouteData[address(pt)][quoteToken] = ptToQuoteSwapRoute; + getRouteData[quoteToken][address(pt)] = quoteToPtSwapRoute; + } + + emit NewPair(address(pt), quoteToken, input.pendleMarket, input.slippage); + + unchecked { + ++i; + } + } } - } } diff --git a/packages/router/hardhat-configs/hardhat-eth-fork.config.ts b/packages/router/hardhat-configs/hardhat-eth-fork.config.ts index f998ffd0..a1b69a71 100644 --- a/packages/router/hardhat-configs/hardhat-eth-fork.config.ts +++ b/packages/router/hardhat-configs/hardhat-eth-fork.config.ts @@ -1,6 +1,9 @@ import '@nomicfoundation/hardhat-toolbox'; import 'solidity-docgen'; import * as defaultConfig from './hardhat.config'; +import { config as dotEnvConfig } from 'dotenv'; + +dotEnvConfig(); const config = { ...defaultConfig.default, @@ -8,7 +11,7 @@ const config = { hardhat: { forking: { enabled: true, - url: 'https://rpc.ankr.com/eth', + url: process.env.ETHEREUM_RPC_URL, blockNumber: 21493100, }, }, diff --git a/packages/router/test/int/PendleCurveNgAdapter.eth.spec.ts b/packages/router/test/int/PendleCurveNgAdapter.eth.spec.ts index b129e004..5c5abdbf 100644 --- a/packages/router/test/int/PendleCurveNgAdapter.eth.spec.ts +++ b/packages/router/test/int/PendleCurveNgAdapter.eth.spec.ts @@ -80,7 +80,7 @@ const PT_wstUSR_USDC_TestCase: TestCase = { forkNumber: 22366500, pendleMarket: '0x09fa04aac9c6d1c6131352ee950cd67ecc6d4fb9', - curvePool:'0x3eE841F47947FEFbE510366E4bbb49e145484195', + curvePool: '0x3eE841F47947FEFbE510366E4bbb49e145484195', ptToken: { address: '0x23e60d1488525bf4685f53b3aa8e676c30321066', symbol: 'PT-wstUSR-25SEP2025', diff --git a/packages/router/test/int/PendleCurveNgAdapterEBTC.eth.spec.ts b/packages/router/test/int/PendleCurveNgAdapterEBTC.eth.spec.ts index 629445b0..81a82915 100644 --- a/packages/router/test/int/PendleCurveNgAdapterEBTC.eth.spec.ts +++ b/packages/router/test/int/PendleCurveNgAdapterEBTC.eth.spec.ts @@ -422,9 +422,7 @@ describe('PendleCurveAdapter PT-eBTC - WBTC', () => { `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` ); const WBTCBalanceBefore = await WBTC.balanceOf(user.address); - console.log( - `WBTCBalanceBefore: ${formatUnits(WBTCBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}` - ); + console.log(`WBTCBalanceBefore: ${formatUnits(WBTCBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}`); const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); const WBTCOut = parseUnits('0.9', 8); diff --git a/packages/router/test/int/PendleCurveRouterNg_WETH_stETH.eth.spec.ts b/packages/router/test/int/PendleCurveRouterNg_WETH_stETH.eth.spec.ts new file mode 100644 index 00000000..48ff4112 --- /dev/null +++ b/packages/router/test/int/PendleCurveRouterNg_WETH_stETH.eth.spec.ts @@ -0,0 +1,562 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + ERC20, + ICurveRouterNg__factory, + MarginlyRouter, + MarginlyRouter__factory, + PendleCurveRouterNgAdapter, + PendleCurveRouterNgAdapter__factory, +} from '../../typechain-types'; +import { constructSwap, Dex, resetFork, showGasUsage, SWAP_ONE, assertSwapEvent } from '../shared/utils'; +import { EthAddress } from '@marginly/common'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { EthereumMainnetERC20BalanceOfSlot, setTokenBalance } from '../shared/tokens'; +import { BigNumberish } from 'ethers'; +import { PromiseOrValue } from '../../typechain-types/common'; + +const curveRouterAddress = '0x45312ea0eFf7E09C83CBE249fa1d7598c4C8cd4e'; + +async function initializeRouter(): Promise<{ + ptToken: ERC20; + quoteToken: ERC20; + ibToken: ERC20; + router: MarginlyRouter; + pendleCurveAdapter: PendleCurveRouterNgAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; + routeInput: PendleCurveRouterNgAdapter.RouteInputStruct; +}> { + const [owner, user] = await ethers.getSigners(); + const ptToken = await ethers.getContractAt('ERC20', '0xf99985822fb361117fcf3768d34a6353e6022f5f'); + const WETH = await ethers.getContractAt('ERC20', '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'); + const wstETH = await ethers.getContractAt('ERC20', '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'); + const pendleMarket = '0xc374f7ec85f8c7de3207a10bb1978ba104bda3b2'; // PT-stETH + + // Route to make swap pt-stETH -> stETH -> ETH -> WETH + const routeInput: PendleCurveRouterNgAdapter.RouteInputStruct = { + pendleMarket: pendleMarket, + slippage: 35, // 20/100 = 20% + curveDxAdjustTokenToPt: -900, // + curveDxAdjustPtToToken: 1000, // + curveRoute: [ + '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', // wstETH + '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', // wstETH -> stETH + '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', // stETH + '0xDC24316b9AE028F1497c275EB9192a3Ea0f67022', // stETH -> ETH + '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // ETH + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // ETH -> WETH + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + ], + curveSwapParams: [ + [1, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [1, 0, 8, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + curvePools: [ + '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', + '0x5dc1BF6f1e983C0b21EfB003c105133736fA0743', + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + ], + }; + const pendleCurveAdapter = await new PendleCurveRouterNgAdapter__factory() + .connect(owner) + .deploy(curveRouterAddress, [routeInput]); + + const routerInput = { + dexIndex: Dex.PendleCurveRouter, + adapter: pendleCurveAdapter.address, + }; + const router = await new MarginlyRouter__factory().connect(owner).deploy([routerInput]); + + await setTokenBalance( + WETH.address, + EthereumMainnetERC20BalanceOfSlot.WETH, + EthAddress.parse(user.address), + parseUnits('10000', 18) + ); + expect(await WETH.balanceOf(user.address)).to.be.eq(parseUnits('10000', 18)); + await setTokenBalance( + ptToken.address, + EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + EthAddress.parse(user.address), + parseUnits('10000', 18) + ); + expect(await ptToken.balanceOf(user.address)).to.be.eq(parseUnits('10000', 18)); + + return { + ptToken, + quoteToken: WETH, + ibToken: wstETH, + router, + pendleCurveAdapter, + owner, + user, + routeInput: routeInput, + }; +} + +// Tests for running in ethereum mainnet fork +describe('Pendle PT-STETH - wETH', () => { + before(async () => { + //await resetFork(22388154); //2025-05-02 + await resetFork(22588100); //2025-05-29 + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let quoteToken: ERC20; + let IbToken: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + let curveRouteInput: PendleCurveRouterNgAdapter.RouteInputStruct; + + beforeEach(async () => { + ({ + ptToken, + quoteToken: quoteToken, + ibToken: IbToken, + router, + pendleCurveAdapter, + owner, + user, + routeInput: curveRouteInput, + } = await initializeRouter()); + }); + + it.only('Curve check route', async () => { + const curveRouter = ICurveRouterNg__factory.connect(curveRouterAddress, user); + const invertedRoute: string[] = []; + for (let i = 0; i < 11; i++) { + invertedRoute.push('0x0000000000000000000000000000000000000000'); + } + + let index = 0; + for (let i = 10; i >= 0; i--) { + if (curveRouteInput.curveRoute[i] == '0x0000000000000000000000000000000000000000') continue; + + invertedRoute[index] = await curveRouteInput.curveRoute[i]; + index++; + } + + const invertedSwapParams: BigNumberish[][] = [ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ]; + + const invertedPools: string[] = []; + for (let i = 0; i < 5; i++) { + invertedPools.push('0x0000000000000000000000000000000000000000'); + } + + index = 0; + for (let j = 4; j >= 0; --j) { + if (curveRouteInput.curveSwapParams[j][0] == 0 && curveRouteInput.curveSwapParams[j][1] == 0) { + continue; // empty element + } + + invertedSwapParams[index][0] = await curveRouteInput.curveSwapParams[j][1]; + invertedSwapParams[index][1] = await curveRouteInput.curveSwapParams[j][0]; + invertedSwapParams[index][2] = await curveRouteInput.curveSwapParams[j][2]; + invertedSwapParams[index][3] = await curveRouteInput.curveSwapParams[j][3]; + invertedSwapParams[index][4] = await curveRouteInput.curveSwapParams[j][4]; + + invertedPools[index] = await curveRouteInput.curvePools[j]; + + ++index; + if (j == 0) break; + } + + const quoteTokenIn = parseUnits('1', 18); + const minDy = 0; + + await quoteToken.connect(user).approve(curveRouter.address, quoteTokenIn); + const tx = await curveRouter + .connect(user) + .exchange(invertedRoute, invertedSwapParams as any, quoteTokenIn, minDy, invertedPools as any, user.address); + await showGasUsage(tx); + + console.log('Curve check route: '); + console.log(`${await quoteToken.symbol()} In: ${formatUnits(quoteTokenIn, await quoteToken.decimals())}`); + console.log( + `${await IbToken.symbol()} Out: ${formatUnits(await IbToken.balanceOf(user.address), await IbToken.decimals())}` + ); + }); + + it('wETH to pt-STETH exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const quoteTokenIn = parseUnits('0.1', 18); + await quoteToken.connect(user).approve(router.address, quoteTokenIn); + + const minPtOut = parseUnits('0.1', 18); //parseUnits('900', 18); + + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, quoteToken.address, ptToken.address, quoteTokenIn, minPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceBefore.sub(tokenBalanceAfter)).to.be.lessThanOrEqual(quoteTokenIn); + + console.log( + `${await quoteToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await quoteToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + }); + + it('wETH to pt-STETH exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('10', await ptToken.decimals()); + const wETHMaxIn = parseUnits('10.5', await quoteToken.decimals()); + await quoteToken.connect(user).approve(router.address, wETHMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, quoteToken.address, ptToken.address, wETHMaxIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + + expect(tokenBalanceBefore).to.be.greaterThan(tokenBalanceAfter); + + const IbTokenOnAdapter = await IbToken.balanceOf(pendleCurveAdapter.address); + + console.log( + `${await quoteToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await quoteToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + console.log( + `${await IbToken.symbol()} stays on adapter: ${formatUnits( + IbTokenOnAdapter, + await IbToken.decimals() + )} ${await IbToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: quoteToken.address, + tokenOut: ptToken.address, + amountIn: tokenBalanceBefore.sub(tokenBalanceAfter), + amountOut: ptBalanceAfter.sub(ptBalanceBefore), + }, + router, + tx + ); + }); + + it('wETH to pt-STETH exact output, small amount', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('0.05', 18); + const wethMaxIn = parseUnits('0.05', 18); + await quoteToken.connect(user).approve(router.address, wethMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, quoteToken.address, ptToken.address, wethMaxIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceBefore).to.be.greaterThan(tokenBalanceAfter); + + const IbTokenOnAdapter = await IbToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await quoteToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await quoteToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + console.log( + `${await IbToken.symbol()} stays on adapter: ${formatUnits( + IbTokenOnAdapter, + await IbToken.decimals() + )} ${await IbToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: quoteToken.address, + tokenOut: ptToken.address, + amountIn: tokenBalanceBefore.sub(tokenBalanceAfter), + amountOut: ptBalanceAfter.sub(ptBalanceBefore), + }, + router, + tx + ); + }); + + it('pt-STETH to wETH exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptIn = parseUnits('2', 18); + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, quoteToken.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceAfter).to.be.greaterThan(tokenBalanceBefore); + + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await quoteToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await quoteToken.decimals() + )}` + ); + + await assertSwapEvent( + { + isExactInput: true, + tokenIn: ptToken.address, + tokenOut: quoteToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + + it('pt-STETH to wETH exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const wethOut = parseUnits('15', 18); + const maxPtIn = parseUnits('16', 18); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, quoteToken.address, maxPtIn, wethOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(wethOut); + + const tokenBalanceOnAdapter = await quoteToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await quoteToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await quoteToken.decimals() + )}` + ); + console.log( + `${await IbToken.symbol()} stays on adapter: ${formatUnits( + tokenBalanceOnAdapter, + await IbToken.decimals() + )} ${await IbToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: ptToken.address, + tokenOut: quoteToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let quoteToken: ERC20; + let IbToken: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + quoteToken: quoteToken, + ibToken: IbToken, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + // move time and make after maturity + await ethers.provider.send('evm_increaseTime', [225 * 24 * 60 * 60]); + await ethers.provider.send('evm_mine', []); + }); + + it('wETH to pt-teth exact input, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + await quoteToken.connect(user).approve(router.address, tokenBalanceBefore); + const tx = router + .connect(user) + .swapExactInput( + swapCalldata, + quoteToken.address, + ptToken.address, + tokenBalanceBefore, + tokenBalanceBefore.mul(9).div(10) + ); + + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + console.log('This swap is forbidden after maturity'); + }); + + it('wETH to pt-teth exact output, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptOut = tokenBalanceBefore.div(2); + await quoteToken.connect(user).approve(router.address, tokenBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCalldata, quoteToken.address, ptToken.address, tokenBalanceBefore, ptOut); + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + }); + + it('pt-STETH to WETH exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptIn = parseUnits('10', 18); + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, quoteToken.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceAfter).to.be.greaterThan(tokenBalanceBefore); + + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await quoteToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await quoteToken.decimals() + )}` + ); + + await assertSwapEvent( + { + isExactInput: true, + tokenIn: ptToken.address, + tokenOut: quoteToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + + it('pt-STETH to WETH exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const tokenOut = parseUnits('250', 18); + await ptToken.connect(user).approve(router.address, ptBalanceBefore); + const maxPtIn = parseUnits('255', 18); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, quoteToken.address, maxPtIn, tokenOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(tokenOut); + + const tokenBalanceOnAdapter = await quoteToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await quoteToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await quoteToken.decimals() + )}` + ); + console.log( + `${await IbToken.symbol()} stays on adapter: ${formatUnits( + tokenBalanceOnAdapter, + await IbToken.decimals() + )} ${await IbToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: ptToken.address, + tokenOut: quoteToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + }); +}); diff --git a/packages/router/test/int/PendleCurveRouterNg_sUSDE_USDE.eth.spec.ts b/packages/router/test/int/PendleCurveRouterNg_sUSDE_USDE.eth.spec.ts new file mode 100644 index 00000000..81431f12 --- /dev/null +++ b/packages/router/test/int/PendleCurveRouterNg_sUSDE_USDE.eth.spec.ts @@ -0,0 +1,491 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + ERC20, + MarginlyRouter, + MarginlyRouter__factory, + PendleCurveRouterNgAdapter, + PendleCurveRouterNgAdapter__factory, +} from '../../typechain-types'; +import { constructSwap, Dex, resetFork, showGasUsage, SWAP_ONE, assertSwapEvent } from '../shared/utils'; +import { EthAddress } from '@marginly/common'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { EthereumMainnetERC20BalanceOfSlot, setTokenBalance } from '../shared/tokens'; + +async function initializeRouter(): Promise<{ + ptToken: ERC20; + sUSDEToken: ERC20; + USDEToken: ERC20; + router: MarginlyRouter; + pendleCurveAdapter: PendleCurveRouterNgAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; +}> { + const [owner, user] = await ethers.getSigners(); + const ptToken = await ethers.getContractAt('ERC20', '0x917459337caac939d41d7493b3999f571d20d667'); + const sUSDEToken = await ethers.getContractAt('ERC20', '0x9d39a5de30e57443bff2a8307a4256c8797a3497'); + const USDEToken = await ethers.getContractAt('ERC20', '0x4c9edd5852cd905f086c759e8383e09bff1e68b3'); + const pendleMarket = '0x9df192d13d61609d1852461c4850595e1f56e714'; // PT-USDE 31 Jul 2025 + const curveRouterAddress = '0x16c6521dff6bab339122a0fe25a9116693265353'; + + // Route to make swap pt-usde -> usde -> frax -> sdai -> susde + const routeInput: PendleCurveRouterNgAdapter.RouteInputStruct = { + pendleMarket: pendleMarket, + slippage: 35, // 20/100 = 20% + curveDxAdjustTokenToPt: -145_000, // + curveDxAdjustPtToToken: 175_000, // + curveRoute: [ + '0x4c9edd5852cd905f086c759e8383e09bff1e68b3', //usde + '0x5dc1BF6f1e983C0b21EfB003c105133736fA0743', // usde -> frax + '0x853d955aCEf822Db058eb8505911ED77F175b99e', //frax + '0xcE6431D21E3fb1036CE9973a3312368ED96F5CE7', // frax -> sDAI + '0x83F20F44975D03b1b09e64809B757c47f942BEeA', //sDAI + '0x167478921b907422F8E88B43C4Af2B8BEa278d3A', // sDAI -> sUSDE + '0x9d39a5de30e57443bff2a8307a4256c8797a3497', // sUSDE + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + ], // curve route sUSDE -> sDAI -> FRAX -> USDE + curveSwapParams: [ + [1, 0, 1, 1, 2], + [0, 1, 1, 1, 2], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + curvePools: [ + '0x5dc1BF6f1e983C0b21EfB003c105133736fA0743', + '0xcE6431D21E3fb1036CE9973a3312368ED96F5CE7', + '0x167478921b907422F8E88B43C4Af2B8BEa278d3A', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + ], + }; + const pendleCurveAdapter = await new PendleCurveRouterNgAdapter__factory() + .connect(owner) + .deploy(curveRouterAddress, [routeInput]); + + const routerInput = { + dexIndex: Dex.PendleCurveRouter, + adapter: pendleCurveAdapter.address, + }; + const router = await new MarginlyRouter__factory().connect(owner).deploy([routerInput]); + + await setTokenBalance( + sUSDEToken.address, + EthereumMainnetERC20BalanceOfSlot.SUSDE, + EthAddress.parse(user.address), + parseUnits('10000', 18) + ); + await setTokenBalance( + ptToken.address, + EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + EthAddress.parse(user.address), + parseUnits('10000', 18) + ); + + return { + ptToken, + sUSDEToken: sUSDEToken, + USDEToken: USDEToken, + router, + pendleCurveAdapter, + owner, + user, + }; +} + +// Tests for running in ethereum mainnet fork +describe('Pendle PT-USDE - sUSDE', () => { + before(async () => { + await resetFork(22388154); //2025-05-02 + //await resetFork(22588100); //2025-05-29 + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let sUSDEToken: ERC20; + let USDEToken: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + sUSDEToken: sUSDEToken, + USDEToken: USDEToken, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + }); + + it('sUSDE to pt-USDE exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const sUSDESwapAmount = parseUnits('1000', 18); + await sUSDEToken.connect(user).approve(router.address, sUSDESwapAmount); + + const minPtAmountOut = parseUnits('950', 18); //parseUnits('900', 18); + + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, sUSDEToken.address, ptToken.address, sUSDESwapAmount, minPtAmountOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + const tokenBalanceAfter = await sUSDEToken.balanceOf(user.address); + expect(tokenBalanceBefore.sub(tokenBalanceAfter)).to.be.lessThanOrEqual(sUSDESwapAmount); + + console.log( + `${await sUSDEToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await sUSDEToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + }); + + it('sUSDE to pt-USDE exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('10000', await ptToken.decimals()); + const sUSDEMaxIn = parseUnits('10000', await sUSDEToken.decimals()); + await sUSDEToken.connect(user).approve(router.address, sUSDEMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, sUSDEToken.address, ptToken.address, sUSDEMaxIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + const tokenBalanceAfter = await sUSDEToken.balanceOf(user.address); + + expect(tokenBalanceBefore).to.be.greaterThan(tokenBalanceAfter); + + const USDEOnAdapter = await USDEToken.balanceOf(pendleCurveAdapter.address); + + console.log( + `${await sUSDEToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await sUSDEToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + console.log( + `${await USDEToken.symbol()} stays on adapter: ${formatUnits( + USDEOnAdapter, + await USDEToken.decimals() + )} ${await USDEToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: sUSDEToken.address, + tokenOut: ptToken.address, + amountIn: tokenBalanceBefore.sub(tokenBalanceAfter), + amountOut: ptBalanceAfter.sub(ptBalanceBefore), + }, + router, + tx + ); + }); + + it('sUSDE to pt-USDE exact output, small amount', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('10', 18); + const wethMaxIn = parseUnits('10', 18); + await sUSDEToken.connect(user).approve(router.address, wethMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, sUSDEToken.address, ptToken.address, wethMaxIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + const tokenBalanceAfter = await sUSDEToken.balanceOf(user.address); + expect(tokenBalanceBefore).to.be.greaterThan(tokenBalanceAfter); + + const USDEOnAdapter = await USDEToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await sUSDEToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await sUSDEToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + console.log( + `${await USDEToken.symbol()} stays on adapter: ${formatUnits( + USDEOnAdapter, + await USDEToken.decimals() + )} ${await USDEToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: sUSDEToken.address, + tokenOut: ptToken.address, + amountIn: tokenBalanceBefore.sub(tokenBalanceAfter), + amountOut: ptBalanceAfter.sub(ptBalanceBefore), + }, + router, + tx + ); + }); + + it('pt-USDE to sUSDE exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptIn = ptBalanceBefore; + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, sUSDEToken.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const tokenBalanceAfter = await sUSDEToken.balanceOf(user.address); + expect(tokenBalanceAfter).to.be.greaterThan(tokenBalanceBefore); + + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await sUSDEToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await sUSDEToken.decimals() + )}` + ); + + await assertSwapEvent( + { + isExactInput: true, + tokenIn: ptToken.address, + tokenOut: sUSDEToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + + it('pt-USDE to sUSDE exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const wethOut = parseUnits('800', 18); + const maxPtIn = parseUnits('1000', 18); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, sUSDEToken.address, maxPtIn, wethOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const tokenBalanceAfter = await sUSDEToken.balanceOf(user.address); + expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(wethOut); + + const tokenBalanceOnAdapter = await sUSDEToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await sUSDEToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await sUSDEToken.decimals() + )}` + ); + console.log( + `${await USDEToken.symbol()} stays on adapter: ${formatUnits( + tokenBalanceOnAdapter, + await USDEToken.decimals() + )} ${await USDEToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: ptToken.address, + tokenOut: sUSDEToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let sUSDEToken: ERC20; + let USDEToken: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + sUSDEToken: sUSDEToken, + USDEToken: USDEToken, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + // move time and make after maturity + await ethers.provider.send('evm_increaseTime', [180 * 24 * 60 * 60]); + await ethers.provider.send('evm_mine', []); + }); + + it('sUSDE to pt-teth exact input, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + await sUSDEToken.connect(user).approve(router.address, tokenBalanceBefore); + const tx = router + .connect(user) + .swapExactInput( + swapCalldata, + sUSDEToken.address, + ptToken.address, + tokenBalanceBefore, + tokenBalanceBefore.mul(9).div(10) + ); + + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + console.log('This swap is forbidden after maturity'); + }); + + it('sUSDE to pt-teth exact output, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptOut = tokenBalanceBefore.div(2); + await sUSDEToken.connect(user).approve(router.address, tokenBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCalldata, sUSDEToken.address, ptToken.address, tokenBalanceBefore, ptOut); + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + }); + + it('pt-USDE to sUSDE exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptIn = ptBalanceBefore; + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, sUSDEToken.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const tokenBalanceAfter = await sUSDEToken.balanceOf(user.address); + expect(tokenBalanceAfter).to.be.greaterThan(tokenBalanceBefore); + + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await sUSDEToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await sUSDEToken.decimals() + )}` + ); + + await assertSwapEvent( + { + isExactInput: true, + tokenIn: ptToken.address, + tokenOut: sUSDEToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + + it('pt-USDE to sUSDE exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await sUSDEToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const tokenOut = parseUnits('900', 18); + await ptToken.connect(user).approve(router.address, ptBalanceBefore); + const maxPtIn = parseUnits('1200', 18); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, sUSDEToken.address, maxPtIn, tokenOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const tokenBalanceAfter = await sUSDEToken.balanceOf(user.address); + expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(tokenOut); + + const tokenBalanceOnAdapter = await sUSDEToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await sUSDEToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await sUSDEToken.decimals() + )}` + ); + console.log( + `${await USDEToken.symbol()} stays on adapter: ${formatUnits( + tokenBalanceOnAdapter, + await USDEToken.decimals() + )} ${await USDEToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: ptToken.address, + tokenOut: sUSDEToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + }); +}); diff --git a/packages/router/test/int/PendleCurveRouterNg_sUSDE_USDS.eth.spec.ts b/packages/router/test/int/PendleCurveRouterNg_sUSDE_USDS.eth.spec.ts new file mode 100644 index 00000000..da18c955 --- /dev/null +++ b/packages/router/test/int/PendleCurveRouterNg_sUSDE_USDS.eth.spec.ts @@ -0,0 +1,559 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + ERC20, + ICurveRouterNg__factory, + MarginlyRouter, + MarginlyRouter__factory, + PendleCurveRouterNgAdapter, + PendleCurveRouterNgAdapter__factory, +} from '../../typechain-types'; +import { constructSwap, Dex, resetFork, showGasUsage, SWAP_ONE, assertSwapEvent } from '../shared/utils'; +import { EthAddress } from '@marginly/common'; +import { formatUnits, parseUnits } from 'ethers/lib/utils'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { EthereumMainnetERC20BalanceOfSlot, setTokenBalance } from '../shared/tokens'; +import { BigNumberish } from 'ethers'; +import { PromiseOrValue } from '../../typechain-types/common'; + +async function initializeRouter(): Promise<{ + ptToken: ERC20; + quoteToken: ERC20; + ibToken: ERC20; + router: MarginlyRouter; + pendleCurveAdapter: PendleCurveRouterNgAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; + routeInput: PendleCurveRouterNgAdapter.RouteInputStruct; +}> { + const [owner, user] = await ethers.getSigners(); + const ptToken = await ethers.getContractAt('ERC20', '0xffec096c087c13cc268497b89a613cace4df9a48'); + const sUSDEToken = await ethers.getContractAt('ERC20', '0x9d39a5de30e57443bff2a8307a4256c8797a3497'); + const usdsToken = await ethers.getContractAt('ERC20', '0xdc035d45d973e3ec169d2276ddab16f1e407384f'); + const pendleMarket = '0xdace1121e10500e9e29d071f01593fd76b000f08'; // PT-USDS-14Aug2025 + const curveRouterAddress = '0x45312ea0eFf7E09C83CBE249fa1d7598c4C8cd4e'; + + // Route to make swap PT-USDS -> USDS -> sUSDS -> sUSDe + const routeInput: PendleCurveRouterNgAdapter.RouteInputStruct = { + pendleMarket: pendleMarket, + slippage: 35, // 20/100 = 20% + curveDxAdjustTokenToPt: -103_100, // + curveDxAdjustPtToToken: 116_000, // + curveRoute: [ + '0xdc035d45d973e3ec169d2276ddab16f1e407384f', // USDS + '0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD', // USDS -> sUSDS + '0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD', // sUSDS + '0x3CEf1AFC0E8324b57293a6E7cE663781bbEFBB79', // sUSDS -> sUSDe + '0x9d39a5de30e57443bff2a8307a4256c8797a3497', // sUSDe + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + ], + curveSwapParams: [ + [0, 1, 9, 0, 0], + [1, 0, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + curvePools: [ + '0x0000000000000000000000000000000000000000', + '0x3CEf1AFC0E8324b57293a6E7cE663781bbEFBB79', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + ], + }; + const pendleCurveAdapter = await new PendleCurveRouterNgAdapter__factory() + .connect(owner) + .deploy(curveRouterAddress, [routeInput]); + + const routerInput = { + dexIndex: Dex.PendleCurveRouter, + adapter: pendleCurveAdapter.address, + }; + const router = await new MarginlyRouter__factory().connect(owner).deploy([routerInput]); + + await setTokenBalance( + sUSDEToken.address, + EthereumMainnetERC20BalanceOfSlot.SUSDE, + EthAddress.parse(user.address), + parseUnits('100000', 18) + ); + expect(await sUSDEToken.balanceOf(user.address)).to.be.eq(parseUnits('100000', 18)); + await setTokenBalance( + ptToken.address, + EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + EthAddress.parse(user.address), + parseUnits('100000', 18) + ); + expect(await ptToken.balanceOf(user.address)).to.be.eq(parseUnits('100000', 18)); + + return { + ptToken, + quoteToken: sUSDEToken, + ibToken: usdsToken, + router, + pendleCurveAdapter, + owner, + user, + routeInput: routeInput, + }; +} + +// Tests for running in ethereum mainnet fork +describe('Pendle PT-USDS - sUSDe', () => { + before(async () => { + //await resetFork(22388154); //2025-05-02 + await resetFork(22588100); //2025-05-29 + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let quoteToken: ERC20; + let IbToken: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + let curveRouteInput: PendleCurveRouterNgAdapter.RouteInputStruct; + + beforeEach(async () => { + ({ + ptToken, + quoteToken: quoteToken, + ibToken: IbToken, + router, + pendleCurveAdapter, + owner, + user, + routeInput: curveRouteInput, + } = await initializeRouter()); + }); + + it.skip('Curve check route', async () => { + const curveRouter = ICurveRouterNg__factory.connect('0x45312ea0eFf7E09C83CBE249fa1d7598c4C8cd4e', user); + const invertedRoute: string[] = []; + for (let i = 0; i < 11; i++) { + invertedRoute.push('0x0000000000000000000000000000000000000000'); + } + + let index = 0; + for (let i = 10; i >= 0; i--) { + if (curveRouteInput.curveRoute[i] == '0x0000000000000000000000000000000000000000') continue; + + invertedRoute[index] = await curveRouteInput.curveRoute[i]; + index++; + } + + const invertedSwapParams: BigNumberish[][] = [ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ]; + + const invertedPools: string[] = []; + for (let i = 0; i < 5; i++) { + invertedPools.push('0x0000000000000000000000000000000000000000'); + } + + index = 0; + for (let j = 4; j >= 0; --j) { + if (curveRouteInput.curveSwapParams[j][0] == 0 && curveRouteInput.curveSwapParams[j][1] == 0) { + continue; // empty element + } + + invertedSwapParams[index][0] = await curveRouteInput.curveSwapParams[j][1]; + invertedSwapParams[index][1] = await curveRouteInput.curveSwapParams[j][0]; + invertedSwapParams[index][2] = await curveRouteInput.curveSwapParams[j][2]; + invertedSwapParams[index][3] = await curveRouteInput.curveSwapParams[j][3]; + invertedSwapParams[index][4] = await curveRouteInput.curveSwapParams[j][4]; + + invertedPools[index] = await curveRouteInput.curvePools[j]; + + ++index; + if (j == 0) break; + } + + const quoteTokenIn = parseUnits('100', 18); + const minDy = 0; + + console.log(invertedRoute); + console.log(invertedSwapParams); + console.log(invertedPools); + + await quoteToken.connect(user).approve(curveRouter.address, quoteTokenIn); + await curveRouter + .connect(user) + .exchange(invertedRoute, invertedSwapParams as any, quoteTokenIn, minDy, invertedPools as any, user.address); + + console.log('Curve check route: '); + console.log(`${await quoteToken.symbol()} In: ${formatUnits(quoteTokenIn, await quoteToken.decimals())}`); + console.log( + `${await IbToken.symbol()} Out: ${formatUnits(await IbToken.balanceOf(user.address), await IbToken.decimals())}` + ); + }); + + it('sUSDe to PT-USDS exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const quoteTokenIn = parseUnits('140', 18); + await quoteToken.connect(user).approve(router.address, quoteTokenIn); + + const minPtOut = parseUnits('140', 18); //parseUnits('900', 18); + + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, quoteToken.address, ptToken.address, quoteTokenIn, minPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceBefore.sub(tokenBalanceAfter)).to.be.lessThanOrEqual(quoteTokenIn); + + console.log( + `${await quoteToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await quoteToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + }); + + it('sUSDe to PT-USDS exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('150', await ptToken.decimals()); + const sUSDeMaxIn = parseUnits('150', await quoteToken.decimals()); + await quoteToken.connect(user).approve(router.address, sUSDeMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, quoteToken.address, ptToken.address, sUSDeMaxIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + + expect(tokenBalanceBefore).to.be.greaterThan(tokenBalanceAfter); + + const IbTokenOnAdapter = await IbToken.balanceOf(pendleCurveAdapter.address); + + console.log( + `${await quoteToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await quoteToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + console.log( + `${await IbToken.symbol()} stays on adapter: ${formatUnits( + IbTokenOnAdapter, + await IbToken.decimals() + )} ${await IbToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: quoteToken.address, + tokenOut: ptToken.address, + amountIn: tokenBalanceBefore.sub(tokenBalanceAfter), + amountOut: ptBalanceAfter.sub(ptBalanceBefore), + }, + router, + tx + ); + }); + + it('sUSDe to PT-USDS exact output, small amount', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('5', 18); + const sUSDeMaxIn = parseUnits('5', 18); + await quoteToken.connect(user).approve(router.address, sUSDeMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, quoteToken.address, ptToken.address, sUSDeMaxIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceBefore).to.be.greaterThan(tokenBalanceAfter); + + const IbTokenOnAdapter = await IbToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await quoteToken.symbol()} In: ${formatUnits( + tokenBalanceBefore.sub(tokenBalanceAfter), + await quoteToken.decimals() + )}` + ); + console.log( + `${await ptToken.symbol()} Out: ${formatUnits(ptBalanceAfter.sub(ptBalanceBefore), await ptToken.decimals())}` + ); + console.log( + `${await IbToken.symbol()} stays on adapter: ${formatUnits( + IbTokenOnAdapter, + await IbToken.decimals() + )} ${await IbToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: quoteToken.address, + tokenOut: ptToken.address, + amountIn: tokenBalanceBefore.sub(tokenBalanceAfter), + amountOut: ptBalanceAfter.sub(ptBalanceBefore), + }, + router, + tx + ); + }); + + it('PT-USDS to sUSDe exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptIn = parseUnits('2', 18); + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, quoteToken.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceAfter).to.be.greaterThan(tokenBalanceBefore); + + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await quoteToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await quoteToken.decimals() + )}` + ); + + await assertSwapEvent( + { + isExactInput: true, + tokenIn: ptToken.address, + tokenOut: quoteToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + + it('PT-USDS to sUSDe exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const sUSDeOut = parseUnits('120', 18); + const maxPtIn = parseUnits('160', 18); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, quoteToken.address, maxPtIn, sUSDeOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(sUSDeOut); + + const tokenBalanceOnAdapter = await quoteToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await quoteToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await quoteToken.decimals() + )}` + ); + console.log( + `${await IbToken.symbol()} stays on adapter: ${formatUnits( + tokenBalanceOnAdapter, + await IbToken.decimals() + )} ${await IbToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: ptToken.address, + tokenOut: quoteToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let quoteToken: ERC20; + let IbToken: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + before(async () => { + // move time and make after maturity + await ethers.provider.send('evm_increaseTime', [90 * 24 * 60 * 60]); + await ethers.provider.send('evm_mine', []); + }); + + beforeEach(async () => { + ({ + ptToken, + quoteToken: quoteToken, + ibToken: IbToken, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + }); + + it('sUSDe to PT-USDS exact input, forbidden', async () => { + const tokenIn = parseUnits('1000', 18); + const minPtOut = parseUnits('1500', 18); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + await quoteToken.connect(user).approve(router.address, tokenIn); + const tx = router + .connect(user) + .swapExactInput(swapCalldata, quoteToken.address, ptToken.address, tokenIn, minPtOut); + + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + console.log('This swap is forbidden after maturity'); + }); + + it('sUSDe to PT-USDS exact output, forbidden', async () => { + const maxTokenIn = parseUnits('1000', 18); + const ptOut = parseUnits('1000', 18); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + await quoteToken.connect(user).approve(router.address, maxTokenIn); + const tx = router + .connect(user) + .swapExactOutput(swapCalldata, quoteToken.address, ptToken.address, maxTokenIn, ptOut); + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + }); + + it('PT-USDS to sUSDe exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptIn = parseUnits('10', 18); + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, quoteToken.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceAfter).to.be.greaterThan(tokenBalanceBefore); + + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await quoteToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await quoteToken.decimals() + )}` + ); + + await assertSwapEvent( + { + isExactInput: true, + tokenIn: ptToken.address, + tokenOut: quoteToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + + it('PT-USDS to sUSDe exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + const tokenBalanceBefore = await quoteToken.balanceOf(user.address); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const tokenOut = parseUnits('230', 18); + const maxPtIn = parseUnits('300', 18); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, quoteToken.address, maxPtIn, tokenOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const tokenBalanceAfter = await quoteToken.balanceOf(user.address); + expect(tokenBalanceAfter.sub(tokenBalanceBefore)).to.be.eq(tokenOut); + + const tokenBalanceOnAdapter = await quoteToken.balanceOf(pendleCurveAdapter.address); + console.log( + `${await ptToken.symbol()} In: ${formatUnits(ptBalanceBefore.sub(ptBalanceAfter), await ptToken.decimals())}` + ); + console.log( + `${await quoteToken.symbol()} Out: ${formatUnits( + tokenBalanceAfter.sub(tokenBalanceBefore), + await quoteToken.decimals() + )}` + ); + console.log( + `${await IbToken.symbol()} stays on adapter: ${formatUnits( + tokenBalanceOnAdapter, + await IbToken.decimals() + )} ${await IbToken.symbol()}` + ); + + await assertSwapEvent( + { + isExactInput: false, + tokenIn: ptToken.address, + tokenOut: quoteToken.address, + amountIn: ptBalanceBefore.sub(ptBalanceAfter), + amountOut: tokenBalanceAfter.sub(tokenBalanceBefore), + }, + router, + tx + ); + }); + }); +});