diff --git a/.github/workflows/unit-tests-router.yml b/.github/workflows/unit-tests-router.yml index 69e28a5a..678fb01e 100644 --- a/.github/workflows/unit-tests-router.yml +++ b/.github/workflows/unit-tests-router.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Use Node.js 18.14.2 + - name: Use Node.js 21.1.0 uses: actions/setup-node@v3 with: - node-version: 18.14.2 + node-version: 21.1.0 - name: Build for testing router run: | cd packages/router diff --git a/packages/deploy/src/config.ts b/packages/deploy/src/config.ts index 853219cb..78916e26 100644 --- a/packages/deploy/src/config.ts +++ b/packages/deploy/src/config.ts @@ -379,6 +379,55 @@ export enum Dex { Woofi, } +export type AdapterPair = + | GeneralAdapterPair + | PendleUniswapAdapterPair + | PendleMarketAdapterPair + | PendleCurveAdapterPair + | PendleCurveRouterAdapterPair; + +export interface GeneralAdapterPair { + tokenAId: string; + tokenBId: string; + poolAddress: string; +} + +export interface PendleUniswapAdapterPair { + tokenAId: string; + tokenBId: string; + poolAddress: string; + ibTokenId: string; + pendleMarket: string; + slippage: number; +} + +export interface PendleMarketAdapterPair { + tokenAId: string; + tokenBId: string; + poolAddress: string; + slippage: number; +} + +export interface PendleCurveAdapterPair { + ibTokenId: string; + quoteTokenId: string; + curvePool: string; + pendleMarket: string; + slippage: number; + curveSlippage: number; +} + +export interface PendleCurveRouterAdapterPair { + tokenAId: string; + tokenBId: string; + pendleMarket: string; + slippage: number; + curveRoute: string[]; + curveSwapParams: number[][]; + curvePools: string[]; + curveSlippage: number; +} + export interface MarginlyDeployConfig { connection: EthConnectionConfig; tokens: MarginlyDeployConfigToken[]; @@ -389,14 +438,8 @@ export interface MarginlyDeployConfig { dexId: number; adapterName: string; balancerVault?: string; - pools: { - tokenAId: string; - tokenBId: string; - poolAddress: string; - ibTokenId?: string; - pendleMarket?: string; - slippage?: number; - }[]; + curveRouter?: string; + pools: AdapterPair[]; }[]; marginlyFactory: { feeHolder: string; diff --git a/packages/deploy/src/deployer/MarginlyRouterDeployer.ts b/packages/deploy/src/deployer/MarginlyRouterDeployer.ts index ce9eebdf..229ae66e 100644 --- a/packages/deploy/src/deployer/MarginlyRouterDeployer.ts +++ b/packages/deploy/src/deployer/MarginlyRouterDeployer.ts @@ -7,8 +7,12 @@ import { AdapterParam, MarginlyAdapterParam, PendleAdapterParam, + PendleCurveAdapterParam, + PendleCurveRouterAdapterParam, PendleMarketAdapterParam, isPendleAdapter, + isPendleCurveAdapter, + isPendleCurveRouterAdapter, isPendleMarketAdapter, } from './configs'; import { EthOptions } from '../config'; @@ -24,7 +28,8 @@ export class MarginlyRouterDeployer extends BaseDeployer { dexId: BigNumber, adapterName: string, pools: AdapterParam[], - balancerVault?: EthAddress + balancerVault?: EthAddress, + curveRouter?: EthAddress ): Promise { let args: any[]; if (isPendleAdapter(pools[0])) { @@ -55,6 +60,39 @@ export class MarginlyRouterDeployer extends BaseDeployer { ]; }), ]; + } else if (isPendleCurveAdapter(pools[0])) { + args = [ + pools.map((x) => { + const locConfig = x as PendleCurveAdapterParam; + return [ + locConfig.pendleMarket.toString(), + locConfig.slippage, + locConfig.curveSlippage, + locConfig.curvePool.toString(), + tokenRepository.getTokenInfo(locConfig.ibToken.id).address.toString(), + tokenRepository.getTokenInfo(locConfig.quoteToken.id).address.toString(), + ]; + }), + ]; + } else if (isPendleCurveRouterAdapter(pools[0])) { + if (!curveRouter) { + throw new Error('CurveRouter address is required for PendleCurveRouterAdapter'); + } + + args = [ + curveRouter.toString(), + pools.map((x) => { + const locConfig = x as PendleCurveRouterAdapterParam; + return [ + locConfig.pendleMarket.toString(), + locConfig.slippage, + locConfig.curveSlippage, + locConfig.curveRoute.map((y) => y.toString()), + locConfig.curveSwapParams, + locConfig.curvePools.map((y) => y.toString()), + ]; + }), + ]; } else { args = [ pools.map((x) => { @@ -66,6 +104,7 @@ export class MarginlyRouterDeployer extends BaseDeployer { ]; }), ]; + if (balancerVault !== undefined) { args.push(balancerVault.toString()); } diff --git a/packages/deploy/src/deployer/configs.ts b/packages/deploy/src/deployer/configs.ts index 7f7db60c..8729b4a2 100644 --- a/packages/deploy/src/deployer/configs.ts +++ b/packages/deploy/src/deployer/configs.ts @@ -2,6 +2,7 @@ import { MarginlyConfigExistingToken, MarginlyConfigMintableToken, MarginlyConfi import { EthAddress, RationalNumber } from '@marginly/common'; import { EthConnectionConfig, + GeneralAdapterPair, isAlgebraDoubleOracleConfig, isAlgebraOracleConfig, isChainlinkOracleConfig, @@ -10,9 +11,6 @@ import { isDoublePairPythOracleDeployConfig, isMarginlyDeployConfigExistingToken, isMarginlyDeployConfigMintableToken, - isMarginlyDeployConfigSwapPoolRegistry, - isMarginlyDeployConfigUniswapGenuine, - isMarginlyDeployConfigUniswapMock, isPendleMarketOracleConfig, isPendleOracleConfig, isPythOracleConfig, @@ -21,6 +19,10 @@ import { isUniswapV3DoubleOracleConfig, isUniswapV3OracleConfig, MarginlyDeployConfig, + PendleCurveAdapterPair, + PendleCurveRouterAdapterPair, + PendleMarketAdapterPair, + PendleUniswapAdapterPair, } from '../config'; import { adapterWriter, Logger } from '../logger'; import { createRootLogger, textFormatter } from '@marginly/logger'; @@ -166,7 +168,12 @@ export interface MarginlyConfigMarginlyPool { priceOracle: PriceOracleConfig; } -export type AdapterParam = MarginlyAdapterParam | PendleAdapterParam | PendleMarketAdapterParam; +export type AdapterParam = + | MarginlyAdapterParam + | PendleAdapterParam + | PendleMarketAdapterParam + | PendleCurveAdapterParam + | PendleCurveRouterAdapterParam; export interface MarginlyAdapterParam { type: 'general'; @@ -193,6 +200,26 @@ export interface PendleMarketAdapterParam { slippage: number; } +export interface PendleCurveRouterAdapterParam { + type: 'pendleCurveRouter'; + pendleMarket: EthAddress; + slippage: number; + curveSlippage: number; + curveRoute: EthAddress[]; // array of fixed length 11 + curveSwapParams: number[][]; // array of fixed length 5 x 5 + curvePools: EthAddress[]; // array of fixed length 5 +} + +export interface PendleCurveAdapterParam { + type: 'pendleCurve'; + pendleMarket: EthAddress; + slippage: number; + curveSlippage: number; + curvePool: EthAddress; + ibToken: MarginlyConfigToken; + quoteToken: MarginlyConfigToken; +} + export function isPendleAdapter(config: AdapterParam): config is PendleAdapterParam { return config.type === 'pendle'; } @@ -205,10 +232,19 @@ export function isGeneralAdapter(config: AdapterParam): config is MarginlyAdapte return config.type === 'general'; } +export function isPendleCurveRouterAdapter(config: AdapterParam): config is PendleCurveRouterAdapterParam { + return config.type === 'pendleCurveRouter'; +} + +export function isPendleCurveAdapter(config: AdapterParam): config is PendleCurveAdapterParam { + return config.type === 'pendleCurve'; +} + export interface MarginlyConfigAdapter { dexId: BigNumber; name: string; balancerVault?: EthAddress; + curveRouter?: EthAddress; marginlyAdapterParams: AdapterParam[]; } @@ -564,28 +600,30 @@ export class StrictMarginlyDeployConfig { const adapterParams: AdapterParam[] = []; for (const pool of adapter.pools) { if (adapter.adapterName === 'PendleAdapter') { - if (!pool.ibTokenId) { + const pairConfig = pool as PendleUniswapAdapterPair; + + if (!pairConfig.ibTokenId) { throw new Error(`IB token id is not set for adapter with dexId ${adapter.dexId}`); } - if (!pool.slippage) { + if (!pairConfig.slippage) { throw new Error(`Slippage is not set for adapter with dexId ${adapter.dexId}`); } - if (!pool.pendleMarket) { + if (!pairConfig.pendleMarket) { throw new Error(`Pendle market is not set for adapter with dexId ${adapter.dexId}`); } - const poolAddress = EthAddress.parse(pool.poolAddress); - const token0 = tokens.get(pool.tokenAId); + const poolAddress = EthAddress.parse(pairConfig.poolAddress); + const token0 = tokens.get(pairConfig.tokenAId); if (token0 === undefined) { - throw new Error(`Can not find token0 '${pool.tokenAId}' for adapter with dexId ${adapter.dexId}`); + throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${adapter.dexId}`); } - const token1 = tokens.get(pool.tokenBId); + const token1 = tokens.get(pairConfig.tokenBId); if (token1 === undefined) { - throw new Error(`Can not find token1 '${pool.tokenBId}' for adapter with dexId ${adapter.dexId}`); + throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${adapter.dexId}`); } - const ibToken = tokens.get(pool.ibTokenId); + const ibToken = tokens.get(pairConfig.ibTokenId); if (ibToken === undefined) { - throw new Error(`Can not find ibToken '${pool.ibTokenId}' for adapter with dexId ${adapter.dexId}`); + throw new Error(`Can not find ibToken '${pairConfig.ibTokenId}' for adapter with dexId ${adapter.dexId}`); } adapterParams.push({ @@ -594,39 +632,101 @@ export class StrictMarginlyDeployConfig { token1: token1, ib: ibToken, uniswapV3LikePool: poolAddress, - pendleMarket: EthAddress.parse(pool.pendleMarket), - slippage: pool.slippage, + pendleMarket: EthAddress.parse(pairConfig.pendleMarket), + slippage: pairConfig.slippage, }); } else if (adapter.adapterName === 'PendleMarketAdapter') { - if (!pool.slippage) { - throw new Error(`Slippage is not set for adapter with dexId ${adapter.dexId}`); - } + const pairConfig = pool as PendleMarketAdapterPair; - const token0 = tokens.get(pool.tokenAId); + const token0 = tokens.get(pairConfig.tokenAId); if (token0 === undefined) { - throw new Error(`Can not find token0 '${pool.tokenAId}' for adapter with dexId ${adapter.dexId}`); + throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${adapter.dexId}`); } - const token1 = tokens.get(pool.tokenBId); + const token1 = tokens.get(pairConfig.tokenBId); if (token1 === undefined) { - throw new Error(`Can not find token1 '${pool.tokenBId}' for adapter with dexId ${adapter.dexId}`); + throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${adapter.dexId}`); } adapterParams.push({ type: 'pendleMarket', ptToken: token0, ibToken: token1, - pendleMarket: EthAddress.parse(pool.poolAddress), - slippage: pool.slippage, + pendleMarket: EthAddress.parse(pairConfig.poolAddress), + slippage: pairConfig.slippage, + }); + } else if (adapter.adapterName === 'PendleCurveAdapter') { + const pairConfig = pool as PendleCurveAdapterPair; + + const ibToken = tokens.get(pairConfig.ibTokenId); + if (ibToken === undefined) { + throw new Error(`Can not find ibToken '${pairConfig.ibTokenId}' for adapter with dexId ${adapter.dexId}`); + } + + const quoteToken = tokens.get(pairConfig.quoteTokenId); + if (quoteToken === undefined) { + throw new Error( + `Can not find quoteToken '${pairConfig.quoteTokenId}' for adapter with dexId ${adapter.dexId}` + ); + } + + adapterParams.push({ + type: 'pendleCurve', + pendleMarket: EthAddress.parse(pairConfig.pendleMarket), + slippage: pairConfig.slippage, + curveSlippage: pairConfig.curveSlippage, + curvePool: EthAddress.parse(pairConfig.curvePool), + ibToken: ibToken, + quoteToken: quoteToken, + }); + } else if (adapter.adapterName === 'PendleCurveRouter') { + const pairConfig = pool as PendleCurveRouterAdapterPair; + + if (pairConfig.curveRoute.length !== 11) { + throw new Error( + `Wrong config for Pendle curve router adapter with dexId ${adapter.dexId}. Curve route length must be 11` + ); + } + + if (pairConfig.curvePools.length !== 5) { + throw new Error( + `Wrong config for Pendle curve router adapter with dexId ${adapter.dexId}. Curve pools length must be 5` + ); + } + + if (pairConfig.curveSwapParams.length !== 5) { + throw new Error( + `Wrong config for Pendle curve router adapter with dexId ${adapter.dexId}. Curve swap params array must be 5x5` + ); + } + + for (let i = 0; i < 5; i++) { + if (pairConfig.curveSwapParams[i].length !== 5) { + throw new Error( + `Wrong config for Pendle curve router adapter with dexId ${adapter.dexId}. Curve swap params array must be 5x5` + ); + } + } + + adapterParams.push({ + type: 'pendleCurveRouter', + pendleMarket: EthAddress.parse(pairConfig.pendleMarket), + slippage: pairConfig.slippage, + curveSlippage: pairConfig.curveSlippage, + curveRoute: pairConfig.curveRoute.map(EthAddress.parse), + curveSwapParams: pairConfig.curveSwapParams, + curvePools: pairConfig.curvePools.map(EthAddress.parse), }); } else { - const poolAddress = EthAddress.parse(pool.poolAddress); - const token0 = tokens.get(pool.tokenAId); + const pairConfig = pool as GeneralAdapterPair; + + const poolAddress = EthAddress.parse(pairConfig.poolAddress); + const token0 = tokens.get(pairConfig.tokenAId); if (token0 === undefined) { - throw new Error(`Can not find token0 '${pool.tokenAId}' for adapter with dexId ${adapter.dexId}`); + throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${adapter.dexId}`); } - const token1 = tokens.get(pool.tokenBId); + const token1 = tokens.get(pairConfig.tokenBId); if (token1 === undefined) { - throw new Error(`Can not find token1 '${pool.tokenBId}' for adapter with dexId ${adapter.dexId}`); + throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${adapter.dexId}`); } adapterParams.push({ type: 'general', @@ -640,6 +740,7 @@ export class StrictMarginlyDeployConfig { adapters.push({ dexId: BigNumber.from(adapter.dexId), balancerVault: adapter.balancerVault ? EthAddress.parse(adapter.balancerVault) : undefined, + curveRouter: adapter.curveRouter ? EthAddress.parse(adapter.curveRouter) : undefined, name: adapter.adapterName, marginlyAdapterParams: adapterParams, }); diff --git a/packages/deploy/src/index.ts b/packages/deploy/src/index.ts index 342f10d5..6a100ad1 100644 --- a/packages/deploy/src/index.ts +++ b/packages/deploy/src/index.ts @@ -219,7 +219,8 @@ async function processMarginlyRouter( adapter.dexId, adapter.name, adapter.marginlyAdapterParams, - adapter.balancerVault + adapter.balancerVault, + adapter.curveRouter ); printDeployState( `Marginly adapter dexId:${adapter.dexId} name:${adapter.name}`, diff --git a/packages/router/contracts/adapters/PendleCurveNgAdapter.sol b/packages/router/contracts/adapters/PendleCurveNgAdapter.sol new file mode 100644 index 00000000..6856ba9d --- /dev/null +++ b/packages/router/contracts/adapters/PendleCurveNgAdapter.sol @@ -0,0 +1,545 @@ +// 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 '@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/ICurvePool.sol'; + +/// @dev Adapter for swapping PT token to IB and IB to quoteToken using Curve pool +contract PendleCurveNgAdapter is IMarginlyAdapter, Ownable2Step { + using PYIndexLib for IPYieldToken; + + struct RouteData { + /// @dev Pendle Market contract + IPMarket pendleMarket; + /// @dev SY token + IStandardizedYield sy; + /// @dev PT token + IPPrincipalToken pt; + /// @dev YT token + IPYieldToken yt; + /// @dev Slippage for Pendle approx swap + uint8 slippage; + /// @dev Slippage for Curve swap exact output + uint32 curveSlippage; + /// @dev Interest bearing token + address ib; + /// @dev Curve pool address + address curvePool; + /// @dev index of coin in curve pool + uint8 curveInputCoinIndex; + /// @dev index of coin in curve pool + uint8 curveOutputCoinIndex; + } + + struct RouteInput { + address pendleMarket; + // slippage, used in pendle approx swap + uint8 slippage; + uint32 curveSlippage; // by default 10, 0.001%, 0.00001 + address curvePool; + address ibToken; + address quoteToken; + } + + 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; + + 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(RouteInput[] memory routes) { + _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); + } + + 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) + uint256 estimatedQuoteAmount = ICurvePool(routeData.curvePool).get_dx( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + ibAmount + ); + 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), routeData.curvePool, estimatedQuoteAmount); + ICurvePool(routeData.curvePool).exchange( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + estimatedQuoteAmount, + ibAmount, // min output ibAmount + 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 _swapExactInputPreMaturity( + RouteData memory routeData, + address recipient, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + bytes calldata data + ) private returns (uint256 amountOut) { + 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), routeData.curvePool, ibAmountIn); + + //swap ib -> quote token in curveRouter + amountOut = ICurvePool(routeData.curvePool).exchange( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + ibAmountIn, + minAmountOut, + recipient + ); + } else { + // transfer quote token into adapter + IMarginlyRouter(msg.sender).adapterCallback(address(this), amountIn, data); + SafeERC20.forceApprove(IERC20(tokenIn), routeData.curvePool, amountIn); + // swap quote token -> ib + uint256 ibAmount = ICurvePool(routeData.curvePool).exchange( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + amountIn, // quoteToken amount In + 0, // unknown minAmountOut of ib + 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 _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 + }); + + if (tokenIn == address(routeData.pt)) { + //estimate ibIn to get quoteToken amountOut in curve + uint256 estimatedIbAmount = ICurvePool(routeData.curvePool).get_dx( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + amountOut //quoteToken amount + ); + 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), routeData.curvePool, ibRedeemed); + + //swap ib to quote token + ICurvePool(routeData.curvePool).exchange( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + ibRedeemed, + amountOut, + 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 _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); + // approve to curve pool + SafeERC20.forceApprove(IERC20(routeData.ib), routeData.curvePool, ibAmount); + + // ib to quote in curve + amountOut = ICurvePool(routeData.curvePool).exchange( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + ibAmount, + minAmountOut, + recipient + ); + } else { + // quote to pt swap is not possible after maturity + revert NotSupported(); + } + } + + 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)) { + // estimate on curve ibAmount to get amountOut + uint256 estimatedIbAmount = ICurvePool(routeData.curvePool).get_dx( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + amountOut // quoteAmountOut + ); + + // 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), routeData.curvePool, ibRedeemed); + + // exchange ib to quoteToken in curve + ICurvePool(routeData.curvePool).exchange( + int128(uint128(routeData.curveInputCoinIndex)), + int128(uint128(routeData.curveOutputCoinIndex)), + ibRedeemed, + amountOut, + 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 _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(); + + (IStandardizedYield sy, IPPrincipalToken pt, IPYieldToken yt) = IPMarket(input.pendleMarket).readTokens(); + if (!sy.isValidTokenIn(input.ibToken)) revert WrongInput(); + if (!sy.isValidTokenOut(input.ibToken)) revert WrongInput(); + + int8 ibTokenCurveIndex = -1; // -1 means not initialized value + int8 quoteTokenCurveIndex = -1; + + uint256 coinsCount = ICurvePool(input.curvePool).N_COINS(); + for (uint256 coinsIdx; coinsIdx < coinsCount; ) { + address coin = ICurvePool(input.curvePool).coins(coinsIdx); + if (coin == input.ibToken) { + ibTokenCurveIndex = int8(int256(coinsIdx)); + } else if (coin == input.quoteToken) { + quoteTokenCurveIndex = int8(int256(coinsIdx)); + } + + unchecked { + ++coinsIdx; + } + } + + if (ibTokenCurveIndex == -1 || quoteTokenCurveIndex == -1) revert WrongInput(); + + RouteData memory ptToQuoteSwapRoute = RouteData({ + pendleMarket: IPMarket(input.pendleMarket), + ib: input.ibToken, + sy: sy, + pt: pt, + yt: yt, + slippage: input.slippage, + curveSlippage: input.curveSlippage, + curvePool: input.curvePool, + curveInputCoinIndex: uint8(ibTokenCurveIndex), + curveOutputCoinIndex: uint8(quoteTokenCurveIndex) + }); + + RouteData memory quoteToPtSwapRoute = RouteData({ + pendleMarket: IPMarket(input.pendleMarket), + ib: input.ibToken, + sy: sy, + pt: pt, + yt: yt, + slippage: input.slippage, + curveSlippage: input.curveSlippage, + curvePool: input.curvePool, + curveInputCoinIndex: uint8(quoteTokenCurveIndex), + curveOutputCoinIndex: uint8(ibTokenCurveIndex) + }); + + getRouteData[address(pt)][input.quoteToken] = ptToQuoteSwapRoute; + getRouteData[input.quoteToken][address(pt)] = quoteToPtSwapRoute; + + emit NewPair(address(pt), input.quoteToken, input.pendleMarket, input.slippage); + + unchecked { + ++i; + } + } + } +} diff --git a/packages/router/contracts/adapters/PendleCurveRouterNgAdapter.sol b/packages/router/contracts/adapters/PendleCurveRouterNgAdapter.sol new file mode 100644 index 00000000..67f5a634 --- /dev/null +++ b/packages/router/contracts/adapters/PendleCurveRouterNgAdapter.sol @@ -0,0 +1,601 @@ +// 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 '@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'; + +/// @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); + } + + 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 * (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); + } + } + + 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 _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 _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 _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 _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 + } + + 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 + }); + 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 + }); + + 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/contracts/adapters/interfaces/ICurvePool.sol b/packages/router/contracts/adapters/interfaces/ICurvePool.sol index fdfdbe50..af434161 100644 --- a/packages/router/contracts/adapters/interfaces/ICurvePool.sol +++ b/packages/router/contracts/adapters/interfaces/ICurvePool.sol @@ -14,12 +14,15 @@ interface ICurvePool { */ function exchange(int128 i, int128 j, uint256 _dx, uint256 _min_dy, address _receiver) external returns (uint256); + /* Returns number of coins */ + function N_COINS() external view returns (uint256); + /* @notice Get address of coin with `i` index @param i Index of coin @return Address of coin with `i` index */ - function coins(uint256 i) external returns (address); + function coins(uint256 i) external view returns (address); /* @notice Calculate the current output dy given input `dx` @@ -31,6 +34,8 @@ interface ICurvePool { */ function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256); + function get_dx(int128 i, int128 j, uint256 dy) external view returns (uint256); + /* @notice price have 18 decimals even if the tokens have different decimals, or both tokens have same decimals != 18 diff --git a/packages/router/contracts/adapters/interfaces/ICurveRouterNg.sol b/packages/router/contracts/adapters/interfaces/ICurveRouterNg.sol new file mode 100644 index 00000000..332daf61 --- /dev/null +++ b/packages/router/contracts/adapters/interfaces/ICurveRouterNg.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +interface ICurveRouterNg { + function exchange( + address[11] memory route, + uint256[5][5] memory swapParams, + uint256 amount, + uint256 minDy, + address[5] memory pools, + address receiver + ) external returns (uint256); + + function get_dy( + address[11] memory route, + uint256[5][5] memory swapParams, + uint256 amount + ) external view returns (uint256); + + function get_dx( + address[11] memory route, + uint256[5][5] memory swapParams, + uint256 outAmount, + address[5] memory pools + ) external view returns (uint256); +} diff --git a/packages/router/contracts/test/CurveEMATest/TestStableSwap2EMAOraclePool.sol b/packages/router/contracts/test/CurveEMATest/TestStableSwap2EMAOraclePool.sol index 0069f7b2..363ff812 100644 --- a/packages/router/contracts/test/CurveEMATest/TestStableSwap2EMAOraclePool.sol +++ b/packages/router/contracts/test/CurveEMATest/TestStableSwap2EMAOraclePool.sol @@ -49,6 +49,14 @@ contract TestStableSwap2EMAOraclePool is ICurvePool { } } + function get_dx(int128 i, int128 j, uint256 dy) public view returns (uint256 dx) { + revert('Not implemented'); + } + + function N_COINS() external view returns (uint256) { + return 2; + } + function price_oracle() external view returns (uint256) { return price; } diff --git a/packages/router/hardhat-configs/hardhat-eth-fork.config.ts b/packages/router/hardhat-configs/hardhat-eth-fork.config.ts index 14a9397b..f998ffd0 100644 --- a/packages/router/hardhat-configs/hardhat-eth-fork.config.ts +++ b/packages/router/hardhat-configs/hardhat-eth-fork.config.ts @@ -9,7 +9,7 @@ const config = { forking: { enabled: true, url: 'https://rpc.ankr.com/eth', - blockNumber: 20032943, + blockNumber: 21493100, }, }, }, diff --git a/packages/router/hardhat-configs/hardhat.config.ts b/packages/router/hardhat-configs/hardhat.config.ts index 80e495dc..efa4b0ff 100644 --- a/packages/router/hardhat-configs/hardhat.config.ts +++ b/packages/router/hardhat-configs/hardhat.config.ts @@ -11,9 +11,10 @@ const config = { solidity: { version: '0.8.19', settings: { + viaIR: true, optimizer: { enabled: true, - runs: 100_000, + runs: 5_000_000, }, }, }, diff --git a/packages/router/test/int/PendleCurveNgAdapterEBTC.eth.spec.ts b/packages/router/test/int/PendleCurveNgAdapterEBTC.eth.spec.ts new file mode 100644 index 00000000..72db766c --- /dev/null +++ b/packages/router/test/int/PendleCurveNgAdapterEBTC.eth.spec.ts @@ -0,0 +1,376 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + ERC20, + MarginlyRouter, + MarginlyRouter__factory, + PendleCurveNgAdapter, + PendleCurveNgAdapter__factory, +} from '../../typechain-types'; +import { constructSwap, Dex, showGasUsage, SWAP_ONE, resetFork } 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; + wbtcToken: ERC20; + ebtcToken: ERC20; + router: MarginlyRouter; + pendleCurveAdapter: PendleCurveNgAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; +}> { + const [owner, user] = await ethers.getSigners(); + const ptToken = await ethers.getContractAt('ERC20', '0x44a7876ca99460ef3218bf08b5f52e2dbe199566'); + const wbtcToken = await ethers.getContractAt('ERC20', '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'); + const ebtcToken = await ethers.getContractAt('ERC20', '0x657e8c867d8b37dcc18fa4caead9c45eb088c642'); + const pendleMarket = '0x2c71ead7ac9ae53d05f8664e77031d4f9eba064b'; + + // Route to make swap PT-eBTC -> usde -> WBTC + const routeInput: PendleCurveNgAdapter.RouteInputStruct = { + pendleMarket: pendleMarket, + slippage: 20, // 20/100 = 20% + curveSlippage: 100, // 10/1000000 = 0.001% + //curvePool: '0xabaf76590478f2fe0b396996f55f0b61101e9502', //TriBTCPool + curvePool: '0x7704d01908afd31bf647d969c295bb45230cd2d6', //ebtc/WBTC pool + ibToken: ebtcToken.address, + quoteToken: wbtcToken.address, + }; + const pendleCurveAdapter = await new PendleCurveNgAdapter__factory().connect(owner).deploy([routeInput]); + + const routerInput = { + dexIndex: Dex.PendleCurve, + adapter: pendleCurveAdapter.address, + }; + const router = await new MarginlyRouter__factory().connect(owner).deploy([routerInput]); + + await setTokenBalance( + wbtcToken.address, + EthereumMainnetERC20BalanceOfSlot.WBTC, + EthAddress.parse(user.address), + parseUnits('10', 8) + ); + await setTokenBalance( + ptToken.address, + EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + EthAddress.parse(user.address), + parseUnits('10', 8) + ); + + return { + ptToken, + wbtcToken: wbtcToken, + ebtcToken: ebtcToken, + router, + pendleCurveAdapter, + owner, + user, + }; +} + +// Tests for running in ethereum mainnet fork +describe('PendleCurveAdapter PT-eBTC - WBTC', () => { + before(async () => { + await resetFork(21493100); + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let WBTC: ERC20; + let ebtc: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + wbtcToken: WBTC, + ebtcToken: ebtc, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + }); + + it('WBTC to PT-eBTC exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `PT-eBTC balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WBTCBalanceBefore = await WBTC.balanceOf(user.address); + console.log( + `WBTC balance before: ${formatUnits(WBTCBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const WBTCSwapAmount = parseUnits('2', 8); + await WBTC.connect(user).approve(router.address, WBTCSwapAmount); + + const minPtAmountOut = parseUnits('1.8', 8); + + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, WBTC.address, ptToken.address, WBTCSwapAmount, minPtAmountOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + const WBTCBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`WBTCBalanceAfter: ${formatUnits(WBTCBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(WBTCBalanceBefore.sub(WBTCBalanceAfter)).to.be.lessThanOrEqual(WBTCSwapAmount); + }); + + it('WBTC to PT-eBTC exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `PT-eBTC balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WBTCBalanceBefore = await WBTC.balanceOf(user.address); + console.log( + `WBTC balance before: ${formatUnits(WBTCBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + + const exactPtOut = parseUnits('1', 8); + const WBTCMaxIn = parseUnits('2.5', 8); + await WBTC.connect(user).approve(router.address, WBTCMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, WBTC.address, ptToken.address, WBTCMaxIn, 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 WBTCBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`WBTCBalanceAfter: ${formatUnits(WBTCBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(WBTCBalanceBefore).to.be.greaterThan(WBTCBalanceAfter); + + const ebtcOnAdapter = await ebtc.balanceOf(pendleCurveAdapter.address); + console.log(`ebtc stays on adapter: ${formatUnits(ebtcOnAdapter, await ebtc.decimals())} ${await ebtc.symbol()}`); + }); + + it('WBTC to PT-eBTC exact output, small amount', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `PT-eBTC balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WBTCBalanceBefore = await WBTC.balanceOf(user.address); + console.log( + `WBTC balance before: ${formatUnits(WBTCBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + + const exactPtOut = parseUnits('0.0006', 8); + const WBTCMaxIn = parseUnits('0.0012', 8); + await WBTC.connect(user).approve(router.address, WBTCMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, WBTC.address, ptToken.address, WBTCMaxIn, 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 WBTCBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`WBTCBalanceAfter: ${formatUnits(WBTCBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(WBTCBalanceBefore).to.be.greaterThan(WBTCBalanceAfter); + + const ebtcOnAdapter = await ebtc.balanceOf(pendleCurveAdapter.address); + console.log(`ebtc stays on adapter: ${formatUnits(ebtcOnAdapter, await ebtc.decimals())} ${await ebtc.symbol()}`); + }); + + it('PT-eBTC to WBTC exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUSDeBalanceBefore = await WBTC.balanceOf(user.address); + console.log( + `WBTCBalanceBefore: ${formatUnits(sUSDeBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const ptIn = parseUnits('0.1', 8); + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, WBTC.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const sUsdeBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`WBTCBalanceAfter: ${formatUnits(sUsdeBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(sUsdeBalanceAfter).to.be.greaterThan(sUSDeBalanceBefore); + }); + + it('PT-eBTC to WBTC exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `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()}`); + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const WBTCOut = parseUnits('1', 8); + const maxPtIn = parseUnits('1.2', 8); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, WBTC.address, maxPtIn, WBTCOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const WBTCBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`WBTCBalanceAfter: ${formatUnits(WBTCBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(WBTCBalanceAfter.sub(WBTCBalanceBefore)).to.be.eq(WBTCOut); + + const WBTCBalanceOnAdapter = await WBTC.balanceOf(pendleCurveAdapter.address); + console.log( + `WBTCBalanceOnAdapter: ${formatUnits(WBTCBalanceOnAdapter, await WBTC.decimals())} ${await WBTC.symbol()}` + ); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let WBTC: ERC20; + let usde: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + wbtcToken: WBTC, + ebtcToken: usde, + 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('WBTC to PT-eBTC exact input, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUsdeBalanceBefore = await WBTC.balanceOf(user.address); + console.log( + `WBTCBalanceBefore: ${formatUnits(sUsdeBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + await WBTC.connect(user).approve(router.address, sUsdeBalanceBefore); + const tx = router + .connect(user) + .swapExactInput( + swapCalldata, + WBTC.address, + ptToken.address, + sUsdeBalanceBefore, + sUsdeBalanceBefore.mul(9).div(10) + ); + + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + const sUsdeBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`WBTCBalanceAfter: ${formatUnits(sUsdeBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(sUsdeBalanceAfter).to.be.eq(sUsdeBalanceBefore); + }); + + it('WBTC to PT-eBTC exact output, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WBTCBalanceBefore = await WBTC.balanceOf(user.address); + console.log( + `sUsdeBalanceBefore: ${formatUnits(WBTCBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const ptOut = WBTCBalanceBefore.div(2); + await WBTC.connect(user).approve(router.address, WBTCBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCalldata, WBTC.address, ptToken.address, WBTCBalanceBefore, ptOut); + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + const WBTCBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`WBTCBalanceAfter: ${formatUnits(WBTCBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(WBTCBalanceAfter).to.be.eq(WBTCBalanceBefore); + }); + + it('PT-eBTC to WBTC exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `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()}`); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const ptIn = ptBalanceBefore; + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, WBTC.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const WBTCBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`WBTCBalanceAfter: ${formatUnits(WBTCBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(WBTCBalanceAfter).to.be.greaterThan(WBTCBalanceBefore); + }); + + it('PT-eBTC to WBTC exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUsdeBalanceBefore = await WBTC.balanceOf(user.address); + console.log( + `sUsdeBalanceBefore: ${formatUnits(sUsdeBalanceBefore, await WBTC.decimals())} ${await WBTC.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const WBTCOut = parseUnits('0.9', 8); + await ptToken.connect(user).approve(router.address, ptBalanceBefore); + const maxPtIn = parseUnits('1.3', 8); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, WBTC.address, maxPtIn, WBTCOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const WBTCBalanceAfter = await WBTC.balanceOf(user.address); + console.log(`sUsdeBalanceAfter: ${formatUnits(WBTCBalanceAfter, await WBTC.decimals())} ${await WBTC.symbol()}`); + expect(WBTCBalanceAfter.sub(sUsdeBalanceBefore)).to.be.eq(WBTCOut); + }); + }); +}); diff --git a/packages/router/test/int/PendleCurveNgAdapterEthena.eth.spec.ts b/packages/router/test/int/PendleCurveNgAdapterEthena.eth.spec.ts new file mode 100644 index 00000000..0a3af2b9 --- /dev/null +++ b/packages/router/test/int/PendleCurveNgAdapterEthena.eth.spec.ts @@ -0,0 +1,317 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + ERC20, + MarginlyRouter, + MarginlyRouter__factory, + PendleCurveNgAdapter, + PendleCurveNgAdapter__factory, +} from '../../typechain-types'; +import { constructSwap, Dex, resetFork, showBalance, showGasUsage, SWAP_ONE } 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; + usdcToken: ERC20; + usdeToken: ERC20; + router: MarginlyRouter; + pendleCurveAdapter: PendleCurveNgAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; +}> { + const [owner, user] = await ethers.getSigners(); + const ptToken = await ethers.getContractAt('ERC20', '0x8a47b431a7d947c6a3ed6e42d501803615a97eaa'); + const usdcToken = await ethers.getContractAt('ERC20', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'); + const usdeToken = await ethers.getContractAt('ERC20', '0x4c9edd5852cd905f086c759e8383e09bff1e68b3'); + const pendleMarket = '0xb451a36c8b6b2eac77ad0737ba732818143a0e25'; + + // Route to make swap pt-USDe -> usde -> usdc + const routeInput: PendleCurveNgAdapter.RouteInputStruct = { + pendleMarket: pendleMarket, + slippage: 20, // 20/100 = 20% + curveSlippage: 10, // 10/1000000 = 0.001% + curvePool: '0x02950460e2b9529d0e00284a5fa2d7bdf3fa4d72', + ibToken: '0x4c9edd5852cd905f086c759e8383e09bff1e68b3', + quoteToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + }; + const pendleCurveAdapter = await new PendleCurveNgAdapter__factory().connect(owner).deploy([routeInput]); + + const routerInput = { + dexIndex: Dex.PendleCurveRouter, + adapter: pendleCurveAdapter.address, + }; + const router = await new MarginlyRouter__factory().connect(owner).deploy([routerInput]); + + await setTokenBalance( + usdcToken.address, + EthereumMainnetERC20BalanceOfSlot.USDC, + EthAddress.parse(user.address), + parseUnits('1000', 6) + ); + await setTokenBalance( + ptToken.address, + EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + EthAddress.parse(user.address), + parseUnits('1000', 18) + ); + + return { + ptToken, + usdcToken, + usdeToken, + router, + pendleCurveAdapter, + owner, + user, + }; +} + +// Tests for running in ethereum mainnet fork +describe('PendleCurveAdapter PT-usde - usdc', () => { + before(async () => { + await resetFork(21493100); + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let usdc: ERC20; + let usde: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + usdcToken: usdc, + usdeToken: usde, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + }); + + it('USDC to pt-USDe exact input', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcSwapAmount = parseUnits('100', 6); + await usdc.connect(user).approve(router.address, usdcSwapAmount); + + const minPtAmountOut = parseUnits('90', 18); //parseUnits('900', 18); + + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, usdc.address, ptToken.address, usdcSwapAmount, minPtAmountOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt-usde balance After:'); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdc balance After:'); + expect(usdcBalanceBefore.sub(usdcBalanceAfter)).to.be.lessThanOrEqual(usdcSwapAmount); + }); + + it('USDC to pt-USDe exact output', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('500', 18); + const usdcMaxIn = parseUnits('1000', 6); + await usdc.connect(user).approve(router.address, usdcMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, usdc.address, ptToken.address, usdcMaxIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'ptBalanceAfter:'); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdcBalanceAfter: '); + expect(usdcBalanceBefore).to.be.greaterThan(usdcBalanceAfter); + + await showBalance(usde, pendleCurveAdapter.address, 'usde stays on adapter: '); + }); + + it('USDC to pt-USDe exact output, small amount', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('1', 18); + const usdcMaxIn = parseUnits('2', 6); + await usdc.connect(user).approve(router.address, usdcMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, usdc.address, ptToken.address, usdcMaxIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt-usde balance After:'); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdc balance After:'); + expect(usdcBalanceBefore).to.be.greaterThan(usdcBalanceAfter); + + await showBalance(usde, pendleCurveAdapter.address, 'usde stays on adapter: '); + }); + + it('pt-USDe to USDC exact input', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + 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, usdc.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'ptBalanceAfter:'); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdc balance After:'); + expect(usdcBalanceAfter).to.be.greaterThan(usdcBalanceBefore); + }); + + it('pt-USDe to USDC exact output', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcOut = parseUnits('500', 6); + const maxPtIn = parseUnits('600', 18); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, usdc.address, maxPtIn, usdcOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'ptBalanceAfter:'); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdc balance After:'); + expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.eq(usdcOut); + + await showBalance(usdc, pendleCurveAdapter.address, 'USDC stays on adapter: '); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let usdc: ERC20; + let usde: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + usdcToken: usdc, + usdeToken: usde, + 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('USDC to pt-USDe exact input, forbidden', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + await usdc.connect(user).approve(router.address, usdcBalanceBefore); + const tx = router + .connect(user) + .swapExactInput( + swapCalldata, + usdc.address, + ptToken.address, + usdcBalanceBefore, + usdcBalanceBefore.mul(9).div(10) + ); + + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'ptBalanceAfter:'); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdc balance After:'); + expect(usdcBalanceAfter).to.be.eq(usdcBalanceBefore); + }); + + it('USDC to pt-USDe exact output, forbidden', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptOut = usdcBalanceBefore.div(2); + await usdc.connect(user).approve(router.address, usdcBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCalldata, usdc.address, ptToken.address, usdcBalanceBefore, ptOut); + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'ptBalanceAfter:'); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdc balance After:'); + expect(usdcBalanceAfter).to.be.eq(usdcBalanceBefore); + }); + + it('pt-USDe to USDC exact input', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + 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, usdc.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'ptBalanceAfter:'); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdc balance After:'); + expect(usdcBalanceAfter).to.be.greaterThan(usdcBalanceBefore); + }); + + it('pt-USDe to USDC exact output', async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt-usde balance Before:'); + const usdcBalanceBefore = await showBalance(usdc, user.address, 'USDC balance before:'); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcOut = parseUnits('900', 6); + await ptToken.connect(user).approve(router.address, ptBalanceBefore); + const maxPtIn = parseUnits('1000', 18); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, usdc.address, maxPtIn, usdcOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'ptBalanceAfter:'); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + + const usdcBalanceAfter = await showBalance(usdc, user.address, 'usdc balance After:'); + expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.eq(usdcOut); + }); + }); +}); diff --git a/packages/router/test/int/PendleCurveNgAdapterWEETH.eth.spec.ts b/packages/router/test/int/PendleCurveNgAdapterWEETH.eth.spec.ts new file mode 100644 index 00000000..7a102873 --- /dev/null +++ b/packages/router/test/int/PendleCurveNgAdapterWEETH.eth.spec.ts @@ -0,0 +1,378 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + ERC20, + MarginlyRouter, + MarginlyRouter__factory, + PendleCurveNgAdapter, + PendleCurveNgAdapter__factory, +} from '../../typechain-types'; +import { constructSwap, Dex, resetFork, showGasUsage, SWAP_ONE } 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; + WETHToken: ERC20; + ebtcToken: ERC20; + router: MarginlyRouter; + pendleCurveAdapter: PendleCurveNgAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; +}> { + const [owner, user] = await ethers.getSigners(); + const ptToken = await ethers.getContractAt('ERC20', '0xef6122835a2bbf575d0117d394fda24ab7d09d4e'); + const wethToken = await ethers.getContractAt('ERC20', '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'); + const weethToken = await ethers.getContractAt('ERC20', '0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee'); + const pendleMarket = '0xf4cf59259d007a96c641b41621ab52c93b9691b1'; + + // Route to make swap PT-eETH -> usde -> WETH + const routeInput: PendleCurveNgAdapter.RouteInputStruct = { + pendleMarket: pendleMarket, + slippage: 35, // 20/100 = 20% + curveSlippage: 10, // 10/1000000 = 0.001% + curvePool: '0xdb74dfdd3bb46be8ce6c33dc9d82777bcfc3ded5', //weETH/WETH pool + ibToken: weethToken.address, + quoteToken: wethToken.address, + }; + const pendleCurveAdapter = await new PendleCurveNgAdapter__factory().connect(owner).deploy([routeInput]); + + const routerInput = { + dexIndex: Dex.PendleCurve, + adapter: pendleCurveAdapter.address, + }; + const router = await new MarginlyRouter__factory().connect(owner).deploy([routerInput]); + + await setTokenBalance( + wethToken.address, + EthereumMainnetERC20BalanceOfSlot.WETH, + EthAddress.parse(user.address), + parseUnits('10', await wethToken.decimals()) + ); + await setTokenBalance( + ptToken.address, + EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + EthAddress.parse(user.address), + parseUnits('10', await ptToken.decimals()) + ); + + return { + ptToken, + WETHToken: wethToken, + ebtcToken: weethToken, + router, + pendleCurveAdapter, + owner, + user, + }; +} + +// Tests for running in ethereum mainnet fork +describe('PendleCurveAdapter PT-eETH - WETH', () => { + before(async () => { + await resetFork(21493100); + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let WETH: ERC20; + let ebtc: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + WETHToken: WETH, + ebtcToken: ebtc, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + }); + + it('WETH to PT-eETH exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `PT-eETH balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WETHBalanceBefore = await WETH.balanceOf(user.address); + console.log( + `WETH balance before: ${formatUnits(WETHBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const WETHSwapAmount = parseUnits('2', 18); + await WETH.connect(user).approve(router.address, WETHSwapAmount); + + const minPtAmountOut = parseUnits('1.8', 18); + + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, WETH.address, ptToken.address, WETHSwapAmount, minPtAmountOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + const WETHBalanceAfter = await WETH.balanceOf(user.address); + console.log(`WETHBalanceAfter: ${formatUnits(WETHBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(WETHBalanceBefore.sub(WETHBalanceAfter)).to.be.lessThanOrEqual(WETHSwapAmount); + }); + + it('WETH to PT-eETH exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `PT-eETH balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WETHBalanceBefore = await WETH.balanceOf(user.address); + console.log( + `WETH balance before: ${formatUnits(WETHBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + + const exactPtOut = parseUnits('5', 18); + const WETHMaxIn = parseUnits('6', 18); + await WETH.connect(user).approve(router.address, WETHMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, WETH.address, ptToken.address, WETHMaxIn, 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 WETHBalanceAfter = await WETH.balanceOf(user.address); + console.log(`WETHBalanceAfter: ${formatUnits(WETHBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(WETHBalanceBefore).to.be.greaterThan(WETHBalanceAfter); + + const ebtcOnAdapter = await ebtc.balanceOf(pendleCurveAdapter.address); + console.log(`ebtc stays on adapter: ${formatUnits(ebtcOnAdapter, await ebtc.decimals())} ${await ebtc.symbol()}`); + }); + + it('WETH to PT-eETH exact output, small amount', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `PT-eETH balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WETHBalanceBefore = await WETH.balanceOf(user.address); + console.log( + `WETH balance before: ${formatUnits(WETHBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + + const exactPtOut = parseUnits('0.001', 18); + const WETHMaxIn = parseUnits('0.01', 18); + await WETH.connect(user).approve(router.address, WETHMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, WETH.address, ptToken.address, WETHMaxIn, 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 WETHBalanceAfter = await WETH.balanceOf(user.address); + console.log(`WETHBalanceAfter: ${formatUnits(WETHBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(WETHBalanceBefore).to.be.greaterThan(WETHBalanceAfter); + + const ebtcOnAdapter = await ebtc.balanceOf(pendleCurveAdapter.address); + console.log(`ebtc stays on adapter: ${formatUnits(ebtcOnAdapter, await ebtc.decimals())} ${await ebtc.symbol()}`); + }); + + it('PT-eETH to WETH exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUSDeBalanceBefore = await WETH.balanceOf(user.address); + console.log( + `WETHBalanceBefore: ${formatUnits(sUSDeBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const ptIn = parseUnits('0.1', 18); + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router.connect(user).swapExactInput(swapCalldata, ptToken.address, WETH.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const sUsdeBalanceAfter = await WETH.balanceOf(user.address); + console.log(`WETHBalanceAfter: ${formatUnits(sUsdeBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(sUsdeBalanceAfter).to.be.greaterThan(sUSDeBalanceBefore); + }); + + it('PT-eETH to WETH exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WETHBalanceBefore = await WETH.balanceOf(user.address); + console.log(`WETHBalanceBefore: ${formatUnits(WETHBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}`); + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const WETHOut = parseUnits('1', 18); + const maxPtIn = parseUnits('1.2', 18); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, WETH.address, maxPtIn, WETHOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const WETHBalanceAfter = await WETH.balanceOf(user.address); + console.log(`WETHBalanceAfter: ${formatUnits(WETHBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(WETHBalanceAfter.sub(WETHBalanceBefore)).to.be.eq(WETHOut); + + const WETHBalanceOnAdapter = await WETH.balanceOf(pendleCurveAdapter.address); + console.log( + `WETHBalanceOnAdapter: ${formatUnits(WETHBalanceOnAdapter, await WETH.decimals())} ${await WETH.symbol()}` + ); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let WETH: ERC20; + let usde: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + WETHToken: WETH, + ebtcToken: usde, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + + // move time and make after maturity + await ethers.provider.send('evm_increaseTime', [300 * 24 * 60 * 60]); + await ethers.provider.send('evm_mine', []); + }); + + it('WETH to PT-eETH exact input, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUsdeBalanceBefore = await WETH.balanceOf(user.address); + console.log( + `WETHBalanceBefore: ${formatUnits(sUsdeBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + await WETH.connect(user).approve(router.address, sUsdeBalanceBefore); + const tx = router + .connect(user) + .swapExactInput( + swapCalldata, + WETH.address, + ptToken.address, + sUsdeBalanceBefore, + sUsdeBalanceBefore.mul(9).div(10) + ); + + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + const sUsdeBalanceAfter = await WETH.balanceOf(user.address); + console.log(`WETHBalanceAfter: ${formatUnits(sUsdeBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(sUsdeBalanceAfter).to.be.eq(sUsdeBalanceBefore); + }); + + it('WETH to PT-eETH exact output, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WETHBalanceBefore = await WETH.balanceOf(user.address); + console.log( + `sUsdeBalanceBefore: ${formatUnits(WETHBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const ptOut = WETHBalanceBefore.div(2); + await WETH.connect(user).approve(router.address, WETHBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCalldata, WETH.address, ptToken.address, WETHBalanceBefore, ptOut); + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + const WETHBalanceAfter = await WETH.balanceOf(user.address); + console.log(`WETHBalanceAfter: ${formatUnits(WETHBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(WETHBalanceAfter).to.be.eq(WETHBalanceBefore); + }); + + it('PT-eETH to WETH exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const WETHBalanceBefore = await WETH.balanceOf(user.address); + console.log(`WETHBalanceBefore: ${formatUnits(WETHBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}`); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const ptIn = parseUnits('1', await ptToken.decimals()); //ptBalanceBefore; + const minWETHAmountOut = parseUnits('0.7', await WETH.decimals()); + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, ptToken.address, WETH.address, ptIn, minWETHAmountOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const WETHBalanceAfter = await WETH.balanceOf(user.address); + console.log(`WETHBalanceAfter: ${formatUnits(WETHBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(WETHBalanceAfter).to.be.greaterThan(WETHBalanceBefore); + }); + + it('PT-eETH to WETH exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUsdeBalanceBefore = await WETH.balanceOf(user.address); + console.log( + `sUsdeBalanceBefore: ${formatUnits(sUsdeBalanceBefore, await WETH.decimals())} ${await WETH.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurve], [SWAP_ONE]); + const WETHOut = parseUnits('0.9', await WETH.decimals()); + await ptToken.connect(user).approve(router.address, ptBalanceBefore); + const maxPtIn = parseUnits('1.3', await ptToken.decimals()); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, WETH.address, maxPtIn, WETHOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const WETHBalanceAfter = await WETH.balanceOf(user.address); + console.log(`sUsdeBalanceAfter: ${formatUnits(WETHBalanceAfter, await WETH.decimals())} ${await WETH.symbol()}`); + expect(WETHBalanceAfter.sub(sUsdeBalanceBefore)).to.be.eq(WETHOut); + }); + }); +}); diff --git a/packages/router/test/int/PendleCurveRouterNgEthena.eth.spec.ts b/packages/router/test/int/PendleCurveRouterNgEthena.eth.spec.ts new file mode 100644 index 00000000..17d25cf2 --- /dev/null +++ b/packages/router/test/int/PendleCurveRouterNgEthena.eth.spec.ts @@ -0,0 +1,374 @@ +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 } 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; + usdcToken: ERC20; + usdeToken: ERC20; + router: MarginlyRouter; + pendleCurveAdapter: PendleCurveRouterNgAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; +}> { + const [owner, user] = await ethers.getSigners(); + const ptToken = await ethers.getContractAt('ERC20', '0x8a47b431a7d947c6a3ed6e42d501803615a97eaa'); + const usdcToken = await ethers.getContractAt('ERC20', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'); + const usdeToken = await ethers.getContractAt('ERC20', '0x4c9edd5852cd905f086c759e8383e09bff1e68b3'); + const pendleMarket = '0xb451a36c8b6b2eac77ad0737ba732818143a0e25'; + const curveRouterAddress = '0x16c6521dff6bab339122a0fe25a9116693265353'; + + // Route to make swap pt-USDe -> usde -> usdc + const routeInput: PendleCurveRouterNgAdapter.RouteInputStruct = { + pendleMarket: pendleMarket, + slippage: 20, // 20/100 = 20% + curveSlippage: 100, // 10/1000000 = 0.001% + curveRoute: [ + '0x4c9edd5852cd905f086c759e8383e09bff1e68b3', + '0x02950460e2b9529d0e00284a5fa2d7bdf3fa4d72', + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + ], // curve route usd0++ -> usd0 -> usdc + curveSwapParams: [ + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + curvePools: [ + '0x02950460e2b9529d0e00284a5fa2d7bdf3fa4d72', + '0x0000000000000000000000000000000000000000', + '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( + usdcToken.address, + EthereumMainnetERC20BalanceOfSlot.USDC, + EthAddress.parse(user.address), + parseUnits('1000', 6) + ); + await setTokenBalance( + ptToken.address, + EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + EthAddress.parse(user.address), + parseUnits('1000', 18) + ); + + return { + ptToken, + usdcToken, + usdeToken, + router, + pendleCurveAdapter, + owner, + user, + }; +} + +// Tests for running in ethereum mainnet fork +describe('PendleCurveRouter PT-usde - usdc', () => { + before(async () => { + await resetFork(21493100); + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let usdc: ERC20; + let usde: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + usdcToken: usdc, + usdeToken: usde, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + }); + + it('USDC to pt-USDe exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `pt-usde balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `USDC balance before: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcSwapAmount = parseUnits('100', 6); + await usdc.connect(user).approve(router.address, usdcSwapAmount); + + const minPtAmountOut = parseUnits('90', 18); //parseUnits('900', 18); + + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, usdc.address, ptToken.address, usdcSwapAmount, minPtAmountOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceBefore.sub(usdcBalanceAfter)).to.be.lessThanOrEqual(usdcSwapAmount); + }); + + it('USDC to pt-USDe exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `pt-USDe balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `USDC balance before: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('500', 18); + const usdcMaxIn = parseUnits('600', 6); + await usdc.connect(user).approve(router.address, usdcMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, usdc.address, ptToken.address, usdcMaxIn, 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 usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceBefore).to.be.greaterThan(usdcBalanceAfter); + + const usd0PlusPlusOnAdapter = await usde.balanceOf(pendleCurveAdapter.address); + console.log( + `usde stays on adapter: ${formatUnits(usd0PlusPlusOnAdapter, await usde.decimals())} ${await usde.symbol()}` + ); + }); + + it('pt-USDe to USDC exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUSDeBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `usdcBalanceBefore: ${formatUnits(sUSDeBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + 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, usdc.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const sUsdeBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(sUsdeBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(sUsdeBalanceAfter).to.be.greaterThan(sUSDeBalanceBefore); + }); + + it('pt-USDe to USDC exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log(`usdcBalanceBefore: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}`); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcOut = parseUnits('500', 6); + const maxPtIn = parseUnits('600', 18); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, usdc.address, maxPtIn, usdcOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.eq(usdcOut); + + const usdcBalanceOnAdapter = await usdc.balanceOf(pendleCurveAdapter.address); + console.log( + `usdcBalanceOnAdapter: ${formatUnits(usdcBalanceOnAdapter, await usdc.decimals())} ${await usdc.symbol()}` + ); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let usdc: ERC20; + let usde: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + usdcToken: usdc, + usdeToken: usde, + 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('USDC to pt-USDe exact input, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUsdeBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `usdcBalanceBefore: ${formatUnits(sUsdeBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + await usdc.connect(user).approve(router.address, sUsdeBalanceBefore); + const tx = router + .connect(user) + .swapExactInput( + swapCalldata, + usdc.address, + ptToken.address, + sUsdeBalanceBefore, + sUsdeBalanceBefore.mul(9).div(10) + ); + + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + const sUsdeBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(sUsdeBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(sUsdeBalanceAfter).to.be.eq(sUsdeBalanceBefore); + }); + + it('USDC to pt-USDe exact output, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `sUsdeBalanceBefore: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptOut = usdcBalanceBefore.div(2); + await usdc.connect(user).approve(router.address, usdcBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCalldata, usdc.address, ptToken.address, usdcBalanceBefore, ptOut); + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceAfter).to.be.eq(usdcBalanceBefore); + }); + + it('pt-USDe to USDC exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log(`usdcBalanceBefore: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}`); + + 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, usdc.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceAfter).to.be.greaterThan(usdcBalanceBefore); + }); + + it('pt-USDe to USDC exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUsdeBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `sUsdeBalanceBefore: ${formatUnits(sUsdeBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcOut = parseUnits('900', 6); + await ptToken.connect(user).approve(router.address, ptBalanceBefore); + const maxPtIn = parseUnits('1000', 18); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, usdc.address, maxPtIn, usdcOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`sUsdeBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceAfter.sub(sUsdeBalanceBefore)).to.be.eq(usdcOut); + }); + }); +}); diff --git a/packages/router/test/int/PendleCurveRouterNgUSD0.eth.spec.ts b/packages/router/test/int/PendleCurveRouterNgUSD0.eth.spec.ts new file mode 100644 index 00000000..2531a5ee --- /dev/null +++ b/packages/router/test/int/PendleCurveRouterNgUSD0.eth.spec.ts @@ -0,0 +1,412 @@ +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 } 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; + usdcToken: ERC20; + usd0PlusPlusToken: ERC20; + router: MarginlyRouter; + pendleCurveAdapter: PendleCurveRouterNgAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; +}> { + const [owner, user] = await ethers.getSigners(); + const ptToken = await ethers.getContractAt('ERC20', '0xd86f4d98b34108cb4c059d540bd513f09b2ddd30'); + const usdcToken = await ethers.getContractAt('ERC20', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'); + const usd0PlusPlusToken = await ethers.getContractAt('ERC20', '0x35d8949372d46b7a3d5a56006ae77b215fc69bc0'); + const pendleMarket = '0x81f3a11db1de16f4f9ba8bf46b71d2b168c64899'; + const curveRouterAddress = '0x16c6521dff6bab339122a0fe25a9116693265353'; + + // Route to make swap pt-usd0++ -> usd0++ -> usd0 -> usdc + const routeInput: PendleCurveRouterNgAdapter.RouteInputStruct = { + pendleMarket: pendleMarket, + slippage: 20, // 20/100 = 20% + curveSlippage: 10, // 10/1000000 = 0.001% + curveRoute: [ + '0x35d8949372d46b7a3d5a56006ae77b215fc69bc0', + '0x1d08e7adc263cfc70b1babe6dc5bb339c16eec52', + '0x73a15fed60bf67631dc6cd7bc5b6e8da8190acf5', + '0x14100f81e33c33ecc7cdac70181fb45b6e78569f', + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + ], // curve route usd0++ -> usd0 -> usdc + curveSwapParams: [ + [1, 0, 1, 1, 2], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + curvePools: [ + '0x1d08e7adc263cfc70b1babe6dc5bb339c16eec52', + '0x14100f81e33c33ecc7cdac70181fb45b6e78569f', + '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( + usdcToken.address, + EthereumMainnetERC20BalanceOfSlot.USDC, + EthAddress.parse(user.address), + parseUnits('1000', 6) + ); + await setTokenBalance( + ptToken.address, + EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + EthAddress.parse(user.address), + parseUnits('1000', 18) + ); + + return { + ptToken, + usdcToken, + usd0PlusPlusToken, + router, + pendleCurveAdapter, + owner, + user, + }; +} + +// Tests for running in ethereum mainnet fork +describe('Pendle PT-usd0++ - usdc', () => { + before(async () => { + await resetFork(21493100); + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let usdc: ERC20; + let usd0PlusPlusToken: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + usdcToken: usdc, + usd0PlusPlusToken, + router, + pendleCurveAdapter, + owner, + user, + } = await initializeRouter()); + }); + + it('USDC to pt-USD0++ exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `pt-usd0++ balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `USDC balance before: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcSwapAmount = parseUnits('1', 6); + await usdc.connect(user).approve(router.address, usdcSwapAmount); + + const minPtAmountOut = parseUnits('0.9', 18); //parseUnits('900', 18); + + const tx = await router + .connect(user) + .swapExactInput(swapCalldata, usdc.address, ptToken.address, usdcSwapAmount, minPtAmountOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceBefore.sub(usdcBalanceAfter)).to.be.lessThanOrEqual(usdcSwapAmount); + }); + + it('USDC to pt-USD0++ exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `pt-usd0++ balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `USDC balance before: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('500', 18); + const usdcMaxIn = parseUnits('1000', 6); + await usdc.connect(user).approve(router.address, usdcMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, usdc.address, ptToken.address, usdcMaxIn, 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 usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceBefore).to.be.greaterThan(usdcBalanceAfter); + + const usd0PlusPlusOnAdapter = await usd0PlusPlusToken.balanceOf(pendleCurveAdapter.address); + console.log( + `usd0PlusPlus stays on adapter: ${formatUnits( + usd0PlusPlusOnAdapter, + await usd0PlusPlusToken.decimals() + )} ${await usd0PlusPlusToken.symbol()}` + ); + }); + + it('USDC to pt-USD0++ exact output, small amount', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `pt-usd0++ balance Before: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `USDC balance before: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + + const exactPtOut = parseUnits('1', 18); + const usdcMaxIn = parseUnits('2', 6); + await usdc.connect(user).approve(router.address, usdcMaxIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, usdc.address, ptToken.address, usdcMaxIn, 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 usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceBefore).to.be.greaterThan(usdcBalanceAfter); + + const usd0PlusPlusOnAdapter = await usd0PlusPlusToken.balanceOf(pendleCurveAdapter.address); + console.log( + `usd0PlusPlus stays on adapter: ${formatUnits( + usd0PlusPlusOnAdapter, + await usd0PlusPlusToken.decimals() + )} ${await usd0PlusPlusToken.symbol()}` + ); + }); + + it('pt-USD0++ to USDC exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUSDeBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `usdcBalanceBefore: ${formatUnits(sUSDeBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + 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, usdc.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const sUsdeBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(sUsdeBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(sUsdeBalanceAfter).to.be.greaterThan(sUSDeBalanceBefore); + }); + + it('pt-USD0++ to USDC exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log(`usdcBalanceBefore: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}`); + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcOut = parseUnits('500', 6); + const maxPtIn = parseUnits('600', 18); + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, usdc.address, maxPtIn, usdcOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceAfter.sub(usdcBalanceBefore)).to.be.eq(usdcOut); + + const usdcBalanceOnAdapter = await usdc.balanceOf(pendleCurveAdapter.address); + console.log( + `usdcBalanceOnAdapter: ${formatUnits(usdcBalanceOnAdapter, await usdc.decimals())} ${await usdc.symbol()}` + ); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let usdc: ERC20; + let usd0PlusPlusToken: ERC20; + let router: MarginlyRouter; + let pendleCurveAdapter: PendleCurveRouterNgAdapter; + let user: SignerWithAddress; + let owner: SignerWithAddress; + + beforeEach(async () => { + ({ + ptToken, + usdcToken: usdc, + usd0PlusPlusToken, + 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('USDC to pt-usd0++ exact input, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUsdeBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `usdcBalanceBefore: ${formatUnits(sUsdeBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + await usdc.connect(user).approve(router.address, sUsdeBalanceBefore); + const tx = router + .connect(user) + .swapExactInput( + swapCalldata, + usdc.address, + ptToken.address, + sUsdeBalanceBefore, + sUsdeBalanceBefore.mul(9).div(10) + ); + + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + const sUsdeBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(sUsdeBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(sUsdeBalanceAfter).to.be.eq(sUsdeBalanceBefore); + }); + + it('USDC to pt-usd0++ exact output, forbidden', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `sUsdeBalanceBefore: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const ptOut = usdcBalanceBefore.div(2); + await usdc.connect(user).approve(router.address, usdcBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCalldata, usdc.address, ptToken.address, usdcBalanceBefore, ptOut); + await expect(tx).to.be.revertedWithCustomError(pendleCurveAdapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceAfter).to.be.eq(usdcBalanceBefore); + }); + + it('pt-usd0++ to USDC exact input', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const usdcBalanceBefore = await usdc.balanceOf(user.address); + console.log(`usdcBalanceBefore: ${formatUnits(usdcBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}`); + + 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, usdc.address, ptIn, 0); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`usdcBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceAfter).to.be.greaterThan(usdcBalanceBefore); + }); + + it('pt-usd0++ to USDC exact output', async () => { + const ptBalanceBefore = await ptToken.balanceOf(user.address); + console.log( + `ptBalanceBefore: ${formatUnits(ptBalanceBefore, await ptToken.decimals())} ${await ptToken.symbol()}` + ); + const sUsdeBalanceBefore = await usdc.balanceOf(user.address); + console.log( + `sUsdeBalanceBefore: ${formatUnits(sUsdeBalanceBefore, await usdc.decimals())} ${await usdc.symbol()}` + ); + + const swapCalldata = constructSwap([Dex.PendleCurveRouter], [SWAP_ONE]); + const usdcOut = parseUnits('900', 6); + await ptToken.connect(user).approve(router.address, ptBalanceBefore); + const maxPtIn = parseUnits('1000', 18); + const tx = await router + .connect(user) + .swapExactOutput(swapCalldata, ptToken.address, usdc.address, maxPtIn, usdcOut); + await showGasUsage(tx); + + const ptBalanceAfter = await ptToken.balanceOf(user.address); + console.log(`ptBalanceAfter: ${formatUnits(ptBalanceAfter, await ptToken.decimals())} ${await ptToken.symbol()}`); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + const usdcBalanceAfter = await usdc.balanceOf(user.address); + console.log(`sUsdeBalanceAfter: ${formatUnits(usdcBalanceAfter, await usdc.decimals())} ${await usdc.symbol()}`); + expect(usdcBalanceAfter.sub(sUsdeBalanceBefore)).to.be.eq(usdcOut); + }); + }); +}); diff --git a/packages/router/test/int/PendleMarketAdapter.eth.spec.ts b/packages/router/test/int/PendleMarketAdapter-Pt-sUSDE-sUSDE.eth.spec.ts similarity index 99% rename from packages/router/test/int/PendleMarketAdapter.eth.spec.ts rename to packages/router/test/int/PendleMarketAdapter-Pt-sUSDE-sUSDE.eth.spec.ts index e2f379b7..ca19111b 100644 --- a/packages/router/test/int/PendleMarketAdapter.eth.spec.ts +++ b/packages/router/test/int/PendleMarketAdapter-Pt-sUSDE-sUSDE.eth.spec.ts @@ -7,7 +7,7 @@ import { PendleMarketAdapter, PendleMarketAdapter__factory, } from '../../typechain-types'; -import { constructSwap, Dex, SWAP_ONE } from '../shared/utils'; +import { constructSwap, Dex, resetFork, SWAP_ONE } from '../shared/utils'; import { EthAddress } from '@marginly/common'; import { formatUnits, parseUnits } from 'ethers/lib/utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; @@ -64,6 +64,10 @@ async function initializeRouterEthSUSDe(): Promise<{ // Tests for running in ethereum mainnet fork describe('Pendle PT-sUSDE - sUSDE', () => { + before(async () => { + await resetFork(20032943); + }); + describe('Pendle swap pre maturity', () => { let ptToken: ERC20; let sUsde: ERC20; diff --git a/packages/router/test/int/PendleMarketAdapter.arb.spec.ts b/packages/router/test/int/PendleMarketAdapterPtWeETH25Jul-weETH.arb.spec.ts similarity index 99% rename from packages/router/test/int/PendleMarketAdapter.arb.spec.ts rename to packages/router/test/int/PendleMarketAdapterPtWeETH25Jul-weETH.arb.spec.ts index 095eb72d..b30dbef7 100644 --- a/packages/router/test/int/PendleMarketAdapter.arb.spec.ts +++ b/packages/router/test/int/PendleMarketAdapterPtWeETH25Jul-weETH.arb.spec.ts @@ -7,7 +7,7 @@ import { PendleMarketAdapter, PendleMarketAdapter__factory, } from '../../typechain-types'; -import { constructSwap, Dex, SWAP_ONE } from '../shared/utils'; +import { constructSwap, Dex, resetFork, SWAP_ONE } from '../shared/utils'; import { EthAddress } from '@marginly/common'; import { formatUnits, parseUnits } from 'ethers/lib/utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; @@ -55,6 +55,10 @@ async function initializeRouterArbWeEth(): Promise<{ } describe('Pendle PT-weETH - weETH', () => { + before(async () => { + await resetFork(20032943); + }); + describe('Pendle swap pre maturity', () => { let ptToken: ERC20; let weETH: ERC20; diff --git a/packages/router/test/shared/tokens.ts b/packages/router/test/shared/tokens.ts index 202b2477..2c8ae9e1 100644 --- a/packages/router/test/shared/tokens.ts +++ b/packages/router/test/shared/tokens.ts @@ -17,6 +17,9 @@ export enum ArbMainnetERC20BalanceOfSlot { // How to get: 1) decompile contract https://ethervm.io/decompile // 2) find balanceOf function and slot export enum EthereumMainnetERC20BalanceOfSlot { + WETH = '0000000000000000000000000000000000000000000000000000000000000003', + WBTC = '0000000000000000000000000000000000000000000000000000000000000000', + USDC = '0000000000000000000000000000000000000000000000000000000000000009', SUSDE = '0000000000000000000000000000000000000000000000000000000000000004', PTSUSDE = '0000000000000000000000000000000000000000000000000000000000000000', } diff --git a/packages/router/test/shared/utils.ts b/packages/router/test/shared/utils.ts index ccb982cb..a2ec9109 100644 --- a/packages/router/test/shared/utils.ts +++ b/packages/router/test/shared/utils.ts @@ -1,4 +1,9 @@ -import { BigNumber } from 'ethers'; +import { BigNumber, ContractTransaction } from 'ethers'; +import { ERC20 } from '../../typechain-types'; +import { formatUnits } from 'ethers/lib/utils'; +import { reset } from '@nomicfoundation/hardhat-network-helpers'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +const hre = require('hardhat'); export const SWAP_ONE = 1 << 15; @@ -18,6 +23,9 @@ export const Dex = { DodoV2: 12, Curve: 13, Pendle: 17, + PendleMarket: 19, + PendleCurveRouter: 20, + PendleCurve: 21, }; export function constructSwap(dex: number[], ratios: number[]): BigNumber { @@ -32,3 +40,23 @@ export function constructSwap(dex: number[], ratios: number[]): BigNumber { swap = (swap << BigInt(4)) + BigInt(dex.length); return BigNumber.from(swap); } + +export async function showGasUsage(tx: ContractTransaction) { + const txReceipt = await tx.wait(); + console.log(`⛽ gas used ${txReceipt.gasUsed}`); +} + +export async function showBalance(token: ERC20, account: string, startPhrase = ''): Promise { + const [balance, symbol, decimals] = await Promise.all([token.balanceOf(account), token.symbol(), token.decimals()]); + + console.log(`${startPhrase} ${formatUnits(balance, decimals)} ${symbol}`); + return balance; +} + +export async function resetFork(blockNumber?: number) { + const hardhatConfig = (hre).config; + const forkingBlockNumber = hardhatConfig.networks.hardhat.forking?.blockNumber; + const forkingUrl = hardhatConfig.networks.hardhat.forking?.url; + + await reset(forkingUrl, blockNumber ?? forkingBlockNumber); +}