From 167670a14b12288e48a021d13ae8b2b3436ce820 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Wed, 12 Feb 2025 16:20:33 +0300 Subject: [PATCH 1/4] add PendlePtToAsset adapter --- .../adapters/PendlePtToAssetAdapter.sol | 407 +++++++++++++ .../test/int/PendlePtToAsset.eth.spec.ts | 541 ++++++++++++++++++ packages/router/test/shared/utils.ts | 1 + 3 files changed, 949 insertions(+) create mode 100644 packages/router/contracts/adapters/PendlePtToAssetAdapter.sol create mode 100644 packages/router/test/int/PendlePtToAsset.eth.spec.ts diff --git a/packages/router/contracts/adapters/PendlePtToAssetAdapter.sol b/packages/router/contracts/adapters/PendlePtToAssetAdapter.sol new file mode 100644 index 00000000..bff39b41 --- /dev/null +++ b/packages/router/contracts/adapters/PendlePtToAssetAdapter.sol @@ -0,0 +1,407 @@ +// 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/interfaces/IERC4626.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'; + +///@dev Pendle adapter for exchanging PT to Asset, if Asset is valid input and output asset for SY +contract PendlePtToAssetAdapter is IMarginlyAdapter, Ownable2Step { + using PYIndexLib for IPYieldToken; + + struct PendleMarketData { + IPMarket market; + IStandardizedYield sy; + IPPrincipalToken pt; + IPYieldToken yt; + IERC20 asset; + uint8 slippage; + } + + struct PoolInput { + address pendleMarket; + uint8 slippage; + address ptToken; + address asset; + } + + struct CallbackData { + address tokenIn; + address tokenOut; + address router; + bytes adapterCallbackData; + bool isExactOutput; + } + + uint256 private constant PENDLE_ONE = 1e18; + uint256 private constant EPSILON = 1e15; + uint256 private constant ONE = 100; + uint256 private constant MAX_ITERATIONS = 10; + + mapping(address => mapping(address => PendleMarketData)) public getMarketData; + + event NewPair(address indexed ptToken, address indexed asset, address pendleMarket, uint8 slippage); + + error ApproximationFailed(); + error UnknownPair(); + error WrongPoolInput(); + + constructor(PoolInput[] memory poolsData) { + _addPools(poolsData); + } + + function addPools(PoolInput[] calldata poolsData) external onlyOwner { + _addPools(poolsData); + } + + /// @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) { + PendleMarketData memory marketData = _getMarketDataSafe(tokenIn, tokenOut); + + if (marketData.yt.isExpired()) { + amountOut = _swapExactInputPostMaturity(marketData, recipient, tokenIn, amountIn, data); + } else { + amountOut = _swapExactInputPreMaturity(marketData, 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) { + PendleMarketData memory marketData = _getMarketDataSafe(tokenIn, tokenOut); + + if (marketData.yt.isExpired()) { + amountIn = _swapExactOutputPostMaturity(marketData, recipient, tokenIn, amountOut, data); + } else { + amountIn = _swapExactOutputPreMaturity(marketData, 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)); + PendleMarketData memory marketData = _getMarketDataSafe(data.tokenIn, data.tokenOut); + require(msg.sender == address(marketData.market)); + + 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 { + // this clause is realized when pt tokens is output + // we need to mint SY from Asset and send to pendle + _pendleMintSy(marketData, msg.sender, uint256(-syToAccount), data); + } + } + + function _getMarketDataSafe( + address tokenA, + address tokenB + ) private view returns (PendleMarketData memory marketData) { + marketData = getMarketData[tokenA][tokenB]; + if (address(marketData.market) == address(0)) revert UnknownPair(); + } + + function _swapExactInputPreMaturity( + PendleMarketData memory marketData, + address recipient, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + bytes calldata data + ) private returns (uint256 amountOut) { + if (tokenIn == address(marketData.pt)) { + // pt to pendle -> sy redeem to asset and send to recipient + IMarginlyRouter(msg.sender).adapterCallback(address(marketData.market), amountIn, data); + (uint256 syAmountOut, ) = marketData.market.swapExactPtForSy(address(this), amountIn, new bytes(0)); + amountOut = _pendleRedeemSy(marketData, recipient, syAmountOut); + } else { + // asset to sy wrap (in swap callback) -> sy to pendle -> pt to recipient + CallbackData memory swapCallbackData = CallbackData({ + tokenIn: tokenIn, + tokenOut: tokenOut, + router: msg.sender, + adapterCallbackData: data, + isExactOutput: false + }); + IMarginlyRouter(msg.sender).adapterCallback(address(this), amountIn, data); + uint256 syAmountIn = marketData.sy.previewDeposit(address(marketData.asset), amountIn); + amountOut = _pendleApproxSwapExactSyForPt( + marketData, + recipient, + syAmountIn, + minAmountOut, + abi.encode(swapCallbackData) + ); + } + } + + function _swapExactOutputPreMaturity( + PendleMarketData memory marketData, + 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, + adapterCallbackData: data, + isExactOutput: true + }); + + if (tokenIn == address(marketData.pt)) { + // Calculate amount of SY could be redeemed from exact amount of Asset + uint256 estimatedSyOut = _assetToSyUpForRedeem(marketData, amountOut); + + // approx Pt to Sy -> in callback send Pt to PendleMarket + // then unwrap Sy to Asset and send to recipient + (uint256 actualSyAmountOut, uint256 ptAmountIn) = _pendleApproxSwapPtForExactSy( + marketData, + address(this), + estimatedSyOut, + maxAmountIn, + abi.encode(swapCallbackData) + ); + amountIn = ptAmountIn; + // use amountOut here, because actualSyAmountOut a little bit more than amountOut + _pendleRedeemSy(marketData, address(this), actualSyAmountOut); + SafeERC20.safeTransfer(marketData.asset, recipient, amountOut); + } else { + // Sy to Pt -> in callback mint Sy from Asset and send to pendleMarket + (amountIn, ) = marketData.market.swapSyForExactPt(recipient, amountOut, abi.encode(swapCallbackData)); + } + } + + function _swapExactInputPostMaturity( + PendleMarketData memory marketData, + address recipient, + address tokenIn, + uint256 amountIn, + bytes calldata data + ) private returns (uint256 amountOut) { + if (tokenIn == address(marketData.pt)) { + // pt redeem -> sy -> unwrap sy to ib + uint256 syRedeemed = _redeemPY(marketData.yt, msg.sender, amountIn, data); + amountOut = _pendleRedeemSy(marketData, recipient, syRedeemed); + } else { + // sy to pt swap is not possible after maturity + revert NotSupported(); + } + } + + ///@dev Calc how much SY need to redeem exact assetAmount + function _assetToSyUpForRedeem( + PendleMarketData memory marketData, + uint256 assetAmount + ) private view returns (uint256) { + uint256 assetsPerSyUnit = IStandardizedYield(marketData.sy).previewRedeem(address(marketData.asset), PENDLE_ONE); + return (PENDLE_ONE * assetAmount + assetsPerSyUnit - 1) / assetsPerSyUnit; + } + + ///@dev Calc how much asset need to deposit and get exact amount of SY + function _syToAssetUpForDeposit(PendleMarketData memory marketData, uint256 syAmount) private view returns (uint256) { + uint256 syPerAssetUnit = IStandardizedYield(marketData.sy).previewDeposit(address(marketData.asset), PENDLE_ONE); + return (PENDLE_ONE * syAmount + syPerAssetUnit - 1) / syPerAssetUnit; + } + + function _swapExactOutputPostMaturity( + PendleMarketData memory marketData, + address recipient, + address tokenIn, + uint256 amountOut, + bytes calldata data + ) private returns (uint256 amountIn) { + if (tokenIn == address(marketData.pt)) { + // Calc how much SY need to redeem exact amount of Asset + uint256 estimatedSyAmountOut = _assetToSyUpForRedeem(marketData, amountOut); + + // https://github.com/pendle-finance/pendle-core-v2-public/blob/bc27b10c33ac16d6e1936a9ddd24d536b00c96a4/contracts/core/YieldContractsV2/PendleYieldTokenV2.sol#L301 + uint256 index = marketData.yt.pyIndexCurrent(); + amountIn = Math.mulDiv(estimatedSyAmountOut, index, PENDLE_ONE, Math.Rounding.Up); + uint256 syAmountOut = _redeemPY(marketData.yt, msg.sender, amountIn, data); + _pendleRedeemSy(marketData, address(this), syAmountOut); + SafeERC20.safeTransfer(marketData.asset, recipient, amountOut); + //small amount of asset left in the adapter contract + } else { + // sy to pt swap is not possible after maturity + revert NotSupported(); + } + } + + function _pendleApproxSwapExactSyForPt( + PendleMarketData memory marketData, + address recipient, + uint256 syAmountIn, + uint256 minPtAmountOut, + bytes memory data + ) private returns (uint256 ptAmountOut) { + uint8 slippage = marketData.slippage; + ApproxParams memory approx = ApproxParams({ + guessMin: minPtAmountOut, + guessMax: (minPtAmountOut * (ONE + slippage)) / (ONE - slippage), + guessOffchain: 0, + maxIteration: MAX_ITERATIONS, + eps: EPSILON + }); + + (ptAmountOut, ) = MarketApproxPtOutLib.approxSwapExactSyForPt( + marketData.market.readState(address(this)), + marketData.yt.newIndex(), + syAmountIn, + block.timestamp, + approx + ); + (uint256 actualSyAmountIn, ) = marketData.market.swapSyForExactPt(recipient, ptAmountOut, data); + if (actualSyAmountIn > syAmountIn) revert ApproximationFailed(); + } + + function _pendleApproxSwapPtForExactSy( + PendleMarketData memory marketData, + address recipient, + uint256 syAmountOut, + uint256 maxPtAmountIn, + bytes memory data + ) private returns (uint256 actualSyAmountOut, uint256 actualPtAmountIn) { + uint8 slippage = marketData.slippage; + ApproxParams memory approx = ApproxParams({ + guessMin: (maxPtAmountIn * (ONE - slippage)) / (ONE + slippage), + guessMax: maxPtAmountIn, + guessOffchain: 0, + maxIteration: MAX_ITERATIONS, + eps: EPSILON + }); + + (actualPtAmountIn, , ) = MarketApproxPtInLib.approxSwapPtForExactSy( + IPMarket(marketData.market).readState(address(this)), + marketData.yt.newIndex(), + syAmountOut, + block.timestamp, + approx + ); + if (actualPtAmountIn > maxPtAmountIn) revert ApproximationFailed(); + + (actualSyAmountOut, ) = marketData.market.swapExactPtForSy(recipient, actualPtAmountIn, data); + if (actualSyAmountOut < syAmountOut) revert ApproximationFailed(); + } + + ///@dev Mint SY from Asset + function _pendleMintSy( + PendleMarketData memory marketData, + address recipient, + uint256 syAmount, + CallbackData memory data + ) private returns (uint256 syMinted) { + // Calculate amount of asset needed to mint syAmount + uint256 estimatedAssetIn = _syToAssetUpForDeposit(marketData, syAmount); + + if (data.isExactOutput) { + //transfer estimatedAssetIn of Asset to adapter + IMarginlyRouter(data.router).adapterCallback(address(this), estimatedAssetIn, data.adapterCallbackData); + } + SafeERC20.forceApprove(marketData.asset, address(marketData.sy), estimatedAssetIn); + syMinted = IStandardizedYield(marketData.sy).deposit( + address(this), + address(marketData.asset), + estimatedAssetIn, + syAmount + ); + + if (syMinted < syAmount) revert InsufficientAmount(); + + // small amount of sy left in the adapter contract + // transfer exact amount of sy to recipient + SafeERC20.safeTransfer(marketData.sy, recipient, syAmount); + } + + ///@dev Redeem for Asset. When asset is YieldAsset of SY then 1:1 swap, otherwise it is not + function _pendleRedeemSy( + PendleMarketData memory marketData, + address recipient, + uint256 syIn + ) private returns (uint256 assetRedeemed) { + assetRedeemed = IStandardizedYield(marketData.sy).redeem(recipient, syIn, address(marketData.asset), syIn, false); + } + + ///@dev Redeem after maturity + 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 _addPools(PoolInput[] memory poolsData) private { + PoolInput memory input; + uint256 length = poolsData.length; + for (uint256 i; i < length; ) { + input = poolsData[i]; + + if ( + input.ptToken == address(0) || + input.asset == address(0) || + input.pendleMarket == address(0) || + input.slippage >= ONE + ) revert WrongPoolInput(); + + (IStandardizedYield sy, IPPrincipalToken pt, IPYieldToken yt) = IPMarket(input.pendleMarket).readTokens(); + if (input.ptToken != address(pt)) revert WrongPoolInput(); + if (!sy.isValidTokenIn(input.asset) || !sy.isValidTokenOut(input.asset)) revert WrongPoolInput(); + + PendleMarketData memory marketData = PendleMarketData({ + market: IPMarket(input.pendleMarket), + sy: sy, + pt: pt, + yt: yt, + asset: IERC20(input.asset), + slippage: input.slippage + }); + + getMarketData[input.ptToken][input.asset] = marketData; + getMarketData[input.asset][input.ptToken] = marketData; + + emit NewPair(input.ptToken, input.asset, input.pendleMarket, input.slippage); + + unchecked { + ++i; + } + } + } +} diff --git a/packages/router/test/int/PendlePtToAsset.eth.spec.ts b/packages/router/test/int/PendlePtToAsset.eth.spec.ts new file mode 100644 index 00000000..89539bee --- /dev/null +++ b/packages/router/test/int/PendlePtToAsset.eth.spec.ts @@ -0,0 +1,541 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + constructSwap, + delay, + Dex, + resetFork, + showBalance, + showBalanceDelta, + showGasUsage, + SWAP_ONE, +} from '../shared/utils'; +import { EthAddress } from '@marginly/common'; +import { parseUnits } from 'ethers/lib/utils'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { EthereumMainnetERC20BalanceOfSlot, setTokenBalance } from '../shared/tokens'; +import { BigNumber } from 'ethers'; +import { + PendlePtToAssetAdapter__factory, + PendlePtToAssetAdapter, + ERC20, + MarginlyRouter, + MarginlyRouter__factory, +} from '../../typechain-types'; + +const swapCallData = constructSwap([Dex.PendlePtToAsset], [SWAP_ONE]); + +interface TokenInfo { + address: string; + symbol: string; + balanceSlot: EthereumMainnetERC20BalanceOfSlot; + initialBalance: BigNumber; +} + +interface TestCase { + forkNumber: number; + + pendleMarket: string; + + ptToken: TokenInfo; + assetToken: TokenInfo; + syToken: TokenInfo; + + timeToMaturity: number; + preMaturity: { + swapExactIbtToPt: { + ibtIn: BigNumber; + minPtOut: BigNumber; + }; + swapExactPtToIbt: { + ptIn: BigNumber; + minIbtOut: BigNumber; + }; + swapIbtToExactPt: { + maxIbtIn: BigNumber; + ptOut: BigNumber; + }; + swapPtToExactIbt: { + maxPtIn: BigNumber; + ibtOut: BigNumber; + }; + }; + postMaturity: { + swapPtToExactIbt: { + maxPtIn: BigNumber; + ibtOut: BigNumber; + }; + swapExactPtToIbt: { + ptIn: BigNumber; + minIbtOut: BigNumber; + }; + }; +} + +const USR_TestCase: TestCase = { + forkNumber: 21830300, + + pendleMarket: '0x353d0b2efb5b3a7987fb06d30ad6160522d08426', + ptToken: { + address: '0xa8c8861b5ccf8cce0ade6811cd2a7a7d3222b0b8', + symbol: 'pt-wstUSR-27MAR2025', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + assetToken: { + address: '0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110', + symbol: 'USR', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.WSTUSR, + initialBalance: parseUnits('100000', 18), + }, + + syToken: { + address: '0x6c78661c00d797c9c7fcbe4bcacbd9612a61c07f', + symbol: 'SY-wstUSR', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('600', 18), + minPtOut: parseUnits('400', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('745.34', 18), + minIbtOut: parseUnits('500', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15000.75', 18), + ibtOut: parseUnits('10000', 18), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('125', 18), + ptOut: parseUnits('100', 18), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('150.576', 18), + minIbtOut: parseUnits('120.0', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('600', 18), + ibtOut: parseUnits('500', 18), + }, + }, +}; + +const wstUSR_TestCase: TestCase = { + forkNumber: 21830300, + + pendleMarket: '0x353d0b2efb5b3a7987fb06d30ad6160522d08426', + ptToken: { + address: '0xa8c8861b5ccf8cce0ade6811cd2a7a7d3222b0b8', + symbol: 'pt-wstUSR-27MAR2025', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + assetToken: { + address: '0x1202F5C7b4B9E47a1A484E8B270be34dbbC75055', + symbol: 'wstUSR', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.WSTUSR, + initialBalance: parseUnits('100000', 18), + }, + + syToken: { + address: '0x6c78661c00d797c9c7fcbe4bcacbd9612a61c07f', + symbol: 'SY-wstUSR', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('600', 18), + minPtOut: parseUnits('400', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('745.34', 18), + minIbtOut: parseUnits('500', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15000.75', 18), + ibtOut: parseUnits('10000', 18), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('125', 18), + ptOut: parseUnits('100', 18), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('150.576', 18), + minIbtOut: parseUnits('120.0', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('600', 18), + ibtOut: parseUnits('500', 18), + }, + }, +}; + +const usual_TestCase: TestCase = { + forkNumber: 21830300, + + pendleMarket: '0xb9b7840ec34094ce1269c38ba7a6ac7407f9c4e3', + ptToken: { + address: '0x36f4ec0a7c46923c4f6508c404ee1c6fbe175e1c', + symbol: 'pt-usualx-27MAR2025', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + assetToken: { + address: '0xc4441c2be5d8fa8126822b9929ca0b81ea0de38e', + symbol: 'usual', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.WSTUSR, + initialBalance: parseUnits('100000', 18), + }, + + syToken: { + address: '0x86e2a16a5abc67467ce502e3dab511c909c185a8', + symbol: 'SY-usual', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('600', 18), + minPtOut: parseUnits('400', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('745.34', 18), + minIbtOut: parseUnits('500', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15000.75', 18), + ibtOut: parseUnits('10000', 18), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('125', 18), + ptOut: parseUnits('100', 18), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('150.576', 18), + minIbtOut: parseUnits('120.0', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('600', 18), + ibtOut: parseUnits('500', 18), + }, + }, +}; + +const testCases = [wstUSR_TestCase, USR_TestCase, usual_TestCase]; + +async function initializeRouter(testCase: TestCase): Promise<{ + ptToken: ERC20; + assetToken: ERC20; + syToken: ERC20; + router: MarginlyRouter; + adapter: PendlePtToAssetAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; +}> { + const [owner, user] = await ethers.getSigners(); + + const ptToken = await ethers.getContractAt('ERC20', testCase.ptToken.address); + const assetToken = await ethers.getContractAt('ERC20', testCase.assetToken.address); + const syToken = await ethers.getContractAt('ERC20', testCase.syToken.address); + + const poolInput: PendlePtToAssetAdapter.PoolInputStruct = { + ptToken: ptToken.address, + asset: assetToken.address, + pendleMarket: testCase.pendleMarket, + slippage: 35, + }; + + const adapter = await new PendlePtToAssetAdapter__factory().connect(owner).deploy([poolInput]); + console.log('Adapter initialized'); + const routerInput = { + dexIndex: Dex.PendlePtToAsset, + adapter: adapter.address, + }; + const router = await new MarginlyRouter__factory().connect(owner).deploy([routerInput]); + + await setTokenBalance( + assetToken.address, + testCase.assetToken.balanceSlot, + EthAddress.parse(user.address), + testCase.assetToken.initialBalance + ); + + await setTokenBalance( + ptToken.address, + testCase.ptToken.balanceSlot, + EthAddress.parse(user.address), + testCase.ptToken.initialBalance + ); + + expect(await ptToken.balanceOf(user.address)).to.be.eq( + testCase.ptToken.initialBalance, + `Wrong initial ${testCase.ptToken.symbol} balance` + ); + expect(await assetToken.balanceOf(user.address)).to.be.eq( + testCase.assetToken.initialBalance, + `Wrong initial ${testCase.assetToken.symbol} balance` + ); + + return { + ptToken, + assetToken, + syToken, + router, + adapter, + owner, + user, + }; +} + +// Tests for running in ethereum mainnet fork +describe.only('PendlePtToAssetAdapter', async () => { + for (const testCase of testCases) { + describe(`PendlePtToAssetAdapter ${testCase.ptToken.symbol} - ${testCase.assetToken.symbol}`, () => { + before(async () => { + await resetFork(testCase.forkNumber); + }); + + describe('Pendle swap pre maturity', () => { + let ptToken: ERC20; + let assetToken: ERC20; + let syToken: ERC20; + let router: MarginlyRouter; + let user: SignerWithAddress; + let adapter: PendlePtToAssetAdapter; + + beforeEach(async () => { + ({ ptToken, assetToken, syToken, router, adapter, user } = await initializeRouter(testCase)); + }); + + it(`${testCase.assetToken.symbol} to ${testCase.ptToken.symbol} exact input`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(assetToken, user.address, 'balance before:'); + + const ibtTokenAmount = testCase.preMaturity.swapExactIbtToPt.ibtIn; + await assetToken.connect(user).approve(router.address, ibtTokenAmount); + + const minPTAmount = testCase.preMaturity.swapExactIbtToPt.minPtOut; + + const tx = await router + .connect(user) + .swapExactInput(swapCallData, assetToken.address, ptToken.address, ibtTokenAmount, minPTAmount); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt balance After:'); + expect(ptBalanceAfter).to.be.greaterThan(ptBalanceBefore); + + const ibtBalanceAfter = await showBalance(assetToken, user.address, 'Asset balance After:'); + expect(ibtBalanceBefore.sub(ibtBalanceAfter)).to.be.lessThanOrEqual(ibtTokenAmount); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, assetToken, 'Asset balance delta:'); + await showBalance(syToken, adapter.address, 'sy balance on adapter:'); + await showBalance(assetToken, adapter.address, 'asset balance on adapter:'); + }); + + it(`${testCase.assetToken.symbol} to ${testCase.ptToken.symbol} exact output`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(assetToken, user.address, 'balance before:'); + + const exactPtOut = testCase.preMaturity.swapIbtToExactPt.ptOut; + const ibtMaxAmountIn = testCase.preMaturity.swapIbtToExactPt.maxIbtIn; + await assetToken.connect(user).approve(router.address, ibtMaxAmountIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCallData, assetToken.address, ptToken.address, ibtMaxAmountIn, exactPtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt balance After:'); + expect(ptBalanceAfter.sub(ptBalanceBefore)).to.be.eq(exactPtOut); + + const ibtBalanceAfter = await showBalance(assetToken, user.address, 'Asset balance After: '); + expect(ibtBalanceBefore).to.be.greaterThan(ibtBalanceAfter); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, assetToken, 'Asset balance delta:'); + await showBalance(syToken, adapter.address, 'sy balance on adapter:'); + await showBalance(assetToken, adapter.address, 'asset balance on adapter:'); + }); + + it(`${testCase.ptToken.symbol} to ${testCase.assetToken.symbol} exact input`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(assetToken, user.address, 'balance before:'); + + const ptIn = testCase.preMaturity.swapExactPtToIbt.ptIn; + const minIbtOut = testCase.preMaturity.swapExactPtToIbt.minIbtOut; + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router + .connect(user) + .swapExactInput(swapCallData, ptToken.address, assetToken.address, ptIn, minIbtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt BalanceAfter:'); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + + const ibtBalanceAfter = await showBalance(assetToken, user.address, 'Asset balance After:'); + expect(ibtBalanceAfter).to.be.greaterThan(ibtBalanceBefore); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, assetToken, 'Asset balance delta:'); + await showBalance(syToken, adapter.address, 'sy balance on adapter:'); + await showBalance(assetToken, adapter.address, 'asset balance on adapter:'); + }); + + it(`${testCase.ptToken.symbol} to ${testCase.assetToken.symbol} exact output`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance before:'); + const ibtBalanceBefore = await showBalance(assetToken, user.address, 'balance before:'); + + const ibtMinOut = testCase.preMaturity.swapPtToExactIbt.ibtOut; + const maxPtIn = testCase.preMaturity.swapPtToExactIbt.maxPtIn; + await ptToken.connect(user).approve(router.address, maxPtIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCallData, ptToken.address, assetToken.address, maxPtIn, ibtMinOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt balanceAfter:'); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + + const ibtBalanceAfter = await showBalance(assetToken, user.address, 'Asset balance After:'); + expect(ibtBalanceAfter.sub(ibtBalanceBefore)).to.be.eq(ibtMinOut); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, assetToken, 'Asset balance delta:'); + await showBalance(syToken, adapter.address, 'sy balance on adapter:'); + await showBalance(assetToken, adapter.address, 'asset balance on adapter:'); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let assetToken: ERC20; + let syToken: ERC20; + let router: MarginlyRouter; + let adapter: PendlePtToAssetAdapter; + let user: SignerWithAddress; + + beforeEach(async () => { + ({ ptToken, assetToken, syToken, router, adapter, user } = await initializeRouter(testCase)); + + // move time and make after maturity + await ethers.provider.send('evm_increaseTime', [testCase.timeToMaturity]); + await ethers.provider.send('evm_mine', []); + }); + + it(`${testCase.assetToken.symbol} to ${testCase.ptToken.symbol} exact input, forbidden`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(assetToken, user.address, 'balance before:'); + + await assetToken.connect(user).approve(router.address, ibtBalanceBefore); + const tx = router + .connect(user) + .swapExactInput(swapCallData, assetToken.address, ptToken.address, ibtBalanceBefore, 0); + + await expect(tx).to.be.revertedWithCustomError(adapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt Balance After:'); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + + const ibtBalanceAfter = await showBalance(assetToken, user.address, 'Asset balance After:'); + expect(ibtBalanceAfter).to.be.eq(ibtBalanceBefore); + }); + + it(`${testCase.assetToken.symbol} to ${testCase.ptToken.symbol} exact output, forbidden`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(assetToken, user.address, 'balance before:'); + + await assetToken.connect(user).approve(router.address, ibtBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCallData, assetToken.address, ptToken.address, ibtBalanceBefore, 1); + await expect(tx).to.be.revertedWithCustomError(adapter, 'NotSupported'); + + console.log('This swap is forbidden after maturity'); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt Balance After:'); + expect(ptBalanceAfter).to.be.eq(ptBalanceBefore); + + const ibtBalanceAfter = await showBalance(assetToken, user.address, 'Asset balance After:'); + expect(ibtBalanceAfter).to.be.eq(ibtBalanceBefore); + }); + + it(`${testCase.ptToken.symbol} to ${testCase.assetToken.symbol} exact input`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(assetToken, user.address, 'balance before:'); + + const ptIn = testCase.postMaturity.swapExactPtToIbt.ptIn; + const minIbtOut = testCase.postMaturity.swapExactPtToIbt.minIbtOut; + await ptToken.connect(user).approve(router.address, ptIn); + const tx = await router + .connect(user) + .swapExactInput(swapCallData, ptToken.address, assetToken.address, ptIn, minIbtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'ptBalanceAfter:'); + expect(ptBalanceBefore.sub(ptBalanceAfter)).to.be.eq(ptIn); + + const ibtBalanceAfter = await showBalance(assetToken, user.address, 'Asset balance After:'); + expect(ibtBalanceAfter).to.be.greaterThan(ibtBalanceBefore); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, assetToken, 'Asset balance delta:'); + await showBalance(syToken, adapter.address, 'sy balance on adapter:'); + await showBalance(assetToken, adapter.address, 'asset balance on adapter:'); + }); + + it(`${testCase.ptToken.symbol} to ${testCase.assetToken.symbol} exact output`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt balance Before:'); + const ibtBalanceBefore = await showBalance(assetToken, user.address, 'Asset balance before:'); + + const ibtOut = testCase.postMaturity.swapPtToExactIbt.ibtOut; + await ptToken.connect(user).approve(router.address, ptBalanceBefore); + const maxPtIn = testCase.postMaturity.swapPtToExactIbt.maxPtIn; + const tx = await router + .connect(user) + .swapExactOutput(swapCallData, ptToken.address, assetToken.address, maxPtIn, ibtOut); + await showGasUsage(tx); + + const ptBalanceAfter = await showBalance(ptToken, user.address, 'pt Balance After:'); + expect(ptBalanceBefore).to.be.greaterThan(ptBalanceAfter); + + const ibtBalanceAfter = await showBalance(assetToken, user.address, 'Asset balance After:'); + expect(ibtBalanceAfter.sub(ibtBalanceBefore)).to.be.eq(ibtOut); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, assetToken, 'Asset balance delta:'); + await showBalance(syToken, adapter.address, 'sy balance on adapter:'); + await showBalance(assetToken, adapter.address, 'asset balance on adapter:'); + }); + }); + }); + + await delay(3000); + } +}); diff --git a/packages/router/test/shared/utils.ts b/packages/router/test/shared/utils.ts index 92bd0f25..c00edf5e 100644 --- a/packages/router/test/shared/utils.ts +++ b/packages/router/test/shared/utils.ts @@ -27,6 +27,7 @@ export const Dex = { PendleCurveRouter: 30, PendleCurve: 31, Spectra: 32, + PendlePtToAsset: 33, }; export function constructSwap(dex: number[], ratios: number[]): BigNumber { From 65b32c665b0f2299006666d38ebb5b4d86ab21be Mon Sep 17 00:00:00 2001 From: rudewalt Date: Wed, 12 Feb 2025 17:04:18 +0300 Subject: [PATCH 2/4] add script for deploying the PendlePtToAssetAdapter --- packages/deploy/src/config.ts | 10 +- .../src/deployer/MarginlyRouterDeployer.ts | 14 + packages/deploy/src/deployer/configs.ts | 365 +++++++++++------- 3 files changed, 251 insertions(+), 138 deletions(-) diff --git a/packages/deploy/src/config.ts b/packages/deploy/src/config.ts index 78916e26..1a45b7ae 100644 --- a/packages/deploy/src/config.ts +++ b/packages/deploy/src/config.ts @@ -384,7 +384,8 @@ export type AdapterPair = | PendleUniswapAdapterPair | PendleMarketAdapterPair | PendleCurveAdapterPair - | PendleCurveRouterAdapterPair; + | PendleCurveRouterAdapterPair + | PendlePtToAssetAdapterPair; export interface GeneralAdapterPair { tokenAId: string; @@ -428,6 +429,13 @@ export interface PendleCurveRouterAdapterPair { curveSlippage: number; } +export interface PendlePtToAssetAdapterPair { + tokenAId: string; + tokenBId: string; + pendleMarket: string; + slippage: number; +} + export interface MarginlyDeployConfig { connection: EthConnectionConfig; tokens: MarginlyDeployConfigToken[]; diff --git a/packages/deploy/src/deployer/MarginlyRouterDeployer.ts b/packages/deploy/src/deployer/MarginlyRouterDeployer.ts index 229ae66e..102f4a9b 100644 --- a/packages/deploy/src/deployer/MarginlyRouterDeployer.ts +++ b/packages/deploy/src/deployer/MarginlyRouterDeployer.ts @@ -10,10 +10,12 @@ import { PendleCurveAdapterParam, PendleCurveRouterAdapterParam, PendleMarketAdapterParam, + PendlePtToAssetAdapterParam, isPendleAdapter, isPendleCurveAdapter, isPendleCurveRouterAdapter, isPendleMarketAdapter, + isPendlePtToAssetAdapter, } from './configs'; import { EthOptions } from '../config'; import { Logger } from '../logger'; @@ -60,6 +62,18 @@ export class MarginlyRouterDeployer extends BaseDeployer { ]; }), ]; + } else if (isPendlePtToAssetAdapter(pools[0])) { + args = [ + pools.map((x) => { + const locConfig = x as PendlePtToAssetAdapterParam; + return [ + locConfig.pendleMarket.toString(), + locConfig.slippage, + tokenRepository.getTokenInfo(locConfig.ptToken.id).address.toString(), + tokenRepository.getTokenInfo(locConfig.assetToken.id).address.toString(), + ]; + }), + ]; } else if (isPendleCurveAdapter(pools[0])) { args = [ pools.map((x) => { diff --git a/packages/deploy/src/deployer/configs.ts b/packages/deploy/src/deployer/configs.ts index 9bc6d507..9d17e3b3 100644 --- a/packages/deploy/src/deployer/configs.ts +++ b/packages/deploy/src/deployer/configs.ts @@ -1,6 +1,7 @@ import { MarginlyConfigExistingToken, MarginlyConfigMintableToken, MarginlyConfigToken, TimeSpan } from '../common'; import { EthAddress, RationalNumber } from '@marginly/common'; import { + AdapterPair, EthConnectionConfig, GeneralAdapterPair, isAlgebraDoubleOracleConfig, @@ -22,6 +23,7 @@ import { PendleCurveAdapterPair, PendleCurveRouterAdapterPair, PendleMarketAdapterPair, + PendlePtToAssetAdapterPair, PendleUniswapAdapterPair, } from '../config'; import { adapterWriter, Logger } from '../logger'; @@ -173,7 +175,8 @@ export type AdapterParam = | PendleAdapterParam | PendleMarketAdapterParam | PendleCurveAdapterParam - | PendleCurveRouterAdapterParam; + | PendleCurveRouterAdapterParam + | PendlePtToAssetAdapterParam; export interface MarginlyAdapterParam { type: 'general'; @@ -220,6 +223,14 @@ export interface PendleCurveAdapterParam { quoteToken: MarginlyConfigToken; } +export interface PendlePtToAssetAdapterParam { + type: 'pendlePtToAsset'; + pendleMarket: EthAddress; + ptToken: MarginlyConfigToken; + assetToken: MarginlyConfigToken; + slippage: number; +} + export function isPendleAdapter(config: AdapterParam): config is PendleAdapterParam { return config.type === 'pendle'; } @@ -228,6 +239,10 @@ export function isPendleMarketAdapter(config: AdapterParam): config is PendleMar return config.type === 'pendleMarket'; } +export function isPendlePtToAssetAdapter(config: AdapterParam): config is PendlePtToAssetAdapterParam { + return config.type === 'pendlePtToAsset'; +} + export function isGeneralAdapter(config: AdapterParam): config is MarginlyAdapterParam { return config.type === 'general'; } @@ -597,144 +612,12 @@ export class StrictMarginlyDeployConfig { const adapters: MarginlyConfigAdapter[] = []; for (const adapter of config.adapters) { + const dexId = adapter.dexId; const adapterParams: AdapterParam[] = []; - for (const pool of adapter.pools) { - if (adapter.adapterName === 'PendleAdapter') { - const pairConfig = pool as PendleUniswapAdapterPair; - - if (!pairConfig.ibTokenId) { - throw new Error(`IB token id is not set for adapter with dexId ${adapter.dexId}`); - } - if (!pairConfig.slippage) { - throw new Error(`Slippage is not set for adapter with dexId ${adapter.dexId}`); - } - if (!pairConfig.pendleMarket) { - throw new Error(`Pendle market is not set for adapter with dexId ${adapter.dexId}`); - } - - const poolAddress = EthAddress.parse(pairConfig.poolAddress); - const token0 = tokens.get(pairConfig.tokenAId); - if (token0 === undefined) { - throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${adapter.dexId}`); - } - const token1 = tokens.get(pairConfig.tokenBId); - if (token1 === undefined) { - throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${adapter.dexId}`); - } - const ibToken = tokens.get(pairConfig.ibTokenId); - if (ibToken === undefined) { - throw new Error(`Can not find ibToken '${pairConfig.ibTokenId}' for adapter with dexId ${adapter.dexId}`); - } - - adapterParams.push({ - type: 'pendle', - token0: token0, - token1: token1, - ib: ibToken, - uniswapV3LikePool: poolAddress, - pendleMarket: EthAddress.parse(pairConfig.pendleMarket), - slippage: pairConfig.slippage, - }); - } else if (adapter.adapterName === 'PendleMarketAdapter') { - const pairConfig = pool as PendleMarketAdapterPair; - - const token0 = tokens.get(pairConfig.tokenAId); - if (token0 === undefined) { - throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${adapter.dexId}`); - } - const token1 = tokens.get(pairConfig.tokenBId); - if (token1 === undefined) { - 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(pairConfig.poolAddress), - slippage: pairConfig.slippage, - }); - } else if (adapter.adapterName === 'PendleCurveNgAdapter') { - 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 === 'PendleCurveRouterNg') { - 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 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 '${pairConfig.tokenAId}' for adapter with dexId ${adapter.dexId}`); - } - const token1 = tokens.get(pairConfig.tokenBId); - if (token1 === undefined) { - throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${adapter.dexId}`); - } - adapterParams.push({ - type: 'general', - token0: token0, - token1: token1, - pool: poolAddress, - }); - } + for (const pool of adapter.pools) { + const adapterParam = this.createAdapterParam(adapter.adapterName, pool, tokens, dexId); + adapterParams.push(adapterParam); } adapters.push({ @@ -787,6 +670,214 @@ export class StrictMarginlyDeployConfig { ); } + private static createAdapterParam( + adapterName: string, + pair: AdapterPair, + tokens: Map, + dexId: number + ): AdapterParam { + if (adapterName === 'PendleAdapter') { + return this.createPendleAdapterParam(pair, tokens, dexId); + } else if (adapterName === 'PendleMarketAdapter') { + return this.createPendleMarketAdapterConfig(pair, tokens, dexId); + } else if (adapterName == 'PendlePtToAssetAdapter') { + return this.createPendlePtToAssetAdapterParam(pair, tokens, dexId); + } else if (adapterName === 'PendleCurveNgAdapter') { + return this.createPendleCurveNgAdapterConfig(pair, tokens, dexId); + } else if (adapterName === 'PendleCurveRouterNg') { + return this.createPendleCurveRouterAdapterConfig(pair, tokens, dexId); + } else { + return this.createSimpleAdapterParam(pair, tokens, dexId); + } + } + + private static createPendleAdapterParam( + pair: AdapterPair, + tokens: Map, + dexId: number + ): PendleAdapterParam { + const pairConfig = pair as PendleUniswapAdapterPair; + + if (!pairConfig.ibTokenId) { + throw new Error(`IB token id is not set for adapter with dexId ${dexId}`); + } + if (!pairConfig.slippage) { + throw new Error(`Slippage is not set for adapter with dexId ${dexId}`); + } + if (!pairConfig.pendleMarket) { + throw new Error(`Pendle market is not set for adapter with dexId ${dexId}`); + } + + const poolAddress = EthAddress.parse(pairConfig.poolAddress); + const token0 = tokens.get(pairConfig.tokenAId); + if (token0 === undefined) { + throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${dexId}`); + } + const token1 = tokens.get(pairConfig.tokenBId); + if (token1 === undefined) { + throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${dexId}`); + } + const ibToken = tokens.get(pairConfig.ibTokenId); + if (ibToken === undefined) { + throw new Error(`Can not find ibToken '${pairConfig.ibTokenId}' for adapter with dexId ${dexId}`); + } + + return { + type: 'pendle', + token0: token0, + token1: token1, + ib: ibToken, + uniswapV3LikePool: poolAddress, + pendleMarket: EthAddress.parse(pairConfig.pendleMarket), + slippage: pairConfig.slippage, + }; + } + + private static createPendleMarketAdapterConfig( + pair: AdapterPair, + tokens: Map, + dexId: number + ): PendleMarketAdapterParam { + const pairConfig = pair as PendleMarketAdapterPair; + + const token0 = tokens.get(pairConfig.tokenAId); + if (token0 === undefined) { + throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${dexId}`); + } + const token1 = tokens.get(pairConfig.tokenBId); + if (token1 === undefined) { + throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${dexId}`); + } + + return { + type: 'pendleMarket', + ptToken: token0, + ibToken: token1, + pendleMarket: EthAddress.parse(pairConfig.poolAddress), + slippage: pairConfig.slippage, + }; + } + + private static createPendlePtToAssetAdapterParam( + pair: AdapterPair, + tokens: Map, + dexId: number + ): PendlePtToAssetAdapterParam { + const pairConfig = pair as PendlePtToAssetAdapterPair; + + const token0 = tokens.get(pairConfig.tokenAId); + if (token0 === undefined) { + throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${dexId}`); + } + const token1 = tokens.get(pairConfig.tokenBId); + if (token1 === undefined) { + throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${dexId}`); + } + + return { + type: 'pendlePtToAsset', + ptToken: token0, + assetToken: token1, + pendleMarket: EthAddress.parse(pairConfig.pendleMarket), + slippage: pairConfig.slippage, + }; + } + + private static createPendleCurveNgAdapterConfig( + pair: AdapterPair, + tokens: Map, + dexId: number + ): PendleCurveAdapterParam { + const pairConfig = pair as PendleCurveAdapterPair; + + const ibToken = tokens.get(pairConfig.ibTokenId); + if (ibToken === undefined) { + throw new Error(`Can not find ibToken '${pairConfig.ibTokenId}' for adapter with dexId ${dexId}`); + } + + const quoteToken = tokens.get(pairConfig.quoteTokenId); + if (quoteToken === undefined) { + throw new Error(`Can not find quoteToken '${pairConfig.quoteTokenId}' for adapter with dexId ${dexId}`); + } + + return { + type: 'pendleCurve', + pendleMarket: EthAddress.parse(pairConfig.pendleMarket), + slippage: pairConfig.slippage, + curveSlippage: pairConfig.curveSlippage, + curvePool: EthAddress.parse(pairConfig.curvePool), + ibToken: ibToken, + quoteToken: quoteToken, + }; + } + + private static createPendleCurveRouterAdapterConfig( + pair: AdapterPair, + tokens: Map, + dexId: number + ): PendleCurveRouterAdapterParam { + const pairConfig = pair as PendleCurveRouterAdapterPair; + + if (pairConfig.curveRoute.length !== 11) { + throw new Error( + `Wrong config for Pendle curve router adapter with dexId ${dexId}. Curve route length must be 11` + ); + } + + if (pairConfig.curvePools.length !== 5) { + throw new Error(`Wrong config for Pendle curve router adapter with dexId ${dexId}. Curve pools length must be 5`); + } + + if (pairConfig.curveSwapParams.length !== 5) { + throw new Error( + `Wrong config for Pendle curve router adapter with dexId ${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 ${dexId}. Curve swap params array must be 5x5` + ); + } + } + + return { + 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), + }; + } + + private static createSimpleAdapterParam( + pair: AdapterPair, + tokens: Map, + dexId: number + ): MarginlyAdapterParam { + const pairConfig = pair as GeneralAdapterPair; + + const poolAddress = EthAddress.parse(pairConfig.poolAddress); + const token0 = tokens.get(pairConfig.tokenAId); + if (token0 === undefined) { + throw new Error(`Can not find token0 '${pairConfig.tokenAId}' for adapter with dexId ${dexId}`); + } + const token1 = tokens.get(pairConfig.tokenBId); + if (token1 === undefined) { + throw new Error(`Can not find token1 '${pairConfig.tokenBId}' for adapter with dexId ${dexId}`); + } + + return { + type: 'general', + token0: token0, + token1: token1, + pool: poolAddress, + }; + } + private static createPriceOracleConfigs( config: MarginlyDeployConfig, tokens: Map From 48a7267ccbdc127e26e63cc4389aa861ece822b6 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Fri, 14 Feb 2025 16:23:13 +0300 Subject: [PATCH 3/4] add new testCases for PendlePt to Asset --- .../test/int/PendlePtToAsset.eth.spec.ts | 181 +++++++++++++++++- .../test/int/SpectraAdapter.eth.spec.ts | 12 +- packages/router/test/shared/tokens.ts | 1 + 3 files changed, 187 insertions(+), 7 deletions(-) diff --git a/packages/router/test/int/PendlePtToAsset.eth.spec.ts b/packages/router/test/int/PendlePtToAsset.eth.spec.ts index 89539bee..99f7ab6c 100644 --- a/packages/router/test/int/PendlePtToAsset.eth.spec.ts +++ b/packages/router/test/int/PendlePtToAsset.eth.spec.ts @@ -246,7 +246,186 @@ const usual_TestCase: TestCase = { }, }; -const testCases = [wstUSR_TestCase, USR_TestCase, usual_TestCase]; +const DAI_sUSDS_TestCase: TestCase = { + forkNumber: 21830300, + + pendleMarket: '0x21d85ff3bedff031ef466c7d5295240c8ab2a2b8', + ptToken: { + address: '0x152b8629fee8105248ba3b7ba6afb94f7a468302', + symbol: 'PT-sUSDS-27MAR2025', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + assetToken: { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + symbol: 'DAI', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.DAI, + initialBalance: parseUnits('100000', 18), + }, + + syToken: { + address: '0xbe3d4ec488a0a042bb86f9176c24f8cd54018ba7', + symbol: 'SY-sUSDS', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('600', 18), + minPtOut: parseUnits('400', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('745.34', 18), + minIbtOut: parseUnits('500', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15000.75', 18), + ibtOut: parseUnits('10000', 18), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('125', 18), + ptOut: parseUnits('100', 18), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('150.576', 18), + minIbtOut: parseUnits('120.0', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('600', 18), + ibtOut: parseUnits('500', 18), + }, + }, +}; + +const USDS_sUSDS_TestCase: TestCase = { + forkNumber: 21830300, + + pendleMarket: '0x21d85ff3bedff031ef466c7d5295240c8ab2a2b8', + ptToken: { + address: '0x152b8629fee8105248ba3b7ba6afb94f7a468302', + symbol: 'PT-sUSDS-27MAR2025', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + assetToken: { + address: '0xdc035d45d973e3ec169d2276ddab16f1e407384f', + symbol: 'USDS', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.DAI, + initialBalance: parseUnits('100000', 18), + }, + + syToken: { + address: '0xbe3d4ec488a0a042bb86f9176c24f8cd54018ba7', + symbol: 'SY-sUSDS', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 18), + }, + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('600', 18), + minPtOut: parseUnits('400', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('745.34', 18), + minIbtOut: parseUnits('500', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15000.75', 18), + ibtOut: parseUnits('10000', 18), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('125', 18), + ptOut: parseUnits('100', 18), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('150.576', 18), + minIbtOut: parseUnits('120.0', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('600', 18), + ibtOut: parseUnits('500', 18), + }, + }, +}; + +const USDC_fluid_TestCase: TestCase = { + forkNumber: 21830300, + + pendleMarket: '0x925cd38a68993819eef0138a463308c840080f17', + ptToken: { + address: '0x6704c353b0c2527863e4ef03dca07175b9318cbf', + symbol: 'PT-fUSDC-26Jun2025', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 6), + }, + + assetToken: { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.USDC, + initialBalance: parseUnits('100000', 6), + }, + + syToken: { + address: '0xf3a4aae37b90810c263c99538a47ad6f31837e19', + symbol: 'SY-fUSDC', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSUSDE, + initialBalance: parseUnits('100000', 6), + }, + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('600', 6), + minPtOut: parseUnits('400', 6), + }, + swapExactPtToIbt: { + ptIn: parseUnits('745.34', 6), + minIbtOut: parseUnits('500', 6), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15000.75', 6), + ibtOut: parseUnits('10000', 6), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('125', 6), + ptOut: parseUnits('100', 6), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('150.576', 6), + minIbtOut: parseUnits('120.0', 6), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('600', 6), + ibtOut: parseUnits('500', 6), + }, + }, +}; + +const testCases = [ + //DAI_sUSDS_TestCase, + //USDS_sUSDS_TestCase, + USDC_fluid_TestCase, + //wstUSR_TestCase, USR_TestCase, usual_TestCase +]; async function initializeRouter(testCase: TestCase): Promise<{ ptToken: ERC20; diff --git a/packages/router/test/int/SpectraAdapter.eth.spec.ts b/packages/router/test/int/SpectraAdapter.eth.spec.ts index dd9ae371..f6753fdb 100644 --- a/packages/router/test/int/SpectraAdapter.eth.spec.ts +++ b/packages/router/test/int/SpectraAdapter.eth.spec.ts @@ -341,11 +341,11 @@ const sDOLA_TestCase: TestCase = { }; const testCases = [ - USR_TestCase, // PT/Underlying case - DOLA_TestCase, //PT/Underlying case - wstUSR_TestCase, // PT-IBT/IBT case - sDOLA_TestCase, //PT-IBT/IBT case - inwstETHs_TestCase, // PT-sw/IBT case + // USR_TestCase, // PT/Underlying case + // DOLA_TestCase, //PT/Underlying case + // wstUSR_TestCase, // PT-IBT/IBT case + // sDOLA_TestCase, //PT-IBT/IBT case + // inwstETHs_TestCase, // PT-sw/IBT case ]; async function initializeRouter(testCase: TestCase): Promise<{ @@ -421,7 +421,7 @@ async function initializeRouter(testCase: TestCase): Promise<{ } // Tests for running in ethereum mainnet fork -describe('SpectraAdapter', async () => { +describe.skip('SpectraAdapter', async () => { for (const testCase of testCases) { describe(`SpectraAdapter ${testCase.ptToken.symbol} - ${testCase.quoteToken.symbol}`, () => { before(async () => { diff --git a/packages/router/test/shared/tokens.ts b/packages/router/test/shared/tokens.ts index 05d89d26..bd622f37 100644 --- a/packages/router/test/shared/tokens.ts +++ b/packages/router/test/shared/tokens.ts @@ -19,6 +19,7 @@ export enum ArbMainnetERC20BalanceOfSlot { // or find slot in blockexplorer statechange e.g. https://etherscan.io/tx/0xd3a83090d4e736aef85302e9835850d925c7d8da5180678fe440edc519966906#statechange export enum EthereumMainnetERC20BalanceOfSlot { WETH = '0000000000000000000000000000000000000000000000000000000000000003', + DAI = '0000000000000000000000000000000000000000000000000000000000000002', WBTC = '0000000000000000000000000000000000000000000000000000000000000000', USDC = '0000000000000000000000000000000000000000000000000000000000000009', SUSDE = '0000000000000000000000000000000000000000000000000000000000000004', From 113ae74a323821d1c0bc9f6e9b188c9fd591f9ee Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 18 Feb 2025 11:41:44 +0300 Subject: [PATCH 4/4] code review --- .../adapters/PendlePtToAssetAdapter.sol | 32 +++++++++---------- .../test/int/PendlePtToAsset.eth.spec.ts | 10 +++--- .../test/int/SpectraAdapter.eth.spec.ts | 12 +++---- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/router/contracts/adapters/PendlePtToAssetAdapter.sol b/packages/router/contracts/adapters/PendlePtToAssetAdapter.sol index bff39b41..b0ae1e84 100644 --- a/packages/router/contracts/adapters/PendlePtToAssetAdapter.sol +++ b/packages/router/contracts/adapters/PendlePtToAssetAdapter.sol @@ -224,21 +224,6 @@ contract PendlePtToAssetAdapter is IMarginlyAdapter, Ownable2Step { } } - ///@dev Calc how much SY need to redeem exact assetAmount - function _assetToSyUpForRedeem( - PendleMarketData memory marketData, - uint256 assetAmount - ) private view returns (uint256) { - uint256 assetsPerSyUnit = IStandardizedYield(marketData.sy).previewRedeem(address(marketData.asset), PENDLE_ONE); - return (PENDLE_ONE * assetAmount + assetsPerSyUnit - 1) / assetsPerSyUnit; - } - - ///@dev Calc how much asset need to deposit and get exact amount of SY - function _syToAssetUpForDeposit(PendleMarketData memory marketData, uint256 syAmount) private view returns (uint256) { - uint256 syPerAssetUnit = IStandardizedYield(marketData.sy).previewDeposit(address(marketData.asset), PENDLE_ONE); - return (PENDLE_ONE * syAmount + syPerAssetUnit - 1) / syPerAssetUnit; - } - function _swapExactOutputPostMaturity( PendleMarketData memory marketData, address recipient, @@ -341,8 +326,6 @@ contract PendlePtToAssetAdapter is IMarginlyAdapter, Ownable2Step { syAmount ); - if (syMinted < syAmount) revert InsufficientAmount(); - // small amount of sy left in the adapter contract // transfer exact amount of sy to recipient SafeERC20.safeTransfer(marketData.sy, recipient, syAmount); @@ -368,6 +351,21 @@ contract PendlePtToAssetAdapter is IMarginlyAdapter, Ownable2Step { syRedeemed = yt.redeemPY(address(this)); } + ///@dev Calc how much SY need to redeem exact assetAmount + function _assetToSyUpForRedeem( + PendleMarketData memory marketData, + uint256 assetAmount + ) private view returns (uint256) { + uint256 assetsPerSyUnit = IStandardizedYield(marketData.sy).previewRedeem(address(marketData.asset), PENDLE_ONE); + return (PENDLE_ONE * assetAmount + assetsPerSyUnit - 1) / assetsPerSyUnit; + } + + ///@dev Calc how much asset need to deposit and get exact amount of SY + function _syToAssetUpForDeposit(PendleMarketData memory marketData, uint256 syAmount) private view returns (uint256) { + uint256 syPerAssetUnit = IStandardizedYield(marketData.sy).previewDeposit(address(marketData.asset), PENDLE_ONE); + return (PENDLE_ONE * syAmount + syPerAssetUnit - 1) / syPerAssetUnit; + } + function _addPools(PoolInput[] memory poolsData) private { PoolInput memory input; uint256 length = poolsData.length; diff --git a/packages/router/test/int/PendlePtToAsset.eth.spec.ts b/packages/router/test/int/PendlePtToAsset.eth.spec.ts index 99f7ab6c..f97e0462 100644 --- a/packages/router/test/int/PendlePtToAsset.eth.spec.ts +++ b/packages/router/test/int/PendlePtToAsset.eth.spec.ts @@ -421,10 +421,12 @@ const USDC_fluid_TestCase: TestCase = { }; const testCases = [ - //DAI_sUSDS_TestCase, - //USDS_sUSDS_TestCase, + DAI_sUSDS_TestCase, + USDS_sUSDS_TestCase, USDC_fluid_TestCase, - //wstUSR_TestCase, USR_TestCase, usual_TestCase + wstUSR_TestCase, + USR_TestCase, + usual_TestCase, ]; async function initializeRouter(testCase: TestCase): Promise<{ @@ -492,7 +494,7 @@ async function initializeRouter(testCase: TestCase): Promise<{ } // Tests for running in ethereum mainnet fork -describe.only('PendlePtToAssetAdapter', async () => { +describe('PendlePtToAssetAdapter', async () => { for (const testCase of testCases) { describe(`PendlePtToAssetAdapter ${testCase.ptToken.symbol} - ${testCase.assetToken.symbol}`, () => { before(async () => { diff --git a/packages/router/test/int/SpectraAdapter.eth.spec.ts b/packages/router/test/int/SpectraAdapter.eth.spec.ts index f6753fdb..dd9ae371 100644 --- a/packages/router/test/int/SpectraAdapter.eth.spec.ts +++ b/packages/router/test/int/SpectraAdapter.eth.spec.ts @@ -341,11 +341,11 @@ const sDOLA_TestCase: TestCase = { }; const testCases = [ - // USR_TestCase, // PT/Underlying case - // DOLA_TestCase, //PT/Underlying case - // wstUSR_TestCase, // PT-IBT/IBT case - // sDOLA_TestCase, //PT-IBT/IBT case - // inwstETHs_TestCase, // PT-sw/IBT case + USR_TestCase, // PT/Underlying case + DOLA_TestCase, //PT/Underlying case + wstUSR_TestCase, // PT-IBT/IBT case + sDOLA_TestCase, //PT-IBT/IBT case + inwstETHs_TestCase, // PT-sw/IBT case ]; async function initializeRouter(testCase: TestCase): Promise<{ @@ -421,7 +421,7 @@ async function initializeRouter(testCase: TestCase): Promise<{ } // Tests for running in ethereum mainnet fork -describe.skip('SpectraAdapter', async () => { +describe('SpectraAdapter', async () => { for (const testCase of testCases) { describe(`SpectraAdapter ${testCase.ptToken.symbol} - ${testCase.quoteToken.symbol}`, () => { before(async () => {