diff --git a/src/interfaces/external/IBooster.sol b/src/interfaces/external/IBooster.sol new file mode 100644 index 00000000..4e9ee400 --- /dev/null +++ b/src/interfaces/external/IBooster.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +// Convex IBooster interface +interface IBooster { + function owner() external view returns(address); + function poolLength() external view returns (uint256); + function poolInfo(uint256 _pid) external view returns(address, address, address, address, address, bool); + + function deposit(uint256 _pid, uint256 _amount, bool _stake) external returns(bool); + function depositAll(uint256 _pid, bool _stake) external returns(bool); + function withdraw(uint256 _pid, uint256 _amount) external returns(bool); + function withdrawTo(uint256 _pid, uint256 _amount, address _to) external returns(bool); + function withdrawAll(uint256 _pid) external returns(bool); + function claimRewards(uint256 _pid, address _gauge) external returns(bool); + function vote(uint256 _voteId, address _votingAddress, bool _support) external returns(bool); + function voteGaugeWeight(address[] calldata _gauge, uint256[] calldata _weight ) external returns(bool); + function setVoteDelegate(address _voteDelegate) external; +} \ No newline at end of file diff --git a/src/interfaces/external/ICurve2Pool.sol b/src/interfaces/external/ICurve2Pool.sol new file mode 100644 index 00000000..c31c1977 --- /dev/null +++ b/src/interfaces/external/ICurve2Pool.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +// ICurvePool interface +interface ICurvePool { + function coins(uint256) external view returns (address); + function balances(uint256) external view returns (uint256); + function get_virtual_price() external view returns (uint256); + function calc_withdraw_one_coin(uint256 token_amount, int128 i) external view returns (uint256); + + function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount, address _receiver) external returns (uint256); + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external; + function remove_liquidity(uint256 _amount, uint256[3] calldata min_amounts) external; + function remove_liquidity_imbalance(uint256[2] calldata amounts, uint256 max_burn_amount) external; + function remove_liquidity_one_coin(uint256 _burn_amount, int128 i, uint256 _min_received, address _receiver) external returns (uint256); +} \ No newline at end of file diff --git a/src/interfaces/external/ICurvePool.sol b/src/interfaces/external/ICurvePool.sol new file mode 100644 index 00000000..39fee28a --- /dev/null +++ b/src/interfaces/external/ICurvePool.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +// ICurvePool interface +interface ICurvePool { + function coins(uint256) external view returns (address); + function balances(uint256) external view returns (uint256); + function get_virtual_price() external view returns (uint256); + function calc_withdraw_one_coin(uint256 token_amount, int128 i) external view returns (uint256); + + function add_liquidity(uint256[3] calldata amounts, uint256 min_mint_amount) external; + function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external; + function remove_liquidity(uint256 _amount, uint256[3] calldata min_amounts) external; + function remove_liquidity_imbalance(uint256[3] calldata amounts, uint256 max_burn_amount) external; + function remove_liquidity_one_coin(uint256 _token_amount, int128 i, uint256 min_amount) external; +} \ No newline at end of file diff --git a/src/interfaces/external/IRewardPool.sol b/src/interfaces/external/IRewardPool.sol new file mode 100644 index 00000000..b52c620a --- /dev/null +++ b/src/interfaces/external/IRewardPool.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +// Convex IRewardPool interface +interface IRewardPool is IERC20 { + function getReward() external returns(bool); + function getReward(address _account, bool _claimExtras) external returns(bool); + function withdrawAllAndUnwrap(bool claim) external; + function withdraw(uint256 amount, bool claim) external; + function withdrawAndUnwrap(uint256 amount, bool claim) external; + function stake(uint256 _amount) external; + function rewardPerToken() external view returns (uint256); +} \ No newline at end of file diff --git a/src/modules/adaptors/Convex/ConvexAdaptor.sol b/src/modules/adaptors/Convex/ConvexAdaptor.sol new file mode 100644 index 00000000..4618da85 --- /dev/null +++ b/src/modules/adaptors/Convex/ConvexAdaptor.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { BaseAdaptor, ERC20, SafeERC20, Cellar, PriceRouter, Registry, Math } from "src/modules/adaptors/BaseAdaptor.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IBooster } from "src/interfaces/external/IBooster.sol"; +import { IRewardPool } from "src/interfaces/external/IRewardPool.sol"; +import { ICurvePool } from "src/interfaces/external/ICurvePool.sol"; + +/** + * @title Convex Adaptor + * @notice Allows Cellars to interact with Convex Positions. + * @author cookiesanddudes, federava + */ +contract ConvexAdaptor is BaseAdaptor { + using SafeERC20 for ERC20; + using Math for uint256; + using SafeCast for uint256; + using Address for address; + + //==================== Adaptor Data Specification ==================== + // adaptorData = abi.encode(uint256 pid, ERC20 lpToken, ICurvePool pool) + // Where: + // - pid is the pool id of the convex pool + // - lpToken is the lp token concerned by the pool + // - ICurvePool is the curve pool where the lp token was minted + //==================================================================== + + //============================================ Global Functions =========================================== + /** + * @dev Identifier unique to this adaptor for a shared registry. + * Normally the identifier would just be the address of this contract, but this + * Identifier is needed during Cellar Delegate Call Operations, so getting the address + * of the adaptor is more difficult. + */ + function identifier() public pure override returns (bytes32) { + return keccak256(abi.encode("Convex Adaptor V 0.0")); + } + + /** + * @notice The Booster contract on Ethereum Mainnet where all deposits happen in Convex + */ + function booster() internal pure returns (IBooster) { + return IBooster(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + } + + //============================================ Implement Base Functions =========================================== + + /** + * @notice User deposits are NOT allowed into this position. + */ + function deposit( + uint256, + bytes memory, + bytes memory + ) public pure override { + revert BaseAdaptor__UserDepositsNotAllowed(); + } + + /** + * @notice User withdraws are NOT allowed from this position. + */ + function withdraw( + uint256, + address, + bytes memory, + bytes memory + ) public pure override { + revert BaseAdaptor__UserWithdrawsNotAllowed(); + } + + /** + * @notice User withdraws are not allowed so this position must return 0 for withdrawableFrom. + */ + function withdrawableFrom(bytes memory, bytes memory) public pure override returns (uint256) { + return 0; + } + + /** + * @notice Calculates this positions LP tokens underlying worth in terms of `token0`. + * @dev Takes into account Cellar LP balance and also staked LP balance + * @dev The unit is the token0 of the curve pool where the LP was minted. See `assetOf()` + */ + function balanceOf(bytes memory adaptorData) public view override returns (uint256) { + (uint256 pid, ERC20 lpToken, ICurvePool pool) = abi.decode(adaptorData, (uint256, ERC20, ICurvePool)); + + // get reward pool where the LP are staked + (, , , address rewardPool, , ) = (booster()).poolInfo(pid); + uint256 stakedLpBalance = IRewardPool(rewardPool).balanceOf(msg.sender); + + // get amount of LP owned + uint256 lpBalance = lpToken.balanceOf(msg.sender); + + // calculate lp owned value + uint256 lpValue; + if (lpBalance != 0) { + lpValue = pool.calc_withdraw_one_coin(lpBalance, 0); + } + + // calculate stakedLp Value + if (stakedLpBalance == 0) return lpValue; + uint256 stakedValue = pool.calc_withdraw_one_coin(stakedLpBalance, 0); + + return stakedValue + lpValue; + } + + /** + * @notice Returns `coins(0)` + */ + function assetOf(bytes memory adaptorData) public view override returns (ERC20) { + (, , ICurvePool pool) = abi.decode(adaptorData, (uint256, ERC20, ICurvePool)); + return ERC20(pool.coins(0)); + } + + //============================================ Strategist Functions =========================================== + + /** + @notice Attempted to deposit into convex but failed + */ + error ConvexAdaptor_DepositFailed(); + + /** + * @notice Allows strategist to open a Convex position. + * @param pid convex pool id + * @param amount of LP to stake + * @param lpToken the corresponding LP token + */ + function openPosition( + uint256 pid, + uint256 amount, + ERC20 lpToken + ) public { + _addToPosition(pid, amount, lpToken); + } + + /** + * @notice Allows strategist to add liquidity to a Convex position. + * @param pid convex pool id + * @param amount of LP to stake + * @param lpToken the corresponding LP token + */ + function addToPosition( + uint256 pid, + uint256 amount, + ERC20 lpToken + ) public { + _addToPosition(pid, amount, lpToken); + } + + function _addToPosition( + uint256 pid, + uint256 amount, + ERC20 lpToken + ) internal { + lpToken.safeApprove(address(booster()), amount); + + // always assume we are staking + if (!(booster()).deposit(pid, amount, true)) { + revert ConvexAdaptor_DepositFailed(); + } + } + + /** + * @notice Strategist attempted to remove all of a positions liquidity using `takeFromPosition`, + * but they need to use `closePosition`. + */ + error ConvexAdaptor__CallClosePosition(); + + /** + * @notice Allows strategist to remove liquidity from a position + * @param pid convex pool id + * @param amount of LP to stake + * @param claim true if rewards should be claimed when withdrawing + */ + function takeFromPosition( + uint256 pid, + uint256 amount, + bool claim + ) public { + (, , , address rewardPool, , ) = (booster()).poolInfo(pid); + + if (IRewardPool(rewardPool).balanceOf(msg.sender) == amount) revert ConvexAdaptor__CallClosePosition(); + + IRewardPool(rewardPool).withdrawAndUnwrap(amount, claim); + } + + /** + * @notice Allows strategist to close a position + * @param pid convex pool id + * @param claim true if rewards should be claimed when withdrawing + */ + function closePosition(uint256 pid, bool claim) public { + (, , , address rewardPool, , ) = (booster()).poolInfo(pid); + + IRewardPool(rewardPool).withdrawAllAndUnwrap(claim); + } + + /** + * @notice Attempted to take from convex position but failed + */ + error ConvexAdaptor_CouldNotClaimRewards(); + + /** + * @notice Allows strategist to claim rewards and extras from convex + * @param pid convex pool id + * TODO: distribute these rewards to timelockERC20 adaptor in feat/timelockERC20 branch (out of scope for the hackathon) + */ + function claimRewards(uint256 pid) public { + (, , , address rewardPool, , ) = (booster()).poolInfo(pid); + + if (!IRewardPool(rewardPool).getReward()) { + revert ConvexAdaptor_CouldNotClaimRewards(); + } + } +} diff --git a/src/modules/adaptors/Curve/Curve2PoolAdaptor.sol b/src/modules/adaptors/Curve/Curve2PoolAdaptor.sol new file mode 100644 index 00000000..57970dc4 --- /dev/null +++ b/src/modules/adaptors/Curve/Curve2PoolAdaptor.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { BaseAdaptor, ERC20, SafeERC20, Cellar, PriceRouter, Registry, Math } from "src/modules/adaptors/BaseAdaptor.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IBooster } from "src/interfaces/external/IBooster.sol"; +import { ICurvePool } from "src/interfaces/external/ICurve2Pool.sol"; + +/** + * @title Curve 2Pool Adaptor + * @notice Allows Cellars to interact with 2Pool Curve Positions. + * @author cookiesanddudes, federava + */ +contract Curve2PoolAdaptor is BaseAdaptor { + using SafeERC20 for ERC20; + using Math for uint256; + using SafeCast for uint256; + using Address for address; + + //==================== Adaptor Data Specification ==================== + // adaptorData = abi.encode(ICurvePool curvePool, address lpToken) + // Where: + // - curvePool is the pool concerned by the position + // - lpToken is the lp generated by the pool (in old curve contracts, + // it is not available as a public method in the pool) + // + // Uses token0. In the case of curve3Pool it is DAI. + //==================================================================== + + //============================================ Global Functions =========================================== + /** + * @dev Identifier unique to this adaptor for a shared registry. + * Normally the identifier would just be the address of this contract, but this + * Identifier is needed during Cellar Delegate Call Operations, so getting the address + * of the adaptor is more difficult. + */ + function identifier() public pure override returns (bytes32) { + return keccak256(abi.encode("Curve 2Pool Adaptor V 0.0")); + } + + //============================================ Implement Base Functions =========================================== + /** + * @notice User deposits are NOT allowed into this position. + */ + function deposit( + uint256, + bytes memory, + bytes memory + ) public pure override { + revert BaseAdaptor__UserDepositsNotAllowed(); + } + + /** + * @notice User withdraws are NOT allowed from this position. + */ + function withdraw( + uint256, + address, + bytes memory, + bytes memory + ) public pure override { + revert BaseAdaptor__UserWithdrawsNotAllowed(); + } + + /** + * @notice User withdraws are not allowed so this position must return 0 for withdrawableFrom. + */ + function withdrawableFrom(bytes memory, bytes memory) public pure override returns (uint256) { + return 0; + } + + /** + * @notice Calculates this positions LP tokens underlying worth in terms of `token0`. + * @notice Curve pools provide a calculation where the amount returned considers the swapping of token1 and token2 + * for token0 considering fees. + */ + function balanceOf(bytes memory adaptorData) public view override returns (uint256) { + (ICurvePool pool, ERC20 lpToken) = abi.decode(adaptorData, (ICurvePool, ERC20)); + + // Calculates amount of token0 is recieved when burning all LP tokens. + uint256 lpBalance = lpToken.balanceOf(msg.sender); + + // return 0 if lp balance is null + if (lpBalance == 0) return 0; + + return pool.calc_withdraw_one_coin(lpBalance, 0); + } + + /** + * @notice Returns `coins(0)` or token0 + */ + function assetOf(bytes memory adaptorData) public view override returns (ERC20) { + (ICurvePool pool, ) = abi.decode(adaptorData, (ICurvePool, ERC20)); + return ERC20(pool.coins(0)); + } + + //============================================ Strategist Functions =========================================== + /** + * @notice Allows strategist to open up arbritray Curve positions. + * @notice Allows to send any combination of token0, token1 and token2 which the pool will + * balance on deposit. + * @notice If minted lp tokens is less than minimumMintAmount function will revert. + * @param amounts token0, token1, and token2 amounts to be deposited. + * @param minimumMintAmount minting at least this amount of lp tokens. + * @param pool specifies the interface of the pool + */ + function openPosition( + uint256[2] memory amounts, + uint256 minimumMintAmount, + ICurvePool pool + ) public { + for (uint256 i; i < amounts.length; ) { + if (amounts[i] != 0) { + ERC20 token = ERC20(pool.coins(i)); + token.safeApprove(address(pool), amounts[i]); + } + // overflow is unrealistic + unchecked { + ++i; + } + } + + pool.add_liquidity(amounts, minimumMintAmount, address(this)); + } + + error Curve2PoolAdaptor__CallClosePosition(); + + /** + * @notice Strategist attempted to remove all of a positions liquidity using `takeFromPosition`, + * but they need to use `closePosition`. + * @notice If receiving amount of token0 is less than minimumMintAmount function will revert. + * @param amount lp token amount to be burned. + * @param minimumAmount receiving at least this amount of token0. + * @param pool specifies the interface of the pool + * @param lpToken specifies the interface of the lp token + */ + function takeFromPosition( + uint256 amount, + uint256 minimumAmount, + ICurvePool pool, + ERC20 lpToken + ) public { + // we should not be closing a position here + if (lpToken.balanceOf(msg.sender) == amount) revert Curve2PoolAdaptor__CallClosePosition(); + _takeFromPosition(amount, minimumAmount, pool); + } + + /** + * @notice Executes the removal of liquidity in one coin: token0. + * @notice If receiving amount of token0 is less than minimumMintAmount function will revert. + * @param amount lp token amount to be burned. + * @param minimumAmount receiving at least this amount of token0. + * @param pool specifies the interface of the pool + */ + function _takeFromPosition( + uint256 amount, + uint256 minimumAmount, + ICurvePool pool + ) internal { + pool.remove_liquidity_one_coin(amount, 0, minimumAmount, address(this)); + } + + error Curve2PoolAdaptor__PositionClosed(); + + /** + * @notice Strategist use `closePosition` to remove all of a positions liquidity. + * @notice If receiving amount of token0 is less than minimumMintAmount function will revert. + * @param minimumAmount receiving at least this amount of token0. + * @param pool specifies the interface of the pool + * @param lpToken specifies the interface of the lp token + */ + function closePosition( + uint256 minimumAmount, + ICurvePool pool, + ERC20 lpToken + ) public { + uint256 amountToWithdraw = lpToken.balanceOf(address(this)); + + if (amountToWithdraw == 0) revert Curve2PoolAdaptor__PositionClosed(); + _takeFromPosition(amountToWithdraw, minimumAmount, pool); + } + + /** + * @notice Allows strategist to add liquidity to a Curve position. + * @notice Allows to send any combination of token0, token1 and token2 which the pool will + * balance on deposit. + * @notice If minted lp tokens is less than minimumMintAmount function will revert. + * @param amounts token0, token1, and token2 amounts to be deposited. + * @param minimumMintAmount minting at least this amount of lp tokens. + * @param pool specifies the interface of the pool + */ + function addToPosition( + uint256[2] memory amounts, + uint256 minimumMintAmount, + ICurvePool pool + ) public { + for (uint256 i; i < amounts.length; ) { + if (amounts[i] != 0) { + ERC20 token = ERC20(pool.coins(i)); + token.safeApprove(address(pool), amounts[i]); + } + + // overflow is unrealistic + unchecked { + ++i; + } + } + + pool.add_liquidity(amounts, minimumMintAmount, address(this)); + } +} diff --git a/src/modules/adaptors/Curve/Curve3PoolAdaptor.sol b/src/modules/adaptors/Curve/Curve3PoolAdaptor.sol new file mode 100644 index 00000000..491d5782 --- /dev/null +++ b/src/modules/adaptors/Curve/Curve3PoolAdaptor.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { BaseAdaptor, ERC20, SafeERC20, Cellar, PriceRouter, Registry, Math } from "src/modules/adaptors/BaseAdaptor.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IBooster } from "src/interfaces/external/IBooster.sol"; +import { ICurvePool } from "src/interfaces/external/ICurvePool.sol"; + +/** + * @title Curve 3Pool Adaptor + * @notice Allows Cellars to interact with 3Pool Curve Positions. + * @author cookiesanddudes, federava + */ +contract Curve3PoolAdaptor is BaseAdaptor { + using SafeERC20 for ERC20; + using Math for uint256; + using SafeCast for uint256; + using Address for address; + + //==================== Adaptor Data Specification ==================== + // adaptorData = abi.encode(ICurvePool curvePool, address lpToken) + // Where: + // - curvePool is the pool concerned by the position + // - lpToken is the lp generated by the pool (in old curve contracts, + // it is not available as a public method in the pool) + // + // Uses token0. In the case of curve3Pool it is DAI. + //==================================================================== + + //============================================ Global Functions =========================================== + /** + * @dev Identifier unique to this adaptor for a shared registry. + * Normally the identifier would just be the address of this contract, but this + * Identifier is needed during Cellar Delegate Call Operations, so getting the address + * of the adaptor is more difficult. + */ + function identifier() public pure override returns (bytes32) { + return keccak256(abi.encode("Curve 3Pool Adaptor V 0.0")); + } + + //============================================ Implement Base Functions =========================================== + /** + * @notice User deposits are NOT allowed into this position. + */ + function deposit( + uint256, + bytes memory, + bytes memory + ) public pure override { + revert BaseAdaptor__UserDepositsNotAllowed(); + } + + /** + * @notice User withdraws are NOT allowed from this position. + */ + function withdraw( + uint256, + address, + bytes memory, + bytes memory + ) public pure override { + revert BaseAdaptor__UserWithdrawsNotAllowed(); + } + + /** + * @notice User withdraws are not allowed so this position must return 0 for withdrawableFrom. + */ + function withdrawableFrom(bytes memory, bytes memory) public pure override returns (uint256) { + return 0; + } + + /** + * @notice Calculates this positions LP tokens underlying worth in terms of `token0`. + * @notice Curve pools provide a calculation where the amount returned considers the swapping of token1 and token2 + * for token0 considering fees. + */ + function balanceOf(bytes memory adaptorData) public view override returns (uint256) { + (ICurvePool pool, ERC20 lpToken) = abi.decode(adaptorData, (ICurvePool, ERC20)); + + // Calculates amount of token0 is recieved when burning all LP tokens. + uint256 lpBalance = lpToken.balanceOf(msg.sender); + + // return 0 if lp balance is null + if (lpBalance == 0) return 0; + + return pool.calc_withdraw_one_coin(lpBalance, 0); + } + + /** + * @notice Returns `coins(0)` or token0 + */ + function assetOf(bytes memory adaptorData) public view override returns (ERC20) { + (ICurvePool pool, ) = abi.decode(adaptorData, (ICurvePool, ERC20)); + return ERC20(pool.coins(0)); + } + + //============================================ Strategist Functions =========================================== + /** + * @notice Allows strategist to open up arbritray Curve positions. + * @notice Allows to send any combination of token0, token1 and token2 which the pool will + * balance on deposit. + * @notice If minted lp tokens is less than minimumMintAmount function will revert. + * @param amounts token0, token1, and token2 amounts to be deposited. + * @param minimumMintAmount minting at least this amount of lp tokens. + * @param pool specifies the interface of the pool + */ + function openPosition( + uint256[3] memory amounts, + uint256 minimumMintAmount, + ICurvePool pool + ) public { + for (uint256 i; i < amounts.length; ) { + if (amounts[i] != 0) { + ERC20 token = ERC20(pool.coins(i)); + token.safeApprove(address(pool), amounts[i]); + } + // overflow is unrealistic + unchecked { + ++i; + } + } + + pool.add_liquidity(amounts, minimumMintAmount); + } + + /** + * @notice Strategist attempted to remove all of a positions liquidity using `takeFromPosition`, + * but they need to use `closePosition`. + */ + error Curve3PoolAdaptor__CallClosePosition(); + + /** + * @notice If receiving amount of token0 is less than minimumMintAmount function will revert. + * @param amount lp token amount to be burned. + * @param minimumAmount receiving at least this amount of token0. + * @param pool specifies the interface of the pool + * @param lpToken specifies the interface of the lp token + */ + function takeFromPosition( + uint256 amount, + uint256 minimumAmount, + ICurvePool pool, + ERC20 lpToken + ) public { + // we should not be closing a position here + if (lpToken.balanceOf(msg.sender) == amount) revert Curve3PoolAdaptor__CallClosePosition(); + _takeFromPosition(amount, minimumAmount, pool); + } + + /** + * @notice Executes the removal of liquidity in one coin: token0. + * @notice If receiving amount of token0 is less than minimumMintAmount function will revert. + * @param amount lp token amount to be burned. + * @param minimumAmount receiving at least this amount of token0. + * @param pool specifies the interface of the pool + */ + function _takeFromPosition( + uint256 amount, + uint256 minimumAmount, + ICurvePool pool + ) internal { + pool.remove_liquidity_one_coin(amount, 0, minimumAmount); + } + + /** + * @notice Strategist attempted to withdraw an already closed position + */ + error Curve3PoolAdaptor__PositionClosed(); + + /** + * @notice Strategist use `closePosition` to remove all of a positions liquidity. + * @notice If receiving amount of token0 is less than minimumMintAmount function will revert. + * @param minimumAmount receiving at least this amount of token0. + * @param pool specifies the interface of the pool + * @param lpToken specifies the interface of the lp token + */ + function closePosition( + uint256 minimumAmount, + ICurvePool pool, + ERC20 lpToken + ) public { + uint256 amountToWithdraw = lpToken.balanceOf(address(this)); + + if (amountToWithdraw == 0) revert Curve3PoolAdaptor__PositionClosed(); + _takeFromPosition(amountToWithdraw, minimumAmount, pool); + } + + /** + * @notice Allows strategist to add liquidity to a Curve position. + * @notice Allows to send any combination of token0, token1 and token2 which the pool will + * balance on deposit. + * @notice If minted lp tokens is less than minimumMintAmount function will revert. + * @param amounts token0, token1, and token2 amounts to be deposited. + * @param minimumMintAmount minting at least this amount of lp tokens. + * @param pool specifies the interface of the pool + */ + function addToPosition( + uint256[3] memory amounts, + uint256 minimumMintAmount, + ICurvePool pool + ) public { + for (uint256 i; i < amounts.length; ) { + if (amounts[i] != 0) { + ERC20 token = ERC20(pool.coins(i)); + token.safeApprove(address(pool), amounts[i]); + } + + // overflow is unrealistic + unchecked { + ++i; + } + } + + pool.add_liquidity(amounts, minimumMintAmount); + } +} diff --git a/test/testAdaptors/Convex.t.sol b/test/testAdaptors/Convex.t.sol new file mode 100644 index 00000000..c3df3ce7 --- /dev/null +++ b/test/testAdaptors/Convex.t.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MockCellar, Cellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; +import { ConvexAdaptor } from "src/modules/adaptors/Convex/ConvexAdaptor.sol"; +import { Curve3PoolAdaptor } from "src/modules/adaptors/Curve/Curve3PoolAdaptor.sol"; + +import { BaseAdaptor } from "src/modules/adaptors/BaseAdaptor.sol"; +import { IBooster } from "src/interfaces/external/IBooster.sol"; +import { IRewardPool } from "src/interfaces/external/IRewardPool.sol"; + +import { ICurvePool } from "src/interfaces/external/ICurvePool.sol"; + +import { Registry } from "src/Registry.sol"; +import { PriceRouter } from "src/modules/price-router/PriceRouter.sol"; +import { Denominations } from "@chainlink/contracts/src/v0.8/Denominations.sol"; +import { ERC20Adaptor } from "src/modules/adaptors/ERC20Adaptor.sol"; +import { SwapRouter, IUniswapV2Router, IUniswapV3Router } from "src/modules/swap-router/SwapRouter.sol"; + +import { Test, stdStorage, console, StdStorage, stdError } from "@forge-std/Test.sol"; +import { Math } from "src/utils/Math.sol"; + +contract CellarConvexTest is Test { + using SafeERC20 for ERC20; + using Math for uint256; + using stdStorage for StdStorage; + + ConvexAdaptor private convexAdaptor; + Curve3PoolAdaptor private curve3PoolAdaptor; + + ERC20Adaptor private erc20Adaptor; + MockCellar private cellar; + PriceRouter private priceRouter; + Registry private registry; + SwapRouter private swapRouter; + + address private immutable strategist = vm.addr(0xBEEF); + + ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ERC20 private CVX = ERC20(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + ERC20 private CRV = ERC20(0xD533a949740bb3306d119CC777fa900bA034cd52); + + ERC20 private LP3CRV = ERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + ERC20 private USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + ERC20 private USDT = ERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); + + IBooster private booster = IBooster(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); + ICurvePool curve3Pool = ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + + uint256 private constant PID_3CRV = 9; + + IRewardPool rewardPool; + + uint32 private lp3crvPosition; + uint32 private daiPosition; + uint32 private curvePosition; + + function setUp() external { + convexAdaptor = new ConvexAdaptor(); + curve3PoolAdaptor = new Curve3PoolAdaptor(); + erc20Adaptor = new ERC20Adaptor(); + priceRouter = new PriceRouter(); + + registry = new Registry(address(this), address(swapRouter), address(priceRouter)); + + priceRouter.addAsset(DAI, 0, 0, false, 0); + priceRouter.addAsset(USDT, 0, 0, false, 0); + priceRouter.addAsset(USDC, 0, 0, false, 0); + + // Setup Cellar: + // Cellar positions array. + uint32[] memory positions = new uint32[](3); + + // Add adaptors and positions to the registry. + registry.trustAdaptor(address(erc20Adaptor), 0, 0); + registry.trustAdaptor(address(convexAdaptor), 0, 0); + registry.trustAdaptor(address(curve3PoolAdaptor), 0, 0); + + daiPosition = registry.trustPosition(address(erc20Adaptor), false, abi.encode(DAI), 0, 0); + lp3crvPosition = registry.trustPosition( + address(convexAdaptor), + false, + abi.encode(PID_3CRV, address(DAI), curve3Pool), + 0, + 0 + ); + curvePosition = registry.trustPosition( + address(curve3PoolAdaptor), + false, + abi.encode( + ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7), + address(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490) + ), + 0, + 0 + ); + + positions[0] = daiPosition; + positions[1] = lp3crvPosition; + positions[2] = curvePosition; + + bytes[] memory positionConfigs = new bytes[](3); + + cellar = new MockCellar(registry, DAI, positions, positionConfigs, "Convex Cellar", "CONVEX-CLR", strategist); + + vm.label(address(curve3Pool), "curve pool"); + vm.label(address(convexAdaptor), "convexAdaptor"); + vm.label(address(this), "tester"); + vm.label(address(cellar), "cellar"); + vm.label(strategist, "strategist"); + vm.label(address(DAI), "dai token"); + vm.label(address(USDC), "usdc token"); + vm.label(address(USDT), "usdt token"); + + cellar.setupAdaptor(address(convexAdaptor)); + cellar.setupAdaptor(address(curve3PoolAdaptor)); + + DAI.safeApprove(address(cellar), type(uint256).max); + USDC.safeApprove(address(cellar), type(uint128).max); + USDT.safeApprove(address(cellar), type(uint128).max); + + // get initialize reward pool + (, , , address rp, , ) = booster.poolInfo(PID_3CRV); + rewardPool = IRewardPool(rp); + + // Manipulate test contracts storage so that minimum shareLockPeriod is zero blocks. + stdstore.target(address(cellar)).sig(cellar.shareLockPeriod.selector).checked_write(uint256(0)); + } + + // opens position in curve and deposits LP into convex + function testOpenPosition() external { + // first, mint dai into the cellar + deal(address(DAI), address(cellar), 100_000e18); + + // then, deposit dai into curve through Curve adaptor + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenCurvePosition(10e18, 0, 0, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + // convexAdaptor.balanceOf(abi.encode(PID_3CRV, LP3CRV, curve3Pool)); + + // last, open position on convex using the freshly minted LP + // Use `callOnAdaptor` to deposit LP into convex pool + data = new Cellar.AdaptorCall[](1); + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(PID_3CRV, 5e18, LP3CRV); + + data[0] = Cellar.AdaptorCall({ adaptor: address(convexAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + // assert we have deposited 5e18 tokens into convex reward + assertEq(rewardPool.balanceOf(address(cellar)), 5e18); + vm.prank(address(cellar)); + assertGe(convexAdaptor.balanceOf(abi.encode(PID_3CRV, LP3CRV, curve3Pool)), 0); + } + + // opens position in curve and deposits LP into convex + function testOpeningAndClosingPosition() external { + // first, mint dai into the cellar + deal(address(DAI), address(cellar), 100_000e18); + + // then, deposit dai into curve through Curve adaptor + + // Use `callOnAdaptor` to deposit token into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenCurvePosition(10e18, 0, 0, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 initialLPBalance = LP3CRV.balanceOf(address(cellar)); + + // convexAdaptor.balanceOf(abi.encode(PID_3CRV, LP3CRV, curve3Pool)); + + // last, open position on convex using the freshly minted LP + // Use `callOnAdaptor` to deposit LP into convex pool + data = new Cellar.AdaptorCall[](1); + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(PID_3CRV, 5e18, LP3CRV); + + data[0] = Cellar.AdaptorCall({ adaptor: address(convexAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + // assert we have deposited 5e18 tokens into convex reward + assertEq(rewardPool.balanceOf(address(cellar)), 5e18); + assertLe(LP3CRV.balanceOf(address(cellar)), initialLPBalance); + + // now, close the position + data = new Cellar.AdaptorCall[](1); + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToClosePosition(PID_3CRV, true); + + data[0] = Cellar.AdaptorCall({ adaptor: address(convexAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 finalLPBalance = LP3CRV.balanceOf(address(cellar)); + + // check we recovered all of our initial LP + assertEq(initialLPBalance, finalLPBalance); + assertEq(rewardPool.balanceOf(address(cellar)), 0); + } + + // opens position in curve and deposits LP into convex + function testAddAndTakeFromPositionAndClaimRewards() external { + // first, mint dai into the cellar + deal(address(DAI), address(cellar), 100_000e18); + + // then, deposit dai into curve through Curve adaptor + + // Use `callOnAdaptor` to deposit token into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenCurvePosition(10e18, 0, 0, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + // last, open position on convex using the freshly minted LP + // Use `callOnAdaptor` to deposit LP into convex pool + data = new Cellar.AdaptorCall[](1); + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(PID_3CRV, 5e18, LP3CRV); + + data[0] = Cellar.AdaptorCall({ adaptor: address(convexAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + // assert we have deposited 5e18 tokens into convex reward + assertEq(rewardPool.balanceOf(address(cellar)), 5e18); + uint256 intermediateLPBalance = LP3CRV.balanceOf(address(cellar)); + + // now, reduce the position + data = new Cellar.AdaptorCall[](1); + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToTakeFromPosition(PID_3CRV, 1e18, true); + + data[0] = Cellar.AdaptorCall({ adaptor: address(convexAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 finalLPBalance = LP3CRV.balanceOf(address(cellar)); + + // check we recovered the 1e18 LP + assertEq(intermediateLPBalance, finalLPBalance - 1e18); + + // time travel to accumulate rewards + vm.warp(block.timestamp + 2000); + vm.roll(block.number + 5); + + // Check that we are poor before claiming + assertEq(CRV.balanceOf(address(cellar)), 0); + assertEq(CVX.balanceOf(address(cellar)), 0); + + // now, add to the position + data = new Cellar.AdaptorCall[](1); + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToAddToPosition(PID_3CRV, 2e18, LP3CRV); + data[0] = Cellar.AdaptorCall({ adaptor: address(convexAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + // now, claim rewards + data = new Cellar.AdaptorCall[](1); + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToClaimRewards(PID_3CRV); + data[0] = Cellar.AdaptorCall({ adaptor: address(convexAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + // Check that we got some juicy rewards + assertGe(CRV.balanceOf(address(cellar)), 0); + assertGe(CVX.balanceOf(address(cellar)), 0); + } + + function _createBytesDataToClaimRewards(uint256 pid) internal pure returns (bytes memory) { + return abi.encodeWithSelector(ConvexAdaptor.claimRewards.selector, pid); + } + + function _createBytesDataToOpenPosition( + uint256 pid, + uint256 amount, + ERC20 lpToken + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(ConvexAdaptor.openPosition.selector, pid, amount, lpToken); + } + + function _createBytesDataToAddToPosition( + uint256 pid, + uint256 amount, + ERC20 lpToken + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(ConvexAdaptor.addToPosition.selector, pid, amount, lpToken); + } + + function _createBytesDataToClosePosition(uint256 pid, bool claim) internal pure returns (bytes memory) { + return abi.encodeWithSelector(ConvexAdaptor.closePosition.selector, pid, claim); + } + + function _createBytesDataToTakeFromPosition( + uint256 pid, + uint256 amount, + bool claim + ) internal pure returns (bytes memory) { + return abi.encodeWithSelector(ConvexAdaptor.takeFromPosition.selector, pid, amount, claim); + } + + function _createBytesDataToOpenCurvePosition( + uint256 amount0, + uint256 amount1, + uint256 amount2, + uint256 minimumMintAmount + ) internal view returns (bytes memory) { + return + abi.encodeWithSelector( + Curve3PoolAdaptor.openPosition.selector, + [amount0, amount1, amount2], + minimumMintAmount, + curve3Pool + ); + } +} diff --git a/test/testAdaptors/Curve2Pool.t.sol b/test/testAdaptors/Curve2Pool.t.sol new file mode 100644 index 00000000..732b13f1 --- /dev/null +++ b/test/testAdaptors/Curve2Pool.t.sol @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MockCellar, Cellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; +import { Curve2PoolAdaptor } from "src/modules/adaptors/Curve/Curve2PoolAdaptor.sol"; +import { BaseAdaptor } from "src/modules/adaptors/BaseAdaptor.sol"; +import { ICurvePool } from "src/interfaces/external/ICurve2Pool.sol"; +import { Registry } from "src/Registry.sol"; +import { PriceRouter } from "src/modules/price-router/PriceRouter.sol"; +import { Denominations } from "@chainlink/contracts/src/v0.8/Denominations.sol"; +import { ERC20Adaptor } from "src/modules/adaptors/ERC20Adaptor.sol"; +import { SwapRouter, IUniswapV2Router, IUniswapV3Router } from "src/modules/swap-router/SwapRouter.sol"; + +import { Test, stdStorage, console, StdStorage, stdError } from "@forge-std/Test.sol"; +import { Math } from "src/utils/Math.sol"; + +contract Curve2PoolTest is Test { + using SafeERC20 for ERC20; + using Math for uint256; + using stdStorage for StdStorage; + + Curve2PoolAdaptor private curve2PoolAdaptor; + ERC20Adaptor private erc20Adaptor; + MockCellar private cellar; + PriceRouter private priceRouter; + Registry private registry; + SwapRouter private swapRouter; + + address private immutable strategist = vm.addr(0xBEEF); + + ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ERC20 private CVX = ERC20(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + ERC20 private FRAX = ERC20(0x853d955aCEf822Db058eb8505911ED77F175b99e); + ERC20 private POOL3 = ERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + + ERC20 private USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + ERC20 private USDT = ERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); + + ERC20 private LP2CRV = ERC20(0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B); + ICurvePool curve2Pool = ICurvePool(0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B); + + uint32 private lp2crvPosition; + uint32 private fraxPosition; + + function setUp() external { + curve2PoolAdaptor = new Curve2PoolAdaptor(); + erc20Adaptor = new ERC20Adaptor(); + priceRouter = new PriceRouter(); + + registry = new Registry(address(this), address(swapRouter), address(priceRouter)); + + priceRouter.addAsset(FRAX, 0, 0, false, 0); + priceRouter.addAsset(DAI, 0, 0, false, 0); + priceRouter.addAsset(USDC, 0, 0, false, 0); + priceRouter.addAsset(USDT, 0, 0, false, 0); + + // Setup Cellar: + // Cellar positions array. + uint32[] memory positions = new uint32[](2); + + // Add adaptors and positions to the registry. + registry.trustAdaptor(address(curve2PoolAdaptor), 0, 0); + registry.trustAdaptor(address(erc20Adaptor), 0, 0); + + fraxPosition = registry.trustPosition(address(erc20Adaptor), false, abi.encode(FRAX), 0, 0); + lp2crvPosition = registry.trustPosition( + address(curve2PoolAdaptor), + false, + abi.encode( + ICurvePool(0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B), + address(0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B) + ), + 0, + 0 + ); + + positions[0] = fraxPosition; + positions[1] = lp2crvPosition; + + bytes[] memory positionConfigs = new bytes[](2); + + cellar = new MockCellar(registry, FRAX, positions, positionConfigs, "Convex Cellar", "CONVEX-CLR", strategist); + + vm.label(address(curve2Pool), "curve pool"); + vm.label(address(curve2PoolAdaptor), "curve2PoolAdaptor"); + + vm.label(address(this), "tester"); + vm.label(address(cellar), "cellar"); + vm.label(strategist, "strategist"); + vm.label(address(FRAX), "frax token"); + vm.label(address(POOL3), "3pool token"); + vm.label(address(DAI), "dai token"); + vm.label(address(USDC), "usdc token"); + vm.label(address(USDT), "usdt token"); + + cellar.setupAdaptor(address(curve2PoolAdaptor)); + + FRAX.safeApprove(address(cellar), type(uint256).max); + POOL3.safeApprove(address(cellar), type(uint256).max); + + DAI.safeApprove(address(cellar), type(uint256).max); + USDC.safeApprove(address(cellar), type(uint256).max); + USDT.safeApprove(address(cellar), type(uint128).max); + + // Manipulate test contracts storage so that minimum shareLockPeriod is zero blocks. + stdstore.target(address(cellar)).sig(cellar.shareLockPeriod.selector).checked_write(uint256(0)); + } + + // ========================================== POSITION MANAGEMENT TEST ========================================== + function testOpenPosition() external { + deal(address(FRAX), address(cellar), 100_000e18); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 0, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve2PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP2CRV.balanceOf(address(cellar)); + + // Assert balanceOf is bigger than 0.9 + vm.prank(address(cellar)); + assertGe(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 1e18 - 1e17); + + // Assert LP is bigger than 0 + assertGe(lpBalance, 0); + } + + function testOpenFRAXPosition() external { + deal(address(FRAX), address(cellar), 100_000e18); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 0, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve2PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP2CRV.balanceOf(address(cellar)); + + // Assert balanceOf is bigger than 0.9 + vm.prank(address(cellar)); + assertGe(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 1e18 - 1e17); + + // Assert LP is bigger than 0 + assertGe(lpBalance, 0); + } + + function testOpenDAIUSDCUSDTPosition() external { + deal(address(FRAX), address(cellar), 100_000e18); + deal(address(POOL3), address(cellar), 100_000e10); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 1e10, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve2PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP2CRV.balanceOf(address(cellar)); + + // Assert balanceOf is bigger than 0 + vm.prank(address(cellar)); + assertGe(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 0); + + // Assert LP is bigger than 0 + assertGe(lpBalance, 0); + } + + function testOpeningAndClosingPosition() external { + deal(address(FRAX), address(cellar), 100_000e18); + deal(address(POOL3), address(cellar), 100_000e10); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 1e10, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve2PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP2CRV.balanceOf(address(cellar)); + uint256 fraxBalanceBefore = FRAX.balanceOf(address(cellar)); + + // assert balanceOf is bigger than 0 + vm.prank(address(cellar)); + assertGe(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 0); + + // assert LP is bigger than 0 + assertGe(lpBalance, 0); + + // Now, close the position + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToClosePosition(0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve2PoolAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + uint256 fraxBalanceAfter = FRAX.balanceOf(address(cellar)); + uint256 lpBalanceAfter = LP2CRV.balanceOf(address(cellar)); + + assertEq(lpBalanceAfter, 0); + + assertGe(fraxBalanceAfter - fraxBalanceBefore, 0); + + // assert adaptor balanceOf is zero as well + vm.prank(address(cellar)); + assertEq(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 0); + } + + function testOpeningAddingAndTakingFromPosition() external { + deal(address(FRAX), address(cellar), 100_000e18); + deal(address(POOL3), address(cellar), 100_000e6); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 1e6, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve2PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP2CRV.balanceOf(address(cellar)); + + // assert balanceOf is bigger than 0 + vm.prank(address(cellar)); + assertGe(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 0); + + // assert LP is bigger than 0 + assertGe(lpBalance, 0); + + // Now, add to the position + adaptorCalls = new bytes[](1); + + adaptorCalls[0] = _createBytesDataToAddToPosition(10e18, 10e6, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve2PoolAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // check that amount has been added + vm.prank(address(cellar)); + assertGe(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 10e18); + + uint256 lpBalanceAfterAdd = LP2CRV.balanceOf(address(cellar)); + + assertGe(lpBalanceAfterAdd, 10e18); + + // Now, remove from the position + adaptorCalls[0] = _createBytesDataToTakeFromPosition(5e18, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve2PoolAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // assert adaptor balanceOf is within range + vm.prank(address(cellar)); + assertGe(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 5e18); + assertLe(curve2PoolAdaptor.balanceOf(abi.encode(curve2Pool, LP2CRV)), 10e18); + } + + function testWithdrawableFromReturnsZero() external { + assertEq( + curve2PoolAdaptor.withdrawableFrom(abi.encode(0), abi.encode(0)), + 0, + "`withdrawableFrom` should return 0." + ); + } + + // ======================================= AUXILIAR FUNCTIONS ====================================== + + function _createBytesDataToOpenPosition( + uint256 amount0, + uint256 amount1, + uint256 minimumMintAmount + ) internal view returns (bytes memory) { + return + abi.encodeWithSelector( + Curve2PoolAdaptor.openPosition.selector, + [amount0, amount1], + minimumMintAmount, + curve2Pool + ); + } + + function _createBytesDataToAddToPosition( + uint256 amount0, + uint256 amount1, + uint256 minimumAmount + ) internal view returns (bytes memory) { + return + abi.encodeWithSelector( + Curve2PoolAdaptor.addToPosition.selector, + [amount0, amount1], + minimumAmount, + curve2Pool + ); + } + + function _createBytesDataToClosePosition(uint256 minimumAmount) internal view returns (bytes memory) { + return abi.encodeWithSelector(Curve2PoolAdaptor.closePosition.selector, minimumAmount, curve2Pool, LP2CRV); + } + + function _createBytesDataToTakeFromPosition(uint256 amount, uint256 minimumAmount) + internal + view + returns (bytes memory) + { + return + abi.encodeWithSelector( + Curve2PoolAdaptor.takeFromPosition.selector, + amount, + minimumAmount, + curve2Pool, + LP2CRV + ); + } +} diff --git a/test/testAdaptors/Curve3Pool.t.sol b/test/testAdaptors/Curve3Pool.t.sol new file mode 100644 index 00000000..7b07147d --- /dev/null +++ b/test/testAdaptors/Curve3Pool.t.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MockCellar, Cellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; +import { Curve3PoolAdaptor } from "src/modules/adaptors/Curve/Curve3PoolAdaptor.sol"; +import { BaseAdaptor } from "src/modules/adaptors/BaseAdaptor.sol"; +import { ICurvePool } from "src/interfaces/external/ICurvePool.sol"; +import { Registry } from "src/Registry.sol"; +import { PriceRouter } from "src/modules/price-router/PriceRouter.sol"; +import { Denominations } from "@chainlink/contracts/src/v0.8/Denominations.sol"; +import { ERC20Adaptor } from "src/modules/adaptors/ERC20Adaptor.sol"; +import { SwapRouter, IUniswapV2Router, IUniswapV3Router } from "src/modules/swap-router/SwapRouter.sol"; + +import { Test, stdStorage, console, StdStorage, stdError } from "@forge-std/Test.sol"; +import { Math } from "src/utils/Math.sol"; + +contract Curve3PoolTest is Test { + using SafeERC20 for ERC20; + using Math for uint256; + using stdStorage for StdStorage; + + Curve3PoolAdaptor private curve3PoolAdaptor; + ERC20Adaptor private erc20Adaptor; + MockCellar private cellar; + PriceRouter private priceRouter; + Registry private registry; + SwapRouter private swapRouter; + + address private immutable strategist = vm.addr(0xBEEF); + + ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ERC20 private CVX = ERC20(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); + + ERC20 private USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + ERC20 private USDT = ERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); + + ERC20 private LP3CRV = ERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490); + ICurvePool curve3Pool = ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7); + + uint32 private lp3crvPosition; + uint32 private daiPosition; + + function setUp() external { + curve3PoolAdaptor = new Curve3PoolAdaptor(); + erc20Adaptor = new ERC20Adaptor(); + priceRouter = new PriceRouter(); + + registry = new Registry(address(this), address(swapRouter), address(priceRouter)); + + priceRouter.addAsset(DAI, 0, 0, false, 0); + priceRouter.addAsset(USDC, 0, 0, false, 0); + priceRouter.addAsset(USDT, 0, 0, false, 0); + + // Setup Cellar: + // Cellar positions array. + uint32[] memory positions = new uint32[](2); + + // Add adaptors and positions to the registry. + registry.trustAdaptor(address(curve3PoolAdaptor), 0, 0); + registry.trustAdaptor(address(erc20Adaptor), 0, 0); + + daiPosition = registry.trustPosition(address(erc20Adaptor), false, abi.encode(DAI), 0, 0); + lp3crvPosition = registry.trustPosition( + address(curve3PoolAdaptor), + false, + abi.encode( + ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7), + address(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490) + ), + 0, + 0 + ); + + positions[0] = daiPosition; + positions[1] = lp3crvPosition; + + bytes[] memory positionConfigs = new bytes[](2); + + cellar = new MockCellar(registry, DAI, positions, positionConfigs, "Convex Cellar", "CONVEX-CLR", strategist); + + vm.label(address(curve3Pool), "curve pool"); + vm.label(address(curve3PoolAdaptor), "curve3PoolAdaptor"); + + vm.label(address(this), "tester"); + vm.label(address(cellar), "cellar"); + vm.label(strategist, "strategist"); + vm.label(address(DAI), "dai token"); + vm.label(address(USDC), "usdc token"); + vm.label(address(USDT), "usdt token"); + + cellar.setupAdaptor(address(curve3PoolAdaptor)); + + DAI.safeApprove(address(cellar), type(uint256).max); + USDC.safeApprove(address(cellar), type(uint128).max); + USDT.safeApprove(address(cellar), type(uint128).max); + + // Manipulate test contracts storage so that minimum shareLockPeriod is zero blocks. + stdstore.target(address(cellar)).sig(cellar.shareLockPeriod.selector).checked_write(uint256(0)); + } + + // ========================================== POSITION MANAGEMENT TEST ========================================== + function testOpenPosition() external { + deal(address(DAI), address(cellar), 100_000e18); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 0, 0, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP3CRV.balanceOf(address(cellar)); + + // Assert balanceOf is bigger than 0.9 + vm.prank(address(cellar)); + assertGe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 1e18 - 1e17); + + // Assert LP is bigger than 0 + assertGe(lpBalance, 0); + } + + function testOpenDAIPosition() external { + deal(address(DAI), address(cellar), 100_000e18); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 0, 0, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP3CRV.balanceOf(address(cellar)); + + // Assert balanceOf is bigger than 0.9 + vm.prank(address(cellar)); + assertGe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 1e18 - 1e17); + + // Assert LP is bigger than 0 + assertGe(lpBalance, 0); + } + + function testOpenDAIUSDCUSDTPosition() external { + deal(address(DAI), address(cellar), 100_000e18); + deal(address(USDC), address(cellar), 100_000e6); + deal(address(USDT), address(cellar), 100_000e6); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 1e6, 1e6, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP3CRV.balanceOf(address(cellar)); + + // Assert balanceOf is bigger than 0 + vm.prank(address(cellar)); + assertGe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 0); + + // Assert LP is bigger than 0 + assertGe(lpBalance, 0); + } + + function testOpeningAndClosingPosition() external { + deal(address(DAI), address(cellar), 100_000e18); + deal(address(USDC), address(cellar), 100_000e6); + deal(address(USDT), address(cellar), 100_000e6); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 1e6, 1e6, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP3CRV.balanceOf(address(cellar)); + uint256 daiBalanceBefore = DAI.balanceOf(address(cellar)); + + // assert balanceOf is bigger than 0 + vm.prank(address(cellar)); + assertGe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 0); + + // assert LP is bigger than 0 + assertGe(lpBalance, 0); + + // Now, close the position + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToClosePosition(0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + uint256 daiBalanceAfter = DAI.balanceOf(address(cellar)); + uint256 lpBalanceAfter = LP3CRV.balanceOf(address(cellar)); + + assertEq(lpBalanceAfter, 0); + + assertGe(daiBalanceAfter - daiBalanceBefore, 0); + + // assert adaptor balanceOf is zero as well + vm.prank(address(cellar)); + assertEq(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 0); + } + + function testOpeningAddingAndTakingFromPosition() external { + deal(address(DAI), address(cellar), 100_000e18); + deal(address(USDC), address(cellar), 100_000e6); + deal(address(USDT), address(cellar), 100_000e6); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 1e6, 1e6, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP3CRV.balanceOf(address(cellar)); + + // assert balanceOf is bigger than 0 + vm.prank(address(cellar)); + assertGe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 0); + + // assert LP is bigger than 0 + assertGe(lpBalance, 0); + + // Now, add to the position + adaptorCalls = new bytes[](1); + + adaptorCalls[0] = _createBytesDataToAddToPosition(10e18, 10e6, 10e6, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // check that amount has been added + vm.prank(address(cellar)); + assertGe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 10e18); + + uint256 lpBalanceAfterAdd = LP3CRV.balanceOf(address(cellar)); + + assertGe(lpBalanceAfterAdd, 10e18); + + // Now, remove from the position + adaptorCalls[0] = _createBytesDataToTakeFromPosition(25e18, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + cellar.callOnAdaptor(data); + + // assert adaptor balanceOf is within range + vm.prank(address(cellar)); + assertGe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 5e18); + assertLe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 10e18); + } + + function testWithdrawableFromReturnsZero() external { + assertEq( + curve3PoolAdaptor.withdrawableFrom(abi.encode(0), abi.encode(0)), + 0, + "`withdrawableFrom` should return 0." + ); + } + + // ========================================== REVERT TEST ========================================== + function testMinimalLPTokenMintUnreached() external { + deal(address(DAI), address(cellar), 100_000e18); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 0, 0, 100e18); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + vm.expectRevert(bytes(abi.encodePacked("Slippage screwed you"))); + cellar.callOnAdaptor(data); + } + + function testMinimalToken0WithdrawMintUnreached() external { + deal(address(DAI), address(cellar), 100_000e18); + deal(address(USDC), address(cellar), 100_000e6); + deal(address(USDT), address(cellar), 100_000e6); + + // Use `callOnAdaptor` to deposit LP into curve pool + Cellar.AdaptorCall[] memory data = new Cellar.AdaptorCall[](1); + bytes[] memory adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToOpenPosition(1e18, 1e6, 1e6, 0); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + + cellar.callOnAdaptor(data); + + uint256 lpBalance = LP3CRV.balanceOf(address(cellar)); + + // assert balanceOf is bigger than 0 + vm.prank(address(cellar)); + assertGe(curve3PoolAdaptor.balanceOf(abi.encode(curve3Pool, LP3CRV)), 0); + + // assert LP is bigger than 0 + assertGe(lpBalance, 0); + + // Now, close the position + adaptorCalls = new bytes[](1); + adaptorCalls[0] = _createBytesDataToClosePosition(100e18); + + data[0] = Cellar.AdaptorCall({ adaptor: address(curve3PoolAdaptor), callData: adaptorCalls }); + vm.expectRevert(bytes(abi.encodePacked("Not enough coins removed"))); + cellar.callOnAdaptor(data); + } + + // ======================================= AUXILIAR FUNCTIONS ====================================== + + function _createBytesDataToOpenPosition( + uint256 amount0, + uint256 amount1, + uint256 amount2, + uint256 minimumMintAmount + ) internal view returns (bytes memory) { + return + abi.encodeWithSelector( + Curve3PoolAdaptor.openPosition.selector, + [amount0, amount1, amount2], + minimumMintAmount, + curve3Pool + ); + } + + function _createBytesDataToAddToPosition( + uint256 amount0, + uint256 amount1, + uint256 amount2, + uint256 minimumAmount + ) internal view returns (bytes memory) { + return + abi.encodeWithSelector( + Curve3PoolAdaptor.addToPosition.selector, + [amount0, amount1, amount2], + minimumAmount, + curve3Pool + ); + } + + function _createBytesDataToClosePosition(uint256 minimumAmount) internal view returns (bytes memory) { + return abi.encodeWithSelector(Curve3PoolAdaptor.closePosition.selector, minimumAmount, curve3Pool, LP3CRV); + } + + function _createBytesDataToTakeFromPosition(uint256 amount, uint256 minimumAmount) + internal + view + returns (bytes memory) + { + return + abi.encodeWithSelector( + Curve3PoolAdaptor.takeFromPosition.selector, + amount, + minimumAmount, + curve3Pool, + LP3CRV + ); + } +}