From 09f7db0a40bff51b42459ee03620a6e080de6768 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Mon, 5 Aug 2024 15:33:39 +0300 Subject: [PATCH 01/10] WIP: added spectra adapter --- .../adapters/SpectraSWIbtPtCurveAdapter.sol | 455 ++++++++++++++++++ .../adapters/interfaces/ICurvePool.sol | 17 + .../TestStableSwap2EMAOraclePool.sol | 20 + .../hardhat-eth-fork.config.ts | 2 +- .../SpectraSwIbtPtCurveAdapter.eth.spec.ts | 398 +++++++++++++++ 5 files changed, 891 insertions(+), 1 deletion(-) create mode 100644 packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol create mode 100644 packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts diff --git a/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol b/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol new file mode 100644 index 00000000..c025b212 --- /dev/null +++ b/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import '../interfaces/IMarginlyRouter.sol'; +import '../interfaces/IMarginlyAdapter.sol'; +import './interfaces/ICurvePool.sol'; + +import 'hardhat/console.sol'; + +interface ISpectraPrincipalToken { + /** + * @notice Returns the unix timestamp (uint256) at which the PT contract expires + * @return The unix timestamp (uint256) when PTs become redeemable + */ + function maturity() external view returns (uint256); + + /** + * @notice Burns owner's shares (PTs and YTs before expiry, PTs after expiry) + * and sends IBTs to receiver + * @param shares The amount of shares to burn + * @param receiver The address that will receive the IBTs + * @param owner The owner of the shares + * @return ibts The actual amount of IBT received for burning the shares + */ + function redeemForIBT(uint256 shares, address receiver, address owner) external returns (uint256 ibts); + + /** + * @notice Burns owner's shares (PTs and YTs before expiry, PTs after expiry) + * and sends IBTs to receiver + * @param shares The amount of shares to burn + * @param receiver The address that will receive the IBTs + * @param owner The owner of the shares + * @param minIbts The minimum IBTs that should be returned to user + * @return ibts The actual amount of IBT received for burning the shares + */ + function redeemForIBT( + uint256 shares, + address receiver, + address owner, + uint256 minIbts + ) external returns (uint256 ibts); + + /** + * @notice Burns owner's shares (before expiry : PTs and YTs) and sends IBTs to receiver + * @param ibts The amount of IBT to be received + * @param receiver The address that will receive the IBTs + * @param owner The owner of the shares (PTs and YTs) + * @return shares The actual amount of shares burnt for receiving the IBTs + */ + function withdrawIBT(uint256 ibts, address receiver, address owner) external returns (uint256 shares); + + /** + * @notice Burns owner's shares (before expiry : PTs and YTs) and sends IBTs to receiver + * @param ibts The amount of IBT to be received + * @param receiver The address that will receive the IBTs + * @param owner The owner of the shares (PTs and YTs) + * @param maxShares The maximum shares allowed to be burnt + * @return shares The actual amount of shares burnt for receiving the IBTs + */ + function withdrawIBT( + uint256 ibts, + address receiver, + address owner, + uint256 maxShares + ) external returns (uint256 shares); +} + +interface ISpectra4626Wrapper { + /// @dev Returns the address of the wrapped vault share. + function vaultShare() external view returns (address); + + /// @dev Allows the owner to deposit vault shares into the wrapper. + /// @param vaultShares The amount of vault shares to deposit. + /// @param receiver The address to receive the wrapper shares. + /// @return The amount of minted wrapper shares. + function wrap(uint256 vaultShares, address receiver) external returns (uint256); + + /// @dev Allows the owner to deposit vault shares into the wrapper, with support for slippage protection. + /// @param vaultShares The amount of vault shares to deposit. + /// @param receiver The address to receive the wrapper shares. + /// @param minShares The minimum allowed wrapper shares from this deposit. + /// @return The amount of minted wrapper shares. + function wrap(uint256 vaultShares, address receiver, uint256 minShares) external returns (uint256); + + /// @dev Allows the owner to withdraw vault shares from the wrapper. + /// @param shares The amount of wrapper shares to redeem. + /// @param receiver The address to receive the vault shares. + /// @param owner The address of the owner of the wrapper shares. + /// @return The amount of withdrawn vault shares. + function unwrap(uint256 shares, address receiver, address owner) external returns (uint256); + + /// @dev Allows the owner to withdraw vault shares from the wrapper, with support for slippage protection. + /// @param shares The amount of wrapper shares to redeem. + /// @param receiver The address to receive the vault shares. + /// @param owner The address of the owner of the wrapper shares. + /// @param minVaultShares The minimum vault shares that should be returned. + /// @return The amount of withdrawn vault shares. + function unwrap(uint256 shares, address receiver, address owner, uint256 minVaultShares) external returns (uint256); + + /// @dev Allows to preview the amount of minted wrapper shares for a given amount of deposited vault shares. + /// @param vaultShares The amount of vault shares to deposit. + /// @return The amount of minted vault shares. + function previewWrap(uint256 vaultShares) external view returns (uint256); + + /// @dev Allows to preview the amount of withdrawn vault shares for a given amount of redeemed wrapper shares. + /// @param shares The amount of wrapper shares to redeem. + /// @return The amount of withdrawn vault shares. + function previewUnwrap(uint256 shares) external view returns (uint256); +} + +///@notice Adapter for Spectra Curve pool of two tokens SpectraWrapped IBT and PT +/// but adapter for IBT and PT +contract SpectraSWIbtPtCurveAdapter is IMarginlyAdapter { + using SafeERC20 for IERC20; + + error WrongPoolInput(); + error UnknownPair(); + + event NewPair(address indexed ptToken, address indexed ibToken, address curvePool); + + struct PoolInput { + address ibToken; // interest bearing token uniBTC + address ptToken; // principal token, ex PT-uniBTC + address pool; // curve pool for swIbt and PT + } + + struct PoolData { + address pool; + bool zeroIndexCoinIsIbt; // curvePool.coins[0] is ibt, curvePool.coins[1] is pt + address swIbt; // address of spectraWrappedIBT + address pt; // address of pt token + } + + mapping(address => mapping(address => PoolData)) public getPoolData; + + constructor(PoolInput[] memory pools) { + _addPools(pools); + } + + function _addPools(PoolInput[] memory pools) private { + PoolInput memory input; + uint256 length = pools.length; + for (uint256 i; i < length; ) { + input = pools[i]; + + if (input.ibToken == address(0) || input.ptToken == address(0) || input.pool == address(0)) + revert WrongPoolInput(); + + address coin0 = ICurvePool(input.pool).coins(0); + address coin1 = ICurvePool(input.pool).coins(1); + + PoolData memory poolData = PoolData({pool: input.pool, zeroIndexCoinIsIbt: true, swIbt: coin0, pt: coin1}); + + if (coin1 == input.ptToken) { + if (ISpectra4626Wrapper(coin0).vaultShare() != input.ibToken) revert WrongPoolInput(); + } else if (coin0 == input.ptToken) { + if (ISpectra4626Wrapper(coin1).vaultShare() != input.ibToken) revert WrongPoolInput(); + + poolData.zeroIndexCoinIsIbt = false; + poolData.swIbt = coin1; + poolData.pt = coin0; + } else { + revert WrongPoolInput(); + } + + getPoolData[input.ptToken][input.ibToken] = poolData; + getPoolData[input.ibToken][input.ptToken] = poolData; + + emit NewPair(input.ptToken, input.ibToken, input.pool); + + unchecked { + ++i; + } + } + } + + function _getPoolDataSafe(address tokenA, address tokenB) private view returns (PoolData memory poolData) { + poolData = getPoolData[tokenA][tokenB]; + if (poolData.pool == address(0)) revert UnknownPair(); + } + + function _swapExactInputPreMaturity( + PoolData memory poolData, + address recipientArg, + address tokenInArg, + uint256 amountInArg, + uint256 minAmountOut + ) private returns (uint256 amountOut) { + bool tokenInIsPt = tokenInArg == poolData.pt; + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt && tokenInIsPt ? 1 : 0; + + address tokenIn = tokenInArg; + uint256 amountIn = amountInArg; + address recipient = recipientArg; + + if (!tokenInIsPt) { + // wrap ib to swIbt and change reciptient to current address(this) to let contract unwrap + IERC20(tokenIn).forceApprove(poolData.swIbt, amountInArg); + amountIn = ISpectra4626Wrapper(poolData.swIbt).wrap(amountInArg, address(this)); + tokenIn = poolData.swIbt; + } else { + // change recipient to address(this), it let make unwrap swIbt for Ibt after swap + recipient = address(this); + } + + amountOut = _curveSwapExactInput(poolData.pool, recipient, tokenIn, tokenInIndex, amountIn, minAmountOut); + + if (tokenInIsPt) { + // unwrap swIbt to ib + amountOut = ISpectra4626Wrapper(poolData.swIbt).unwrap(amountOut, recipientArg, address(this)); + } + } + + function _swapExactInputPostMaturiy( + PoolData memory poolData, + address recipient, + address tokenIn, + uint256 amountIn + ) private returns (uint256 amountOut) { + if (tokenIn == poolData.pt) { + uint256 swAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); + amountOut = ISpectra4626Wrapper(poolData.swIbt).unwrap(swAmountOut, recipient, address(this)); + } else { + // swIbt to pt swap is not possible after maturity + revert NotSupported(); + } + } + + /// @notice Swap maxAmountIn of pt token for swIbt token, + /// unwrap swIbt token to ibt + /// check excessive amount out and wrap back to swIbt + /// swap excessive amount swIbt to pt + /// transfer pt to recipient + function _swapExactOutputPtToIbtPreMaturity( + PoolData memory poolData, + address recipient, + address ibOut, + uint256 ibAmountOut, + uint256 maxPtAmountIn + ) private returns (uint256 ptAmountIn) { + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 1 : 0; + + // convert ibAmountOut to swAmountOut + uint256 swAmountOut = ISpectra4626Wrapper(poolData.swIbt).previewWrap(ibAmountOut); + + // swap maxAmountPt to swIbt + uint256 swActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + tokenInIndex, + maxPtAmountIn, + swAmountOut + ); + + // unwrap swIbt to ibt + uint256 ibActualAmountOut = ISpectra4626Wrapper(poolData.swIbt).unwrap( + swActualAmountOut, + address(this), + address(this) + ); + + IERC20(ibOut).safeTransfer(recipient, ibAmountOut); + + if (ibActualAmountOut < ibAmountOut) { + revert TooMuchRequested(); + } + + if (ibActualAmountOut == ibAmountOut) { + return maxPtAmountIn; + } + + uint256 deltaIbAmountOut = ibActualAmountOut - ibAmountOut; + + // wrap extra amountOut ibt to swIbt + IERC20(ibOut).forceApprove(poolData.swIbt, deltaIbAmountOut); + uint256 deltaSwAmountOut = ISpectra4626Wrapper(poolData.swIbt).wrap(deltaIbAmountOut, address(this)); + + // swap and move excessive tokenIn directly to recipient. + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessivePtAmountIn = _curveSwapExactInput( + poolData.pool, + recipient, + poolData.swIbt, + 1 - tokenInIndex, + deltaSwAmountOut, + 0 + ); + + ptAmountIn = maxPtAmountIn - excessivePtAmountIn; + } + + function _swapExactOutputIbtToPtPreMaturity( + PoolData memory poolData, + address recipient, + address ibIn, + uint256 ptAmountOut, + uint256 maxIbAmountIn + ) private returns (uint256 ibAmountIn) { + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; + + // wrap ibt to swIbt + IERC20(ibIn).forceApprove(poolData.swIbt, maxIbAmountIn); + uint256 swMaxAmountIn = ISpectra4626Wrapper(poolData.swIbt).wrap(maxIbAmountIn, address(this)); + + // swap all swMaxAmountIn to pt tokens + uint256 ptActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.swIbt, + tokenInIndex, + swMaxAmountIn, + ptAmountOut + ); + + if (ptActualAmountOut < ptAmountOut) { + revert TooMuchRequested(); + } + + IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); + + if (ptActualAmountOut == ptAmountOut) { + return maxIbAmountIn; + } + + uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; + // swap and move excessive tokenIn + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessiveSwAmountIn = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + 1 - tokenInIndex, + deltaPtAmountOut, + 0 + ); + // unwrap exessive sw into ib and transfer back to recipient + uint256 excessiveIbAmountIn = ISpectra4626Wrapper(poolData.swIbt).unwrap( + excessiveSwAmountIn, + recipient, + address(this) + ); + ibAmountIn = maxIbAmountIn - excessiveIbAmountIn; + } + + function _swapExactOutputPostMaturity( + PoolData memory poolData, + address recipient, + address tokenOut, + uint256 amountOut, + uint256 maxAmountIn + ) private returns (uint256 amountIn) { + if (tokenOut == poolData.pt) { + // swap swIbt to pt is not possible after maturity + revert NotSupported(); + } else { + // calc swAmount from amountOut + uint256 swAmountOut = ISpectra4626Wrapper(poolData.swIbt).previewWrap(amountOut); + + amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(swAmountOut, address(this), address(this)); + uint256 actualAmountOut = ISpectra4626Wrapper(poolData.swIbt).unwrap(swAmountOut, address(this), address(this)); + + if (actualAmountOut < amountOut) revert InsufficientAmount(); + + // actualAmountOut could be more than amountOut, but it's not possible to change it back to PT aftrer maturity + // dust may be left on the contract + IERC20(tokenOut).safeTransfer(recipient, amountOut); + + if (maxAmountIn == amountIn) { + return amountIn; + } + + // return tokenIn pt-uniBTC back to recipient + IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); + } + } + + function _curveSwapExactInput( + address poolAddress, + address recipient, + address tokenIn, + uint256 tokenInIndex, + uint256 amountIn, + uint256 minAmountOut + ) private returns (uint256 amountOut) { + SafeERC20.forceApprove(IERC20(tokenIn), poolAddress, amountIn); + + amountOut = ICurvePool(poolAddress).exchange( + tokenInIndex, + 1 - tokenInIndex, + amountIn, + minAmountOut, + false, + recipient + ); + } + + function _ptIsExpired(address pt) private view returns (bool) { + return ISpectraPrincipalToken(pt).maturity() < block.timestamp; + } + + function swapExactInput( + address recipient, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + bytes calldata data + ) external returns (uint256 amountOut) { + PoolData memory poolData = _getPoolDataSafe(tokenIn, tokenOut); + + // move all input tokens from router to this adapter + IMarginlyRouter(msg.sender).adapterCallback(address(this), amountIn, data); + + if (_ptIsExpired(poolData.pt)) { + amountOut = _swapExactInputPostMaturiy(poolData, recipient, tokenIn, amountIn); + } else { + amountOut = _swapExactInputPreMaturity(poolData, recipient, tokenIn, amountIn, minAmountOut); + } + + if (amountOut < minAmountOut) revert InsufficientAmount(); + } + + function swapExactOutput( + address recipient, + address tokenIn, + address tokenOut, + uint256 maxAmountIn, + uint256 amountOut, + bytes calldata data + ) external returns (uint256 amountIn) { + PoolData memory poolData = _getPoolDataSafe(tokenIn, tokenOut); + + IMarginlyRouter(msg.sender).adapterCallback(address(this), maxAmountIn, data); + + if (_ptIsExpired(poolData.pt)) { + amountIn = _swapExactOutputPostMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + } else { + if (tokenIn == poolData.pt) { + amountIn = _swapExactOutputPtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + } else { + amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); + } + } + } + + function addPools(PoolInput[] calldata poolsData) external { + _addPools(poolsData); + } +} diff --git a/packages/router/contracts/adapters/interfaces/ICurvePool.sol b/packages/router/contracts/adapters/interfaces/ICurvePool.sol index fdfdbe50..a3ac4bb4 100644 --- a/packages/router/contracts/adapters/interfaces/ICurvePool.sol +++ b/packages/router/contracts/adapters/interfaces/ICurvePool.sol @@ -38,4 +38,21 @@ interface ICurvePool { function price_oracle() external view returns (uint256); function last_price() external view returns (uint256); + + /* + @dev some pools have last_prices method, example https://etherscan.io/address/0xb09fc8bbdcc8dc9d8b3775132c52fcebf1c7dbb3#readContract + */ + function last_prices() external view returns (uint256); + + /* + @dev Spectra version of curve pool + */ + function exchange( + uint256 i, + uint256 j, + uint256 dx, + uint256 min_dy, + bool use_eth, + address receiver + ) external returns (uint256); } diff --git a/packages/router/contracts/test/CurveEMATest/TestStableSwap2EMAOraclePool.sol b/packages/router/contracts/test/CurveEMATest/TestStableSwap2EMAOraclePool.sol index 0069f7b2..d1f0f950 100644 --- a/packages/router/contracts/test/CurveEMATest/TestStableSwap2EMAOraclePool.sol +++ b/packages/router/contracts/test/CurveEMATest/TestStableSwap2EMAOraclePool.sol @@ -31,6 +31,22 @@ contract TestStableSwap2EMAOraclePool is ICurvePool { TransferHelper.safeTransfer(j == 0 ? token0 : token1, _receiver, dy); } + function exchange( + uint256 i, + uint256 j, + uint256 _dx, + uint256 _min_dy, + bool, + address _receiver + ) external returns (uint256 dy) { + dy = get_dy(int128(uint128(i)), int128(uint128(j)), _dx); + + if (dy < _min_dy) revert('dy < _min_dy'); + + TransferHelper.safeTransferFrom(i == 0 ? token0 : token1, msg.sender, address(this), _dx); + TransferHelper.safeTransfer(j == 0 ? token0 : token1, _receiver, dy); + } + function coins(uint256 i) external view returns (address) { if (i == 0) { return token0; @@ -56,4 +72,8 @@ contract TestStableSwap2EMAOraclePool is ICurvePool { function last_price() external view returns (uint256) { return price; } + + function last_prices() 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..564f3be4 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: 20460915, }, }, }, diff --git a/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts b/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts new file mode 100644 index 00000000..6b215603 --- /dev/null +++ b/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts @@ -0,0 +1,398 @@ +import { ethers } from 'hardhat'; +import { ERC20, ERC20__factory, ICurvePool, MarginlyRouter, SpectraSWIbtPtCurveAdapter } from '../../typechain-types'; +import { AdapterInputStruct } from '@marginly/periphery/typechain-types/contracts/admin/abstract/RouterActions'; +import { BigNumber, Contract } from 'ethers'; +import { EthAddress } from '@marginly/common'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ArbMainnetERC20BalanceOfSlot, EthereumMainnetERC20BalanceOfSlot, setTokenBalance } from '../shared/tokens'; +import { parseEther, parseUnits } from 'ethers/lib/utils'; + +interface TokenInfo { + contract: ERC20; + symbol: string; + decimals: number; + balanceOfSlot: string; +} + +function formatTokenBalance(token: TokenInfo, amount: BigNumber): string { + return `${ethers.utils.formatUnits(amount, token.decimals)} ${token.symbol}`; +} + +describe.only('Curve adapter for sw-uniBTC/Pt-uniBTC pool (CurveAdapter)', () => { + // sw-uniBTC/Pt-uniBTC pool - https://etherscan.io/address/0xb09fc8bbdcc8dc9d8b3775132c52fcebf1c7dbb3 + const poolAddress = '0xb09fc8bbdcc8dc9d8b3775132c52fcebf1c7dbb3'; + const uniBtcAddress = '0x004E9C3EF86bc1ca1f0bB5C7662861Ee93350568'; + + const uniBtcHolderAddress = '0x447D5867d07be4E8e87fD08CBA5C8426F7835632'; + let uniBtcHolder: SignerWithAddress; + const ptUniBtcHolderAddress = '0x14975679e5f87c25fa2c54958e735a79B5B93043'; + let ptUniBtcHolder: SignerWithAddress; + + let swUniBtc: TokenInfo; + let ptUniBtc: TokenInfo; + let uniBtc: TokenInfo; + let pool: ICurvePool; + let router: MarginlyRouter; + let adapter: SpectraSWIbtPtCurveAdapter; + + before(async () => { + pool = await ethers.getContractAt('ICurvePool', poolAddress); + const adapterFactory = await ethers.getContractFactory('SpectraSWIbtPtCurveAdapter'); + + const token0Address = await pool.callStatic.coins(0); + const token1Address = await pool.callStatic.coins(1); + const token0Contract = await ethers.getContractAt('ERC20', token0Address); + const token1Contract = await ethers.getContractAt('ERC20', token1Address); + const uniBtcContract = await ethers.getContractAt('ERC20', uniBtcAddress); + const token0Symbol = await token0Contract.symbol(); + const token1Symbol = await token1Contract.symbol(); + const token0Decimals = await token0Contract.decimals(); + const token1Decimals = await token1Contract.decimals(); + + swUniBtc = { + contract: token0Contract, + symbol: token0Symbol, + decimals: token0Decimals, + balanceOfSlot: EthereumMainnetERC20BalanceOfSlot.UNIBTC, + }; + ptUniBtc = { + contract: token1Contract, + symbol: token1Symbol, + decimals: token1Decimals, + balanceOfSlot: EthereumMainnetERC20BalanceOfSlot.UNIBTC, + }; + uniBtc = { + contract: uniBtcContract, + symbol: await uniBtcContract.symbol(), + decimals: await uniBtcContract.decimals(), + balanceOfSlot: EthereumMainnetERC20BalanceOfSlot.UNIBTC, + }; + + adapter = await adapterFactory.deploy([ + { + ibToken: uniBtcAddress, + ptToken: token1Address, + pool: poolAddress, + }, + ]); + console.log('Adapter address: ', adapter.address); + + const routerFactory = await ethers.getContractFactory('MarginlyRouter'); + router = await routerFactory.deploy([{ dexIndex: 0, adapter: adapter.address }]); + + const [owner, user1, user2] = await ethers.getSigners(); + uniBtcHolder = await ethers.getImpersonatedSigner(uniBtcHolderAddress); + ptUniBtcHolder = await ethers.getImpersonatedSigner(ptUniBtcHolderAddress); + + await owner.sendTransaction({ + to: uniBtcHolderAddress, + value: parseEther('1.0'), + }); + + await owner.sendTransaction({ + to: ptUniBtcHolderAddress, + value: parseEther('1.0'), + }); + + const token0InitBalance = BigNumber.from(10).pow(8); + await uniBtc.contract.connect(uniBtcHolder).transfer(owner.address, token0InitBalance); + await uniBtc.contract.connect(uniBtcHolder).transfer(user1.address, token0InitBalance); + await uniBtc.contract.connect(uniBtcHolder).transfer(user2.address, token0InitBalance); + + const token1InitBalance = BigNumber.from(10).pow(8); + await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(owner.address, token1InitBalance); + await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(user1.address, token1InitBalance); + await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(user2.address, token1InitBalance); + }); + + function printPrice(priceInToken0: BigNumber) { + const priceStr = ethers.utils.formatEther(priceInToken0); + const inversePrice = 1 / Number.parseFloat(priceStr); + console.log(`1 ${ptUniBtc.symbol} = ${priceStr} ${uniBtc.symbol}`); + console.log(`1 ${uniBtc.symbol} = ${inversePrice} ${ptUniBtc.symbol}`); + } + + function printPriceWithDelta(newPriceInToken0: BigNumber, oldPriceInToken0: BigNumber) { + const newPriceStr = ethers.utils.formatEther(newPriceInToken0); + const inverseNewPrice = 1 / Number.parseFloat(newPriceStr); + + const oldPriceStr = ethers.utils.formatEther(oldPriceInToken0); + const inverseOldPrice = 1 / Number.parseFloat(oldPriceStr); + + const deltaPrice = newPriceInToken0.sub(oldPriceInToken0); + const deltaPriceStr = ethers.utils.formatEther(deltaPrice); + const deltaInversePrice = inverseNewPrice - inverseOldPrice; + + console.log(`1 ${ptUniBtc.symbol} = ${newPriceStr} ${uniBtc.symbol}, delta: ${deltaPriceStr} ${uniBtc.symbol}`); + console.log( + `1 ${uniBtc.symbol} = ${inverseNewPrice} ${ptUniBtc.symbol}, ` + `delta: ${deltaInversePrice} ${ptUniBtc.symbol}` + ); + } + + async function printAdapterBalances() { + console.log( + `\nAdapter uniBTC balance after swap:${formatTokenBalance( + uniBtc, + await uniBtc.contract.balanceOf(adapter.address) + )}` + ); + + console.log( + `Adapter ptUniBTC balance after swap:${formatTokenBalance( + ptUniBtc, + await ptUniBtc.contract.balanceOf(adapter.address) + )}` + ); + } + + async function postMaturity() { + // move time and make after maturity + await ethers.provider.send('evm_increaseTime', [180 * 24 * 60 * 60]); + await ethers.provider.send('evm_mine', []); + } + + async function swapExactInput( + signer: SignerWithAddress, + zeroToOne: boolean, + amountIn: BigNumber, + minAmountOut: BigNumber + ) { + const inToken = zeroToOne ? uniBtc : ptUniBtc; + const outToken = zeroToOne ? ptUniBtc : uniBtc; + const inTokenBalanceBefore = await inToken.contract.balanceOf(signer.address); + const outTokenBalanceBefore = await outToken.contract.balanceOf(signer.address); + console.log( + `signer balance before swap: ${formatTokenBalance(inToken, inTokenBalanceBefore)}, ` + + `${formatTokenBalance(outToken, outTokenBalanceBefore)}` + ); + const amountInStr = formatTokenBalance(inToken, amountIn); + const minAmountOutStr = formatTokenBalance(outToken, minAmountOut); + console.log(`swapExactInput:`); + console.log(`amountIn: ${amountInStr}`); + console.log(`minAmountOut: ${minAmountOutStr}`); + const priceInToken0Before = await pool.last_prices(); + await router.swapExactInput( + BigNumber.from(0), + inToken.contract.address, + outToken.contract.address, + amountIn, + minAmountOut + ); + const inTokenBalanceAfter = await inToken.contract.balanceOf(signer.address); + const outTokenBalanceAfter = await outToken.contract.balanceOf(signer.address); + console.log( + `\nsigner balance after swap: ${formatTokenBalance(inToken, inTokenBalanceAfter)}, ` + + `${formatTokenBalance(outToken, outTokenBalanceAfter)}` + ); + const inTokenDelta = inTokenBalanceBefore.sub(inTokenBalanceAfter); + const outTokenDelta = outTokenBalanceAfter.sub(outTokenBalanceBefore); + console.log( + `signer balances delta: -${formatTokenBalance(inToken, inTokenDelta)}, ` + + `${formatTokenBalance(outToken, outTokenDelta)}` + ); + const one = BigNumber.from(10).pow(18); + let actualPriceInToken0: BigNumber; + if (zeroToOne) { + actualPriceInToken0 = inTokenDelta.mul(one).div(outTokenDelta); + } else { + actualPriceInToken0 = outTokenDelta.mul(one).div(inTokenDelta); + } + console.log(`\nPrice before swap (fees not included):`); + printPrice(priceInToken0Before); + console.log(`\nActual swap price (with fees):`); + printPriceWithDelta(actualPriceInToken0, priceInToken0Before); + expect(inTokenBalanceAfter).to.be.equal(inTokenBalanceBefore.sub(amountIn)); + expect(outTokenBalanceAfter).to.be.greaterThanOrEqual(outTokenBalanceBefore.add(minAmountOut)); + + await printAdapterBalances(); + } + + async function swapExactOutput( + signer: SignerWithAddress, + zeroToOne: boolean, + maxAmountIn: BigNumber, + amountOut: BigNumber + ) { + const inToken = zeroToOne ? uniBtc : ptUniBtc; + const outToken = zeroToOne ? ptUniBtc : uniBtc; + const inTokenBalanceBefore = await inToken.contract.balanceOf(signer.address); + const outTokenBalanceBefore = await outToken.contract.balanceOf(signer.address); + + console.log( + `signer balance before swap: ${formatTokenBalance(inToken, inTokenBalanceBefore)}, ` + + `${formatTokenBalance(outToken, outTokenBalanceBefore)}` + ); + const maxAmountInStr = formatTokenBalance(inToken, maxAmountIn); + const amountOutStr = formatTokenBalance(outToken, amountOut); + + console.log(`swapExactOutput:`); + console.log(`maxAmountIn: ${maxAmountInStr}`); + console.log(`amountOut: ${amountOutStr}`); + + const priceInToken0Before = await pool.last_prices(); + + await router.swapExactOutput( + BigNumber.from(0), + inToken.contract.address, + outToken.contract.address, + maxAmountIn, + amountOut + ); + + const inTokenBalanceAfter = await inToken.contract.balanceOf(signer.address); + const outTokenBalanceAfter = await outToken.contract.balanceOf(signer.address); + + console.log( + `\nsigner balance after swap: ${formatTokenBalance(inToken, inTokenBalanceAfter)}, ` + + `${formatTokenBalance(outToken, outTokenBalanceAfter)}` + ); + + const inTokenDelta = inTokenBalanceBefore.sub(inTokenBalanceAfter); + const outTokenDelta = outTokenBalanceAfter.sub(outTokenBalanceBefore); + console.log( + `signer balances delta: -${formatTokenBalance(inToken, inTokenDelta)}, ` + + `${formatTokenBalance(outToken, outTokenDelta)}` + ); + const one = BigNumber.from(10).pow(18); + let actualPriceInToken0: BigNumber; + if (zeroToOne) { + actualPriceInToken0 = inTokenDelta.mul(one).div(outTokenDelta); + } else { + actualPriceInToken0 = outTokenDelta.mul(one).div(inTokenDelta); + } + + console.log(`\nPrice before swap (fees not included):`); + printPrice(priceInToken0Before); + console.log(`\nActual swap price (with fees):`); + printPriceWithDelta(actualPriceInToken0, priceInToken0Before); + + expect(inTokenBalanceAfter).to.be.greaterThanOrEqual(inTokenBalanceBefore.sub(maxAmountIn)); + expect(outTokenBalanceAfter).to.be.equal(outTokenBalanceBefore.add(amountOut)); + + await printAdapterBalances(); + } + + it('swapExactInput pre maturity uniBtc to pt-uniBTC', async () => { + const [owner] = await ethers.getSigners(); + const amountIn = parseUnits('0.01', 8); + const minAmountOut = amountIn.div(100); + + await uniBtc.contract.connect(owner).approve(router.address, amountIn); + + await swapExactInput(owner, true, amountIn, minAmountOut); + }); + + it('swapExactInput pre maturity pt-uniBTC to uniBTC', async () => { + const [owner] = await ethers.getSigners(); + const amountIn = parseUnits('0.05', 8); + const minAmountOut = amountIn.div(10); + + await ptUniBtc.contract.approve(router.address, amountIn); + + await swapExactInput(owner, false, amountIn, minAmountOut); + }); + + it('swapExactOutput pre maturity uniBTC to pt-uniBTC', async () => { + const [owner] = await ethers.getSigners(); + + const maxAmountIn = parseUnits('0.05', 8); + const amountOut = parseUnits('0.01', 8); + + console.log(`Balance of ${await uniBtc.contract.balanceOf(owner.address)}`); + await uniBtc.contract.connect(owner).approve(router.address, maxAmountIn); + + await swapExactOutput(owner, true, maxAmountIn, amountOut); + }); + + it('swapExactOutput pre maturity pt-uniBTC to uniBTC', async () => { + const [owner] = await ethers.getSigners(); + + const maxAmountIn = parseUnits('0.05', 8); // pt-uniBTC + const amountOut = parseUnits('0.01', 8); // uniBTC + + await ptUniBtc.contract.approve(router.address, maxAmountIn); + + await swapExactOutput(owner, false, maxAmountIn, amountOut); + }); + + it('swapExactInput post maturity uniBTC to ptUniBtc, NotSupported', async () => { + await postMaturity(); + const [owner] = await ethers.getSigners(); + + const amountIn = parseUnits('0.05', 8); // uniBTC + const minAmountOut = parseUnits('0.05', 8); // pt-uniBTC + + await uniBtc.contract.approve(router.address, amountIn); + + await expect(swapExactInput(owner, true, amountIn, minAmountOut)).to.be.revertedWithCustomError( + adapter, + 'NotSupported' + ); + }); + + it('swapExactInput post maturity uniBtc to ptUniBtc. NotSupported', async () => { + await postMaturity(); + const [owner] = await ethers.getSigners(); + + const maxAmountIn = parseUnits('0.05', 8); // uniBTC + const amountOut = parseUnits('0.05', 8); // pt-uniBTC + + await uniBtc.contract.approve(router.address, maxAmountIn); + + await expect(swapExactOutput(owner, true, amountOut, maxAmountIn)).to.be.revertedWithCustomError( + adapter, + 'NotSupported' + ); + }); + + it('swapExactInput post maturity pt-uniBtc to uniBTC', async () => { + await postMaturity(); + const [owner] = await ethers.getSigners(); + + const amountIn = parseUnits('0.01', 8); // 0.01 pt-uniBtc + const minAmountOut = amountIn.div(100); + + await ptUniBtc.contract.connect(owner).approve(router.address, amountIn); + + await swapExactInput(owner, false, amountIn, minAmountOut); + }); + + it.only('swapExactOutput post maturity pt-uniBtc to uniBTC', async () => { + await postMaturity(); + const [user1] = await ethers.getSigners(); + + const maxAmountIn = parseUnits('0.015', 8); // pt-uniBTC + const amountOut = parseUnits('0.01', 8); // uniBTC + + await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); + + await swapExactOutput(user1, false, maxAmountIn, amountOut); + }); + + // it('swapExactOutput WETH to frxETH. TooMuchRequested', async () => { + // const [owner] = await ethers.getSigners(); + + // const maxAmountIn = BigNumber.from(10).pow(15); // 0.0001 WETH + // const amountOut = maxAmountIn.mul(1000); + + // await token0.contract.approve(router.address, maxAmountIn); + + // await expect(swapExactOutput(owner, true, maxAmountIn, amountOut)).to.be.revertedWith( + // 'Exchange resulted in fewer coins than expected' + // ); + // }); + + // it('swapExactOutput frxETH to WETH. TooMuchRequested', async () => { + // const [owner] = await ethers.getSigners(); + + // const maxAmountIn = BigNumber.from(10).pow(15); // 0.0001 frxETH + // const amountOut = maxAmountIn.mul(1000); + + // await token1.contract.approve(router.address, maxAmountIn); + + // await expect(swapExactOutput(owner, false, maxAmountIn, amountOut)).to.be.revertedWith( + // 'Exchange resulted in fewer coins than expected' + // ); + // }); +}); From 05ec5535bc5f15d9768ad8e39e846e587acf46d4 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Mon, 5 Aug 2024 18:59:22 +0300 Subject: [PATCH 02/10] added owner and sweepDust method --- .../adapters/SpectraSWIbtPtCurveAdapter.sol | 11 +- .../SpectraSwIbtPtCurveAdapter.eth.spec.ts | 396 +++++++++--------- 2 files changed, 215 insertions(+), 192 deletions(-) diff --git a/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol b/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol index c025b212..5b500648 100644 --- a/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol +++ b/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.19; import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import '@openzeppelin/contracts/access/Ownable2Step.sol'; import '../interfaces/IMarginlyRouter.sol'; import '../interfaces/IMarginlyAdapter.sol'; import './interfaces/ICurvePool.sol'; @@ -111,7 +112,7 @@ interface ISpectra4626Wrapper { ///@notice Adapter for Spectra Curve pool of two tokens SpectraWrapped IBT and PT /// but adapter for IBT and PT -contract SpectraSWIbtPtCurveAdapter is IMarginlyAdapter { +contract SpectraSWIbtPtCurveAdapter is IMarginlyAdapter, Ownable2Step { using SafeERC20 for IERC20; error WrongPoolInput(); @@ -452,4 +453,12 @@ contract SpectraSWIbtPtCurveAdapter is IMarginlyAdapter { function addPools(PoolInput[] calldata poolsData) external { _addPools(poolsData); } + + /// @dev During swap Pt to exact SW after maturity a little amount of sw-ibt might stay at the adapter contract + function sweepDust(address tokenA, address tokenB, address recipient) external onlyOwner { + PoolData memory poolData = getPoolData[tokenA][tokenB]; + + uint256 dust = IERC20(poolData.swIbt).balanceOf(address(this)); + IERC20(poolData.swIbt).safeTransfer(recipient, dust); + } } diff --git a/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts b/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts index 6b215603..bfea91a6 100644 --- a/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts +++ b/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts @@ -1,11 +1,9 @@ import { ethers } from 'hardhat'; -import { ERC20, ERC20__factory, ICurvePool, MarginlyRouter, SpectraSWIbtPtCurveAdapter } from '../../typechain-types'; +import { ERC20, ICurvePool, MarginlyRouter, SpectraSWIbtPtCurveAdapter } from '../../typechain-types'; import { AdapterInputStruct } from '@marginly/periphery/typechain-types/contracts/admin/abstract/RouterActions'; -import { BigNumber, Contract } from 'ethers'; -import { EthAddress } from '@marginly/common'; +import { BigNumber } from 'ethers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; -import { ArbMainnetERC20BalanceOfSlot, EthereumMainnetERC20BalanceOfSlot, setTokenBalance } from '../shared/tokens'; import { parseEther, parseUnits } from 'ethers/lib/utils'; interface TokenInfo { @@ -19,7 +17,7 @@ function formatTokenBalance(token: TokenInfo, amount: BigNumber): string { return `${ethers.utils.formatUnits(amount, token.decimals)} ${token.symbol}`; } -describe.only('Curve adapter for sw-uniBTC/Pt-uniBTC pool (CurveAdapter)', () => { +describe.only('Spectra adapter uniBTC/Pt-uniBTC (CurvePool sw-uniBTC/pt-uniBTC)', () => { // sw-uniBTC/Pt-uniBTC pool - https://etherscan.io/address/0xb09fc8bbdcc8dc9d8b3775132c52fcebf1c7dbb3 const poolAddress = '0xb09fc8bbdcc8dc9d8b3775132c52fcebf1c7dbb3'; const uniBtcAddress = '0x004E9C3EF86bc1ca1f0bB5C7662861Ee93350568'; @@ -29,83 +27,12 @@ describe.only('Curve adapter for sw-uniBTC/Pt-uniBTC pool (CurveAdapter)', () => const ptUniBtcHolderAddress = '0x14975679e5f87c25fa2c54958e735a79B5B93043'; let ptUniBtcHolder: SignerWithAddress; - let swUniBtc: TokenInfo; let ptUniBtc: TokenInfo; let uniBtc: TokenInfo; let pool: ICurvePool; let router: MarginlyRouter; let adapter: SpectraSWIbtPtCurveAdapter; - before(async () => { - pool = await ethers.getContractAt('ICurvePool', poolAddress); - const adapterFactory = await ethers.getContractFactory('SpectraSWIbtPtCurveAdapter'); - - const token0Address = await pool.callStatic.coins(0); - const token1Address = await pool.callStatic.coins(1); - const token0Contract = await ethers.getContractAt('ERC20', token0Address); - const token1Contract = await ethers.getContractAt('ERC20', token1Address); - const uniBtcContract = await ethers.getContractAt('ERC20', uniBtcAddress); - const token0Symbol = await token0Contract.symbol(); - const token1Symbol = await token1Contract.symbol(); - const token0Decimals = await token0Contract.decimals(); - const token1Decimals = await token1Contract.decimals(); - - swUniBtc = { - contract: token0Contract, - symbol: token0Symbol, - decimals: token0Decimals, - balanceOfSlot: EthereumMainnetERC20BalanceOfSlot.UNIBTC, - }; - ptUniBtc = { - contract: token1Contract, - symbol: token1Symbol, - decimals: token1Decimals, - balanceOfSlot: EthereumMainnetERC20BalanceOfSlot.UNIBTC, - }; - uniBtc = { - contract: uniBtcContract, - symbol: await uniBtcContract.symbol(), - decimals: await uniBtcContract.decimals(), - balanceOfSlot: EthereumMainnetERC20BalanceOfSlot.UNIBTC, - }; - - adapter = await adapterFactory.deploy([ - { - ibToken: uniBtcAddress, - ptToken: token1Address, - pool: poolAddress, - }, - ]); - console.log('Adapter address: ', adapter.address); - - const routerFactory = await ethers.getContractFactory('MarginlyRouter'); - router = await routerFactory.deploy([{ dexIndex: 0, adapter: adapter.address }]); - - const [owner, user1, user2] = await ethers.getSigners(); - uniBtcHolder = await ethers.getImpersonatedSigner(uniBtcHolderAddress); - ptUniBtcHolder = await ethers.getImpersonatedSigner(ptUniBtcHolderAddress); - - await owner.sendTransaction({ - to: uniBtcHolderAddress, - value: parseEther('1.0'), - }); - - await owner.sendTransaction({ - to: ptUniBtcHolderAddress, - value: parseEther('1.0'), - }); - - const token0InitBalance = BigNumber.from(10).pow(8); - await uniBtc.contract.connect(uniBtcHolder).transfer(owner.address, token0InitBalance); - await uniBtc.contract.connect(uniBtcHolder).transfer(user1.address, token0InitBalance); - await uniBtc.contract.connect(uniBtcHolder).transfer(user2.address, token0InitBalance); - - const token1InitBalance = BigNumber.from(10).pow(8); - await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(owner.address, token1InitBalance); - await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(user1.address, token1InitBalance); - await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(user2.address, token1InitBalance); - }); - function printPrice(priceInToken0: BigNumber) { const priceStr = ethers.utils.formatEther(priceInToken0); const inversePrice = 1 / Number.parseFloat(priceStr); @@ -130,22 +57,6 @@ describe.only('Curve adapter for sw-uniBTC/Pt-uniBTC pool (CurveAdapter)', () => ); } - async function printAdapterBalances() { - console.log( - `\nAdapter uniBTC balance after swap:${formatTokenBalance( - uniBtc, - await uniBtc.contract.balanceOf(adapter.address) - )}` - ); - - console.log( - `Adapter ptUniBTC balance after swap:${formatTokenBalance( - ptUniBtc, - await ptUniBtc.contract.balanceOf(adapter.address) - )}` - ); - } - async function postMaturity() { // move time and make after maturity await ethers.provider.send('evm_increaseTime', [180 * 24 * 60 * 60]); @@ -172,13 +83,9 @@ describe.only('Curve adapter for sw-uniBTC/Pt-uniBTC pool (CurveAdapter)', () => console.log(`amountIn: ${amountInStr}`); console.log(`minAmountOut: ${minAmountOutStr}`); const priceInToken0Before = await pool.last_prices(); - await router.swapExactInput( - BigNumber.from(0), - inToken.contract.address, - outToken.contract.address, - amountIn, - minAmountOut - ); + await router + .connect(signer) + .swapExactInput(BigNumber.from(0), inToken.contract.address, outToken.contract.address, amountIn, minAmountOut); const inTokenBalanceAfter = await inToken.contract.balanceOf(signer.address); const outTokenBalanceAfter = await outToken.contract.balanceOf(signer.address); console.log( @@ -205,7 +112,8 @@ describe.only('Curve adapter for sw-uniBTC/Pt-uniBTC pool (CurveAdapter)', () => expect(inTokenBalanceAfter).to.be.equal(inTokenBalanceBefore.sub(amountIn)); expect(outTokenBalanceAfter).to.be.greaterThanOrEqual(outTokenBalanceBefore.add(minAmountOut)); - await printAdapterBalances(); + expect(await ptUniBtc.contract.balanceOf(adapter.address)).to.eq(0); + expect(await uniBtc.contract.balanceOf(adapter.address)).to.eq(0); } async function swapExactOutput( @@ -232,13 +140,9 @@ describe.only('Curve adapter for sw-uniBTC/Pt-uniBTC pool (CurveAdapter)', () => const priceInToken0Before = await pool.last_prices(); - await router.swapExactOutput( - BigNumber.from(0), - inToken.contract.address, - outToken.contract.address, - maxAmountIn, - amountOut - ); + await router + .connect(signer) + .swapExactOutput(BigNumber.from(0), inToken.contract.address, outToken.contract.address, maxAmountIn, amountOut); const inTokenBalanceAfter = await inToken.contract.balanceOf(signer.address); const outTokenBalanceAfter = await outToken.contract.balanceOf(signer.address); @@ -270,129 +174,239 @@ describe.only('Curve adapter for sw-uniBTC/Pt-uniBTC pool (CurveAdapter)', () => expect(inTokenBalanceAfter).to.be.greaterThanOrEqual(inTokenBalanceBefore.sub(maxAmountIn)); expect(outTokenBalanceAfter).to.be.equal(outTokenBalanceBefore.add(amountOut)); - await printAdapterBalances(); + expect(await ptUniBtc.contract.balanceOf(adapter.address)).to.eq(0); + expect(await uniBtc.contract.balanceOf(adapter.address)).to.eq(0); } - it('swapExactInput pre maturity uniBtc to pt-uniBTC', async () => { - const [owner] = await ethers.getSigners(); - const amountIn = parseUnits('0.01', 8); - const minAmountOut = amountIn.div(100); + before(async () => { + pool = await ethers.getContractAt('ICurvePool', poolAddress); + const adapterFactory = await ethers.getContractFactory('SpectraSWIbtPtCurveAdapter'); - await uniBtc.contract.connect(owner).approve(router.address, amountIn); + const token0Address = await pool.callStatic.coins(0); + const token1Address = await pool.callStatic.coins(1); + const token0Contract = await ethers.getContractAt('ERC20', token0Address); + const token1Contract = await ethers.getContractAt('ERC20', token1Address); + const uniBtcContract = await ethers.getContractAt('ERC20', uniBtcAddress); + const token0Symbol = await token0Contract.symbol(); + const token1Symbol = await token1Contract.symbol(); + const token0Decimals = await token0Contract.decimals(); + const token1Decimals = await token1Contract.decimals(); - await swapExactInput(owner, true, amountIn, minAmountOut); - }); + ptUniBtc = { + contract: token1Contract, + symbol: token1Symbol, + decimals: token1Decimals, + }; + uniBtc = { + contract: uniBtcContract, + symbol: await uniBtcContract.symbol(), + decimals: await uniBtcContract.decimals(), + }; - it('swapExactInput pre maturity pt-uniBTC to uniBTC', async () => { - const [owner] = await ethers.getSigners(); - const amountIn = parseUnits('0.05', 8); - const minAmountOut = amountIn.div(10); + adapter = await adapterFactory.deploy([ + { + ibToken: uniBtcAddress, + ptToken: token1Address, + pool: poolAddress, + }, + ]); + console.log('Adapter address: ', adapter.address); - await ptUniBtc.contract.approve(router.address, amountIn); + const routerFactory = await ethers.getContractFactory('MarginlyRouter'); + router = await routerFactory.deploy([{ dexIndex: 0, adapter: adapter.address }]); - await swapExactInput(owner, false, amountIn, minAmountOut); - }); + const [owner, user1, user2] = await ethers.getSigners(); + uniBtcHolder = await ethers.getImpersonatedSigner(uniBtcHolderAddress); + ptUniBtcHolder = await ethers.getImpersonatedSigner(ptUniBtcHolderAddress); - it('swapExactOutput pre maturity uniBTC to pt-uniBTC', async () => { - const [owner] = await ethers.getSigners(); + await owner.sendTransaction({ + to: uniBtcHolderAddress, + value: parseEther('1.0'), + }); - const maxAmountIn = parseUnits('0.05', 8); - const amountOut = parseUnits('0.01', 8); + await owner.sendTransaction({ + to: ptUniBtcHolderAddress, + value: parseEther('1.0'), + }); - console.log(`Balance of ${await uniBtc.contract.balanceOf(owner.address)}`); - await uniBtc.contract.connect(owner).approve(router.address, maxAmountIn); + const token0InitBalance = BigNumber.from(10).pow(8); + await uniBtc.contract.connect(uniBtcHolder).transfer(owner.address, token0InitBalance); + await uniBtc.contract.connect(uniBtcHolder).transfer(user1.address, token0InitBalance); + await uniBtc.contract.connect(uniBtcHolder).transfer(user2.address, token0InitBalance); - await swapExactOutput(owner, true, maxAmountIn, amountOut); + const token1InitBalance = BigNumber.from(10).pow(8); + await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(owner.address, token1InitBalance); + await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(user1.address, token1InitBalance); + await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(user2.address, token1InitBalance); }); - it('swapExactOutput pre maturity pt-uniBTC to uniBTC', async () => { - const [owner] = await ethers.getSigners(); + describe('Pre maturuty', () => { + it('swapExactInput uniBtc to pt-uniBTC', async () => { + const [, user1] = await ethers.getSigners(); + const amountIn = parseUnits('0.01', 8); + const minAmountOut = amountIn.div(100); - const maxAmountIn = parseUnits('0.05', 8); // pt-uniBTC - const amountOut = parseUnits('0.01', 8); // uniBTC + await uniBtc.contract.connect(user1).approve(router.address, amountIn); - await ptUniBtc.contract.approve(router.address, maxAmountIn); + await swapExactInput(user1, true, amountIn, minAmountOut); + }); - await swapExactOutput(owner, false, maxAmountIn, amountOut); - }); + it('swapExactInput pt-uniBTC to uniBTC', async () => { + const [, user1] = await ethers.getSigners(); + const amountIn = parseUnits('0.05', 8); + const minAmountOut = amountIn.div(10); - it('swapExactInput post maturity uniBTC to ptUniBtc, NotSupported', async () => { - await postMaturity(); - const [owner] = await ethers.getSigners(); + await ptUniBtc.contract.connect(user1).approve(router.address, amountIn); - const amountIn = parseUnits('0.05', 8); // uniBTC - const minAmountOut = parseUnits('0.05', 8); // pt-uniBTC + await swapExactInput(user1, false, amountIn, minAmountOut); + }); - await uniBtc.contract.approve(router.address, amountIn); + it('swapExactOutput uniBTC to pt-uniBTC', async () => { + const [, user1] = await ethers.getSigners(); - await expect(swapExactInput(owner, true, amountIn, minAmountOut)).to.be.revertedWithCustomError( - adapter, - 'NotSupported' - ); - }); + const maxAmountIn = parseUnits('0.05', 8); + const amountOut = parseUnits('0.01', 8); - it('swapExactInput post maturity uniBtc to ptUniBtc. NotSupported', async () => { - await postMaturity(); - const [owner] = await ethers.getSigners(); + console.log(`Balance of ${await uniBtc.contract.balanceOf(user1.address)}`); + await uniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - const maxAmountIn = parseUnits('0.05', 8); // uniBTC - const amountOut = parseUnits('0.05', 8); // pt-uniBTC + await swapExactOutput(user1, true, maxAmountIn, amountOut); + }); - await uniBtc.contract.approve(router.address, maxAmountIn); + it('swapExactOutput pt-uniBTC to uniBTC', async () => { + const [, user1] = await ethers.getSigners(); - await expect(swapExactOutput(owner, true, amountOut, maxAmountIn)).to.be.revertedWithCustomError( - adapter, - 'NotSupported' - ); - }); + const maxAmountIn = parseUnits('0.05', 8); // pt-uniBTC + const amountOut = parseUnits('0.01', 8); // uniBTC - it('swapExactInput post maturity pt-uniBtc to uniBTC', async () => { - await postMaturity(); - const [owner] = await ethers.getSigners(); + await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - const amountIn = parseUnits('0.01', 8); // 0.01 pt-uniBtc - const minAmountOut = amountIn.div(100); + await swapExactOutput(user1, false, maxAmountIn, amountOut); + }); - await ptUniBtc.contract.connect(owner).approve(router.address, amountIn); + it('swapExactInput uniBtc to pt-uniBTC, Curve slippage', async () => { + const [, user1] = await ethers.getSigners(); + const amountIn = parseUnits('0.01', 8); + const minAmountOut = parseUnits('0.015', 8); - await swapExactInput(owner, false, amountIn, minAmountOut); - }); + await uniBtc.contract.connect(user1).approve(router.address, amountIn); + + // Curve rejected with Slippage reason string + await expect(swapExactInput(user1, true, amountIn, minAmountOut)).to.be.rejected; + }); + + it('swapExactInput pt-uniBTC to uniBTC, Curve slippage', async () => { + const [, user1] = await ethers.getSigners(); + const amountIn = parseUnits('0.01', 8); + const minAmountOut = parseUnits('0.015', 8); + + await ptUniBtc.contract.connect(user1).approve(router.address, amountIn); + + await expect(swapExactInput(user1, false, amountIn, minAmountOut)).to.be.rejected; + }); + + it('swapExactOutput uniBTC to pt-uniBTC, Curve slippage', async () => { + const [, user1] = await ethers.getSigners(); + + const maxAmountIn = parseUnits('0.005', 8); + const amountOut = parseUnits('0.01', 8); + + await uniBtc.contract.connect(user1).approve(router.address, maxAmountIn); + + await expect(swapExactOutput(user1, true, maxAmountIn, amountOut)).to.be.rejected; + }); - it.only('swapExactOutput post maturity pt-uniBtc to uniBTC', async () => { - await postMaturity(); - const [user1] = await ethers.getSigners(); + it('swapExactOutput pt-uniBTC to uniBTC, Curve slippage', async () => { + const [, user1] = await ethers.getSigners(); - const maxAmountIn = parseUnits('0.015', 8); // pt-uniBTC - const amountOut = parseUnits('0.01', 8); // uniBTC + const maxAmountIn = parseUnits('0.005', 8); // pt-uniBTC + const amountOut = parseUnits('0.01', 8); // uniBTC - await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); + await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - await swapExactOutput(user1, false, maxAmountIn, amountOut); + await expect(swapExactOutput(user1, false, maxAmountIn, amountOut)).to.be.rejected; + }); }); - // it('swapExactOutput WETH to frxETH. TooMuchRequested', async () => { - // const [owner] = await ethers.getSigners(); + describe('post maturity', () => { + beforeEach(async () => { + await postMaturity(); + }); - // const maxAmountIn = BigNumber.from(10).pow(15); // 0.0001 WETH - // const amountOut = maxAmountIn.mul(1000); + it('swapExactInput post maturity uniBTC to ptUniBtc, NotSupported', async () => { + const [, user1] = await ethers.getSigners(); - // await token0.contract.approve(router.address, maxAmountIn); + const amountIn = parseUnits('0.05', 8); // uniBTC + const minAmountOut = parseUnits('0.05', 8); // pt-uniBTC - // await expect(swapExactOutput(owner, true, maxAmountIn, amountOut)).to.be.revertedWith( - // 'Exchange resulted in fewer coins than expected' - // ); - // }); + await uniBtc.contract.connect(user1).approve(router.address, amountIn); - // it('swapExactOutput frxETH to WETH. TooMuchRequested', async () => { - // const [owner] = await ethers.getSigners(); + await expect(swapExactInput(user1, true, amountIn, minAmountOut)).to.be.revertedWithCustomError( + adapter, + 'NotSupported' + ); + }); - // const maxAmountIn = BigNumber.from(10).pow(15); // 0.0001 frxETH - // const amountOut = maxAmountIn.mul(1000); + it('swapExactInput post maturity uniBtc to ptUniBtc. NotSupported', async () => { + const [, user1] = await ethers.getSigners(); - // await token1.contract.approve(router.address, maxAmountIn); + const maxAmountIn = parseUnits('0.05', 8); // uniBTC + const amountOut = parseUnits('0.05', 8); // pt-uniBTC - // await expect(swapExactOutput(owner, false, maxAmountIn, amountOut)).to.be.revertedWith( - // 'Exchange resulted in fewer coins than expected' - // ); - // }); + await uniBtc.contract.connect(user1).approve(router.address, maxAmountIn); + + await expect(swapExactOutput(user1, true, amountOut, maxAmountIn)).to.be.revertedWithCustomError( + adapter, + 'NotSupported' + ); + }); + + it('swapExactInput post maturity pt-uniBtc to uniBTC', async () => { + const [, user1] = await ethers.getSigners(); + + const amountIn = parseUnits('0.01', 8); // 0.01 pt-uniBtc + const minAmountOut = amountIn.div(100); + + await ptUniBtc.contract.connect(user1).approve(router.address, amountIn); + + await swapExactInput(user1, false, amountIn, minAmountOut); + }); + + it('swapExactOutput post maturity pt-uniBtc to uniBTC', async () => { + const [, user1] = await ethers.getSigners(); + + const maxAmountIn = parseUnits('0.015', 8); // pt-uniBTC + const amountOut = parseUnits('0.01', 8); // uniBTC + + await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); + + await swapExactOutput(user1, false, maxAmountIn, amountOut); + }); + + it('swapExactInput post maturity pt-uniBtc to uniBTC, InsufficientAmount', async () => { + const [, user1] = await ethers.getSigners(); + + const amountIn = parseUnits('0.01', 8); // 0.01 pt-uniBtc + const minAmountOut = parseUnits('0.02', 8); //0.02 uniBTC + + await ptUniBtc.contract.connect(user1).approve(router.address, amountIn); + + await expect(swapExactInput(user1, false, amountIn, minAmountOut)).to.be.revertedWithCustomError( + adapter, + 'InsufficientAmount' + ); + }); + + it('swapExactOutput post maturity pt-uniBtc to uniBTC', async () => { + const [, user1] = await ethers.getSigners(); + + const maxAmountIn = parseUnits('0.01', 8); // pt-uniBTC + const amountOut = parseUnits('0.02', 8); // uniBTC + + await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); + + //error in SpectraWrapper-uniBTC + await expect(swapExactOutput(user1, false, maxAmountIn, amountOut)).to.be.rejected; + }); + }); }); From 91813491fa227feeaa054e368ecd10c26ff62786 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Mon, 27 Jan 2025 19:01:23 +0300 Subject: [PATCH 03/10] add Spectra interfaces and update utility functions for token handling --- .../contracts/adapters/SpectraAdapter.sol | 486 ++++++++++++++++++ .../adapters/SpectraSWIbtPtCurveAdapter.sol | 464 ----------------- .../interfaces/ISpectraErc4626Wrapper.sol | 45 ++ .../interfaces/ISpectraPrincipalToken.sol | 60 +++ .../test/int/SpectraAdapter.eth.spec.ts | 460 +++++++++++++++++ .../SpectraSwIbtPtCurveAdapter.eth.spec.ts | 412 --------------- packages/router/test/shared/tokens.ts | 5 + packages/router/test/shared/utils.ts | 11 +- 8 files changed, 1064 insertions(+), 879 deletions(-) create mode 100644 packages/router/contracts/adapters/SpectraAdapter.sol delete mode 100644 packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol create mode 100644 packages/router/contracts/adapters/interfaces/ISpectraErc4626Wrapper.sol create mode 100644 packages/router/contracts/adapters/interfaces/ISpectraPrincipalToken.sol create mode 100644 packages/router/test/int/SpectraAdapter.eth.spec.ts delete mode 100644 packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts diff --git a/packages/router/contracts/adapters/SpectraAdapter.sol b/packages/router/contracts/adapters/SpectraAdapter.sol new file mode 100644 index 00000000..6950a326 --- /dev/null +++ b/packages/router/contracts/adapters/SpectraAdapter.sol @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import '@openzeppelin/contracts/access/Ownable2Step.sol'; +import '../interfaces/IMarginlyRouter.sol'; +import '../interfaces/IMarginlyAdapter.sol'; +import './interfaces/ICurvePool.sol'; +import './interfaces/ISpectraErc4626Wrapper.sol'; +import './interfaces/ISpectraPrincipalToken.sol'; + +/// @notice Adapter for Spectra finance pool (old curve pool) of two tokens IBT/PT +/// @dev IBT is ERC4626 compliant or SpectraWrapped IBT +contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { + using SafeERC20 for IERC20; + + error WrongPoolInput(); + error UnknownPair(); + + event NewPair(address indexed ptToken, address indexed ibToken, address curvePool); + + struct PoolInput { + /// @dev Address of Principal Token + address pt; + /// @dev Address of Interest Bearing Token. Address of protocol token (not Spectra wrapper) + address ibt; + /// @dev Address of spectra curve pool IBT or Spectra Wrapped IBT / PT + address pool; + } + + struct PoolData { + /// @dev Address of spectra curve pool IBT/PT-IBT or sw-IBT/PT-sw-IBT + address pool; + /// @dev Address of IBT (or sw-IBT) + address ibt; + /// @dev Address of pt token + address pt; + /// @dev True if curvePool.coins[0] is ibt, curvePool.coins[1] is pt + bool zeroIndexCoinIsIbt; + /// @dev True if ibt is spectraWrapped token + bool isSpectraWrappedIbt; + } + + mapping(address => mapping(address => PoolData)) public getPoolData; + + constructor(PoolInput[] memory pools) { + _addPools(pools); + } + + function _addPools(PoolInput[] memory pools) private { + PoolInput memory input; + uint256 length = pools.length; + for (uint256 i; i < length; ) { + input = pools[i]; + + address coin0 = ICurvePool(input.pool).coins(0); + address coin1 = ICurvePool(input.pool).coins(1); + + PoolData memory poolData = PoolData({ + pool: input.pool, + zeroIndexCoinIsIbt: true, + ibt: coin0, + pt: coin1, + isSpectraWrappedIbt: false + }); + + if (coin1 == input.pt) { + //check other token is spectra wrapper or not + if (coin0 != input.ibt) { + if (ISpectraErc4626Wrapper(coin0).vaultShare() != input.ibt) revert WrongPoolInput(); + + poolData.isSpectraWrappedIbt = true; + } + } else if (coin0 == input.pt) { + if (coin1 != input.ibt) { + if (ISpectraErc4626Wrapper(coin1).vaultShare() != input.ibt) revert WrongPoolInput(); + + poolData.isSpectraWrappedIbt = true; + } + + poolData.zeroIndexCoinIsIbt = false; + poolData.ibt = coin1; + poolData.pt = coin0; + } else { + revert WrongPoolInput(); + } + + getPoolData[input.pt][input.ibt] = poolData; + getPoolData[input.ibt][input.pt] = poolData; + + emit NewPair(input.pt, input.ibt, input.pool); + + unchecked { + ++i; + } + } + } + + function _getPoolDataSafe(address tokenA, address tokenB) private view returns (PoolData memory poolData) { + poolData = getPoolData[tokenA][tokenB]; + if (poolData.pool == address(0)) revert UnknownPair(); + } + + function _swapExactInputPreMaturity( + PoolData memory poolData, + address recipientArg, + address tokenInArg, + uint256 amountInArg, + uint256 minAmountOut + ) private returns (uint256 amountOut) { + bool tokenInIsPt = tokenInArg == poolData.pt; + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt && tokenInIsPt ? 1 : 0; + + if (tokenInIsPt) { + // PT -> IBT + address recipient = recipientArg; + + if (poolData.isSpectraWrappedIbt) { + // change recipient to address(this), it let make unwrap swIbt for Ibt after swap + recipient = address(this); + } + + amountOut = _curveSwapExactInput(poolData.pool, recipient, tokenInArg, tokenInIndex, amountInArg, minAmountOut); + + if (poolData.isSpectraWrappedIbt) { + amountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(amountOut, recipientArg, address(this)); + } + } else { + // IBT -> PT + + uint256 amountIn = amountInArg; + address tokenIn = tokenInArg; + + if (poolData.isSpectraWrappedIbt) { + // wrap IBT to sw-IBT and change recipient to current address(this) + IERC20(tokenIn).forceApprove(poolData.ibt, amountInArg); + amountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(amountInArg, address(this)); + tokenIn = poolData.ibt; // tokenIn is sw-IBT + } + + // swap in curve IBT to PT + amountOut = _curveSwapExactInput(poolData.pool, recipientArg, tokenIn, tokenInIndex, amountIn, minAmountOut); + } + } + + function _swapExactInputPostMaturity( + PoolData memory poolData, + address recipient, + address tokenIn, + uint256 amountIn + ) private returns (uint256 amountOut) { + if (tokenIn == poolData.pt) { + if (poolData.isSpectraWrappedIbt) { + // redeem sw-IBT + uint256 swAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); + // unwrap sw-IBT to IBT + amountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(swAmountOut, recipient, address(this)); + } else { + amountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, recipient, address(this)); + } + } else { + // IBT to PT swap is not possible after maturity + revert NotSupported(); + } + } + + function _swapExactOutputPtToIbtPreMaturity( + PoolData memory poolData, + address recipient, + address ibtOut, + uint256 ibtAmountOut, + uint256 maxPtAmountIn + ) private returns (uint256 ptAmountIn) { + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 1 : 0; + + if (poolData.isSpectraWrappedIbt) { + // PT -> sw-IBT -> IBT + // convert ibAmountOut to swAmountOut + uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewWrap(ibtAmountOut); + + // swap maxAmountPt to swIbt + uint256 swActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + tokenInIndex, + maxPtAmountIn, + swAmountOut + ); + + // unwrap swIbt to ibt + uint256 ibtActualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( + swActualAmountOut, + address(this), + address(this) + ); + + IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); + + if (ibtActualAmountOut < ibtAmountOut) { + revert TooMuchRequested(); + } + + if (ibtActualAmountOut == ibtAmountOut) { + return maxPtAmountIn; + } + + uint256 deltaIbtAmountOut = ibtActualAmountOut - ibtAmountOut; + + // wrap extra amountOut ibt to swIbt + IERC20(ibtOut).forceApprove(poolData.ibt, deltaIbtAmountOut); + uint256 deltaSwAmountOut = ISpectraErc4626Wrapper(poolData.ibt).wrap(deltaIbtAmountOut, address(this)); + + // swap and move excessive tokenIn directly to recipient. + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessivePtAmountIn = _curveSwapExactInput( + poolData.pool, + recipient, + poolData.ibt, + 1 - tokenInIndex, + deltaSwAmountOut, + 0 + ); + + ptAmountIn = maxPtAmountIn - excessivePtAmountIn; + } else { + // PT -> IBT + // swap maxAmountPt to ibt + uint256 ibtActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + tokenInIndex, + maxPtAmountIn, + ibtAmountOut + ); + + IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); + + if (ibtActualAmountOut < ibtAmountOut) { + revert TooMuchRequested(); + } + + if (ibtActualAmountOut == ibtAmountOut) { + return maxPtAmountIn; + } + + // swap and move excessive tokenIn directly to recipient. + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessivePtAmountIn = _curveSwapExactInput( + poolData.pool, + recipient, + poolData.ibt, + 1 - tokenInIndex, + ibtActualAmountOut - ibtAmountOut, + 0 + ); + + ptAmountIn = maxPtAmountIn - excessivePtAmountIn; + } + } + + function _swapExactOutputIbtToPtPreMaturity( + PoolData memory poolData, + address recipient, + address ibtIn, + uint256 ptAmountOut, + uint256 maxIbtAmountIn + ) private returns (uint256 ibAmountIn) { + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; + + if (poolData.isSpectraWrappedIbt) { + // sw-IBT -> IBT -> PT + // wrap ibt to swIbt + IERC20(ibtIn).forceApprove(poolData.ibt, maxIbtAmountIn); + uint256 swMaxAmountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(maxIbtAmountIn, address(this)); + + // swap all swMaxAmountIn to pt tokens + uint256 ptActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.ibt, + tokenInIndex, + swMaxAmountIn, + ptAmountOut + ); + + if (ptActualAmountOut < ptAmountOut) { + revert TooMuchRequested(); + } + + IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); + + if (ptActualAmountOut == ptAmountOut) { + return maxIbtAmountIn; + } + + uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; + // swap and move excessive tokenIn + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessiveSwAmountIn = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + 1 - tokenInIndex, + deltaPtAmountOut, + 0 + ); + + // unwrap exessive sw into ib and transfer back to recipient + uint256 excessiveIbAmountIn = ISpectraErc4626Wrapper(poolData.ibt).unwrap( + excessiveSwAmountIn, + recipient, + address(this) + ); + ibAmountIn = maxIbtAmountIn - excessiveIbAmountIn; + } else { + // IBT -> PT + // swap all swMaxAmountIn to pt tokens + uint256 ptActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.ibt, + tokenInIndex, + maxIbtAmountIn, + ptAmountOut + ); + + if (ptActualAmountOut < ptAmountOut) { + revert TooMuchRequested(); + } + + IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); + + if (ptActualAmountOut == ptAmountOut) { + return maxIbtAmountIn; + } + + uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; + // swap and move excessive tokenIn + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessiveIbtAmountIn = _curveSwapExactInput( + poolData.pool, + recipient, + poolData.pt, + 1 - tokenInIndex, + deltaPtAmountOut, + 0 + ); + + ibAmountIn = maxIbtAmountIn - excessiveIbtAmountIn; + } + } + + function _swapExactOutputPostMaturity( + PoolData memory poolData, + address recipient, + address tokenOut, + uint256 amountOut, + uint256 maxAmountIn + ) private returns (uint256 amountIn) { + if (tokenOut == poolData.pt) { + // swap IBT to PT is not possible after maturity + revert NotSupported(); + } else { + if (poolData.isSpectraWrappedIbt) { + // PT withdraw to sw-IBT, then unwrap sw-IBT to IBT + // calc sw-IBT amount from amountOut + uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewWrap(amountOut); + amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(swAmountOut, address(this), address(this)); + + uint256 actualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( + swAmountOut, + address(this), + address(this) + ); + if (actualAmountOut < amountOut) revert InsufficientAmount(); + + // actualAmountOut could be more than amountOut, but it's not possible to change it back to PT aftrer maturity + // dust may be left on the contract + IERC20(tokenOut).safeTransfer(recipient, amountOut); + + if (maxAmountIn == amountIn) { + return amountIn; + } + + // return rest of PT token back to recipient + IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); + } else { + amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(amountOut, recipient, address(this)); + + if (maxAmountIn == amountIn) { + return amountIn; + } + IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); + } + } + } + + function _curveSwapExactInput( + address poolAddress, + address recipient, + address tokenIn, + uint256 tokenInIndex, + uint256 amountIn, + uint256 minAmountOut + ) private returns (uint256 amountOut) { + SafeERC20.forceApprove(IERC20(tokenIn), poolAddress, amountIn); + + amountOut = ICurvePool(poolAddress).exchange( + tokenInIndex, + 1 - tokenInIndex, + amountIn, + minAmountOut, + false, + recipient + ); + } + + function _ptIsExpired(address pt) private view returns (bool) { + return ISpectraPrincipalToken(pt).maturity() < block.timestamp; + } + + function swapExactInput( + address recipient, + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 minAmountOut, + bytes calldata data + ) external returns (uint256 amountOut) { + PoolData memory poolData = _getPoolDataSafe(tokenIn, tokenOut); + + // move all input tokens from router to this adapter + IMarginlyRouter(msg.sender).adapterCallback(address(this), amountIn, data); + + if (_ptIsExpired(poolData.pt)) { + amountOut = _swapExactInputPostMaturity(poolData, recipient, tokenIn, amountIn); + } else { + amountOut = _swapExactInputPreMaturity(poolData, recipient, tokenIn, amountIn, minAmountOut); + } + + if (amountOut < minAmountOut) revert InsufficientAmount(); + } + + function swapExactOutput( + address recipient, + address tokenIn, + address tokenOut, + uint256 maxAmountIn, + uint256 amountOut, + bytes calldata data + ) external returns (uint256 amountIn) { + PoolData memory poolData = _getPoolDataSafe(tokenIn, tokenOut); + + IMarginlyRouter(msg.sender).adapterCallback(address(this), maxAmountIn, data); + + if (_ptIsExpired(poolData.pt)) { + amountIn = _swapExactOutputPostMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + } else { + if (tokenIn == poolData.pt) { + amountIn = _swapExactOutputPtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + } else { + amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); + } + } + } + + function addPools(PoolInput[] calldata poolsData) external onlyOwner { + _addPools(poolsData); + } + + /// @dev During swap Pt to exact SW after maturity a little amount of sw-ibt might stay at the adapter contract + function sweepDust(address token, address recipient) external onlyOwner { + uint256 dust = IERC20(token).balanceOf(address(this)); + IERC20(token).safeTransfer(recipient, dust); + } +} diff --git a/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol b/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol deleted file mode 100644 index 5b500648..00000000 --- a/packages/router/contracts/adapters/SpectraSWIbtPtCurveAdapter.sol +++ /dev/null @@ -1,464 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.19; - -import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; -import '@openzeppelin/contracts/access/Ownable2Step.sol'; -import '../interfaces/IMarginlyRouter.sol'; -import '../interfaces/IMarginlyAdapter.sol'; -import './interfaces/ICurvePool.sol'; - -import 'hardhat/console.sol'; - -interface ISpectraPrincipalToken { - /** - * @notice Returns the unix timestamp (uint256) at which the PT contract expires - * @return The unix timestamp (uint256) when PTs become redeemable - */ - function maturity() external view returns (uint256); - - /** - * @notice Burns owner's shares (PTs and YTs before expiry, PTs after expiry) - * and sends IBTs to receiver - * @param shares The amount of shares to burn - * @param receiver The address that will receive the IBTs - * @param owner The owner of the shares - * @return ibts The actual amount of IBT received for burning the shares - */ - function redeemForIBT(uint256 shares, address receiver, address owner) external returns (uint256 ibts); - - /** - * @notice Burns owner's shares (PTs and YTs before expiry, PTs after expiry) - * and sends IBTs to receiver - * @param shares The amount of shares to burn - * @param receiver The address that will receive the IBTs - * @param owner The owner of the shares - * @param minIbts The minimum IBTs that should be returned to user - * @return ibts The actual amount of IBT received for burning the shares - */ - function redeemForIBT( - uint256 shares, - address receiver, - address owner, - uint256 minIbts - ) external returns (uint256 ibts); - - /** - * @notice Burns owner's shares (before expiry : PTs and YTs) and sends IBTs to receiver - * @param ibts The amount of IBT to be received - * @param receiver The address that will receive the IBTs - * @param owner The owner of the shares (PTs and YTs) - * @return shares The actual amount of shares burnt for receiving the IBTs - */ - function withdrawIBT(uint256 ibts, address receiver, address owner) external returns (uint256 shares); - - /** - * @notice Burns owner's shares (before expiry : PTs and YTs) and sends IBTs to receiver - * @param ibts The amount of IBT to be received - * @param receiver The address that will receive the IBTs - * @param owner The owner of the shares (PTs and YTs) - * @param maxShares The maximum shares allowed to be burnt - * @return shares The actual amount of shares burnt for receiving the IBTs - */ - function withdrawIBT( - uint256 ibts, - address receiver, - address owner, - uint256 maxShares - ) external returns (uint256 shares); -} - -interface ISpectra4626Wrapper { - /// @dev Returns the address of the wrapped vault share. - function vaultShare() external view returns (address); - - /// @dev Allows the owner to deposit vault shares into the wrapper. - /// @param vaultShares The amount of vault shares to deposit. - /// @param receiver The address to receive the wrapper shares. - /// @return The amount of minted wrapper shares. - function wrap(uint256 vaultShares, address receiver) external returns (uint256); - - /// @dev Allows the owner to deposit vault shares into the wrapper, with support for slippage protection. - /// @param vaultShares The amount of vault shares to deposit. - /// @param receiver The address to receive the wrapper shares. - /// @param minShares The minimum allowed wrapper shares from this deposit. - /// @return The amount of minted wrapper shares. - function wrap(uint256 vaultShares, address receiver, uint256 minShares) external returns (uint256); - - /// @dev Allows the owner to withdraw vault shares from the wrapper. - /// @param shares The amount of wrapper shares to redeem. - /// @param receiver The address to receive the vault shares. - /// @param owner The address of the owner of the wrapper shares. - /// @return The amount of withdrawn vault shares. - function unwrap(uint256 shares, address receiver, address owner) external returns (uint256); - - /// @dev Allows the owner to withdraw vault shares from the wrapper, with support for slippage protection. - /// @param shares The amount of wrapper shares to redeem. - /// @param receiver The address to receive the vault shares. - /// @param owner The address of the owner of the wrapper shares. - /// @param minVaultShares The minimum vault shares that should be returned. - /// @return The amount of withdrawn vault shares. - function unwrap(uint256 shares, address receiver, address owner, uint256 minVaultShares) external returns (uint256); - - /// @dev Allows to preview the amount of minted wrapper shares for a given amount of deposited vault shares. - /// @param vaultShares The amount of vault shares to deposit. - /// @return The amount of minted vault shares. - function previewWrap(uint256 vaultShares) external view returns (uint256); - - /// @dev Allows to preview the amount of withdrawn vault shares for a given amount of redeemed wrapper shares. - /// @param shares The amount of wrapper shares to redeem. - /// @return The amount of withdrawn vault shares. - function previewUnwrap(uint256 shares) external view returns (uint256); -} - -///@notice Adapter for Spectra Curve pool of two tokens SpectraWrapped IBT and PT -/// but adapter for IBT and PT -contract SpectraSWIbtPtCurveAdapter is IMarginlyAdapter, Ownable2Step { - using SafeERC20 for IERC20; - - error WrongPoolInput(); - error UnknownPair(); - - event NewPair(address indexed ptToken, address indexed ibToken, address curvePool); - - struct PoolInput { - address ibToken; // interest bearing token uniBTC - address ptToken; // principal token, ex PT-uniBTC - address pool; // curve pool for swIbt and PT - } - - struct PoolData { - address pool; - bool zeroIndexCoinIsIbt; // curvePool.coins[0] is ibt, curvePool.coins[1] is pt - address swIbt; // address of spectraWrappedIBT - address pt; // address of pt token - } - - mapping(address => mapping(address => PoolData)) public getPoolData; - - constructor(PoolInput[] memory pools) { - _addPools(pools); - } - - function _addPools(PoolInput[] memory pools) private { - PoolInput memory input; - uint256 length = pools.length; - for (uint256 i; i < length; ) { - input = pools[i]; - - if (input.ibToken == address(0) || input.ptToken == address(0) || input.pool == address(0)) - revert WrongPoolInput(); - - address coin0 = ICurvePool(input.pool).coins(0); - address coin1 = ICurvePool(input.pool).coins(1); - - PoolData memory poolData = PoolData({pool: input.pool, zeroIndexCoinIsIbt: true, swIbt: coin0, pt: coin1}); - - if (coin1 == input.ptToken) { - if (ISpectra4626Wrapper(coin0).vaultShare() != input.ibToken) revert WrongPoolInput(); - } else if (coin0 == input.ptToken) { - if (ISpectra4626Wrapper(coin1).vaultShare() != input.ibToken) revert WrongPoolInput(); - - poolData.zeroIndexCoinIsIbt = false; - poolData.swIbt = coin1; - poolData.pt = coin0; - } else { - revert WrongPoolInput(); - } - - getPoolData[input.ptToken][input.ibToken] = poolData; - getPoolData[input.ibToken][input.ptToken] = poolData; - - emit NewPair(input.ptToken, input.ibToken, input.pool); - - unchecked { - ++i; - } - } - } - - function _getPoolDataSafe(address tokenA, address tokenB) private view returns (PoolData memory poolData) { - poolData = getPoolData[tokenA][tokenB]; - if (poolData.pool == address(0)) revert UnknownPair(); - } - - function _swapExactInputPreMaturity( - PoolData memory poolData, - address recipientArg, - address tokenInArg, - uint256 amountInArg, - uint256 minAmountOut - ) private returns (uint256 amountOut) { - bool tokenInIsPt = tokenInArg == poolData.pt; - uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt && tokenInIsPt ? 1 : 0; - - address tokenIn = tokenInArg; - uint256 amountIn = amountInArg; - address recipient = recipientArg; - - if (!tokenInIsPt) { - // wrap ib to swIbt and change reciptient to current address(this) to let contract unwrap - IERC20(tokenIn).forceApprove(poolData.swIbt, amountInArg); - amountIn = ISpectra4626Wrapper(poolData.swIbt).wrap(amountInArg, address(this)); - tokenIn = poolData.swIbt; - } else { - // change recipient to address(this), it let make unwrap swIbt for Ibt after swap - recipient = address(this); - } - - amountOut = _curveSwapExactInput(poolData.pool, recipient, tokenIn, tokenInIndex, amountIn, minAmountOut); - - if (tokenInIsPt) { - // unwrap swIbt to ib - amountOut = ISpectra4626Wrapper(poolData.swIbt).unwrap(amountOut, recipientArg, address(this)); - } - } - - function _swapExactInputPostMaturiy( - PoolData memory poolData, - address recipient, - address tokenIn, - uint256 amountIn - ) private returns (uint256 amountOut) { - if (tokenIn == poolData.pt) { - uint256 swAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); - amountOut = ISpectra4626Wrapper(poolData.swIbt).unwrap(swAmountOut, recipient, address(this)); - } else { - // swIbt to pt swap is not possible after maturity - revert NotSupported(); - } - } - - /// @notice Swap maxAmountIn of pt token for swIbt token, - /// unwrap swIbt token to ibt - /// check excessive amount out and wrap back to swIbt - /// swap excessive amount swIbt to pt - /// transfer pt to recipient - function _swapExactOutputPtToIbtPreMaturity( - PoolData memory poolData, - address recipient, - address ibOut, - uint256 ibAmountOut, - uint256 maxPtAmountIn - ) private returns (uint256 ptAmountIn) { - uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 1 : 0; - - // convert ibAmountOut to swAmountOut - uint256 swAmountOut = ISpectra4626Wrapper(poolData.swIbt).previewWrap(ibAmountOut); - - // swap maxAmountPt to swIbt - uint256 swActualAmountOut = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.pt, - tokenInIndex, - maxPtAmountIn, - swAmountOut - ); - - // unwrap swIbt to ibt - uint256 ibActualAmountOut = ISpectra4626Wrapper(poolData.swIbt).unwrap( - swActualAmountOut, - address(this), - address(this) - ); - - IERC20(ibOut).safeTransfer(recipient, ibAmountOut); - - if (ibActualAmountOut < ibAmountOut) { - revert TooMuchRequested(); - } - - if (ibActualAmountOut == ibAmountOut) { - return maxPtAmountIn; - } - - uint256 deltaIbAmountOut = ibActualAmountOut - ibAmountOut; - - // wrap extra amountOut ibt to swIbt - IERC20(ibOut).forceApprove(poolData.swIbt, deltaIbAmountOut); - uint256 deltaSwAmountOut = ISpectra4626Wrapper(poolData.swIbt).wrap(deltaIbAmountOut, address(this)); - - // swap and move excessive tokenIn directly to recipient. - // last arg minAmountOut is zero because we made worst allowed by user swap - // and an additional swap with whichever output only improves it, - // so the tx shouldn't be reverted - uint256 excessivePtAmountIn = _curveSwapExactInput( - poolData.pool, - recipient, - poolData.swIbt, - 1 - tokenInIndex, - deltaSwAmountOut, - 0 - ); - - ptAmountIn = maxPtAmountIn - excessivePtAmountIn; - } - - function _swapExactOutputIbtToPtPreMaturity( - PoolData memory poolData, - address recipient, - address ibIn, - uint256 ptAmountOut, - uint256 maxIbAmountIn - ) private returns (uint256 ibAmountIn) { - uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; - - // wrap ibt to swIbt - IERC20(ibIn).forceApprove(poolData.swIbt, maxIbAmountIn); - uint256 swMaxAmountIn = ISpectra4626Wrapper(poolData.swIbt).wrap(maxIbAmountIn, address(this)); - - // swap all swMaxAmountIn to pt tokens - uint256 ptActualAmountOut = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.swIbt, - tokenInIndex, - swMaxAmountIn, - ptAmountOut - ); - - if (ptActualAmountOut < ptAmountOut) { - revert TooMuchRequested(); - } - - IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); - - if (ptActualAmountOut == ptAmountOut) { - return maxIbAmountIn; - } - - uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; - // swap and move excessive tokenIn - // last arg minAmountOut is zero because we made worst allowed by user swap - // and an additional swap with whichever output only improves it, - // so the tx shouldn't be reverted - uint256 excessiveSwAmountIn = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.pt, - 1 - tokenInIndex, - deltaPtAmountOut, - 0 - ); - // unwrap exessive sw into ib and transfer back to recipient - uint256 excessiveIbAmountIn = ISpectra4626Wrapper(poolData.swIbt).unwrap( - excessiveSwAmountIn, - recipient, - address(this) - ); - ibAmountIn = maxIbAmountIn - excessiveIbAmountIn; - } - - function _swapExactOutputPostMaturity( - PoolData memory poolData, - address recipient, - address tokenOut, - uint256 amountOut, - uint256 maxAmountIn - ) private returns (uint256 amountIn) { - if (tokenOut == poolData.pt) { - // swap swIbt to pt is not possible after maturity - revert NotSupported(); - } else { - // calc swAmount from amountOut - uint256 swAmountOut = ISpectra4626Wrapper(poolData.swIbt).previewWrap(amountOut); - - amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(swAmountOut, address(this), address(this)); - uint256 actualAmountOut = ISpectra4626Wrapper(poolData.swIbt).unwrap(swAmountOut, address(this), address(this)); - - if (actualAmountOut < amountOut) revert InsufficientAmount(); - - // actualAmountOut could be more than amountOut, but it's not possible to change it back to PT aftrer maturity - // dust may be left on the contract - IERC20(tokenOut).safeTransfer(recipient, amountOut); - - if (maxAmountIn == amountIn) { - return amountIn; - } - - // return tokenIn pt-uniBTC back to recipient - IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); - } - } - - function _curveSwapExactInput( - address poolAddress, - address recipient, - address tokenIn, - uint256 tokenInIndex, - uint256 amountIn, - uint256 minAmountOut - ) private returns (uint256 amountOut) { - SafeERC20.forceApprove(IERC20(tokenIn), poolAddress, amountIn); - - amountOut = ICurvePool(poolAddress).exchange( - tokenInIndex, - 1 - tokenInIndex, - amountIn, - minAmountOut, - false, - recipient - ); - } - - function _ptIsExpired(address pt) private view returns (bool) { - return ISpectraPrincipalToken(pt).maturity() < block.timestamp; - } - - function swapExactInput( - address recipient, - address tokenIn, - address tokenOut, - uint256 amountIn, - uint256 minAmountOut, - bytes calldata data - ) external returns (uint256 amountOut) { - PoolData memory poolData = _getPoolDataSafe(tokenIn, tokenOut); - - // move all input tokens from router to this adapter - IMarginlyRouter(msg.sender).adapterCallback(address(this), amountIn, data); - - if (_ptIsExpired(poolData.pt)) { - amountOut = _swapExactInputPostMaturiy(poolData, recipient, tokenIn, amountIn); - } else { - amountOut = _swapExactInputPreMaturity(poolData, recipient, tokenIn, amountIn, minAmountOut); - } - - if (amountOut < minAmountOut) revert InsufficientAmount(); - } - - function swapExactOutput( - address recipient, - address tokenIn, - address tokenOut, - uint256 maxAmountIn, - uint256 amountOut, - bytes calldata data - ) external returns (uint256 amountIn) { - PoolData memory poolData = _getPoolDataSafe(tokenIn, tokenOut); - - IMarginlyRouter(msg.sender).adapterCallback(address(this), maxAmountIn, data); - - if (_ptIsExpired(poolData.pt)) { - amountIn = _swapExactOutputPostMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); - } else { - if (tokenIn == poolData.pt) { - amountIn = _swapExactOutputPtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); - } else { - amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); - } - } - } - - function addPools(PoolInput[] calldata poolsData) external { - _addPools(poolsData); - } - - /// @dev During swap Pt to exact SW after maturity a little amount of sw-ibt might stay at the adapter contract - function sweepDust(address tokenA, address tokenB, address recipient) external onlyOwner { - PoolData memory poolData = getPoolData[tokenA][tokenB]; - - uint256 dust = IERC20(poolData.swIbt).balanceOf(address(this)); - IERC20(poolData.swIbt).safeTransfer(recipient, dust); - } -} diff --git a/packages/router/contracts/adapters/interfaces/ISpectraErc4626Wrapper.sol b/packages/router/contracts/adapters/interfaces/ISpectraErc4626Wrapper.sol new file mode 100644 index 00000000..998fe027 --- /dev/null +++ b/packages/router/contracts/adapters/interfaces/ISpectraErc4626Wrapper.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +interface ISpectraErc4626Wrapper { + /// @dev Returns the address of the wrapped vault share. + function vaultShare() external view returns (address); + + /// @dev Allows the owner to deposit vault shares into the wrapper. + /// @param vaultShares The amount of vault shares to deposit. + /// @param receiver The address to receive the wrapper shares. + /// @return The amount of minted wrapper shares. + function wrap(uint256 vaultShares, address receiver) external returns (uint256); + + /// @dev Allows the owner to deposit vault shares into the wrapper, with support for slippage protection. + /// @param vaultShares The amount of vault shares to deposit. + /// @param receiver The address to receive the wrapper shares. + /// @param minShares The minimum allowed wrapper shares from this deposit. + /// @return The amount of minted wrapper shares. + function wrap(uint256 vaultShares, address receiver, uint256 minShares) external returns (uint256); + + /// @dev Allows the owner to withdraw vault shares from the wrapper. + /// @param shares The amount of wrapper shares to redeem. + /// @param receiver The address to receive the vault shares. + /// @param owner The address of the owner of the wrapper shares. + /// @return The amount of withdrawn vault shares. + function unwrap(uint256 shares, address receiver, address owner) external returns (uint256); + + /// @dev Allows the owner to withdraw vault shares from the wrapper, with support for slippage protection. + /// @param shares The amount of wrapper shares to redeem. + /// @param receiver The address to receive the vault shares. + /// @param owner The address of the owner of the wrapper shares. + /// @param minVaultShares The minimum vault shares that should be returned. + /// @return The amount of withdrawn vault shares. + function unwrap(uint256 shares, address receiver, address owner, uint256 minVaultShares) external returns (uint256); + + /// @dev Allows to preview the amount of minted wrapper shares for a given amount of deposited vault shares. + /// @param vaultShares The amount of vault shares to deposit. + /// @return The amount of minted vault shares. + function previewWrap(uint256 vaultShares) external view returns (uint256); + + /// @dev Allows to preview the amount of withdrawn vault shares for a given amount of redeemed wrapper shares. + /// @param shares The amount of wrapper shares to redeem. + /// @return The amount of withdrawn vault shares. + function previewUnwrap(uint256 shares) external view returns (uint256); +} diff --git a/packages/router/contracts/adapters/interfaces/ISpectraPrincipalToken.sol b/packages/router/contracts/adapters/interfaces/ISpectraPrincipalToken.sol new file mode 100644 index 00000000..737cc262 --- /dev/null +++ b/packages/router/contracts/adapters/interfaces/ISpectraPrincipalToken.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +interface ISpectraPrincipalToken { + /** + * @notice Returns the unix timestamp (uint256) at which the PT contract expires + * @return The unix timestamp (uint256) when PTs become redeemable + */ + function maturity() external view returns (uint256); + + /** + * @notice Burns owner's shares (PTs and YTs before expiry, PTs after expiry) + * and sends IBTs to receiver + * @param shares The amount of shares to burn + * @param receiver The address that will receive the IBTs + * @param owner The owner of the shares + * @return ibts The actual amount of IBT received for burning the shares + */ + function redeemForIBT(uint256 shares, address receiver, address owner) external returns (uint256 ibts); + + /** + * @notice Burns owner's shares (PTs and YTs before expiry, PTs after expiry) + * and sends IBTs to receiver + * @param shares The amount of shares to burn + * @param receiver The address that will receive the IBTs + * @param owner The owner of the shares + * @param minIbts The minimum IBTs that should be returned to user + * @return ibts The actual amount of IBT received for burning the shares + */ + function redeemForIBT( + uint256 shares, + address receiver, + address owner, + uint256 minIbts + ) external returns (uint256 ibts); + + /** + * @notice Burns owner's shares (before expiry : PTs and YTs) and sends IBTs to receiver + * @param ibts The amount of IBT to be received + * @param receiver The address that will receive the IBTs + * @param owner The owner of the shares (PTs and YTs) + * @return shares The actual amount of shares burnt for receiving the IBTs + */ + function withdrawIBT(uint256 ibts, address receiver, address owner) external returns (uint256 shares); + + /** + * @notice Burns owner's shares (before expiry : PTs and YTs) and sends IBTs to receiver + * @param ibts The amount of IBT to be received + * @param receiver The address that will receive the IBTs + * @param owner The owner of the shares (PTs and YTs) + * @param maxShares The maximum shares allowed to be burnt + * @return shares The actual amount of shares burnt for receiving the IBTs + */ + function withdrawIBT( + uint256 ibts, + address receiver, + address owner, + uint256 maxShares + ) external returns (uint256 shares); +} diff --git a/packages/router/test/int/SpectraAdapter.eth.spec.ts b/packages/router/test/int/SpectraAdapter.eth.spec.ts new file mode 100644 index 00000000..2d7c3b5d --- /dev/null +++ b/packages/router/test/int/SpectraAdapter.eth.spec.ts @@ -0,0 +1,460 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + ERC20, + MarginlyRouter, + MarginlyRouter__factory, + SpectraAdapter, + SpectraAdapter__factory, +} from '../../typechain-types'; +import { constructSwap, delay, Dex, resetFork, showBalance, 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'; + +const swapCallData = constructSwap([Dex.Spectra], [SWAP_ONE]); + +type TestCase = { + forkNumber: number; + spectraPool: string; + ptToken: string; + ptSymbol: string; + ptBalanceSlot: EthereumMainnetERC20BalanceOfSlot; + ptInitialBalance: BigNumber; + ibtToken: string; + ibtSymbol: string; + ibtBalanceSlot: EthereumMainnetERC20BalanceOfSlot; + ibtInitialBalance: BigNumber; + 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 inwstETHs_TestCase: TestCase = { + forkNumber: 21714750, + + spectraPool: '0xe119bad8a35b999f65b1e5fd48c626c327daa16b', + ptToken: '0x4ae0154f83427a5864e5de6513a47dac9e5d5a69', + ptSymbol: 'pt-sw-inwstETHs', + ptBalanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, + ptInitialBalance: parseUnits('1000', 18), + + ibtToken: '0x8e0789d39db454dbe9f4a77acef6dc7c69f6d552', + ibtSymbol: 'inwstETHs', + ibtBalanceSlot: EthereumMainnetERC20BalanceOfSlot.INWSTETHS, + ibtInitialBalance: parseUnits('1000', 18), + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('1.5', 18), + minPtOut: parseUnits('1.0', 18), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('1.0', 18), + ptOut: parseUnits('1.0', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('10.75', 18), + minIbtOut: parseUnits('10.0', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('10.75', 18), + ibtOut: parseUnits('10.2', 18), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('15.576', 18), + minIbtOut: parseUnits('15.0', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('20', 18), + ibtOut: parseUnits('18.87', 18), + }, + }, +}; + +const wstUSR_TestCase: TestCase = { + forkNumber: 21714750, + + spectraPool: '0x0d89f4583a6b5eceb76551d573ad49cd435f6064', + ptToken: '0xd0097149aa4cc0d0e1fc99b8bd73fc17dc32c1e9', + ptSymbol: 'pt-wstUSR', + ptBalanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, + ptInitialBalance: parseUnits('10000', 18), + + ibtToken: '0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055', + ibtSymbol: 'wstUSR', + ibtBalanceSlot: EthereumMainnetERC20BalanceOfSlot.WSTUSR, + ibtInitialBalance: parseUnits('10000', 18), + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('500', 18), + minPtOut: parseUnits('500', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('745.34', 18), + minIbtOut: parseUnits('650', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15.75', 18), + ibtOut: parseUnits('10.2', 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 sDOLA_TestCase: TestCase = { + forkNumber: 21714750, + + spectraPool: '0x69ba1b7dba7eb3b7a73f4e35fd04a27ad06c55fe', + ptToken: '0xf4ca2ce6eaa1b507570c4b340007f6266c7d5698', + ptSymbol: 'pt-sDOLA', + ptBalanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, + ptInitialBalance: parseUnits('10000', 18), + + ibtToken: '0xb45ad160634c528cc3d2926d9807104fa3157305', + ibtSymbol: 'sDOLA', + ibtBalanceSlot: EthereumMainnetERC20BalanceOfSlot.SDOLA, + ibtInitialBalance: parseUnits('10000', 18), + + timeToMaturity: 365 * 24 * 60 * 60, // 365 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('500', 18), + minPtOut: parseUnits('500', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('800.34', 18), + minIbtOut: parseUnits('650', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15.75', 18), + ibtOut: parseUnits('10.2', 18), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('600', 18), + ptOut: parseUnits('600', 18), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('600', 18), + minIbtOut: parseUnits('500', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('600', 18), + ibtOut: parseUnits('500', 18), + }, + }, +}; + +const testCases = [wstUSR_TestCase, sDOLA_TestCase, inwstETHs_TestCase]; + +async function initializeRouter(testCase: TestCase): Promise<{ + ptToken: ERC20; + ibtToken: ERC20; + router: MarginlyRouter; + spectraAdapter: SpectraAdapter; + owner: SignerWithAddress; + user: SignerWithAddress; +}> { + const [owner, user] = await ethers.getSigners(); + + const ptToken = await ethers.getContractAt('ERC20', testCase.ptToken); + const ibtToken = await ethers.getContractAt('ERC20', testCase.ibtToken); + const spectraPool = testCase.spectraPool; + + // pool wstUSR/PT-wstUSR + const poolInput: SpectraAdapter.PoolInputStruct = { + pt: ptToken.address, + ibt: ibtToken.address, + pool: spectraPool, + }; + + const spectraAdapter = await new SpectraAdapter__factory().connect(owner).deploy([poolInput]); + + const routerInput = { + dexIndex: Dex.Spectra, + adapter: spectraAdapter.address, + }; + const router = await new MarginlyRouter__factory().connect(owner).deploy([routerInput]); + + await setTokenBalance( + ibtToken.address, + testCase.ibtBalanceSlot, + EthAddress.parse(user.address), + testCase.ibtInitialBalance + ); + + await setTokenBalance( + ptToken.address, + testCase.ptBalanceSlot, + EthAddress.parse(user.address), + testCase.ptInitialBalance + ); + + expect(await ptToken.balanceOf(user.address)).to.be.eq( + testCase.ptInitialBalance, + `Wrong initial ${testCase.ptSymbol} balance` + ); + expect(await ibtToken.balanceOf(user.address)).to.be.eq( + testCase.ibtInitialBalance, + `Wrong initial ${testCase.ibtSymbol} balance` + ); + + return { + ptToken, + ibtToken, + router, + spectraAdapter, + owner, + user, + }; +} + +describe.only('SpectraAdapter', async () => { + for (const testCase of testCases) { + // Tests for running in ethereum mainnet fork + describe(`SpectraAdapter ${testCase.ptSymbol} - ${testCase.ibtSymbol}`, () => { + before(async () => { + await resetFork(testCase.forkNumber); + }); + + describe('Spectra swap pre maturity', () => { + let ptToken: ERC20; + let ibtToken: ERC20; + let router: MarginlyRouter; + let spectraAdapter: SpectraAdapter; + let user: SignerWithAddress; + + beforeEach(async () => { + ({ ptToken, ibtToken, router, spectraAdapter, user } = await initializeRouter(testCase)); + }); + + it(`${testCase.ibtSymbol} to ${testCase.ptSymbol} exact input`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); + + const ibtTokenAmount = testCase.preMaturity.swapExactIbtToPt.ibtIn; + await ibtToken.connect(user).approve(router.address, ibtTokenAmount); + + const minPTAmount = testCase.preMaturity.swapExactIbtToPt.minPtOut; + + const tx = await router + .connect(user) + .swapExactInput(swapCallData, ibtToken.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(ibtToken, user.address, 'ibt balance After:'); + expect(ibtBalanceBefore.sub(ibtBalanceAfter)).to.be.lessThanOrEqual(ibtTokenAmount); + }); + + it(`${testCase.ibtSymbol} to ${testCase.ptSymbol} exact output`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); + + const exactPtOut = testCase.preMaturity.swapIbtToExactPt.ptOut; + const ibtMaxAmountIn = testCase.preMaturity.swapIbtToExactPt.maxIbtIn; + await ibtToken.connect(user).approve(router.address, ibtMaxAmountIn); + const tx = await router + .connect(user) + .swapExactOutput(swapCallData, ibtToken.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(ibtToken, user.address, 'ibt balance After: '); + expect(ibtBalanceBefore).to.be.greaterThan(ibtBalanceAfter); + }); + + it(`${testCase.ptSymbol} to ${testCase.ibtSymbol} exact input`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(ibtToken, 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, ibtToken.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(ibtToken, user.address, 'ibt balance After:'); + expect(ibtBalanceAfter).to.be.greaterThan(ibtBalanceBefore); + }); + + it(`${testCase.ptSymbol} to ${testCase.ibtSymbol} exact output`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance before:'); + const ibtBalanceBefore = await showBalance(ibtToken, 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, ibtToken.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(ibtToken, user.address, 'ibt balance After:'); + expect(ibtBalanceAfter.sub(ibtBalanceBefore)).to.be.eq(ibtMinOut); + }); + }); + + describe('Pendle swap post maturity', () => { + let ptToken: ERC20; + let ibtToken: ERC20; + let router: MarginlyRouter; + let spectraAdapter: SpectraAdapter; + let user: SignerWithAddress; + + beforeEach(async () => { + ({ ptToken, ibtToken, router, spectraAdapter, 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.ibtSymbol} to ${testCase.ptSymbol} exact input, forbidden`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); + + await ibtToken.connect(user).approve(router.address, ibtBalanceBefore); + const tx = router + .connect(user) + .swapExactInput(swapCallData, ibtToken.address, ptToken.address, ibtBalanceBefore, 0); + + await expect(tx).to.be.revertedWithCustomError(spectraAdapter, '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(ibtToken, user.address, 'ibt balance After:'); + expect(ibtBalanceAfter).to.be.eq(ibtBalanceBefore); + }); + + it(`${testCase.ibtSymbol} to ${testCase.ptSymbol} exact output, forbidden`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); + + await ibtToken.connect(user).approve(router.address, ibtBalanceBefore); + const tx = router + .connect(user) + .swapExactOutput(swapCallData, ibtToken.address, ptToken.address, ibtBalanceBefore, 1); + await expect(tx).to.be.revertedWithCustomError(spectraAdapter, '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(ibtToken, user.address, 'ibt balance After:'); + expect(ibtBalanceAfter).to.be.eq(ibtBalanceBefore); + }); + + it(`${testCase.ptSymbol} to ${testCase.ibtSymbol} exact input`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); + const ibtBalanceBefore = await showBalance(ibtToken, 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, ibtToken.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(ibtToken, user.address, 'ibt balance After:'); + expect(ibtBalanceAfter).to.be.greaterThan(ibtBalanceBefore); + }); + + it(`${testCase.ptSymbol} to ${testCase.ibtSymbol} exact output`, async () => { + const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt balance Before:'); + const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'ibt 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, ibtToken.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(ibtToken, user.address, 'ibt balance After:'); + expect(ibtBalanceAfter.sub(ibtBalanceBefore)).to.be.eq(ibtOut); + }); + }); + }); + + await delay(3000); + } +}); diff --git a/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts b/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts deleted file mode 100644 index bfea91a6..00000000 --- a/packages/router/test/int/SpectraSwIbtPtCurveAdapter.eth.spec.ts +++ /dev/null @@ -1,412 +0,0 @@ -import { ethers } from 'hardhat'; -import { ERC20, ICurvePool, MarginlyRouter, SpectraSWIbtPtCurveAdapter } from '../../typechain-types'; -import { AdapterInputStruct } from '@marginly/periphery/typechain-types/contracts/admin/abstract/RouterActions'; -import { BigNumber } from 'ethers'; -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { parseEther, parseUnits } from 'ethers/lib/utils'; - -interface TokenInfo { - contract: ERC20; - symbol: string; - decimals: number; - balanceOfSlot: string; -} - -function formatTokenBalance(token: TokenInfo, amount: BigNumber): string { - return `${ethers.utils.formatUnits(amount, token.decimals)} ${token.symbol}`; -} - -describe.only('Spectra adapter uniBTC/Pt-uniBTC (CurvePool sw-uniBTC/pt-uniBTC)', () => { - // sw-uniBTC/Pt-uniBTC pool - https://etherscan.io/address/0xb09fc8bbdcc8dc9d8b3775132c52fcebf1c7dbb3 - const poolAddress = '0xb09fc8bbdcc8dc9d8b3775132c52fcebf1c7dbb3'; - const uniBtcAddress = '0x004E9C3EF86bc1ca1f0bB5C7662861Ee93350568'; - - const uniBtcHolderAddress = '0x447D5867d07be4E8e87fD08CBA5C8426F7835632'; - let uniBtcHolder: SignerWithAddress; - const ptUniBtcHolderAddress = '0x14975679e5f87c25fa2c54958e735a79B5B93043'; - let ptUniBtcHolder: SignerWithAddress; - - let ptUniBtc: TokenInfo; - let uniBtc: TokenInfo; - let pool: ICurvePool; - let router: MarginlyRouter; - let adapter: SpectraSWIbtPtCurveAdapter; - - function printPrice(priceInToken0: BigNumber) { - const priceStr = ethers.utils.formatEther(priceInToken0); - const inversePrice = 1 / Number.parseFloat(priceStr); - console.log(`1 ${ptUniBtc.symbol} = ${priceStr} ${uniBtc.symbol}`); - console.log(`1 ${uniBtc.symbol} = ${inversePrice} ${ptUniBtc.symbol}`); - } - - function printPriceWithDelta(newPriceInToken0: BigNumber, oldPriceInToken0: BigNumber) { - const newPriceStr = ethers.utils.formatEther(newPriceInToken0); - const inverseNewPrice = 1 / Number.parseFloat(newPriceStr); - - const oldPriceStr = ethers.utils.formatEther(oldPriceInToken0); - const inverseOldPrice = 1 / Number.parseFloat(oldPriceStr); - - const deltaPrice = newPriceInToken0.sub(oldPriceInToken0); - const deltaPriceStr = ethers.utils.formatEther(deltaPrice); - const deltaInversePrice = inverseNewPrice - inverseOldPrice; - - console.log(`1 ${ptUniBtc.symbol} = ${newPriceStr} ${uniBtc.symbol}, delta: ${deltaPriceStr} ${uniBtc.symbol}`); - console.log( - `1 ${uniBtc.symbol} = ${inverseNewPrice} ${ptUniBtc.symbol}, ` + `delta: ${deltaInversePrice} ${ptUniBtc.symbol}` - ); - } - - async function postMaturity() { - // move time and make after maturity - await ethers.provider.send('evm_increaseTime', [180 * 24 * 60 * 60]); - await ethers.provider.send('evm_mine', []); - } - - async function swapExactInput( - signer: SignerWithAddress, - zeroToOne: boolean, - amountIn: BigNumber, - minAmountOut: BigNumber - ) { - const inToken = zeroToOne ? uniBtc : ptUniBtc; - const outToken = zeroToOne ? ptUniBtc : uniBtc; - const inTokenBalanceBefore = await inToken.contract.balanceOf(signer.address); - const outTokenBalanceBefore = await outToken.contract.balanceOf(signer.address); - console.log( - `signer balance before swap: ${formatTokenBalance(inToken, inTokenBalanceBefore)}, ` + - `${formatTokenBalance(outToken, outTokenBalanceBefore)}` - ); - const amountInStr = formatTokenBalance(inToken, amountIn); - const minAmountOutStr = formatTokenBalance(outToken, minAmountOut); - console.log(`swapExactInput:`); - console.log(`amountIn: ${amountInStr}`); - console.log(`minAmountOut: ${minAmountOutStr}`); - const priceInToken0Before = await pool.last_prices(); - await router - .connect(signer) - .swapExactInput(BigNumber.from(0), inToken.contract.address, outToken.contract.address, amountIn, minAmountOut); - const inTokenBalanceAfter = await inToken.contract.balanceOf(signer.address); - const outTokenBalanceAfter = await outToken.contract.balanceOf(signer.address); - console.log( - `\nsigner balance after swap: ${formatTokenBalance(inToken, inTokenBalanceAfter)}, ` + - `${formatTokenBalance(outToken, outTokenBalanceAfter)}` - ); - const inTokenDelta = inTokenBalanceBefore.sub(inTokenBalanceAfter); - const outTokenDelta = outTokenBalanceAfter.sub(outTokenBalanceBefore); - console.log( - `signer balances delta: -${formatTokenBalance(inToken, inTokenDelta)}, ` + - `${formatTokenBalance(outToken, outTokenDelta)}` - ); - const one = BigNumber.from(10).pow(18); - let actualPriceInToken0: BigNumber; - if (zeroToOne) { - actualPriceInToken0 = inTokenDelta.mul(one).div(outTokenDelta); - } else { - actualPriceInToken0 = outTokenDelta.mul(one).div(inTokenDelta); - } - console.log(`\nPrice before swap (fees not included):`); - printPrice(priceInToken0Before); - console.log(`\nActual swap price (with fees):`); - printPriceWithDelta(actualPriceInToken0, priceInToken0Before); - expect(inTokenBalanceAfter).to.be.equal(inTokenBalanceBefore.sub(amountIn)); - expect(outTokenBalanceAfter).to.be.greaterThanOrEqual(outTokenBalanceBefore.add(minAmountOut)); - - expect(await ptUniBtc.contract.balanceOf(adapter.address)).to.eq(0); - expect(await uniBtc.contract.balanceOf(adapter.address)).to.eq(0); - } - - async function swapExactOutput( - signer: SignerWithAddress, - zeroToOne: boolean, - maxAmountIn: BigNumber, - amountOut: BigNumber - ) { - const inToken = zeroToOne ? uniBtc : ptUniBtc; - const outToken = zeroToOne ? ptUniBtc : uniBtc; - const inTokenBalanceBefore = await inToken.contract.balanceOf(signer.address); - const outTokenBalanceBefore = await outToken.contract.balanceOf(signer.address); - - console.log( - `signer balance before swap: ${formatTokenBalance(inToken, inTokenBalanceBefore)}, ` + - `${formatTokenBalance(outToken, outTokenBalanceBefore)}` - ); - const maxAmountInStr = formatTokenBalance(inToken, maxAmountIn); - const amountOutStr = formatTokenBalance(outToken, amountOut); - - console.log(`swapExactOutput:`); - console.log(`maxAmountIn: ${maxAmountInStr}`); - console.log(`amountOut: ${amountOutStr}`); - - const priceInToken0Before = await pool.last_prices(); - - await router - .connect(signer) - .swapExactOutput(BigNumber.from(0), inToken.contract.address, outToken.contract.address, maxAmountIn, amountOut); - - const inTokenBalanceAfter = await inToken.contract.balanceOf(signer.address); - const outTokenBalanceAfter = await outToken.contract.balanceOf(signer.address); - - console.log( - `\nsigner balance after swap: ${formatTokenBalance(inToken, inTokenBalanceAfter)}, ` + - `${formatTokenBalance(outToken, outTokenBalanceAfter)}` - ); - - const inTokenDelta = inTokenBalanceBefore.sub(inTokenBalanceAfter); - const outTokenDelta = outTokenBalanceAfter.sub(outTokenBalanceBefore); - console.log( - `signer balances delta: -${formatTokenBalance(inToken, inTokenDelta)}, ` + - `${formatTokenBalance(outToken, outTokenDelta)}` - ); - const one = BigNumber.from(10).pow(18); - let actualPriceInToken0: BigNumber; - if (zeroToOne) { - actualPriceInToken0 = inTokenDelta.mul(one).div(outTokenDelta); - } else { - actualPriceInToken0 = outTokenDelta.mul(one).div(inTokenDelta); - } - - console.log(`\nPrice before swap (fees not included):`); - printPrice(priceInToken0Before); - console.log(`\nActual swap price (with fees):`); - printPriceWithDelta(actualPriceInToken0, priceInToken0Before); - - expect(inTokenBalanceAfter).to.be.greaterThanOrEqual(inTokenBalanceBefore.sub(maxAmountIn)); - expect(outTokenBalanceAfter).to.be.equal(outTokenBalanceBefore.add(amountOut)); - - expect(await ptUniBtc.contract.balanceOf(adapter.address)).to.eq(0); - expect(await uniBtc.contract.balanceOf(adapter.address)).to.eq(0); - } - - before(async () => { - pool = await ethers.getContractAt('ICurvePool', poolAddress); - const adapterFactory = await ethers.getContractFactory('SpectraSWIbtPtCurveAdapter'); - - const token0Address = await pool.callStatic.coins(0); - const token1Address = await pool.callStatic.coins(1); - const token0Contract = await ethers.getContractAt('ERC20', token0Address); - const token1Contract = await ethers.getContractAt('ERC20', token1Address); - const uniBtcContract = await ethers.getContractAt('ERC20', uniBtcAddress); - const token0Symbol = await token0Contract.symbol(); - const token1Symbol = await token1Contract.symbol(); - const token0Decimals = await token0Contract.decimals(); - const token1Decimals = await token1Contract.decimals(); - - ptUniBtc = { - contract: token1Contract, - symbol: token1Symbol, - decimals: token1Decimals, - }; - uniBtc = { - contract: uniBtcContract, - symbol: await uniBtcContract.symbol(), - decimals: await uniBtcContract.decimals(), - }; - - adapter = await adapterFactory.deploy([ - { - ibToken: uniBtcAddress, - ptToken: token1Address, - pool: poolAddress, - }, - ]); - console.log('Adapter address: ', adapter.address); - - const routerFactory = await ethers.getContractFactory('MarginlyRouter'); - router = await routerFactory.deploy([{ dexIndex: 0, adapter: adapter.address }]); - - const [owner, user1, user2] = await ethers.getSigners(); - uniBtcHolder = await ethers.getImpersonatedSigner(uniBtcHolderAddress); - ptUniBtcHolder = await ethers.getImpersonatedSigner(ptUniBtcHolderAddress); - - await owner.sendTransaction({ - to: uniBtcHolderAddress, - value: parseEther('1.0'), - }); - - await owner.sendTransaction({ - to: ptUniBtcHolderAddress, - value: parseEther('1.0'), - }); - - const token0InitBalance = BigNumber.from(10).pow(8); - await uniBtc.contract.connect(uniBtcHolder).transfer(owner.address, token0InitBalance); - await uniBtc.contract.connect(uniBtcHolder).transfer(user1.address, token0InitBalance); - await uniBtc.contract.connect(uniBtcHolder).transfer(user2.address, token0InitBalance); - - const token1InitBalance = BigNumber.from(10).pow(8); - await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(owner.address, token1InitBalance); - await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(user1.address, token1InitBalance); - await ptUniBtc.contract.connect(ptUniBtcHolder).transfer(user2.address, token1InitBalance); - }); - - describe('Pre maturuty', () => { - it('swapExactInput uniBtc to pt-uniBTC', async () => { - const [, user1] = await ethers.getSigners(); - const amountIn = parseUnits('0.01', 8); - const minAmountOut = amountIn.div(100); - - await uniBtc.contract.connect(user1).approve(router.address, amountIn); - - await swapExactInput(user1, true, amountIn, minAmountOut); - }); - - it('swapExactInput pt-uniBTC to uniBTC', async () => { - const [, user1] = await ethers.getSigners(); - const amountIn = parseUnits('0.05', 8); - const minAmountOut = amountIn.div(10); - - await ptUniBtc.contract.connect(user1).approve(router.address, amountIn); - - await swapExactInput(user1, false, amountIn, minAmountOut); - }); - - it('swapExactOutput uniBTC to pt-uniBTC', async () => { - const [, user1] = await ethers.getSigners(); - - const maxAmountIn = parseUnits('0.05', 8); - const amountOut = parseUnits('0.01', 8); - - console.log(`Balance of ${await uniBtc.contract.balanceOf(user1.address)}`); - await uniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - - await swapExactOutput(user1, true, maxAmountIn, amountOut); - }); - - it('swapExactOutput pt-uniBTC to uniBTC', async () => { - const [, user1] = await ethers.getSigners(); - - const maxAmountIn = parseUnits('0.05', 8); // pt-uniBTC - const amountOut = parseUnits('0.01', 8); // uniBTC - - await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - - await swapExactOutput(user1, false, maxAmountIn, amountOut); - }); - - it('swapExactInput uniBtc to pt-uniBTC, Curve slippage', async () => { - const [, user1] = await ethers.getSigners(); - const amountIn = parseUnits('0.01', 8); - const minAmountOut = parseUnits('0.015', 8); - - await uniBtc.contract.connect(user1).approve(router.address, amountIn); - - // Curve rejected with Slippage reason string - await expect(swapExactInput(user1, true, amountIn, minAmountOut)).to.be.rejected; - }); - - it('swapExactInput pt-uniBTC to uniBTC, Curve slippage', async () => { - const [, user1] = await ethers.getSigners(); - const amountIn = parseUnits('0.01', 8); - const minAmountOut = parseUnits('0.015', 8); - - await ptUniBtc.contract.connect(user1).approve(router.address, amountIn); - - await expect(swapExactInput(user1, false, amountIn, minAmountOut)).to.be.rejected; - }); - - it('swapExactOutput uniBTC to pt-uniBTC, Curve slippage', async () => { - const [, user1] = await ethers.getSigners(); - - const maxAmountIn = parseUnits('0.005', 8); - const amountOut = parseUnits('0.01', 8); - - await uniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - - await expect(swapExactOutput(user1, true, maxAmountIn, amountOut)).to.be.rejected; - }); - - it('swapExactOutput pt-uniBTC to uniBTC, Curve slippage', async () => { - const [, user1] = await ethers.getSigners(); - - const maxAmountIn = parseUnits('0.005', 8); // pt-uniBTC - const amountOut = parseUnits('0.01', 8); // uniBTC - - await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - - await expect(swapExactOutput(user1, false, maxAmountIn, amountOut)).to.be.rejected; - }); - }); - - describe('post maturity', () => { - beforeEach(async () => { - await postMaturity(); - }); - - it('swapExactInput post maturity uniBTC to ptUniBtc, NotSupported', async () => { - const [, user1] = await ethers.getSigners(); - - const amountIn = parseUnits('0.05', 8); // uniBTC - const minAmountOut = parseUnits('0.05', 8); // pt-uniBTC - - await uniBtc.contract.connect(user1).approve(router.address, amountIn); - - await expect(swapExactInput(user1, true, amountIn, minAmountOut)).to.be.revertedWithCustomError( - adapter, - 'NotSupported' - ); - }); - - it('swapExactInput post maturity uniBtc to ptUniBtc. NotSupported', async () => { - const [, user1] = await ethers.getSigners(); - - const maxAmountIn = parseUnits('0.05', 8); // uniBTC - const amountOut = parseUnits('0.05', 8); // pt-uniBTC - - await uniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - - await expect(swapExactOutput(user1, true, amountOut, maxAmountIn)).to.be.revertedWithCustomError( - adapter, - 'NotSupported' - ); - }); - - it('swapExactInput post maturity pt-uniBtc to uniBTC', async () => { - const [, user1] = await ethers.getSigners(); - - const amountIn = parseUnits('0.01', 8); // 0.01 pt-uniBtc - const minAmountOut = amountIn.div(100); - - await ptUniBtc.contract.connect(user1).approve(router.address, amountIn); - - await swapExactInput(user1, false, amountIn, minAmountOut); - }); - - it('swapExactOutput post maturity pt-uniBtc to uniBTC', async () => { - const [, user1] = await ethers.getSigners(); - - const maxAmountIn = parseUnits('0.015', 8); // pt-uniBTC - const amountOut = parseUnits('0.01', 8); // uniBTC - - await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - - await swapExactOutput(user1, false, maxAmountIn, amountOut); - }); - - it('swapExactInput post maturity pt-uniBtc to uniBTC, InsufficientAmount', async () => { - const [, user1] = await ethers.getSigners(); - - const amountIn = parseUnits('0.01', 8); // 0.01 pt-uniBtc - const minAmountOut = parseUnits('0.02', 8); //0.02 uniBTC - - await ptUniBtc.contract.connect(user1).approve(router.address, amountIn); - - await expect(swapExactInput(user1, false, amountIn, minAmountOut)).to.be.revertedWithCustomError( - adapter, - 'InsufficientAmount' - ); - }); - - it('swapExactOutput post maturity pt-uniBtc to uniBTC', async () => { - const [, user1] = await ethers.getSigners(); - - const maxAmountIn = parseUnits('0.01', 8); // pt-uniBTC - const amountOut = parseUnits('0.02', 8); // uniBTC - - await ptUniBtc.contract.connect(user1).approve(router.address, maxAmountIn); - - //error in SpectraWrapper-uniBTC - await expect(swapExactOutput(user1, false, maxAmountIn, amountOut)).to.be.rejected; - }); - }); -}); diff --git a/packages/router/test/shared/tokens.ts b/packages/router/test/shared/tokens.ts index 2c8ae9e1..4d10db77 100644 --- a/packages/router/test/shared/tokens.ts +++ b/packages/router/test/shared/tokens.ts @@ -16,12 +16,17 @@ export enum ArbMainnetERC20BalanceOfSlot { // How to get: 1) decompile contract https://ethervm.io/decompile // 2) find balanceOf function and slot +// or find slot in blockexplorer statechange e.g. https://etherscan.io/tx/0xd3a83090d4e736aef85302e9835850d925c7d8da5180678fe440edc519966906#statechange export enum EthereumMainnetERC20BalanceOfSlot { WETH = '0000000000000000000000000000000000000000000000000000000000000003', WBTC = '0000000000000000000000000000000000000000000000000000000000000000', USDC = '0000000000000000000000000000000000000000000000000000000000000009', SUSDE = '0000000000000000000000000000000000000000000000000000000000000004', PTSUSDE = '0000000000000000000000000000000000000000000000000000000000000000', + PTSWINWSTETHS = '52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00', + INWSTETHS = '0000000000000000000000000000000000000000000000000000000000000065', + WSTUSR = '52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00', + SDOLA = '0000000000000000000000000000000000000000000000000000000000000003', } function getAccountBalanceStorageSlot(account: EthAddress, tokenMappingSlot: string): string { diff --git a/packages/router/test/shared/utils.ts b/packages/router/test/shared/utils.ts index a2ec9109..f5634afe 100644 --- a/packages/router/test/shared/utils.ts +++ b/packages/router/test/shared/utils.ts @@ -24,8 +24,9 @@ export const Dex = { Curve: 13, Pendle: 17, PendleMarket: 19, - PendleCurveRouter: 20, - PendleCurve: 21, + PendleCurveRouter: 30, + PendleCurve: 31, + Spectra: 32, }; export function constructSwap(dex: number[], ratios: number[]): BigNumber { @@ -49,7 +50,7 @@ export async function showGasUsage(tx: ContractTransaction) { 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}`); + console.log(`${startPhrase.replace('$symbol', symbol)} ${formatUnits(balance, decimals)} ${symbol}`); return balance; } @@ -60,3 +61,7 @@ export async function resetFork(blockNumber?: number) { await reset(forkingUrl, blockNumber ?? forkingBlockNumber); } + +export function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} From 857cc7831d79d38e51ae30be23802003bf40e18d Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 28 Jan 2025 16:13:50 +0300 Subject: [PATCH 04/10] update comments, refactor: split large functions to small ones --- .../contracts/adapters/SpectraAdapter.sol | 366 ++++++++++-------- .../test/int/SpectraAdapter.eth.spec.ts | 181 ++++++--- packages/router/test/shared/tokens.ts | 1 + packages/router/test/shared/utils.ts | 13 + 4 files changed, 336 insertions(+), 225 deletions(-) diff --git a/packages/router/contracts/adapters/SpectraAdapter.sol b/packages/router/contracts/adapters/SpectraAdapter.sol index 6950a326..35c29675 100644 --- a/packages/router/contracts/adapters/SpectraAdapter.sol +++ b/packages/router/contracts/adapters/SpectraAdapter.sol @@ -9,8 +9,11 @@ import './interfaces/ICurvePool.sol'; import './interfaces/ISpectraErc4626Wrapper.sol'; import './interfaces/ISpectraPrincipalToken.sol'; -/// @notice Adapter for Spectra finance pool (old curve pool) of two tokens IBT/PT -/// @dev IBT is ERC4626 compliant or SpectraWrapped IBT +/// @title Adapter for Spectra finance pool (old curve pool) of two tokens IBT/PT +/// @dev Two cases supported: +/// 1) Spectra pool PT/sw-IBT. Adapter will wrap/unwrap IBT to sw-IBT during swaps +/// 2) Spectra pool PT/IBT + contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { using SafeERC20 for IERC20; @@ -164,7 +167,8 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { } } - function _swapExactOutputPtToIbtPreMaturity( + /// @dev Swap PT to exact amount of IBT. When IBT is SpectraWrapper + function _swapExactOutputPtToSwIbtToIbtPreMaturity( PoolData memory poolData, address recipient, address ibtOut, @@ -173,191 +177,209 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { ) private returns (uint256 ptAmountIn) { uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 1 : 0; - if (poolData.isSpectraWrappedIbt) { - // PT -> sw-IBT -> IBT - // convert ibAmountOut to swAmountOut - uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewWrap(ibtAmountOut); - - // swap maxAmountPt to swIbt - uint256 swActualAmountOut = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.pt, - tokenInIndex, - maxPtAmountIn, - swAmountOut - ); - - // unwrap swIbt to ibt - uint256 ibtActualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( - swActualAmountOut, - address(this), - address(this) - ); - - IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); - - if (ibtActualAmountOut < ibtAmountOut) { - revert TooMuchRequested(); - } + // PT -> sw-IBT -> IBT + // convert ibAmountOut to swAmountOut + uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewWrap(ibtAmountOut); - if (ibtActualAmountOut == ibtAmountOut) { - return maxPtAmountIn; - } + // swap maxAmountPt to swIbt + uint256 swActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + tokenInIndex, + maxPtAmountIn, + swAmountOut + ); - uint256 deltaIbtAmountOut = ibtActualAmountOut - ibtAmountOut; - - // wrap extra amountOut ibt to swIbt - IERC20(ibtOut).forceApprove(poolData.ibt, deltaIbtAmountOut); - uint256 deltaSwAmountOut = ISpectraErc4626Wrapper(poolData.ibt).wrap(deltaIbtAmountOut, address(this)); - - // swap and move excessive tokenIn directly to recipient. - // last arg minAmountOut is zero because we made worst allowed by user swap - // and an additional swap with whichever output only improves it, - // so the tx shouldn't be reverted - uint256 excessivePtAmountIn = _curveSwapExactInput( - poolData.pool, - recipient, - poolData.ibt, - 1 - tokenInIndex, - deltaSwAmountOut, - 0 - ); - - ptAmountIn = maxPtAmountIn - excessivePtAmountIn; - } else { - // PT -> IBT - // swap maxAmountPt to ibt - uint256 ibtActualAmountOut = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.pt, - tokenInIndex, - maxPtAmountIn, - ibtAmountOut - ); - - IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); - - if (ibtActualAmountOut < ibtAmountOut) { - revert TooMuchRequested(); - } + // unwrap swIbt to ibt + uint256 ibtActualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( + swActualAmountOut, + address(this), + address(this) + ); - if (ibtActualAmountOut == ibtAmountOut) { - return maxPtAmountIn; - } + IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); + + if (ibtActualAmountOut < ibtAmountOut) { + revert TooMuchRequested(); + } - // swap and move excessive tokenIn directly to recipient. - // last arg minAmountOut is zero because we made worst allowed by user swap - // and an additional swap with whichever output only improves it, - // so the tx shouldn't be reverted - uint256 excessivePtAmountIn = _curveSwapExactInput( - poolData.pool, - recipient, - poolData.ibt, - 1 - tokenInIndex, - ibtActualAmountOut - ibtAmountOut, - 0 - ); - - ptAmountIn = maxPtAmountIn - excessivePtAmountIn; + if (ibtActualAmountOut == ibtAmountOut) { + return maxPtAmountIn; } + + uint256 deltaIbtAmountOut = ibtActualAmountOut - ibtAmountOut; + + // wrap extra amountOut ibt to swIbt + IERC20(ibtOut).forceApprove(poolData.ibt, deltaIbtAmountOut); + uint256 deltaSwAmountOut = ISpectraErc4626Wrapper(poolData.ibt).wrap(deltaIbtAmountOut, address(this)); + + // swap and move excessive tokenIn directly to recipient. + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessivePtAmountIn = _curveSwapExactInput( + poolData.pool, + recipient, + poolData.ibt, + 1 - tokenInIndex, + deltaSwAmountOut, + 0 + ); + + ptAmountIn = maxPtAmountIn - excessivePtAmountIn; } - function _swapExactOutputIbtToPtPreMaturity( + function _swapExactOutputPtToIbtPreMaturity( + PoolData memory poolData, + address recipient, + address ibtOut, + uint256 ibtAmountOut, + uint256 maxPtAmountIn + ) private returns (uint256 ptAmountIn) { + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 1 : 0; + + // PT -> IBT + // swap maxAmountPt to ibt + uint256 ibtActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + tokenInIndex, + maxPtAmountIn, + ibtAmountOut + ); + + IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); + + if (ibtActualAmountOut < ibtAmountOut) { + revert TooMuchRequested(); + } + + if (ibtActualAmountOut == ibtAmountOut) { + return maxPtAmountIn; + } + + // swap and move excessive tokenIn directly to recipient. + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessivePtAmountIn = _curveSwapExactInput( + poolData.pool, + recipient, + poolData.ibt, + 1 - tokenInIndex, + ibtActualAmountOut - ibtAmountOut, + 0 + ); + + ptAmountIn = maxPtAmountIn - excessivePtAmountIn; + } + + /// @dev Swap IBT to exact amount of PT. When IBT is SpectraWrapper + function _swapExactOutputIbtToSwIbtToPtPreMaturity( PoolData memory poolData, address recipient, address ibtIn, uint256 ptAmountOut, uint256 maxIbtAmountIn ) private returns (uint256 ibAmountIn) { + // IBT -> sw-IBT -> PT uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; - if (poolData.isSpectraWrappedIbt) { - // sw-IBT -> IBT -> PT - // wrap ibt to swIbt - IERC20(ibtIn).forceApprove(poolData.ibt, maxIbtAmountIn); - uint256 swMaxAmountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(maxIbtAmountIn, address(this)); - - // swap all swMaxAmountIn to pt tokens - uint256 ptActualAmountOut = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.ibt, - tokenInIndex, - swMaxAmountIn, - ptAmountOut - ); - - if (ptActualAmountOut < ptAmountOut) { - revert TooMuchRequested(); - } + // wrap ibt to swIbt + IERC20(ibtIn).forceApprove(poolData.ibt, maxIbtAmountIn); + uint256 swMaxAmountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(maxIbtAmountIn, address(this)); - IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); + // swap swMaxAmountIn to pt tokens + uint256 ptActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.ibt, + tokenInIndex, + swMaxAmountIn, + ptAmountOut + ); - if (ptActualAmountOut == ptAmountOut) { - return maxIbtAmountIn; - } + if (ptActualAmountOut < ptAmountOut) { + revert TooMuchRequested(); + } - uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; - // swap and move excessive tokenIn - // last arg minAmountOut is zero because we made worst allowed by user swap - // and an additional swap with whichever output only improves it, - // so the tx shouldn't be reverted - uint256 excessiveSwAmountIn = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.pt, - 1 - tokenInIndex, - deltaPtAmountOut, - 0 - ); - - // unwrap exessive sw into ib and transfer back to recipient - uint256 excessiveIbAmountIn = ISpectraErc4626Wrapper(poolData.ibt).unwrap( - excessiveSwAmountIn, - recipient, - address(this) - ); - ibAmountIn = maxIbtAmountIn - excessiveIbAmountIn; - } else { - // IBT -> PT - // swap all swMaxAmountIn to pt tokens - uint256 ptActualAmountOut = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.ibt, - tokenInIndex, - maxIbtAmountIn, - ptAmountOut - ); - - if (ptActualAmountOut < ptAmountOut) { - revert TooMuchRequested(); - } + IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); - IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); + if (ptActualAmountOut == ptAmountOut) { + return maxIbtAmountIn; + } - if (ptActualAmountOut == ptAmountOut) { - return maxIbtAmountIn; - } + uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; + // swap and move excessive tokenIn + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessiveSwAmountIn = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + 1 - tokenInIndex, + deltaPtAmountOut, + 0 + ); - uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; - // swap and move excessive tokenIn - // last arg minAmountOut is zero because we made worst allowed by user swap - // and an additional swap with whichever output only improves it, - // so the tx shouldn't be reverted - uint256 excessiveIbtAmountIn = _curveSwapExactInput( - poolData.pool, - recipient, - poolData.pt, - 1 - tokenInIndex, - deltaPtAmountOut, - 0 - ); - - ibAmountIn = maxIbtAmountIn - excessiveIbtAmountIn; + // unwrap exessive sw into ib and transfer back to recipient + uint256 excessiveIbAmountIn = ISpectraErc4626Wrapper(poolData.ibt).unwrap( + excessiveSwAmountIn, + recipient, + address(this) + ); + ibAmountIn = maxIbtAmountIn - excessiveIbAmountIn; + } + + /// @dev Swap IBT to exact amount of PT + function _swapExactOutputIbtToPtPreMaturity( + PoolData memory poolData, + address recipient, + address ibtIn, + uint256 ptAmountOut, + uint256 maxIbtAmountIn + ) private returns (uint256 ibAmountIn) { + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; + + // IBT -> PT + // swap all swMaxAmountIn to pt tokens + uint256 ptActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + ibtIn, + tokenInIndex, + maxIbtAmountIn, + ptAmountOut + ); + + if (ptActualAmountOut < ptAmountOut) { + revert TooMuchRequested(); } + + IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); + + if (ptActualAmountOut == ptAmountOut) { + return maxIbtAmountIn; + } + + uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; + // swap and move excessive tokenIn + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessiveIbtAmountIn = _curveSwapExactInput( + poolData.pool, + recipient, + poolData.pt, + 1 - tokenInIndex, + deltaPtAmountOut, + 0 + ); + + ibAmountIn = maxIbtAmountIn - excessiveIbtAmountIn; } function _swapExactOutputPostMaturity( @@ -467,9 +489,17 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { amountIn = _swapExactOutputPostMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); } else { if (tokenIn == poolData.pt) { - amountIn = _swapExactOutputPtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + if (poolData.isSpectraWrappedIbt) { + amountIn = _swapExactOutputPtToSwIbtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + } else { + amountIn = _swapExactOutputPtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + } } else { - amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); + if (poolData.isSpectraWrappedIbt) { + amountIn = _swapExactOutputIbtToSwIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); + } else { + amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); + } } } } diff --git a/packages/router/test/int/SpectraAdapter.eth.spec.ts b/packages/router/test/int/SpectraAdapter.eth.spec.ts index 2d7c3b5d..3117b088 100644 --- a/packages/router/test/int/SpectraAdapter.eth.spec.ts +++ b/packages/router/test/int/SpectraAdapter.eth.spec.ts @@ -7,7 +7,16 @@ import { SpectraAdapter, SpectraAdapter__factory, } from '../../typechain-types'; -import { constructSwap, delay, Dex, resetFork, showBalance, showGasUsage, SWAP_ONE } from '../shared/utils'; +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'; @@ -16,17 +25,29 @@ import { BigNumber } from 'ethers'; const swapCallData = constructSwap([Dex.Spectra], [SWAP_ONE]); -type TestCase = { +interface TokenInfo { + address: string; + symbol: string; + balanceSlot: EthereumMainnetERC20BalanceOfSlot; + initialBalance: BigNumber; +} + +// For testing case when somebody make direct IBT transfer to sw-IBT and change rate IBT/sw-IBT +interface SWToken { + address: string; + symbol: string; + ibtTransferAmount: BigNumber; +} + +interface TestCase { forkNumber: number; + spectraPool: string; - ptToken: string; - ptSymbol: string; - ptBalanceSlot: EthereumMainnetERC20BalanceOfSlot; - ptInitialBalance: BigNumber; - ibtToken: string; - ibtSymbol: string; - ibtBalanceSlot: EthereumMainnetERC20BalanceOfSlot; - ibtInitialBalance: BigNumber; + + ptToken: TokenInfo; + ibtToken: TokenInfo; + swIbt?: SWToken; + timeToMaturity: number; preMaturity: { swapExactIbtToPt: { @@ -56,21 +77,31 @@ type TestCase = { minIbtOut: BigNumber; }; }; -}; +} const inwstETHs_TestCase: TestCase = { forkNumber: 21714750, spectraPool: '0xe119bad8a35b999f65b1e5fd48c626c327daa16b', - ptToken: '0x4ae0154f83427a5864e5de6513a47dac9e5d5a69', - ptSymbol: 'pt-sw-inwstETHs', - ptBalanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, - ptInitialBalance: parseUnits('1000', 18), + ptToken: { + address: '0x4ae0154f83427a5864e5de6513a47dac9e5d5a69', + symbol: 'pt-sw-inwstETHs', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, + initialBalance: parseUnits('1000', 18), + }, + + ibtToken: { + address: '0x8e0789d39db454dbe9f4a77acef6dc7c69f6d552', + symbol: 'inwstETHs', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.INWSTETHS, + initialBalance: parseUnits('2000', 18), + }, - ibtToken: '0x8e0789d39db454dbe9f4a77acef6dc7c69f6d552', - ibtSymbol: 'inwstETHs', - ibtBalanceSlot: EthereumMainnetERC20BalanceOfSlot.INWSTETHS, - ibtInitialBalance: parseUnits('1000', 18), + // swIbt: { + // address: '0xd89fc47aacbb31e2bf23ec599f593a4876d8c18c', + // symbol: 'sw-inwstETHs', + // ibtTransferAmount: parseUnits('500', 18), + // }, timeToMaturity: 180 * 24 * 60 * 60, // 180 days @@ -109,15 +140,19 @@ const wstUSR_TestCase: TestCase = { forkNumber: 21714750, spectraPool: '0x0d89f4583a6b5eceb76551d573ad49cd435f6064', - ptToken: '0xd0097149aa4cc0d0e1fc99b8bd73fc17dc32c1e9', - ptSymbol: 'pt-wstUSR', - ptBalanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, - ptInitialBalance: parseUnits('10000', 18), + ptToken: { + address: '0xd0097149aa4cc0d0e1fc99b8bd73fc17dc32c1e9', + symbol: 'pt-wstUSR', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, + initialBalance: parseUnits('10000', 18), + }, - ibtToken: '0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055', - ibtSymbol: 'wstUSR', - ibtBalanceSlot: EthereumMainnetERC20BalanceOfSlot.WSTUSR, - ibtInitialBalance: parseUnits('10000', 18), + ibtToken: { + address: '0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055', + symbol: 'wstUSR', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.WSTUSR, + initialBalance: parseUnits('10000', 18), + }, timeToMaturity: 180 * 24 * 60 * 60, // 180 days @@ -156,15 +191,19 @@ const sDOLA_TestCase: TestCase = { forkNumber: 21714750, spectraPool: '0x69ba1b7dba7eb3b7a73f4e35fd04a27ad06c55fe', - ptToken: '0xf4ca2ce6eaa1b507570c4b340007f6266c7d5698', - ptSymbol: 'pt-sDOLA', - ptBalanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, - ptInitialBalance: parseUnits('10000', 18), + ptToken: { + address: '0xf4ca2ce6eaa1b507570c4b340007f6266c7d5698', + symbol: 'pt-sDOLA', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, + initialBalance: parseUnits('10000', 18), + }, - ibtToken: '0xb45ad160634c528cc3d2926d9807104fa3157305', - ibtSymbol: 'sDOLA', - ibtBalanceSlot: EthereumMainnetERC20BalanceOfSlot.SDOLA, - ibtInitialBalance: parseUnits('10000', 18), + ibtToken: { + address: '0xb45ad160634c528cc3d2926d9807104fa3157305', + symbol: 'sDOLA', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.SDOLA, + initialBalance: parseUnits('10000', 18), + }, timeToMaturity: 365 * 24 * 60 * 60, // 365 days @@ -211,11 +250,10 @@ async function initializeRouter(testCase: TestCase): Promise<{ }> { const [owner, user] = await ethers.getSigners(); - const ptToken = await ethers.getContractAt('ERC20', testCase.ptToken); - const ibtToken = await ethers.getContractAt('ERC20', testCase.ibtToken); + const ptToken = await ethers.getContractAt('ERC20', testCase.ptToken.address); + const ibtToken = await ethers.getContractAt('ERC20', testCase.ibtToken.address); const spectraPool = testCase.spectraPool; - // pool wstUSR/PT-wstUSR const poolInput: SpectraAdapter.PoolInputStruct = { pt: ptToken.address, ibt: ibtToken.address, @@ -232,27 +270,38 @@ async function initializeRouter(testCase: TestCase): Promise<{ await setTokenBalance( ibtToken.address, - testCase.ibtBalanceSlot, + testCase.ibtToken.balanceSlot, EthAddress.parse(user.address), - testCase.ibtInitialBalance + testCase.ibtToken.initialBalance ); await setTokenBalance( ptToken.address, - testCase.ptBalanceSlot, + testCase.ptToken.balanceSlot, EthAddress.parse(user.address), - testCase.ptInitialBalance + testCase.ptToken.initialBalance ); expect(await ptToken.balanceOf(user.address)).to.be.eq( - testCase.ptInitialBalance, - `Wrong initial ${testCase.ptSymbol} balance` + testCase.ptToken.initialBalance, + `Wrong initial ${testCase.ptToken.symbol} balance` ); expect(await ibtToken.balanceOf(user.address)).to.be.eq( - testCase.ibtInitialBalance, - `Wrong initial ${testCase.ibtSymbol} balance` + testCase.ibtToken.initialBalance, + `Wrong initial ${testCase.ibtToken.symbol} balance` ); + if (testCase.swIbt) { + await setTokenBalance( + ibtToken.address, + testCase.ibtToken.balanceSlot, + EthAddress.parse(user.address), + testCase.ibtToken.initialBalance.add(testCase.swIbt.ibtTransferAmount) + ); + + await ibtToken.connect(user).transfer(testCase.swIbt.address, testCase.swIbt.ibtTransferAmount); + } + return { ptToken, ibtToken, @@ -263,10 +312,10 @@ async function initializeRouter(testCase: TestCase): Promise<{ }; } -describe.only('SpectraAdapter', async () => { +// Tests for running in ethereum mainnet fork +describe('SpectraAdapter', async () => { for (const testCase of testCases) { - // Tests for running in ethereum mainnet fork - describe(`SpectraAdapter ${testCase.ptSymbol} - ${testCase.ibtSymbol}`, () => { + describe(`SpectraAdapter ${testCase.ptToken.symbol} - ${testCase.ibtToken.symbol}`, () => { before(async () => { await resetFork(testCase.forkNumber); }); @@ -282,7 +331,7 @@ describe.only('SpectraAdapter', async () => { ({ ptToken, ibtToken, router, spectraAdapter, user } = await initializeRouter(testCase)); }); - it(`${testCase.ibtSymbol} to ${testCase.ptSymbol} exact input`, async () => { + it(`${testCase.ibtToken.symbol} to ${testCase.ptToken.symbol} exact input`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -301,9 +350,12 @@ describe.only('SpectraAdapter', async () => { const ibtBalanceAfter = await showBalance(ibtToken, user.address, 'ibt balance After:'); expect(ibtBalanceBefore.sub(ibtBalanceAfter)).to.be.lessThanOrEqual(ibtTokenAmount); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it(`${testCase.ibtSymbol} to ${testCase.ptSymbol} exact output`, async () => { + it(`${testCase.ibtToken.symbol} to ${testCase.ptToken.symbol} exact output`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -320,9 +372,12 @@ describe.only('SpectraAdapter', async () => { const ibtBalanceAfter = await showBalance(ibtToken, user.address, 'ibt balance After: '); expect(ibtBalanceBefore).to.be.greaterThan(ibtBalanceAfter); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it(`${testCase.ptSymbol} to ${testCase.ibtSymbol} exact input`, async () => { + it(`${testCase.ptToken.symbol} to ${testCase.ibtToken.symbol} exact input`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -339,9 +394,12 @@ describe.only('SpectraAdapter', async () => { const ibtBalanceAfter = await showBalance(ibtToken, user.address, 'ibt balance After:'); expect(ibtBalanceAfter).to.be.greaterThan(ibtBalanceBefore); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it(`${testCase.ptSymbol} to ${testCase.ibtSymbol} exact output`, async () => { + it(`${testCase.ptToken.symbol} to ${testCase.ibtToken.symbol} exact output`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -358,6 +416,9 @@ describe.only('SpectraAdapter', async () => { const ibtBalanceAfter = await showBalance(ibtToken, user.address, 'ibt balance After:'); expect(ibtBalanceAfter.sub(ibtBalanceBefore)).to.be.eq(ibtMinOut); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); }); @@ -376,7 +437,7 @@ describe.only('SpectraAdapter', async () => { await ethers.provider.send('evm_mine', []); }); - it(`${testCase.ibtSymbol} to ${testCase.ptSymbol} exact input, forbidden`, async () => { + it(`${testCase.ibtToken.symbol} to ${testCase.ptToken.symbol} exact input, forbidden`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -396,7 +457,7 @@ describe.only('SpectraAdapter', async () => { expect(ibtBalanceAfter).to.be.eq(ibtBalanceBefore); }); - it(`${testCase.ibtSymbol} to ${testCase.ptSymbol} exact output, forbidden`, async () => { + it(`${testCase.ibtToken.symbol} to ${testCase.ptToken.symbol} exact output, forbidden`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -415,7 +476,7 @@ describe.only('SpectraAdapter', async () => { expect(ibtBalanceAfter).to.be.eq(ibtBalanceBefore); }); - it(`${testCase.ptSymbol} to ${testCase.ibtSymbol} exact input`, async () => { + it(`${testCase.ptToken.symbol} to ${testCase.ibtToken.symbol} exact input`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -432,9 +493,12 @@ describe.only('SpectraAdapter', async () => { const ibtBalanceAfter = await showBalance(ibtToken, user.address, 'ibt balance After:'); expect(ibtBalanceAfter).to.be.greaterThan(ibtBalanceBefore); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it(`${testCase.ptSymbol} to ${testCase.ibtSymbol} exact output`, async () => { + it(`${testCase.ptToken.symbol} to ${testCase.ibtToken.symbol} exact output`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'ibt balance before:'); @@ -451,6 +515,9 @@ describe.only('SpectraAdapter', async () => { const ibtBalanceAfter = await showBalance(ibtToken, user.address, 'ibt balance After:'); expect(ibtBalanceAfter.sub(ibtBalanceBefore)).to.be.eq(ibtOut); + + await showBalanceDelta(ptBalanceBefore, ptBalanceAfter, ptToken, 'PT balance delta:'); + await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); }); }); diff --git a/packages/router/test/shared/tokens.ts b/packages/router/test/shared/tokens.ts index 4d10db77..097a42fc 100644 --- a/packages/router/test/shared/tokens.ts +++ b/packages/router/test/shared/tokens.ts @@ -27,6 +27,7 @@ export enum EthereumMainnetERC20BalanceOfSlot { INWSTETHS = '0000000000000000000000000000000000000000000000000000000000000065', WSTUSR = '52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00', SDOLA = '0000000000000000000000000000000000000000000000000000000000000003', + USR = '52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00', } function getAccountBalanceStorageSlot(account: EthAddress, tokenMappingSlot: string): string { diff --git a/packages/router/test/shared/utils.ts b/packages/router/test/shared/utils.ts index f5634afe..5ce1bb75 100644 --- a/packages/router/test/shared/utils.ts +++ b/packages/router/test/shared/utils.ts @@ -54,6 +54,19 @@ export async function showBalance(token: ERC20, account: string, startPhrase = ' return balance; } +export async function showBalanceDelta( + balanceBefore: BigNumber, + balanceAfter: BigNumber, + token: ERC20, + startPhrase = '' +) { + const [symbol, decimals] = await Promise.all([token.symbol(), token.decimals()]); + + console.log( + `${startPhrase.replace('$symbol', symbol)} ${formatUnits(balanceAfter.sub(balanceBefore), decimals)} ${symbol}` + ); +} + export async function resetFork(blockNumber?: number) { const hardhatConfig = (hre).config; const forkingBlockNumber = hardhatConfig.networks.hardhat.forking?.blockNumber; From f091fe2b1488f8c4db71592de73f3748a5c80716 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 28 Jan 2025 16:25:01 +0300 Subject: [PATCH 05/10] update test description --- packages/router/test/int/SpectraAdapter.eth.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/test/int/SpectraAdapter.eth.spec.ts b/packages/router/test/int/SpectraAdapter.eth.spec.ts index 3117b088..456aec73 100644 --- a/packages/router/test/int/SpectraAdapter.eth.spec.ts +++ b/packages/router/test/int/SpectraAdapter.eth.spec.ts @@ -422,7 +422,7 @@ describe('SpectraAdapter', async () => { }); }); - describe('Pendle swap post maturity', () => { + describe('Spectra swap post maturity', () => { let ptToken: ERC20; let ibtToken: ERC20; let router: MarginlyRouter; From 42c04397f6a6549e501d9c01476a40393381c16e Mon Sep 17 00:00:00 2001 From: rudewalt Date: Thu, 30 Jan 2025 11:58:05 +0300 Subject: [PATCH 06/10] WIP: spectra adpter add new case for underlying --- .../contracts/adapters/SpectraAdapter.sol | 117 ++++++++++++++---- .../adapters/interfaces/IERC4626.sol | 36 ++++++ 2 files changed, 126 insertions(+), 27 deletions(-) create mode 100644 packages/router/contracts/adapters/interfaces/IERC4626.sol diff --git a/packages/router/contracts/adapters/SpectraAdapter.sol b/packages/router/contracts/adapters/SpectraAdapter.sol index 35c29675..2c16e9db 100644 --- a/packages/router/contracts/adapters/SpectraAdapter.sol +++ b/packages/router/contracts/adapters/SpectraAdapter.sol @@ -8,12 +8,15 @@ import '../interfaces/IMarginlyAdapter.sol'; import './interfaces/ICurvePool.sol'; import './interfaces/ISpectraErc4626Wrapper.sol'; import './interfaces/ISpectraPrincipalToken.sol'; +import './interfaces/IERC4626.sol'; /// @title Adapter for Spectra finance pool (old curve pool) of two tokens IBT/PT /// @dev Two cases supported: -/// 1) Spectra pool PT/sw-IBT. Adapter will wrap/unwrap IBT to sw-IBT during swaps -/// 2) Spectra pool PT/IBT - +/// 1) Spectra pool PT / sw-IBT for marginly pool PT / IBT. +/// Adapter will wrap/unwrap IBT to sw-IBT during swaps. +/// 2) Spectra pool PT / IBT, marginly pool PT / IBT +/// 3) Spectra pool PT / IBT, marginly pool PT / Underlying of IBT. +/// Adapter will wrap/unwrap IBT to Underlying. Only for IBT tokens that supports immediate withdraw contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { using SafeERC20 for IERC20; @@ -22,11 +25,18 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { event NewPair(address indexed ptToken, address indexed ibToken, address curvePool); + // TODO: use this enum + enum QuoteTokenKind { + IbtCompatibleWithERC4626, + IbtNotCompatibleWithERC4626, + UnderlyingOfIbt + } + struct PoolInput { /// @dev Address of Principal Token address pt; - /// @dev Address of Interest Bearing Token. Address of protocol token (not Spectra wrapper) - address ibt; + /// @dev Address of QuoteToken. Could be IBT or Underlying asset of IBT + address quoteToken; /// @dev Address of spectra curve pool IBT or Spectra Wrapped IBT / PT address pool; } @@ -40,8 +50,10 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { address pt; /// @dev True if curvePool.coins[0] is ibt, curvePool.coins[1] is pt bool zeroIndexCoinIsIbt; - /// @dev True if ibt is spectraWrapped token - bool isSpectraWrappedIbt; + /// @dev True if QuoteToken is IBT not compatible with ERC4626. Spectra pool PT-sw-IBT / sw-IBT + bool unwrapSwIbtToIbt; + /// @dev True if QuoteToken is Underlying and ibt should be unwrapped to underlying token + bool unwrapIbtToUnderlying; } mapping(address => mapping(address => PoolData)) public getPoolData; @@ -64,21 +76,30 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { zeroIndexCoinIsIbt: true, ibt: coin0, pt: coin1, - isSpectraWrappedIbt: false + unwrapSwIbtToIbt: false, + unwrapIbtToUnderlying: false }); if (coin1 == input.pt) { //check other token is spectra wrapper or not - if (coin0 != input.ibt) { - if (ISpectraErc4626Wrapper(coin0).vaultShare() != input.ibt) revert WrongPoolInput(); - - poolData.isSpectraWrappedIbt = true; + if (coin0 != input.quoteToken) { + if (ISpectraErc4626Wrapper(coin0).vaultShare() == input.quoteToken) { + poolData.unwrapSwIbtToIbt = true; + } else if (IERC4626(coin0).asset() == input.quoteToken) { + poolData.unwrapIbtToUnderlying = true; + } else { + revert WrongPoolInput(); + } } } else if (coin0 == input.pt) { - if (coin1 != input.ibt) { - if (ISpectraErc4626Wrapper(coin1).vaultShare() != input.ibt) revert WrongPoolInput(); - - poolData.isSpectraWrappedIbt = true; + if (coin1 != input.quoteToken) { + if (ISpectraErc4626Wrapper(coin1).vaultShare() == input.quoteToken) { + poolData.unwrapSwIbtToIbt = true; + } else if (IERC4626(coin0).asset() == input.quoteToken) { + poolData.unwrapIbtToUnderlying = true; + } else { + revert WrongPoolInput(); + } } poolData.zeroIndexCoinIsIbt = false; @@ -88,10 +109,10 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { revert WrongPoolInput(); } - getPoolData[input.pt][input.ibt] = poolData; - getPoolData[input.ibt][input.pt] = poolData; + getPoolData[input.pt][input.quoteToken] = poolData; + getPoolData[input.quoteToken][input.pt] = poolData; - emit NewPair(input.pt, input.ibt, input.pool); + emit NewPair(input.pt, input.quoteToken, input.pool); unchecked { ++i; @@ -118,15 +139,19 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { // PT -> IBT address recipient = recipientArg; - if (poolData.isSpectraWrappedIbt) { + if (poolData.unwrapIbtToUnderlying || poolData.unwrapSwIbtToIbt) { // change recipient to address(this), it let make unwrap swIbt for Ibt after swap recipient = address(this); } amountOut = _curveSwapExactInput(poolData.pool, recipient, tokenInArg, tokenInIndex, amountInArg, minAmountOut); - if (poolData.isSpectraWrappedIbt) { + if (poolData.unwrapSwIbtToIbt) { + // sw-IBT unwrap to IBT amountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(amountOut, recipientArg, address(this)); + } else if (poolData.unwrapIbtToUnderlying) { + // IBT redeem to underlying asset + amountOut = IERC4626(poolData.ibt).redeem(amountOut, recipientArg, address(this)); } } else { // IBT -> PT @@ -134,11 +159,16 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { uint256 amountIn = amountInArg; address tokenIn = tokenInArg; - if (poolData.isSpectraWrappedIbt) { + if (poolData.unwrapSwIbtToIbt) { // wrap IBT to sw-IBT and change recipient to current address(this) IERC20(tokenIn).forceApprove(poolData.ibt, amountInArg); amountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(amountInArg, address(this)); tokenIn = poolData.ibt; // tokenIn is sw-IBT + } else if (poolData.unwrapIbtToUnderlying) { + // wrap Underlying asset to IBT and change recipient to current address(this) + IERC20(tokenIn).forceApprove(poolData.ibt, amountInArg); + amountIn = IERC4626(poolData.ibt).deposit(amountInArg, address(this)); + tokenIn = poolData.ibt; } // swap in curve IBT to PT @@ -153,11 +183,17 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { uint256 amountIn ) private returns (uint256 amountOut) { if (tokenIn == poolData.pt) { - if (poolData.isSpectraWrappedIbt) { + if (poolData.unwrapSwIbtToIbt) { // redeem sw-IBT uint256 swAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); // unwrap sw-IBT to IBT amountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(swAmountOut, recipient, address(this)); + } + if (poolData.unwrapIbtToUnderlying) { + // redeem IBT + uint ibtAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, recipient, address(this)); + // redeem Underlying + amountOut = IERC4626(poolData.ibt).redeem(ibtAmountOut, recipient, address(this)); } else { amountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, recipient, address(this)); } @@ -393,10 +429,10 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { // swap IBT to PT is not possible after maturity revert NotSupported(); } else { - if (poolData.isSpectraWrappedIbt) { + if (poolData.unwrapSwIbtToIbt) { // PT withdraw to sw-IBT, then unwrap sw-IBT to IBT // calc sw-IBT amount from amountOut - uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewWrap(amountOut); + uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewUnwrap(amountOut); amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(swAmountOut, address(this), address(this)); uint256 actualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( @@ -414,6 +450,29 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { return amountIn; } + // return rest of PT token back to recipient + IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); + } else if (poolData.unwrapIbtToUnderlying) { + // PT withdraw to sw-IBT, then unwrap sw-IBT to IBT + // calc sw-IBT amount from amountOut + uint256 ibtAmountOut = IERC4626(poolData.ibt).previewMint(amountOut); + amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(ibtAmountOut, address(this), address(this)); + + uint256 actualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( + swAmountOut, + address(this), + address(this) + ); + if (actualAmountOut < amountOut) revert InsufficientAmount(); + + // actualAmountOut could be more than amountOut, but it's not possible to change it back to PT aftrer maturity + // dust may be left on the contract + IERC20(tokenOut).safeTransfer(recipient, amountOut); + + if (maxAmountIn == amountIn) { + return amountIn; + } + // return rest of PT token back to recipient IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); } else { @@ -489,14 +548,18 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { amountIn = _swapExactOutputPostMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); } else { if (tokenIn == poolData.pt) { - if (poolData.isSpectraWrappedIbt) { + if (poolData.unwrapSwIbtToIbt) { amountIn = _swapExactOutputPtToSwIbtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + } else if (poolData.unwrapIbtToUnderlying) { + amountIn = 0; // _swapExactOutputPtToIbtToUnderlyingPreMaturity(...) TODO: not implemented } else { amountIn = _swapExactOutputPtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); } } else { - if (poolData.isSpectraWrappedIbt) { + if (poolData.unwrapSwIbtToIbt) { amountIn = _swapExactOutputIbtToSwIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); + } else if (poolData.unwrapIbtToUnderlying) { + amountIn = 0; //_swapExactOutputUnderlyingToIbtToPtPreMaturity(...); TODO: not implemented } else { amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); } diff --git a/packages/router/contracts/adapters/interfaces/IERC4626.sol b/packages/router/contracts/adapters/interfaces/IERC4626.sol new file mode 100644 index 00000000..f8d72b24 --- /dev/null +++ b/packages/router/contracts/adapters/interfaces/IERC4626.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.19; + +interface IERC4626 { + function asset() external view returns (address assetTokenAddress); + + function totalAssets() external view returns (uint256 totalManagedAssets); + + function convertToShares(uint256 assets) external view returns (uint256 shares); + + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + function maxDeposit(address receiver) external view returns (uint256 maxAssets); + + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + function maxMint(address receiver) external view returns (uint256 maxShares); + + function previewMint(uint256 shares) external view returns (uint256 assets); + + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + function maxRedeem(address owner) external view returns (uint256 maxShares); + + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); +} From 0c9359e501b9e1d4571f0df6038cf959c1666caf Mon Sep 17 00:00:00 2001 From: rudewalt Date: Fri, 31 Jan 2025 10:48:20 +0300 Subject: [PATCH 07/10] add new case PT/Underlying --- .../contracts/adapters/SpectraAdapter.sol | 240 ++++++++++++------ .../test/int/SpectraAdapter.eth.spec.ts | 146 +++++++++-- packages/router/test/shared/tokens.ts | 1 + packages/router/test/shared/utils.ts | 3 +- 4 files changed, 291 insertions(+), 99 deletions(-) diff --git a/packages/router/contracts/adapters/SpectraAdapter.sol b/packages/router/contracts/adapters/SpectraAdapter.sol index 2c16e9db..3b718ad2 100644 --- a/packages/router/contracts/adapters/SpectraAdapter.sol +++ b/packages/router/contracts/adapters/SpectraAdapter.sol @@ -12,7 +12,7 @@ import './interfaces/IERC4626.sol'; /// @title Adapter for Spectra finance pool (old curve pool) of two tokens IBT/PT /// @dev Two cases supported: -/// 1) Spectra pool PT / sw-IBT for marginly pool PT / IBT. +/// 1) Spectra pool PT / sw-IBT, marginly pool PT / IBT. /// Adapter will wrap/unwrap IBT to sw-IBT during swaps. /// 2) Spectra pool PT / IBT, marginly pool PT / IBT /// 3) Spectra pool PT / IBT, marginly pool PT / Underlying of IBT. @@ -25,11 +25,10 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { event NewPair(address indexed ptToken, address indexed ibToken, address curvePool); - // TODO: use this enum enum QuoteTokenKind { - IbtCompatibleWithERC4626, - IbtNotCompatibleWithERC4626, - UnderlyingOfIbt + Ibt, // IBT token compatible with ERC4626 + IbtNotCompatibleWithERC4626, // Quote token is IBT not compatible with ERC4626 + UnderlyingOfIbt // Quote token is Underlying asset of IBT token compatible with ERC4626 } struct PoolInput { @@ -50,10 +49,8 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { address pt; /// @dev True if curvePool.coins[0] is ibt, curvePool.coins[1] is pt bool zeroIndexCoinIsIbt; - /// @dev True if QuoteToken is IBT not compatible with ERC4626. Spectra pool PT-sw-IBT / sw-IBT - bool unwrapSwIbtToIbt; - /// @dev True if QuoteToken is Underlying and ibt should be unwrapped to underlying token - bool unwrapIbtToUnderlying; + /// @dev Type of quote token + QuoteTokenKind quoteTokenKind; } mapping(address => mapping(address => PoolData)) public getPoolData; @@ -62,6 +59,23 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { _addPools(pools); } + /// @dev Returns type of quote token + /// @param coin Address of coin in Spectra pool + /// @param quoteToken Address of quote token in Adapter + function _getQuoteTokenKind(address coin, address quoteToken) private view returns (QuoteTokenKind) { + if (coin == quoteToken) { + return QuoteTokenKind.Ibt; + } else if (IERC4626(coin).asset() == quoteToken) { + return QuoteTokenKind.UnderlyingOfIbt; + } else if (ISpectraErc4626Wrapper(coin).vaultShare() == quoteToken) { + return QuoteTokenKind.IbtNotCompatibleWithERC4626; + } else { + revert WrongPoolInput(); + } + } + + /// @dev Add array of pools + /// @param pools Array of input pools function _addPools(PoolInput[] memory pools) private { PoolInput memory input; uint256 length = pools.length; @@ -71,37 +85,18 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { address coin0 = ICurvePool(input.pool).coins(0); address coin1 = ICurvePool(input.pool).coins(1); - PoolData memory poolData = PoolData({ - pool: input.pool, - zeroIndexCoinIsIbt: true, - ibt: coin0, - pt: coin1, - unwrapSwIbtToIbt: false, - unwrapIbtToUnderlying: false - }); + PoolData memory poolData; + poolData.pool = input.pool; if (coin1 == input.pt) { - //check other token is spectra wrapper or not - if (coin0 != input.quoteToken) { - if (ISpectraErc4626Wrapper(coin0).vaultShare() == input.quoteToken) { - poolData.unwrapSwIbtToIbt = true; - } else if (IERC4626(coin0).asset() == input.quoteToken) { - poolData.unwrapIbtToUnderlying = true; - } else { - revert WrongPoolInput(); - } - } + // check quote token is IBT, underlying of ibt or wrapped ibt + poolData.quoteTokenKind = _getQuoteTokenKind(coin0, input.quoteToken); + poolData.zeroIndexCoinIsIbt = true; + poolData.ibt = coin0; + poolData.pt = coin1; } else if (coin0 == input.pt) { - if (coin1 != input.quoteToken) { - if (ISpectraErc4626Wrapper(coin1).vaultShare() == input.quoteToken) { - poolData.unwrapSwIbtToIbt = true; - } else if (IERC4626(coin0).asset() == input.quoteToken) { - poolData.unwrapIbtToUnderlying = true; - } else { - revert WrongPoolInput(); - } - } - + // check quote token is IBT, underlying of ibt or wrapped ibt + poolData.quoteTokenKind = _getQuoteTokenKind(coin1, input.quoteToken); poolData.zeroIndexCoinIsIbt = false; poolData.ibt = coin1; poolData.pt = coin0; @@ -120,6 +115,7 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { } } + /// @dev Returns pool data by pair of tokens function _getPoolDataSafe(address tokenA, address tokenB) private view returns (PoolData memory poolData) { poolData = getPoolData[tokenA][tokenB]; if (poolData.pool == address(0)) revert UnknownPair(); @@ -139,17 +135,17 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { // PT -> IBT address recipient = recipientArg; - if (poolData.unwrapIbtToUnderlying || poolData.unwrapSwIbtToIbt) { + if (poolData.quoteTokenKind != QuoteTokenKind.Ibt) { // change recipient to address(this), it let make unwrap swIbt for Ibt after swap recipient = address(this); } amountOut = _curveSwapExactInput(poolData.pool, recipient, tokenInArg, tokenInIndex, amountInArg, minAmountOut); - if (poolData.unwrapSwIbtToIbt) { + if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { // sw-IBT unwrap to IBT amountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(amountOut, recipientArg, address(this)); - } else if (poolData.unwrapIbtToUnderlying) { + } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { // IBT redeem to underlying asset amountOut = IERC4626(poolData.ibt).redeem(amountOut, recipientArg, address(this)); } @@ -159,12 +155,12 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { uint256 amountIn = amountInArg; address tokenIn = tokenInArg; - if (poolData.unwrapSwIbtToIbt) { + if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { // wrap IBT to sw-IBT and change recipient to current address(this) IERC20(tokenIn).forceApprove(poolData.ibt, amountInArg); amountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(amountInArg, address(this)); tokenIn = poolData.ibt; // tokenIn is sw-IBT - } else if (poolData.unwrapIbtToUnderlying) { + } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { // wrap Underlying asset to IBT and change recipient to current address(this) IERC20(tokenIn).forceApprove(poolData.ibt, amountInArg); amountIn = IERC4626(poolData.ibt).deposit(amountInArg, address(this)); @@ -183,16 +179,15 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { uint256 amountIn ) private returns (uint256 amountOut) { if (tokenIn == poolData.pt) { - if (poolData.unwrapSwIbtToIbt) { - // redeem sw-IBT + if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { + // convert PT to sw-IBT uint256 swAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); - // unwrap sw-IBT to IBT + // convert sw-IBT to IBT amountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(swAmountOut, recipient, address(this)); - } - if (poolData.unwrapIbtToUnderlying) { - // redeem IBT - uint ibtAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, recipient, address(this)); - // redeem Underlying + } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { + // convert PT to IBT + uint ibtAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); + // convert IBT to Underlying amountOut = IERC4626(poolData.ibt).redeem(ibtAmountOut, recipient, address(this)); } else { amountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, recipient, address(this)); @@ -203,6 +198,54 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { } } + function _swapExactOutputPtToIbtToUnderlyingPreMaturity( + PoolData memory poolData, + address recipient, + uint256 quoteAmountOut, + uint256 maxPtAmountIn + ) private returns (uint256 ptAmountIn) { + // PT -> IBT -> underlying + + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 1 : 0; + + // calculate amount of IBT for exact quoteAmountOut + uint256 ibtAmountOut = IERC4626(poolData.ibt).previewWithdraw(quoteAmountOut); + + // swap maxAmountPt to IBT + uint256 ibtActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + tokenInIndex, + maxPtAmountIn, + ibtAmountOut + ); + + // withdraw exact amount of Underlying asset from IBT + IERC4626(poolData.ibt).withdraw(quoteAmountOut, recipient, address(this)); + + if (ibtActualAmountOut == ibtAmountOut) { + return maxPtAmountIn; + } + + uint256 deltaIbtAmountOut = ibtActualAmountOut - ibtAmountOut; + + // swap and move excessive tokenIn directly to recipient. + // last arg minAmountOut is zero because we made worst allowed by user swap + // and an additional swap with whichever output only improves it, + // so the tx shouldn't be reverted + uint256 excessivePtAmountIn = _curveSwapExactInput( + poolData.pool, + recipient, + poolData.ibt, + 1 - tokenInIndex, + deltaIbtAmountOut, + 0 + ); + + ptAmountIn = maxPtAmountIn - excessivePtAmountIn; + } + /// @dev Swap PT to exact amount of IBT. When IBT is SpectraWrapper function _swapExactOutputPtToSwIbtToIbtPreMaturity( PoolData memory poolData, @@ -312,7 +355,57 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { ptAmountIn = maxPtAmountIn - excessivePtAmountIn; } - /// @dev Swap IBT to exact amount of PT. When IBT is SpectraWrapper + function _swapExactOutputUnderlyingToIbtToPtPreMaturity( + PoolData memory poolData, + address recipient, + address quoteTokenIn, + uint256 ptAmountOut, + uint256 maxUnderlyingAmountIn + ) private returns (uint256 underlyingAmountIn) { + // Underlying -> IBT -> PT + uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; + + // Convert all Underlying to IBT + IERC20(quoteTokenIn).forceApprove(poolData.ibt, maxUnderlyingAmountIn); + uint256 ibtMaxAmountIn = IERC4626(poolData.ibt).deposit(maxUnderlyingAmountIn, address(this)); + + // swap all IBT to PT + uint256 ptActualAmountOut = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.ibt, + tokenInIndex, + ibtMaxAmountIn, + ptAmountOut + ); + + if (ptActualAmountOut < ptAmountOut) { + revert TooMuchRequested(); + } + + // send exact amount of PT to recipient + IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); + + if (ptActualAmountOut == ptAmountOut) { + return maxUnderlyingAmountIn; + } + + uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; + // swap and move excessive PT amount back to IBT + uint256 excessiveIbtIn = _curveSwapExactInput( + poolData.pool, + address(this), + poolData.pt, + 1 - tokenInIndex, + deltaPtAmountOut, + 0 + ); + + // convert excessive IBT to Underlying and return to recipient + uint256 excessiveUnderlyingAmountIn = IERC4626(poolData.ibt).redeem(excessiveIbtIn, recipient, address(this)); + underlyingAmountIn = maxUnderlyingAmountIn - excessiveUnderlyingAmountIn; + } + function _swapExactOutputIbtToSwIbtToPtPreMaturity( PoolData memory poolData, address recipient, @@ -429,12 +522,11 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { // swap IBT to PT is not possible after maturity revert NotSupported(); } else { - if (poolData.unwrapSwIbtToIbt) { - // PT withdraw to sw-IBT, then unwrap sw-IBT to IBT + if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { + // PT -> sw-IBT -> IBT // calc sw-IBT amount from amountOut - uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewUnwrap(amountOut); + uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewWrap(amountOut); amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(swAmountOut, address(this), address(this)); - uint256 actualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( swAmountOut, address(this), @@ -449,25 +541,15 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { if (maxAmountIn == amountIn) { return amountIn; } - // return rest of PT token back to recipient IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); - } else if (poolData.unwrapIbtToUnderlying) { - // PT withdraw to sw-IBT, then unwrap sw-IBT to IBT - // calc sw-IBT amount from amountOut - uint256 ibtAmountOut = IERC4626(poolData.ibt).previewMint(amountOut); + } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { + // PT -> IBT -> Underlying + // calc IBT needed to withdraw amountOut of Underlying + uint256 ibtAmountOut = IERC4626(poolData.ibt).previewWithdraw(amountOut); amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(ibtAmountOut, address(this), address(this)); - uint256 actualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( - swAmountOut, - address(this), - address(this) - ); - if (actualAmountOut < amountOut) revert InsufficientAmount(); - - // actualAmountOut could be more than amountOut, but it's not possible to change it back to PT aftrer maturity - // dust may be left on the contract - IERC20(tokenOut).safeTransfer(recipient, amountOut); + IERC4626(poolData.ibt).withdraw(amountOut, recipient, address(this)); if (maxAmountIn == amountIn) { return amountIn; @@ -548,18 +630,24 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { amountIn = _swapExactOutputPostMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); } else { if (tokenIn == poolData.pt) { - if (poolData.unwrapSwIbtToIbt) { + if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { amountIn = _swapExactOutputPtToSwIbtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); - } else if (poolData.unwrapIbtToUnderlying) { - amountIn = 0; // _swapExactOutputPtToIbtToUnderlyingPreMaturity(...) TODO: not implemented + } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { + amountIn = _swapExactOutputPtToIbtToUnderlyingPreMaturity(poolData, recipient, amountOut, maxAmountIn); } else { amountIn = _swapExactOutputPtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); } } else { - if (poolData.unwrapSwIbtToIbt) { + if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { amountIn = _swapExactOutputIbtToSwIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); - } else if (poolData.unwrapIbtToUnderlying) { - amountIn = 0; //_swapExactOutputUnderlyingToIbtToPtPreMaturity(...); TODO: not implemented + } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { + amountIn = _swapExactOutputUnderlyingToIbtToPtPreMaturity( + poolData, + recipient, + tokenIn, + amountOut, + maxAmountIn + ); } else { amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); } diff --git a/packages/router/test/int/SpectraAdapter.eth.spec.ts b/packages/router/test/int/SpectraAdapter.eth.spec.ts index 456aec73..2edd3b58 100644 --- a/packages/router/test/int/SpectraAdapter.eth.spec.ts +++ b/packages/router/test/int/SpectraAdapter.eth.spec.ts @@ -45,7 +45,7 @@ interface TestCase { spectraPool: string; ptToken: TokenInfo; - ibtToken: TokenInfo; + quoteToken: TokenInfo; swIbt?: SWToken; timeToMaturity: number; @@ -90,7 +90,7 @@ const inwstETHs_TestCase: TestCase = { initialBalance: parseUnits('1000', 18), }, - ibtToken: { + quoteToken: { address: '0x8e0789d39db454dbe9f4a77acef6dc7c69f6d552', symbol: 'inwstETHs', balanceSlot: EthereumMainnetERC20BalanceOfSlot.INWSTETHS, @@ -136,6 +136,57 @@ const inwstETHs_TestCase: TestCase = { }, }; +const USR_TestCase: TestCase = { + forkNumber: 21714750, + + spectraPool: '0x0d89f4583a6b5eceb76551d573ad49cd435f6064', + ptToken: { + address: '0xd0097149aa4cc0d0e1fc99b8bd73fc17dc32c1e9', + symbol: 'pt-wstUSR', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, + initialBalance: parseUnits('10000', 18), + }, + + quoteToken: { + address: '0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110', + symbol: 'USR', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.WSTUSR, + initialBalance: parseUnits('10000', 18), + }, + + timeToMaturity: 180 * 24 * 60 * 60, // 180 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('500', 18), + minPtOut: parseUnits('500', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('745.34', 18), + minIbtOut: parseUnits('650', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15.75', 18), + ibtOut: parseUnits('10.2', 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: 21714750, @@ -147,7 +198,7 @@ const wstUSR_TestCase: TestCase = { initialBalance: parseUnits('10000', 18), }, - ibtToken: { + quoteToken: { address: '0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055', symbol: 'wstUSR', balanceSlot: EthereumMainnetERC20BalanceOfSlot.WSTUSR, @@ -187,6 +238,57 @@ const wstUSR_TestCase: TestCase = { }, }; +const DOLA_TestCase: TestCase = { + forkNumber: 21714750, + + spectraPool: '0x69ba1b7dba7eb3b7a73f4e35fd04a27ad06c55fe', + ptToken: { + address: '0xf4ca2ce6eaa1b507570c4b340007f6266c7d5698', + symbol: 'pt-sDOLA', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.PTSWINWSTETHS, + initialBalance: parseUnits('10000', 18), + }, + + quoteToken: { + address: '0x865377367054516e17014ccded1e7d814edc9ce4', + symbol: 'DOLA', + balanceSlot: EthereumMainnetERC20BalanceOfSlot.DOLA, + initialBalance: parseUnits('10000', 18), + }, + + timeToMaturity: 365 * 24 * 60 * 60, // 365 days + + // swap params + preMaturity: { + swapExactIbtToPt: { + ibtIn: parseUnits('500', 18), + minPtOut: parseUnits('500', 18), + }, + swapExactPtToIbt: { + ptIn: parseUnits('800.34', 18), + minIbtOut: parseUnits('650', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('15.75', 18), + ibtOut: parseUnits('10.2', 18), + }, + swapIbtToExactPt: { + maxIbtIn: parseUnits('600', 18), + ptOut: parseUnits('600', 18), + }, + }, + postMaturity: { + swapExactPtToIbt: { + ptIn: parseUnits('600', 18), + minIbtOut: parseUnits('500', 18), + }, + swapPtToExactIbt: { + maxPtIn: parseUnits('600', 18), + ibtOut: parseUnits('500', 18), + }, + }, +}; + const sDOLA_TestCase: TestCase = { forkNumber: 21714750, @@ -198,7 +300,7 @@ const sDOLA_TestCase: TestCase = { initialBalance: parseUnits('10000', 18), }, - ibtToken: { + quoteToken: { address: '0xb45ad160634c528cc3d2926d9807104fa3157305', symbol: 'sDOLA', balanceSlot: EthereumMainnetERC20BalanceOfSlot.SDOLA, @@ -238,7 +340,7 @@ const sDOLA_TestCase: TestCase = { }, }; -const testCases = [wstUSR_TestCase, sDOLA_TestCase, inwstETHs_TestCase]; +const testCases = [USR_TestCase, DOLA_TestCase, wstUSR_TestCase, sDOLA_TestCase, inwstETHs_TestCase]; async function initializeRouter(testCase: TestCase): Promise<{ ptToken: ERC20; @@ -251,12 +353,12 @@ async function initializeRouter(testCase: TestCase): Promise<{ const [owner, user] = await ethers.getSigners(); const ptToken = await ethers.getContractAt('ERC20', testCase.ptToken.address); - const ibtToken = await ethers.getContractAt('ERC20', testCase.ibtToken.address); + const ibtToken = await ethers.getContractAt('ERC20', testCase.quoteToken.address); const spectraPool = testCase.spectraPool; const poolInput: SpectraAdapter.PoolInputStruct = { pt: ptToken.address, - ibt: ibtToken.address, + quoteToken: ibtToken.address, pool: spectraPool, }; @@ -270,9 +372,9 @@ async function initializeRouter(testCase: TestCase): Promise<{ await setTokenBalance( ibtToken.address, - testCase.ibtToken.balanceSlot, + testCase.quoteToken.balanceSlot, EthAddress.parse(user.address), - testCase.ibtToken.initialBalance + testCase.quoteToken.initialBalance ); await setTokenBalance( @@ -287,16 +389,16 @@ async function initializeRouter(testCase: TestCase): Promise<{ `Wrong initial ${testCase.ptToken.symbol} balance` ); expect(await ibtToken.balanceOf(user.address)).to.be.eq( - testCase.ibtToken.initialBalance, - `Wrong initial ${testCase.ibtToken.symbol} balance` + testCase.quoteToken.initialBalance, + `Wrong initial ${testCase.quoteToken.symbol} balance` ); if (testCase.swIbt) { await setTokenBalance( ibtToken.address, - testCase.ibtToken.balanceSlot, + testCase.quoteToken.balanceSlot, EthAddress.parse(user.address), - testCase.ibtToken.initialBalance.add(testCase.swIbt.ibtTransferAmount) + testCase.quoteToken.initialBalance.add(testCase.swIbt.ibtTransferAmount) ); await ibtToken.connect(user).transfer(testCase.swIbt.address, testCase.swIbt.ibtTransferAmount); @@ -315,7 +417,7 @@ async function initializeRouter(testCase: TestCase): Promise<{ // Tests for running in ethereum mainnet fork describe('SpectraAdapter', async () => { for (const testCase of testCases) { - describe(`SpectraAdapter ${testCase.ptToken.symbol} - ${testCase.ibtToken.symbol}`, () => { + describe(`SpectraAdapter ${testCase.ptToken.symbol} - ${testCase.quoteToken.symbol}`, () => { before(async () => { await resetFork(testCase.forkNumber); }); @@ -331,7 +433,7 @@ describe('SpectraAdapter', async () => { ({ ptToken, ibtToken, router, spectraAdapter, user } = await initializeRouter(testCase)); }); - it(`${testCase.ibtToken.symbol} to ${testCase.ptToken.symbol} exact input`, async () => { + it(`${testCase.quoteToken.symbol} to ${testCase.ptToken.symbol} exact input`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -355,7 +457,7 @@ describe('SpectraAdapter', async () => { await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it(`${testCase.ibtToken.symbol} to ${testCase.ptToken.symbol} exact output`, async () => { + it(`${testCase.quoteToken.symbol} to ${testCase.ptToken.symbol} exact output`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -377,7 +479,7 @@ describe('SpectraAdapter', async () => { await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it(`${testCase.ptToken.symbol} to ${testCase.ibtToken.symbol} exact input`, async () => { + it(`${testCase.ptToken.symbol} to ${testCase.quoteToken.symbol} exact input`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -399,7 +501,7 @@ describe('SpectraAdapter', async () => { await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it(`${testCase.ptToken.symbol} to ${testCase.ibtToken.symbol} exact output`, async () => { + it(`${testCase.ptToken.symbol} to ${testCase.quoteToken.symbol} exact output`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -437,7 +539,7 @@ describe('SpectraAdapter', async () => { await ethers.provider.send('evm_mine', []); }); - it(`${testCase.ibtToken.symbol} to ${testCase.ptToken.symbol} exact input, forbidden`, async () => { + it(`${testCase.quoteToken.symbol} to ${testCase.ptToken.symbol} exact input, forbidden`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -457,7 +559,7 @@ describe('SpectraAdapter', async () => { expect(ibtBalanceAfter).to.be.eq(ibtBalanceBefore); }); - it(`${testCase.ibtToken.symbol} to ${testCase.ptToken.symbol} exact output, forbidden`, async () => { + it(`${testCase.quoteToken.symbol} to ${testCase.ptToken.symbol} exact output, forbidden`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -476,7 +578,7 @@ describe('SpectraAdapter', async () => { expect(ibtBalanceAfter).to.be.eq(ibtBalanceBefore); }); - it(`${testCase.ptToken.symbol} to ${testCase.ibtToken.symbol} exact input`, async () => { + it(`${testCase.ptToken.symbol} to ${testCase.quoteToken.symbol} exact input`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'balance before:'); @@ -498,7 +600,7 @@ describe('SpectraAdapter', async () => { await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it(`${testCase.ptToken.symbol} to ${testCase.ibtToken.symbol} exact output`, async () => { + it.only(`${testCase.ptToken.symbol} to ${testCase.quoteToken.symbol} exact output`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'ibt balance before:'); diff --git a/packages/router/test/shared/tokens.ts b/packages/router/test/shared/tokens.ts index 097a42fc..05d89d26 100644 --- a/packages/router/test/shared/tokens.ts +++ b/packages/router/test/shared/tokens.ts @@ -28,6 +28,7 @@ export enum EthereumMainnetERC20BalanceOfSlot { WSTUSR = '52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00', SDOLA = '0000000000000000000000000000000000000000000000000000000000000003', USR = '52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00', + DOLA = '0000000000000000000000000000000000000000000000000000000000000006', } function getAccountBalanceStorageSlot(account: EthAddress, tokenMappingSlot: string): string { diff --git a/packages/router/test/shared/utils.ts b/packages/router/test/shared/utils.ts index 5ce1bb75..92bd0f25 100644 --- a/packages/router/test/shared/utils.ts +++ b/packages/router/test/shared/utils.ts @@ -44,7 +44,8 @@ export function constructSwap(dex: number[], ratios: number[]): BigNumber { export async function showGasUsage(tx: ContractTransaction) { const txReceipt = await tx.wait(); - console.log(`⛽ gas used ${txReceipt.gasUsed}`); + const warningLimit = 1_000_000; + console.log(`⛽ gas used ${txReceipt.gasUsed} ${txReceipt.gasUsed.gt(warningLimit) ? '!!! WARNING' : ''}`); } export async function showBalance(token: ERC20, account: string, startPhrase = ''): Promise { From 6d372add8e0494419ef692f54636123f5108ad0b Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 4 Feb 2025 10:50:33 +0300 Subject: [PATCH 08/10] use interface from --- .../contracts/adapters/SpectraAdapter.sol | 3 +- .../adapters/interfaces/IERC4626.sol | 36 ------------------- 2 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 packages/router/contracts/adapters/interfaces/IERC4626.sol diff --git a/packages/router/contracts/adapters/SpectraAdapter.sol b/packages/router/contracts/adapters/SpectraAdapter.sol index 3b718ad2..0fbdbaa8 100644 --- a/packages/router/contracts/adapters/SpectraAdapter.sol +++ b/packages/router/contracts/adapters/SpectraAdapter.sol @@ -3,12 +3,13 @@ pragma solidity 0.8.19; import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import '@openzeppelin/contracts/access/Ownable2Step.sol'; +import '@openzeppelin/contracts/interfaces/IERC4626.sol'; + import '../interfaces/IMarginlyRouter.sol'; import '../interfaces/IMarginlyAdapter.sol'; import './interfaces/ICurvePool.sol'; import './interfaces/ISpectraErc4626Wrapper.sol'; import './interfaces/ISpectraPrincipalToken.sol'; -import './interfaces/IERC4626.sol'; /// @title Adapter for Spectra finance pool (old curve pool) of two tokens IBT/PT /// @dev Two cases supported: diff --git a/packages/router/contracts/adapters/interfaces/IERC4626.sol b/packages/router/contracts/adapters/interfaces/IERC4626.sol deleted file mode 100644 index f8d72b24..00000000 --- a/packages/router/contracts/adapters/interfaces/IERC4626.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.19; - -interface IERC4626 { - function asset() external view returns (address assetTokenAddress); - - function totalAssets() external view returns (uint256 totalManagedAssets); - - function convertToShares(uint256 assets) external view returns (uint256 shares); - - function convertToAssets(uint256 shares) external view returns (uint256 assets); - - function maxDeposit(address receiver) external view returns (uint256 maxAssets); - - function previewDeposit(uint256 assets) external view returns (uint256 shares); - - function deposit(uint256 assets, address receiver) external returns (uint256 shares); - - function maxMint(address receiver) external view returns (uint256 maxShares); - - function previewMint(uint256 shares) external view returns (uint256 assets); - - function mint(uint256 shares, address receiver) external returns (uint256 assets); - - function maxWithdraw(address owner) external view returns (uint256 maxAssets); - - function previewWithdraw(uint256 assets) external view returns (uint256 shares); - - function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); - - function maxRedeem(address owner) external view returns (uint256 maxShares); - - function previewRedeem(uint256 shares) external view returns (uint256 assets); - - function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); -} From ed14c80d0a630dcee12d0a9d847d1e18e494cd5b Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 4 Feb 2025 15:36:56 +0300 Subject: [PATCH 09/10] code review --- .../contracts/adapters/SpectraAdapter.sol | 270 ++++++------------ .../interfaces/ISpectraPrincipalToken.sol | 19 ++ .../test/int/SpectraAdapter.eth.spec.ts | 12 +- 3 files changed, 116 insertions(+), 185 deletions(-) diff --git a/packages/router/contracts/adapters/SpectraAdapter.sol b/packages/router/contracts/adapters/SpectraAdapter.sol index 0fbdbaa8..42a8bc1f 100644 --- a/packages/router/contracts/adapters/SpectraAdapter.sol +++ b/packages/router/contracts/adapters/SpectraAdapter.sol @@ -154,22 +154,20 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { // IBT -> PT uint256 amountIn = amountInArg; - address tokenIn = tokenInArg; if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { // wrap IBT to sw-IBT and change recipient to current address(this) - IERC20(tokenIn).forceApprove(poolData.ibt, amountInArg); + IERC20(tokenInArg).forceApprove(poolData.ibt, amountInArg); amountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(amountInArg, address(this)); - tokenIn = poolData.ibt; // tokenIn is sw-IBT + // tokenIn is sw-IBT } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { // wrap Underlying asset to IBT and change recipient to current address(this) - IERC20(tokenIn).forceApprove(poolData.ibt, amountInArg); + IERC20(tokenInArg).forceApprove(poolData.ibt, amountInArg); amountIn = IERC4626(poolData.ibt).deposit(amountInArg, address(this)); - tokenIn = poolData.ibt; } // swap in curve IBT to PT - amountOut = _curveSwapExactInput(poolData.pool, recipientArg, tokenIn, tokenInIndex, amountIn, minAmountOut); + amountOut = _curveSwapExactInput(poolData.pool, recipientArg, poolData.ibt, tokenInIndex, amountIn, minAmountOut); } } @@ -179,24 +177,22 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { address tokenIn, uint256 amountIn ) private returns (uint256 amountOut) { - if (tokenIn == poolData.pt) { - if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { - // convert PT to sw-IBT - uint256 swAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); - // convert sw-IBT to IBT - amountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(swAmountOut, recipient, address(this)); - } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { - // convert PT to IBT - uint ibtAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); - // convert IBT to Underlying - amountOut = IERC4626(poolData.ibt).redeem(ibtAmountOut, recipient, address(this)); - } else { - amountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, recipient, address(this)); - } - } else { + if (tokenIn != poolData.pt) { // IBT to PT swap is not possible after maturity revert NotSupported(); } + + if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { + // convert PT to sw-IBT + uint256 swAmountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, address(this), address(this)); + // convert sw-IBT to IBT + amountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(swAmountOut, recipient, address(this)); + } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { + // convert PT to Underlying + amountOut = ISpectraPrincipalToken(poolData.pt).redeem(amountIn, recipient, address(this)); + } else { + amountOut = ISpectraPrincipalToken(poolData.pt).redeemForIBT(amountIn, recipient, address(this)); + } } function _swapExactOutputPtToIbtToUnderlyingPreMaturity( @@ -251,7 +247,6 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { function _swapExactOutputPtToSwIbtToIbtPreMaturity( PoolData memory poolData, address recipient, - address ibtOut, uint256 ibtAmountOut, uint256 maxPtAmountIn ) private returns (uint256 ptAmountIn) { @@ -271,28 +266,14 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { swAmountOut ); - // unwrap swIbt to ibt - uint256 ibtActualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( - swActualAmountOut, - address(this), - address(this) - ); - - IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); - - if (ibtActualAmountOut < ibtAmountOut) { - revert TooMuchRequested(); - } + // unwrap swAmountOut to get exact ibtAmountOut + ISpectraErc4626Wrapper(poolData.ibt).unwrap(swAmountOut, recipient, address(this)); - if (ibtActualAmountOut == ibtAmountOut) { + if (swActualAmountOut == swAmountOut) { return maxPtAmountIn; } - uint256 deltaIbtAmountOut = ibtActualAmountOut - ibtAmountOut; - - // wrap extra amountOut ibt to swIbt - IERC20(ibtOut).forceApprove(poolData.ibt, deltaIbtAmountOut); - uint256 deltaSwAmountOut = ISpectraErc4626Wrapper(poolData.ibt).wrap(deltaIbtAmountOut, address(this)); + uint256 deltaSwAmountOut = swActualAmountOut - swAmountOut; // swap and move excessive tokenIn directly to recipient. // last arg minAmountOut is zero because we made worst allowed by user swap @@ -330,12 +311,12 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { ibtAmountOut ); - IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); - if (ibtActualAmountOut < ibtAmountOut) { revert TooMuchRequested(); } + IERC20(ibtOut).safeTransfer(recipient, ibtAmountOut); + if (ibtActualAmountOut == ibtAmountOut) { return maxPtAmountIn; } @@ -356,51 +337,63 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { ptAmountIn = maxPtAmountIn - excessivePtAmountIn; } - function _swapExactOutputUnderlyingToIbtToPtPreMaturity( + /// @dev Swap IBT to exact amount of PT. Returns excessive input amount + function _swapExactOutputIbtToPt( PoolData memory poolData, address recipient, - address quoteTokenIn, - uint256 ptAmountOut, - uint256 maxUnderlyingAmountIn - ) private returns (uint256 underlyingAmountIn) { - // Underlying -> IBT -> PT + bool sendExcessiveToRecipient, + uint256 ibtInMaximum, + uint256 amountPtOut + ) private returns (uint256 excessiveIbtAmount) { uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; - - // Convert all Underlying to IBT - IERC20(quoteTokenIn).forceApprove(poolData.ibt, maxUnderlyingAmountIn); - uint256 ibtMaxAmountIn = IERC4626(poolData.ibt).deposit(maxUnderlyingAmountIn, address(this)); - // swap all IBT to PT - uint256 ptActualAmountOut = _curveSwapExactInput( + uint256 actualAmountPtOut = _curveSwapExactInput( poolData.pool, address(this), poolData.ibt, tokenInIndex, - ibtMaxAmountIn, - ptAmountOut + ibtInMaximum, + amountPtOut ); - if (ptActualAmountOut < ptAmountOut) { + if (actualAmountPtOut < amountPtOut) { revert TooMuchRequested(); } - // send exact amount of PT to recipient - IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); + // send exact amount of tokenOut to recipient + IERC20(poolData.pt).safeTransfer(recipient, amountPtOut); - if (ptActualAmountOut == ptAmountOut) { - return maxUnderlyingAmountIn; + if (actualAmountPtOut == amountPtOut) { + return 0; // all input amount was used } - uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; + uint256 deltaAmountPtOut = actualAmountPtOut - amountPtOut; // swap and move excessive PT amount back to IBT - uint256 excessiveIbtIn = _curveSwapExactInput( + excessiveIbtAmount = _curveSwapExactInput( poolData.pool, - address(this), + sendExcessiveToRecipient ? recipient : address(this), poolData.pt, 1 - tokenInIndex, - deltaPtAmountOut, + deltaAmountPtOut, 0 ); + } + + function _swapExactOutputUnderlyingToIbtToPtPreMaturity( + PoolData memory poolData, + address recipient, + address quoteTokenIn, + uint256 ptAmountOut, + uint256 maxUnderlyingAmountIn + ) private returns (uint256 underlyingAmountIn) { + // Underlying -> IBT -> PT + // Convert all Underlying to IBT + IERC20(quoteTokenIn).forceApprove(poolData.ibt, maxUnderlyingAmountIn); + uint256 ibtMaxAmountIn = IERC4626(poolData.ibt).deposit(maxUnderlyingAmountIn, address(this)); + + // swap all IBT to PT + uint256 excessiveIbtIn = _swapExactOutputIbtToPt(poolData, recipient, false, ibtMaxAmountIn, ptAmountOut); + if (excessiveIbtIn == 0) return maxUnderlyingAmountIn; // convert excessive IBT to Underlying and return to recipient uint256 excessiveUnderlyingAmountIn = IERC4626(poolData.ibt).redeem(excessiveIbtIn, recipient, address(this)); @@ -415,47 +408,14 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { uint256 maxIbtAmountIn ) private returns (uint256 ibAmountIn) { // IBT -> sw-IBT -> PT - uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; - // wrap ibt to swIbt IERC20(ibtIn).forceApprove(poolData.ibt, maxIbtAmountIn); - uint256 swMaxAmountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(maxIbtAmountIn, address(this)); + uint256 maxSwAmountIn = ISpectraErc4626Wrapper(poolData.ibt).wrap(maxIbtAmountIn, address(this)); - // swap swMaxAmountIn to pt tokens - uint256 ptActualAmountOut = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.ibt, - tokenInIndex, - swMaxAmountIn, - ptAmountOut - ); - - if (ptActualAmountOut < ptAmountOut) { - revert TooMuchRequested(); - } - - IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); - - if (ptActualAmountOut == ptAmountOut) { - return maxIbtAmountIn; - } - - uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; - // swap and move excessive tokenIn - // last arg minAmountOut is zero because we made worst allowed by user swap - // and an additional swap with whichever output only improves it, - // so the tx shouldn't be reverted - uint256 excessiveSwAmountIn = _curveSwapExactInput( - poolData.pool, - address(this), - poolData.pt, - 1 - tokenInIndex, - deltaPtAmountOut, - 0 - ); + uint256 excessiveSwAmountIn = _swapExactOutputIbtToPt(poolData, recipient, false, maxSwAmountIn, ptAmountOut); + if (excessiveSwAmountIn == 0) return maxIbtAmountIn; - // unwrap exessive sw into ib and transfer back to recipient + // unwrap excessive sw into ib and transfer back to recipient uint256 excessiveIbAmountIn = ISpectraErc4626Wrapper(poolData.ibt).unwrap( excessiveSwAmountIn, recipient, @@ -468,46 +428,12 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { function _swapExactOutputIbtToPtPreMaturity( PoolData memory poolData, address recipient, - address ibtIn, uint256 ptAmountOut, uint256 maxIbtAmountIn ) private returns (uint256 ibAmountIn) { - uint256 tokenInIndex = poolData.zeroIndexCoinIsIbt ? 0 : 1; - // IBT -> PT - // swap all swMaxAmountIn to pt tokens - uint256 ptActualAmountOut = _curveSwapExactInput( - poolData.pool, - address(this), - ibtIn, - tokenInIndex, - maxIbtAmountIn, - ptAmountOut - ); - - if (ptActualAmountOut < ptAmountOut) { - revert TooMuchRequested(); - } - - IERC20(poolData.pt).safeTransfer(recipient, ptAmountOut); - - if (ptActualAmountOut == ptAmountOut) { - return maxIbtAmountIn; - } - - uint256 deltaPtAmountOut = ptActualAmountOut - ptAmountOut; - // swap and move excessive tokenIn - // last arg minAmountOut is zero because we made worst allowed by user swap - // and an additional swap with whichever output only improves it, - // so the tx shouldn't be reverted - uint256 excessiveIbtAmountIn = _curveSwapExactInput( - poolData.pool, - recipient, - poolData.pt, - 1 - tokenInIndex, - deltaPtAmountOut, - 0 - ); + uint256 excessiveIbtAmountIn = _swapExactOutputIbtToPt(poolData, recipient, true, maxIbtAmountIn, ptAmountOut); + if (excessiveIbtAmountIn == 0) return maxIbtAmountIn; ibAmountIn = maxIbtAmountIn - excessiveIbtAmountIn; } @@ -522,51 +448,31 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { if (tokenOut == poolData.pt) { // swap IBT to PT is not possible after maturity revert NotSupported(); - } else { - if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { - // PT -> sw-IBT -> IBT - // calc sw-IBT amount from amountOut - uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewWrap(amountOut); - amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(swAmountOut, address(this), address(this)); - uint256 actualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap( - swAmountOut, - address(this), - address(this) - ); - if (actualAmountOut < amountOut) revert InsufficientAmount(); - - // actualAmountOut could be more than amountOut, but it's not possible to change it back to PT aftrer maturity - // dust may be left on the contract - IERC20(tokenOut).safeTransfer(recipient, amountOut); - - if (maxAmountIn == amountIn) { - return amountIn; - } - // return rest of PT token back to recipient - IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); - } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { - // PT -> IBT -> Underlying - // calc IBT needed to withdraw amountOut of Underlying - uint256 ibtAmountOut = IERC4626(poolData.ibt).previewWithdraw(amountOut); - amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(ibtAmountOut, address(this), address(this)); - - IERC4626(poolData.ibt).withdraw(amountOut, recipient, address(this)); - - if (maxAmountIn == amountIn) { - return amountIn; - } + } - // return rest of PT token back to recipient - IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); - } else { - amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(amountOut, recipient, address(this)); + if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { + // PT -> sw-IBT -> IBT + // calc sw-IBT amount from amountOut + uint256 swAmountOut = ISpectraErc4626Wrapper(poolData.ibt).previewWrap(amountOut); + amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(swAmountOut, address(this), address(this)); + uint256 actualAmountOut = ISpectraErc4626Wrapper(poolData.ibt).unwrap(swAmountOut, address(this), address(this)); + if (actualAmountOut < amountOut) revert InsufficientAmount(); + + // actualAmountOut could be more than amountOut, but it's not possible to change it back to PT after maturity + // dust may be left on the contract + IERC20(tokenOut).safeTransfer(recipient, amountOut); + } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { + // PT -> Underlying + amountIn = ISpectraPrincipalToken(poolData.pt).withdraw(amountOut, recipient, address(this)); + } else { + amountIn = ISpectraPrincipalToken(poolData.pt).withdrawIBT(amountOut, recipient, address(this)); + } - if (maxAmountIn == amountIn) { - return amountIn; - } - IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); - } + if (maxAmountIn == amountIn) { + return amountIn; } + // return rest of PT token back to recipient + IERC20(poolData.pt).safeTransfer(recipient, maxAmountIn - amountIn); } function _curveSwapExactInput( @@ -632,7 +538,7 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { } else { if (tokenIn == poolData.pt) { if (poolData.quoteTokenKind == QuoteTokenKind.IbtNotCompatibleWithERC4626) { - amountIn = _swapExactOutputPtToSwIbtToIbtPreMaturity(poolData, recipient, tokenOut, amountOut, maxAmountIn); + amountIn = _swapExactOutputPtToSwIbtToIbtPreMaturity(poolData, recipient, amountOut, maxAmountIn); } else if (poolData.quoteTokenKind == QuoteTokenKind.UnderlyingOfIbt) { amountIn = _swapExactOutputPtToIbtToUnderlyingPreMaturity(poolData, recipient, amountOut, maxAmountIn); } else { @@ -650,7 +556,7 @@ contract SpectraAdapter is IMarginlyAdapter, Ownable2Step { maxAmountIn ); } else { - amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, tokenIn, amountOut, maxAmountIn); + amountIn = _swapExactOutputIbtToPtPreMaturity(poolData, recipient, amountOut, maxAmountIn); } } } diff --git a/packages/router/contracts/adapters/interfaces/ISpectraPrincipalToken.sol b/packages/router/contracts/adapters/interfaces/ISpectraPrincipalToken.sol index 737cc262..e57c771a 100644 --- a/packages/router/contracts/adapters/interfaces/ISpectraPrincipalToken.sol +++ b/packages/router/contracts/adapters/interfaces/ISpectraPrincipalToken.sol @@ -57,4 +57,23 @@ interface ISpectraPrincipalToken { address owner, uint256 maxShares ) external returns (uint256 shares); + + /** + * @notice Burns owner's shares (PTs and YTs before expiry, PTs after expiry) + * and sends assets to receiver + * @param shares The amount of shares to burn + * @param receiver The address that will receive the assets + * @param owner The owner of the shares + * @return assets The actual amount of assets received for burning the shares + */ + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + /** + * @notice Burns owner's shares (before expiry : PTs and YTs) and sends assets to receiver + * @param assets The amount of assets to be received + * @param receiver The address that will receive the assets + * @param owner The owner of the shares (PTs and YTs) + * @return shares The actual amount of shares burnt for receiving the assets + */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); } diff --git a/packages/router/test/int/SpectraAdapter.eth.spec.ts b/packages/router/test/int/SpectraAdapter.eth.spec.ts index 2edd3b58..88cfffc2 100644 --- a/packages/router/test/int/SpectraAdapter.eth.spec.ts +++ b/packages/router/test/int/SpectraAdapter.eth.spec.ts @@ -340,7 +340,13 @@ const sDOLA_TestCase: TestCase = { }, }; -const testCases = [USR_TestCase, DOLA_TestCase, wstUSR_TestCase, sDOLA_TestCase, inwstETHs_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 +]; async function initializeRouter(testCase: TestCase): Promise<{ ptToken: ERC20; @@ -415,7 +421,7 @@ async function initializeRouter(testCase: TestCase): Promise<{ } // Tests for running in ethereum mainnet fork -describe('SpectraAdapter', async () => { +describe.only('SpectraAdapter', async () => { for (const testCase of testCases) { describe(`SpectraAdapter ${testCase.ptToken.symbol} - ${testCase.quoteToken.symbol}`, () => { before(async () => { @@ -600,7 +606,7 @@ describe('SpectraAdapter', async () => { await showBalanceDelta(ibtBalanceBefore, ibtBalanceAfter, ibtToken, 'IBT balance delta:'); }); - it.only(`${testCase.ptToken.symbol} to ${testCase.quoteToken.symbol} exact output`, async () => { + it(`${testCase.ptToken.symbol} to ${testCase.quoteToken.symbol} exact output`, async () => { const ptBalanceBefore = await showBalance(ptToken, user.address, 'pt balance Before:'); const ibtBalanceBefore = await showBalance(ibtToken, user.address, 'ibt balance before:'); From c9e71fa14f6dabfeb3b214c334f541c4e4d5e266 Mon Sep 17 00:00:00 2001 From: rudewalt Date: Tue, 4 Feb 2025 16:24:55 +0300 Subject: [PATCH 10/10] remove only from tests --- packages/router/test/int/SpectraAdapter.eth.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/test/int/SpectraAdapter.eth.spec.ts b/packages/router/test/int/SpectraAdapter.eth.spec.ts index 88cfffc2..dd9ae371 100644 --- a/packages/router/test/int/SpectraAdapter.eth.spec.ts +++ b/packages/router/test/int/SpectraAdapter.eth.spec.ts @@ -421,7 +421,7 @@ async function initializeRouter(testCase: TestCase): Promise<{ } // Tests for running in ethereum mainnet fork -describe.only('SpectraAdapter', async () => { +describe('SpectraAdapter', async () => { for (const testCase of testCases) { describe(`SpectraAdapter ${testCase.ptToken.symbol} - ${testCase.quoteToken.symbol}`, () => { before(async () => {