diff --git a/.env.example b/.env.example index 32b79e8..9224f35 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,12 @@ export SCAN_API_KEY= # Arbitrum Sepolia API key # Forking Arbitrum -FORKING_ARBITRUM_BLOCK_NUMBER=62653925 +FORKING_ARBITRUM_BLOCK_NUMBER=238439389 +FORKING_BLOCK_NUMBER=238639389 +ARB_WETH="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" +ARB_USDC="0xaf88d065e77c8cC2239327C5EDb3A432268e5831" +STARGATEV2_POOL="0xe8CDF27AcD73a434D661C84887215F7598e7d0d3" +STARGATEV2_FARM="0x3da4f8E456AC648c489c286B99Ca37B666be7C4C" ## ------------------ BINANCE ------------------ BINANCE_WALLET_ADDRESS="0xb38e8c17e38363af6ebdcb3dae12e0243582891d" diff --git a/.gitmodules b/.gitmodules index 8698e9b..02cd27d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,7 +4,6 @@ [submodule "lib/tap-yieldbox"] path = lib/tap-yieldbox url = https://github.com/Tapioca-DAO/tap-yieldbox - branch = fix-dependencies [submodule "lib/tap-utils"] path = lib/tap-utils url = https://github.com/Tapioca-DAO/tap-utils diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol new file mode 100644 index 0000000..c0a0888 --- /dev/null +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -0,0 +1,515 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// External +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +// Tapioca +import {IStargateV2MultiRewarder} from "tapioca-strategies/interfaces/stargatev2/IStargateV2MultiRewarder.sol"; +import {IStargateV2Staking} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Staking.sol"; +import {IStargateV2Pool} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Pool.sol"; +import {ITapiocaOracle} from "tap-utils/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tap-utils/interfaces/periph/IZeroXSwapper.sol"; +import {IPearlmit} from "tap-utils/interfaces/periph/IPearlmit.sol"; +import {BaseERC20Strategy} from "yieldbox/strategies/BaseStrategy.sol"; +import {ICluster} from "tap-utils/interfaces/periph/ICluster.sol"; +import {ITOFT} from "tap-utils/interfaces/oft/ITOFT.sol"; +import {IYieldBox} from "yieldbox/interfaces/IYieldBox.sol"; + +/* +████████╗ █████╗ ██████╗ ██╗ ██████╗ ██████╗ █████╗ +╚══██╔══╝██╔══██╗██╔══██╗██║██╔═══██╗██╔════╝██╔══██╗ + ██║ ███████║██████╔╝██║██║ ██║██║ ███████║ + ██║ ██╔══██║██╔═══╝ ██║██║ ██║██║ ██╔══██║ + ██║ ██║ ██║██║ ██║╚██████╔╝╚██████╗██║ ██║ + ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝ +*/ + +contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + using SafeCast for uint256; + + IStargateV2Pool public immutable pool; + IStargateV2Staking public farm; + IERC20 public immutable inputToken; //erc20 of token.erc20() + IERC20 public immutable lpToken; + IZeroXSwapper public swapper; + + ITapiocaOracle public stgInputTokenOracle; + bytes public stgInputTokenOracleData; + + ITapiocaOracle public arbInputTokenOracle; + bytes public arbInputTokenOracleData; + + ITapiocaOracle public zroInputTokenOracle; + bytes public zroInputTokenOracleData; + + ICluster internal cluster; + + /// @dev StargateBase: The rate between local decimals and shared decimals. + uint256 public immutable stargateConvertRate; + + bool public depositPaused; + bool public withdrawPaused; + + enum PauseType { + Deposit, + Withdraw + } + + struct SSwapData { + uint256 minAmountOut; + IZeroXSwapper.SZeroXSwapData data; + } + + address public constant STG = 0x6694340fc020c5E6B96567843da2df01b2CE1eb6; + address public constant ARB = 0x912CE59144191C1204E64559FE8253a0e49E6548; + address public constant ZRO = 0x6985884C4392D348587B19cb9eAAf157F13271cd; + + // ************** // + // *** EVENTS *** // + // ************** // + event AmountDeposited(uint256 amount); + event AmountWithdrawn(address indexed to, uint256 amount); + event ClusterUpdated(ICluster indexed oldCluster, ICluster indexed newCluster); + event SwapperUpdated(IZeroXSwapper indexed oldCluster, IZeroXSwapper indexed newCluster); + event FarmUpdated(address indexed oldAddy, address indexed newAddy); + event Paused(bool prev, bool crt, bool isDepositType); + event ArbOracleUpdated(address indexed oldAddy, address indexed newAddy); + event StgOracleUpdated(address indexed oldAddy, address indexed newAddy); + event ZroOracleUpdated(address indexed oldAddy, address indexed newAddy); + + // ************** // + // *** ERRORS *** // + // ************** // + error TokenNotValid(); + error TransferFailed(); + error DepositPaused(); + error WithdrawPaused(); + error PauserNotAuthorized(); + error EmptyAddress(); + error SwapFailed(); + + struct StargateV2InitData { + IYieldBox yieldBox; + ICluster cluster; + address token; + address pool; + address farm; + ITapiocaOracle stgInputTokenOracle; + bytes stgInputTokenOracleData; + ITapiocaOracle arbInputTokenOracle; + bytes arbInputTokenOracleData; + ITapiocaOracle zroInputTokenOracle; + bytes zroInputTokenOracleData; + IZeroXSwapper swapper; + address owner; + } + constructor( + StargateV2InitData memory initData + ) BaseERC20Strategy(initData.yieldBox, initData.token) { + if (initData.pool == address(0)) revert EmptyAddress(); + if (initData.farm == address(0)) revert EmptyAddress(); + + cluster = initData.cluster; + + pool = IStargateV2Pool(initData.pool); + farm = IStargateV2Staking(initData.farm); + inputToken = IERC20(ITOFT(initData.token).erc20()); + lpToken = IERC20(pool.lpToken()); + + stgInputTokenOracle = initData.stgInputTokenOracle; + stgInputTokenOracleData = initData.stgInputTokenOracleData; + + arbInputTokenOracle = initData.arbInputTokenOracle; + arbInputTokenOracleData = initData.arbInputTokenOracleData; + + zroInputTokenOracle = initData.zroInputTokenOracle; + zroInputTokenOracleData = initData.zroInputTokenOracleData; + + swapper = initData.swapper; + + stargateConvertRate = 10 ** (IERC20Metadata(address(inputToken)).decimals() - pool.sharedDecimals()); + + transferOwnership(initData.owner); + } + + // *********************** // + // *** OWNER FUNCTIONS *** // + // *********************** // + + /// @notice updates the pause state + /// @param _val the new state + /// @param depositType if true, pause refers to deposits + function setPause(bool _val, PauseType depositType) external { + if (!cluster.hasRole(msg.sender, keccak256("PAUSABLE")) && msg.sender != owner()) revert PauserNotAuthorized(); + + if (depositType == PauseType.Deposit) { + emit Paused(depositPaused, _val, true); + depositPaused = _val; + } else { + emit Paused(withdrawPaused, _val, false); + withdrawPaused = _val; + } + } + + /// @notice rescues unused ETH from the contract + /// @param amount the amount to rescue + /// @param to the recipient + function rescueEth(uint256 amount, address to) external onlyOwner { + (bool success,) = to.call{value: amount}(""); + if (!success) revert TransferFailed(); + } + + /// @notice withdraws everything from the strategy + /// @dev Withdraws everything from the strategy and pauses it + function emergencyWithdraw() external onlyOwner { + // Pause the strategy + depositPaused = true; + withdrawPaused = true; + + uint256 amount = farm.balanceOf(address(lpToken), address(this)); + + // withdraw from farm + farm.withdraw(address(lpToken), amount); + + // withdraw from pool + uint256 received = pool.redeem(amount, address(this)); + + // wrap `inputToken` into `contractAddress` + ITOFT toft = ITOFT(contractAddress); + IPearlmit pearlmit = toft.pearlmit(); + + // - approve pearlmit + inputToken.safeApprove(address(pearlmit), received); + pearlmit.approve(20, address(inputToken), 0, contractAddress, received.toUint200(), block.timestamp.toUint48()); + + toft.wrap(address(this), address(this), received); // `received` should be == `wrapped` + + // - reset approvals + inputToken.safeApprove(address(pearlmit), 0); + } + + /** + * @notice updates the Cluster address. + * @dev can only be called by the owner. + * @param _cluster the new address. + */ + function setCluster(ICluster _cluster) external onlyOwner { + if (address(_cluster) == address(0)) revert EmptyAddress(); + emit ClusterUpdated(cluster, _cluster); + cluster = _cluster; + } + + /** + * @notice updates the Swapper address. + * @dev can only be called by the owner. + * @param _swapper the new address. + */ + function setSwapper(IZeroXSwapper _swapper) external onlyOwner { + if (address(_swapper) == address(0)) revert EmptyAddress(); + emit SwapperUpdated(swapper, _swapper); + swapper = _swapper; + } + + /** + * @notice updates the StargateV2 staking address. + * @dev can only be called by the owner. + * @param _farm the new address. + */ + function setFarm(address _farm) external onlyOwner { + if (address(_farm) == address(0)) revert EmptyAddress(); + // Withdraw and claim rewards from the previous farm + uint256 stakeAmount = farm.balanceOf(address(lpToken), address(this)); + farm.withdraw(address(lpToken), stakeAmount); + + emit FarmUpdated(address(farm), _farm); + farm = IStargateV2Staking(_farm); + + // Deposit in new farm + uint256 lpBalance = lpToken.balanceOf(address(this)); + if (lpBalance > 0) { + lpToken.safeApprove(address(farm), lpBalance); + farm.deposit(address(lpToken), lpBalance); + lpToken.safeApprove(address(farm), 0); + } + } + + /** + * @notice updates the oracle address. + * @dev can only be called by the owner. + * @param _oracle the new address. + * @param _oracleData the new data. + */ + function setArbOracle(ITapiocaOracle _oracle, bytes calldata _oracleData) external onlyOwner { + if (address(_oracle) == address(0)) revert EmptyAddress(); + emit ArbOracleUpdated(address(arbInputTokenOracle), address(_oracle)); + arbInputTokenOracle = _oracle; + arbInputTokenOracleData = _oracleData; + } + + /** + * @notice updates the oracle address. + * @dev can only be called by the owner. + * @param _oracle the new address. + * @param _oracleData the new data. + */ + function setStgOracle(ITapiocaOracle _oracle, bytes calldata _oracleData) external onlyOwner { + if (address(_oracle) == address(0)) revert EmptyAddress(); + emit StgOracleUpdated(address(stgInputTokenOracle), address(_oracle)); + stgInputTokenOracle = _oracle; + stgInputTokenOracleData = _oracleData; + } + + /** + * @notice updates the oracle address. + * @dev can only be called by the owner. + * @param _oracle the new address. + * @param _oracleData the new data. + */ + function setZroOracle(ITapiocaOracle _oracle, bytes calldata _oracleData) external onlyOwner { + if (address(_oracle) == address(0)) revert EmptyAddress(); + emit ZroOracleUpdated(address(zroInputTokenOracle), address(_oracle)); + zroInputTokenOracle = _oracle; + zroInputTokenOracleData = _oracleData; + } + + + /** + * @notice invests currently available STG for compounding interest + */ + function invest(bytes calldata arbData, bytes calldata stgData, bytes calldata zroData) external onlyOwner { + IERC20 _stg = IERC20(STG); + IERC20 _arb = IERC20(ARB); + IERC20 _zro = IERC20(ZRO); + + // should only harvest for the current `lpToken` + uint256 availableStg = _stg.balanceOf(address(this)); + uint256 availableArb = _arb.balanceOf(address(this)); + uint256 availableZro = _zro.balanceOf(address(this)); + if (availableStg == 0 && availableArb == 0 && availableZro == 0) return; + + uint256 totalAmountOut; + if(availableStg > 0) { + // swap STG to usdc + SSwapData memory swapData = abi.decode(stgData, (SSwapData)); + if (address(swapData.data.buyToken) != address(inputToken)) revert TokenNotValid(); + _stg.safeApprove(address(swapper), availableStg); + uint256 amountOut = swapper.swap(swapData.data, availableStg, swapData.minAmountOut); + _stg.safeApprove(address(swapper), 0); + if (amountOut < swapData.minAmountOut) revert SwapFailed(); + + totalAmountOut += amountOut; + } + + if (availableArb > 0) { + // swap STG to usdc + SSwapData memory swapData = abi.decode(arbData, (SSwapData)); + if (address(swapData.data.buyToken) != address(inputToken)) revert TokenNotValid(); + _arb.safeApprove(address(swapper), availableArb); + uint256 amountOut = swapper.swap(swapData.data, availableArb, swapData.minAmountOut); + _arb.safeApprove(address(swapper), 0); + if (amountOut < swapData.minAmountOut) revert SwapFailed(); + + totalAmountOut += amountOut; + } + + if (availableZro > 0) { + // swap ZRO to usdc + SSwapData memory swapData = abi.decode(zroData, (SSwapData)); + if (address(swapData.data.buyToken) != address(inputToken)) revert TokenNotValid(); + _zro.safeApprove(address(swapper), availableZro); + uint256 amountOut = swapper.swap(swapData.data, availableZro, swapData.minAmountOut); + _zro.safeApprove(address(swapper), 0); + if (amountOut < swapData.minAmountOut) revert SwapFailed(); + + totalAmountOut += amountOut; + } + + if (totalAmountOut > 0) { + // _deposit & stake + _depositAndStake(totalAmountOut); + } + } + + // ********************** // + // *** VIEW FUNCTIONS *** // + // ********************** // + /** + * @notice Returns the name of this strategy + */ + function name() external pure override returns (string memory name_) { + return "STG V2"; + } + + /** + * @notice Returns the description of this strategy + */ + function description() external pure override returns (string memory description_) { + return "Stargate V2 Strategy"; + } + + /** + * @notice Returns The estimate the pending rewards. + * @return amount The amount of STG, ARB and ZRO that should be harvested + */ + function pendingRewards() public view returns (uint256 amount) { + uint256 tokenIndex; + address _rewarder = farm.rewarder(address(lpToken)); + (address[] memory tokens, uint256[] memory rewards) = IStargateV2MultiRewarder(_rewarder).getRewards(address(lpToken), address(this)); + + tokenIndex = _findIndex(tokens, STG); + if (tokenIndex != 404 ) { + (bool stgOracleActive, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); + if (stgOracleActive) { + amount += (rewards[tokenIndex] * stgPrice) / 1e18; + } + } + + tokenIndex = _findIndex(tokens, ARB); + if (tokenIndex != 404 ) { + (bool arbOracleActive, uint256 arbPrice) = arbInputTokenOracle.peek(arbInputTokenOracleData); + if (arbOracleActive) { + amount += ( rewards[tokenIndex] * arbPrice) / 1e18; + } + } + + tokenIndex = _findIndex(tokens, ZRO); + if (tokenIndex != 404 ) { + (bool zroOracleActive, uint256 zroPrice) = zroInputTokenOracle.peek(zroInputTokenOracleData); + if (zroOracleActive) { + amount += ( rewards[tokenIndex] * zroPrice) / 1e18; + } + } + + return amount; + } + + /** + * @notice claims STG from farm + */ + function claim(address[] calldata tokens) external { + // should only harvest for the current `lpToken` + if (tokens.length > 1 || tokens[0] != address(lpToken)) revert TokenNotValid(); + farm.claim(tokens); + } + + // *********************************** // + /* ============ INTERNAL ============ */ + // *********************************** // + function _currentBalance() internal view override returns (uint256 amount) { + /// @dev: de-dust balance of LP token to follow StargatePool.redeem() logic if StargatePool convertRate is > 1 + amount = _sd2ld(_ld2sd(farm.balanceOf(address(lpToken), address(this)))); + amount += IERC20(contractAddress).balanceOf(address(this)); + amount += pendingRewards(); + } + + function _deposited(uint256 amount) internal override nonReentrant { + if (depositPaused) revert DepositPaused(); + + // unwrap; fees are 0 + ITOFT(contractAddress).unwrap(address(this), amount); + + // pool deposit & farm staking + _depositAndStake(amount); + } + + + /// @dev If StargatePool convertRate is > 1 the amount received will not be exact due Stargate conversion between LD and SD . + function _withdraw(address to, uint256 amount) internal override nonReentrant { + if (withdrawPaused) revert WithdrawPaused(); + + uint256 assetInContract = IERC20(contractAddress).balanceOf(address(this)); + + // check first if `contractAddress` is already available without performing any withdrawal action + uint256 toWithdrawFromPool; + unchecked { + toWithdrawFromPool = amount > assetInContract ? amount - assetInContract : 0; // Asset to withdraw from the pool if not enough available in the contract + } + + if (toWithdrawFromPool == 0) { + IERC20(contractAddress).safeTransfer(to, amount); + emit AmountWithdrawn(to, amount); + return; + } + + // withdraw remaining + // - withdraw from farm + farm.withdraw(address(lpToken), toWithdrawFromPool); + + // - withdraw from pool + uint256 received = pool.redeem(toWithdrawFromPool, address(this)); + + // - wrap `inputToken` into `contractAddress` + ITOFT toft = ITOFT(contractAddress); + IPearlmit pearlmit = toft.pearlmit(); + + // - approve pearlmit + inputToken.safeApprove(address(pearlmit), received); + pearlmit.approve(20, address(inputToken), 0, contractAddress, received.toUint200(), block.timestamp.toUint48()); + toft.wrap(address(this), address(this), received); // wrap fees are 0 + // - reset approvals + inputToken.safeApprove(address(pearlmit), 0); + pearlmit.clearAllowance(address(this), 20, address(inputToken), 0); + + // retrieve total amount to withdraw, due received from Stargate can diff from `toWithdrawFromPool` if StargatePool convertRate is > 1 + uint256 withdrawalAmount = assetInContract + received; + + // send `contractAddress` + IERC20(contractAddress).safeTransfer(to, withdrawalAmount); + emit AmountWithdrawn(to, withdrawalAmount); + } + + /// @notice Translate an amount in SD to LD + /// @dev Since SD <= LD by definition, convertRate >= 1, so there is no rounding errors in this function. + /// @param _amountSD The amount in SD + /// @return amountLD The same value expressed in LD + function _sd2ld(uint64 _amountSD) internal view returns (uint256 amountLD) { + unchecked { + amountLD = _amountSD * stargateConvertRate; + } + } + + /// @notice Translate an value in LD to SD + /// @dev Since SD <= LD by definition, convertRate >= 1, so there might be rounding during the cast. + /// @param _amountLD The value in LD + /// @return amountSD The same value expressed in SD + function _ld2sd(uint256 _amountLD) internal view returns (uint64 amountSD) { + unchecked { + amountSD = SafeCast.toUint64(_amountLD / stargateConvertRate); + } + } + + + // ********************************* // + /* ============ PRIVATE ============ */ + // ********************************* // + function _findIndex(address[] memory _tokens, address _token) private pure returns (uint256) { + uint256 len = _tokens.length; + for (uint256 i; i < len; i++) { + if (_tokens[i] == _token) { + return i; + } + } + // if index not found, return an arbitrary number 404, unexpected to have 404 different rewards in one staking contract + return 404; + } + + function _depositAndStake(uint256 amount) private { + inputToken.safeApprove(address(pool), type(uint256).max); + uint256 lpAmount = pool.deposit(address(this), amount); + inputToken.safeApprove(address(pool), 0); + + // farm deposit + lpToken.safeApprove(address(farm), type(uint256).max); + farm.deposit(address(lpToken), lpAmount); + lpToken.safeApprove(address(farm), 0); + + emit AmountDeposited(lpAmount); + } + +} \ No newline at end of file diff --git a/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol b/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol new file mode 100644 index 0000000..78f565f --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +interface IStargateV2MultiRewarder { + function getRewards(address stakingToken, address user) external view returns (address[] memory, uint256[] memory); + function staking() external view returns (address); +} \ No newline at end of file diff --git a/contracts/interfaces/stargatev2/IStargateV2Pool.sol b/contracts/interfaces/stargatev2/IStargateV2Pool.sol new file mode 100644 index 0000000..c1b627b --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2Pool.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +interface IStargateV2Pool { + function lpToken() external view returns (address); + + function deposit( + address _receiver, + uint256 _amountLD + ) external payable returns (uint256 amountLD); + + function redeem(uint256 _amountLD, address _receiver) external returns (uint256 amountLD); + + function sharedDecimals() external view returns (uint8); +} \ No newline at end of file diff --git a/contracts/interfaces/stargatev2/IStargateV2Staking.sol b/contracts/interfaces/stargatev2/IStargateV2Staking.sol new file mode 100644 index 0000000..b679bc0 --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2Staking.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +interface IStargateV2Staking { + function balanceOf(address token, address user) external view returns (uint256); + function rewarder(address token) external view returns (address); + function deposit(address token, uint256 amount) external; + function withdraw(address token, uint256 amount) external; + function claim(address[] calldata lpTokens) external; + function isPool(address) external view returns(bool); + function owner() external view returns (address); + function setPool(address token, address newRewarder) external; +} \ No newline at end of file diff --git a/contracts/mocks/StargateV2RewarderMock.sol b/contracts/mocks/StargateV2RewarderMock.sol new file mode 100644 index 0000000..634d3a9 --- /dev/null +++ b/contracts/mocks/StargateV2RewarderMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +contract StargateV2RewarderMock { + address[] public tokens; + uint256[] public amounts; + + function connect(address) external pure {} + + function setRewards(address[] memory _tokens, uint256[] memory _amounts) external { + tokens = _tokens; + amounts = _amounts; + } + function getRewards(address, address) external view returns (address[] memory, uint256[] memory) { + address[] memory _tokens = tokens; + uint256[] memory _amounts = amounts; + return (_tokens, _amounts); + } +} \ No newline at end of file diff --git a/contracts/mocks/ToftMock.sol b/contracts/mocks/ToftMock.sol index a78688e..f58c196 100644 --- a/contracts/mocks/ToftMock.sol +++ b/contracts/mocks/ToftMock.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.22; import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Pearlmit, IPearlmit, PearlmitHash} from "tap-utils/pearlmit/Pearlmit.sol"; // Tapioca import {PearlmitHandler} from "tap-utils/pearlmit/PearlmitHandler.sol"; @@ -21,6 +22,8 @@ contract ToftMock is ERC20, PearlmitHandler { address public erc20; + error FailedToWrap(); + constructor(address erc20_, string memory name_, string memory symbol_, IPearlmit _pearlmit) ERC20(name_, symbol_) PearlmitHandler(_pearlmit) @@ -30,12 +33,13 @@ contract ToftMock is ERC20, PearlmitHandler { function wrap(address from, address to, uint256 amount) external payable returns (uint256 minted) { bool isErr = pearlmit.transferFromERC20(from, address(this), erc20, amount); - if (isErr) revert TOFT_NotValid(); + if (isErr) revert FailedToWrap(); + _mint(to, amount); return amount; } - function unwrap(address to, uint256 amount) external returns (uint256) { + function unwrap(address to, uint256 amount) external returns (uint256 unwrapped) { _burn(msg.sender, amount); IERC20(erc20).transfer(to, amount); return amount; diff --git a/contracts/mocks/ZeroXSwapperMockTarget.sol b/contracts/mocks/ZeroXSwapperMockTarget.sol new file mode 100644 index 0000000..c0e76f3 --- /dev/null +++ b/contracts/mocks/ZeroXSwapperMockTarget.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// External +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract ZeroXSwapperMockTarget { + using SafeERC20 for IERC20; + + bool public state = true; + + receive() external payable {} + + function toggleState() public payable { + state = !state; + } + + function transferTokens(address token, uint256 amount) public payable { + IERC20(token).safeTransfer(msg.sender, amount); + } + + function transferTokensWithDust(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut) + public + payable + { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + IERC20(tokenOut).safeTransfer(msg.sender, amountOut); + if (amountIn > amountOut) { + IERC20(tokenIn).safeTransfer(msg.sender, amountIn - amountOut); + } + } +} diff --git a/lib/forge-std b/lib/forge-std index ae570fe..1714bee 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/lib/tap-utils b/lib/tap-utils index b6bb703..129e968 160000 --- a/lib/tap-utils +++ b/lib/tap-utils @@ -1 +1 @@ -Subproject commit b6bb703b25b4fcb0d006247e2c69dbe85143fd60 +Subproject commit 129e96846778c961c1385eb9bf44733457469a75 diff --git a/lib/tap-yieldbox b/lib/tap-yieldbox index 2c643f3..9e90dea 160000 --- a/lib/tap-yieldbox +++ b/lib/tap-yieldbox @@ -1 +1 @@ -Subproject commit 2c643f35962ec4775c2a9edf00cb2dc33b4ac9c7 +Subproject commit 9e90deae668f04b257d16490fcc980bcc9ea7d66 diff --git a/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol new file mode 100644 index 0000000..61028f6 --- /dev/null +++ b/test/StargateV2Strategy.t.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// External +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +// Tapioca +import {IStargateV2MultiRewarder} from "tapioca-strategies/interfaces/stargatev2/IStargateV2MultiRewarder.sol"; +import {YieldBox, YieldBoxURIBuilder, IWrappedNative, TokenType, IStrategy} from "yieldbox/YieldBox.sol"; +import {IStargateV2Staking} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Staking.sol"; +import {StargateV2Strategy} from "tapioca-strategies/StargateV2Strategy/StargateV2Strategy.sol"; +import {IStargateV2Pool} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Pool.sol"; +import {Pearlmit, IPearlmit, PearlmitHash} from "tap-utils/pearlmit/Pearlmit.sol"; +import {ITapiocaOracle} from "tap-utils/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tap-utils/interfaces/periph/IZeroXSwapper.sol"; +import {BaseERC20Strategy} from "yieldbox/strategies/BaseStrategy.sol"; +import {ICluster} from "tap-utils/interfaces/periph/ICluster.sol"; +import {ZeroXSwapper} from "tap-utils/Swapper/ZeroXSwapper.sol"; +import {ITOFT} from "tap-utils/interfaces/oft/ITOFT.sol"; +import {IYieldBox} from "yieldbox/interfaces/IYieldBox.sol"; +import {Cluster} from "tap-utils/Cluster/Cluster.sol"; +import {OracleMock} from "tapioca-strategies/mocks/OracleMock.sol"; +import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {StargateMultiRewarder} from "./external/StargateMultiRewarder.sol"; +import {ZeroXSwapperMockTarget} from "tapioca-strategies/mocks/ZeroXSwapperMockTarget.sol"; +import {ToftMock} from "tapioca-strategies/mocks/ToftMock.sol"; + +import "forge-std/Test.sol"; + +contract StargateV2StrategyTest is Test { + address owner; + string constant ENV_BINANCE_WALLET_ADDRESS = "BINANCE_WALLET_ADDRESS"; + string constant ENV_POOL_ADDRESS = "STARGATEV2_POOL"; + string constant ENV_FARM_ADDRESS = "STARGATEV2_FARM"; + string constant ENV_USDC = "USDC"; + string constant ENV_WETH = "WETH"; + string constant RPC_URL = "ARBITRUM_RPC_URL"; + string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; + uint256 ARB_FORK; + + address public binanceWalletAddr; + address public weth; + address public usdc; + IStargateV2Pool pool; + IStargateV2Staking farm; + OracleMock public stgOracleMock; + OracleMock public arbOracleMock; + OracleMock public zroOracleMock; + ToftMock tUsdc; + StargateV2Strategy strat; + ZeroXSwapperMockTarget swapperTarget; + ZeroXSwapper swapper; + Cluster cluster; + YieldBox yieldBox; + Pearlmit pearlmit; + uint256 tUsdcAssetId; + + /** + * Modifiers + */ + modifier isArbFork() { + vm.selectFork(ARB_FORK); + _; + } + + function setUp() public { + string memory rpcUrl = vm.envString(RPC_URL); + uint256 forkingBlockNumber = vm.envUint(FORKING_BLOCK_NUMBER); + ARB_FORK = vm.createSelectFork(rpcUrl, forkingBlockNumber); + + binanceWalletAddr = vm.envAddress(ENV_BINANCE_WALLET_ADDRESS); + vm.label(binanceWalletAddr, "binanceWalletAddr"); + weth = vm.envAddress(ENV_WETH); + vm.label(weth, "weth"); + usdc = vm.envAddress(ENV_USDC); + vm.label(usdc, "usdc"); + pool = IStargateV2Pool(vm.envAddress(ENV_POOL_ADDRESS)); + vm.label(address(pool), "IStargateV2Pool"); + farm = IStargateV2Staking(vm.envAddress(ENV_FARM_ADDRESS)); + vm.label(address(farm), "IStargateV2Staking"); + + pearlmit = new Pearlmit("Test", "1", address(this), 0); + stgOracleMock = new OracleMock("stgOracleMock", "SOM", 1e18); + arbOracleMock = new OracleMock("arbOracleMock", "SOM", 1e18); + zroOracleMock = new OracleMock("zroOracleMock", "SOM", 1e18); + tUsdc = new ToftMock(address(usdc), "Toft", "TOFT", IPearlmit(address(pearlmit))); + tUsdc.setPearlmit(IPearlmit(address(pearlmit))); + yieldBox = new YieldBox(IWrappedNative(address(weth)), new YieldBoxURIBuilder(), pearlmit, address(this)); + cluster = new Cluster(0, address(this)); + swapperTarget = new ZeroXSwapperMockTarget(); + swapper = new ZeroXSwapper(address(swapperTarget), ICluster(address(cluster)), address(this)); + + strat = new StargateV2Strategy( + StargateV2Strategy.StargateV2InitData( + IYieldBox(address(yieldBox)), + ICluster(address(cluster)), + address(tUsdc), + address(pool), + address(farm), + ITapiocaOracle(address(stgOracleMock)), + "0x", + ITapiocaOracle(address(arbOracleMock)), + "0x", + ITapiocaOracle(address(zroOracleMock)), + "0x", + IZeroXSwapper(address(swapper)), + address(this) + ) + ); + vm.label(address(strat), "StrategyV2Strategy"); + + yieldBox.registerAsset(TokenType.ERC20, address(tUsdc), IStrategy(address(strat)), 0); + tUsdcAssetId = yieldBox.ids(TokenType.ERC20, address(tUsdc), IStrategy(address(strat)), 0); + } + + /** + * Tests + */ + function test_constructor_stg() public isArbFork { + assertEq(address(strat.pool()), address(pool)); + assertEq(address(strat.farm()), address(farm)); + assertEq(strat.contractAddress(), address(tUsdc)); + assertEq(address(strat.inputToken()), address(usdc)); + } + + function test_deposit_stg() public isArbFork { + uint256 amount = 10_000_000; // 10 USDC + + _deposit(amount); + + uint256 farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, amount); + } + + function test_withdraw_stg() public isArbFork { + uint256 amount = 10_000_000; // 10 USDC + + _deposit(amount); + + // make sure it was deposited + uint256 farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, amount); + + yieldBox.withdraw(tUsdcAssetId, address(this), address(this), amount, 0); + uint256 tUsdcBalance = tUsdc.balanceOf(address(this)); + assertEq(tUsdcBalance, amount); + + farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, 0); + } + + function test_claim_stg() public isArbFork { + uint256 amount = 10_000_000; // 10 USDC + + _deposit(amount); + + uint256 farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, amount); + + vm.warp(1921684352); + + address[] memory tokens = new address[](1); + tokens[0] = address(strat.lpToken()); + strat.claim(tokens); + + address stg = strat.STG(); + address arb = strat.ARB(); + uint256 stgBalance = IERC20(stg).balanceOf(address(strat)); + uint256 arbBalance = IERC20(arb).balanceOf(address(strat)); + + bool arbOrStgRewards = stgBalance > 0 || arbBalance > 0; + assertTrue(arbOrStgRewards); + } + + function test_invest_stg() public isArbFork { + uint256 amount = 10_000_000; // 10 USDC + + _deposit(amount); + + uint256 farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, amount); + + vm.warp(1921684352); + + address[] memory tokens = new address[](1); + tokens[0] = address(strat.lpToken()); + strat.claim(tokens); + + address stg = strat.STG(); + address arb = strat.ARB(); + uint256 stgBalance = IERC20(stg).balanceOf(address(strat)); + uint256 arbBalance = IERC20(arb).balanceOf(address(strat)); + + bool arbOrStgRewards = stgBalance > 0 || arbBalance > 0; + assertTrue(arbOrStgRewards); + + //arb swap data + IZeroXSwapper.SZeroXSwapData memory arbZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(arb), + buyToken: IERC20(address(usdc)), + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector( + ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), arbBalance / 1e12 + ) + }); + + vm.prank(binanceWalletAddr); + IERC20(usdc).transfer(address(swapperTarget), 10_000_000_000); + + IZeroXSwapper.SZeroXSwapData memory stgZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(stg), + buyToken: IERC20(address(usdc)), + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector( + ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), stgBalance / 1e12 + ) + }); + + StargateV2Strategy.SSwapData memory stgSwapData = + StargateV2Strategy.SSwapData({minAmountOut: 0, data: stgZeroXSwapData}); + + StargateV2Strategy.SSwapData memory arbSwapData = + StargateV2Strategy.SSwapData({minAmountOut: 0, data: arbZeroXSwapData}); + + StargateV2Strategy.SSwapData memory zroSwapData = + StargateV2Strategy.SSwapData({minAmountOut: 0, data: arbZeroXSwapData}); + + uint256 farmBalanceBefore = farm.balanceOf(address(strat.lpToken()), address(strat)); + + cluster.updateContract(0, address(strat), true); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData), abi.encode(zroSwapData)); + + uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertGt(farmBalanceAfter, farmBalanceBefore); + } + + function test_invest_stg_must_revert_if_wrong_token() public isArbFork { + //arb swap data + address arb = strat.ARB(); + uint256 arbBalance = IERC20(arb).balanceOf(address(strat)); + + IZeroXSwapper.SZeroXSwapData memory arbZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(usdc), + buyToken: IERC20(address(arb)), // replace USDC address with ARB address to ensure it fails due invalid output token + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector(ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), arbBalance/1e12) + }); + + StargateV2Strategy.SSwapData memory arbSwapData = StargateV2Strategy.SSwapData({ + minAmountOut: 0, + data: arbZeroXSwapData + }); + + StargateV2Strategy.SSwapData memory stgSwapData; + StargateV2Strategy.SSwapData memory zroSwapData; + + // Set ARB 1 wei balance to enter condition in strategy + deal(arb, address(strat), 1); + + cluster.updateContract(0, address(strat), true); + + vm.expectRevert(StargateV2Strategy.TokenNotValid.selector); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData), abi.encode(zroSwapData)); + } + + function test_emergencyWithdraw_stg() public isArbFork { + uint256 amount = 10_000_000; // 10 USDC + + _deposit(amount); + + // make sure it was deposited + uint256 farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, amount); + + assertFalse(strat.withdrawPaused()); + assertFalse(strat.depositPaused()); + strat.emergencyWithdraw(); + assertTrue(strat.withdrawPaused()); + assertTrue(strat.depositPaused()); + + uint256 tUsdcBalance = tUsdc.balanceOf(address(strat)); + assertGt(tUsdcBalance, 0); + + vm.expectRevert(); + yieldBox.withdraw(tUsdcAssetId, address(this), address(this), amount, 0); + + strat.setPause(false, StargateV2Strategy.PauseType.Withdraw); + + yieldBox.withdraw(tUsdcAssetId, address(this), address(this), amount, 0); + + tUsdcBalance = tUsdc.balanceOf(address(this)); + assertEq(tUsdcBalance, amount); + + farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, 0); + } + + function _deposit(uint256 amount) private { + vm.prank(binanceWalletAddr); + IERC20(usdc).transfer(address(this), amount); + + IERC20(usdc).approve(address(pearlmit), type(uint256).max); + pearlmit.approve(20, address(usdc), 0, address(tUsdc), type(uint200).max, uint48(block.timestamp)); + tUsdc.wrap(address(this), address(this), amount); + + IERC20(tUsdc).approve(address(yieldBox), type(uint256).max); + yieldBox.depositAsset(tUsdcAssetId, address(this), address(this), amount, 0); + } + + function test_setFarm_e2e_withRewards() public isArbFork { + address newOwner = makeAddr("OWNER"); + address newFarm = makeAddr('NEW_FARM'); + /// Setup new StargateV2Staking and MultiRewarder /// + + // Clone a StargateV2Staking instance via vm.etch and set owner + bytes memory farmBytecode = address(farm).code; + vm.etch(newFarm, farmBytecode); + vm.store(newFarm, bytes32(uint256(0)), bytes32(uint256(uint160(newOwner)))); + + // Deploy StargateMultiRewarder + address newRewarder = deployContract(abi.encodePacked(StargateMultiRewarder.creationCode(), abi.encode(newFarm))); + + IStargateV2Staking newFarmContract = IStargateV2Staking(newFarm); + IStargateV2MultiRewarder newRewarderContract = IStargateV2MultiRewarder(newRewarder); + + assertEq(newRewarderContract.staking(), newFarm, "New rewarder immutable staking address is not correctly initialized"); + + // Stargate: Setup pool to new farm and rewarder + vm.startPrank(newOwner); + newFarmContract.setPool(pool.lpToken(), newRewarder); + vm.stopPrank(); + + assertTrue(newFarmContract.isPool(pool.lpToken()), "Cloned staking pool is not initialized"); + assertEq(newFarmContract.rewarder(pool.lpToken()), newRewarder, "Cloned staking pool rewarder is not initialized"); + + /// Simulate deposits and pending rewards /// + + uint256 amount = 10_000_000; // 10 USDC + + deal(usdc, binanceWalletAddr, amount); + _deposit(amount); + + vm.warp(1921684352); + + // Check pending stake before setFarm + assertEq(newFarmContract.balanceOf(pool.lpToken(), address(strat)), 0, "Current stake in new farm should be zero"); + assertEq(farm.balanceOf(pool.lpToken(), address(strat)), amount, "Current stake in old farm should be deposited USDC LP amount"); + + + // Set new farm at strategy + strat.setFarm(newFarm); + assertEq(address(strat.farm()), newFarm); + + // Check pending stake post setFarm + assertEq(newFarmContract.balanceOf(pool.lpToken(), address(strat)), amount, "Current stake in new farm should be previous farm amount"); + assertEq(farm.balanceOf(pool.lpToken(), address(strat)), 0, "Current stake in old farm should be zero"); + } + + + function deployContract(bytes memory bytecode) internal returns (address instance) { + assembly{ + instance := create(0, add(bytecode, 0x20), mload(bytecode)) + if iszero(extcodesize(instance)) { + revert(0, 0) + } + } + } +} diff --git a/test/btt/Base_Test.t.sol b/test/btt/Base_Test.t.sol new file mode 100644 index 0000000..84a130a --- /dev/null +++ b/test/btt/Base_Test.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +import {IPearlmit, Pearlmit} from "tap-utils/pearlmit/Pearlmit.sol"; +import {Cluster} from "tap-utils/Cluster/Cluster.sol"; +import {YieldBox} from "yieldbox/YieldBox.sol"; + +// tests +import {Utils} from "./utils/Utils.sol"; + +abstract contract Base_Test is Utils { + // ************ // + // *** VARS *** // + // ************ // + // users + address public userA; + address public userB; + uint256 public initialBalance = LARGE_AMOUNT; + + // common general storage + YieldBox yieldBox; + Pearlmit pearlmit; + Cluster cluster; + + // ************* // + // *** SETUP *** // + // ************* // + function setUp() public virtual { + // *** *** // + userA = _createUser(USER_A_PKEY, "User A"); + userB = _createUser(USER_B_PKEY, "User B"); + + // create real Cluster + cluster = _createCluster(address(this)); + // create real Pearlmit + pearlmit = _createPearlmit(address(this)); + // create real YieldBox + yieldBox = _createYieldBox(address(this), pearlmit); + } + + // ***************** // + // *** MODIFIERS *** // + // ***************** // + modifier isArbFork() { + vm.selectFork(ARB_FORK); + _; + } + modifier resetPrank(address user) { + _resetPrank(user); + _; + } + + /// @notice Modifier to approve an operator in YB via Pearlmit. + modifier whenApprovedViaPearlmit( + address _token, + uint256 _tokenId, + address _from, + address _operator, + uint256 _amount, + uint256 _expiration + ) { + _approveViaPearlmit({ + token: _token, + pearlmit: IPearlmit(address(pearlmit)), + from: _from, + operator: _operator, + amount: _amount, + expiration: _expiration, + tokenId: _tokenId + }); + _; + } + + /// @notice Modifier to approve an operator via regular ERC20. + modifier whenApprovedViaERC20(address _token, address _from, address _operator, uint256 _amount) { + _approveViaERC20({token: _token, from: _from, operator: _operator, amount: _amount}); + _; + } +} diff --git a/test/btt/shared/StargateV2_Shared.t.sol b/test/btt/shared/StargateV2_Shared.t.sol new file mode 100644 index 0000000..39d0ae3 --- /dev/null +++ b/test/btt/shared/StargateV2_Shared.t.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// external +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// Tapioca +import {IStargateV2Staking} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Staking.sol"; +import {StargateV2Strategy} from "tapioca-strategies/StargateV2Strategy/StargateV2Strategy.sol"; +import {IStargateV2Pool} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Pool.sol"; +import {ZeroXSwapper} from "tap-utils/Swapper/ZeroXSwapper.sol"; +import {TokenType} from "yieldbox/enums/YieldBoxTokenType.sol"; + +import {ITapiocaOracle} from "tap-utils/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tap-utils/interfaces/periph/IZeroXSwapper.sol"; +import {ICluster} from "tap-utils/interfaces/periph/ICluster.sol"; +import {IYieldBox} from "yieldbox/interfaces/IYieldBox.sol"; +import {IStrategy} from "yieldbox/interfaces/IStrategy.sol"; + +// mocks +import {ZeroXSwapperMockTarget} from "tapioca-strategies/mocks/ZeroXSwapperMockTarget.sol"; +import {OracleMock} from "tapioca-strategies/mocks/OracleMock.sol"; +import {MockERC20} from "tapioca-strategies/mocks/MockERC20.sol"; +import {ToftMock} from "tapioca-strategies/mocks/ToftMock.sol"; + +// tests +import {Base_Test} from "../Base_Test.t.sol"; +import {Events} from "../utils/Events.sol"; + +abstract contract StargateV2_Shared is Base_Test, Events { + // ************ // + // *** VARS *** // + // ************ // + ZeroXSwapperMockTarget swapperTarget; + OracleMock stgOracle; + OracleMock arbOracle; + OracleMock zroOracle; + ToftMock tUsdc; + uint256 tUsdcAssetId; + + address usdc; + address binanceWalletAddr; + + StargateV2Strategy strat; + + IStargateV2Pool pool; + IStargateV2Staking farm; + + ZeroXSwapper swapper; + + function setUp() public virtual override { + super.setUp(); + + string memory rpcUrl = vm.envString(RPC_URL); + uint256 forkingBlockNumber = vm.envUint(FORKING_BLOCK_NUMBER); + ARB_FORK = vm.createSelectFork(rpcUrl, forkingBlockNumber); + + binanceWalletAddr = vm.envAddress(ENV_BINANCE_WALLET_ADDRESS); + vm.label(binanceWalletAddr, "Binance Wallet Address"); + + usdc = vm.envAddress(ENV_USDC); + vm.label(usdc, "UDSC token"); + + pool = IStargateV2Pool(vm.envAddress(ENV_POOL_ADDRESS)); + vm.label(address(pool), "IStargateV2Pool"); + farm = IStargateV2Staking(vm.envAddress(ENV_FARM_ADDRESS)); + vm.label(address(farm), "IStargateV2Staking"); + + // create TOFT + tUsdc = _createToft(address(usdc), address(pearlmit)); + + // create arb > usdc oracle + arbOracle = _createOracle("ARBUSDC"); + stgOracle = _createOracle("STGUSDC"); + zroOracle = _createOracle("ZROUSDC"); + + // create 0xSwapper + swapperTarget = new ZeroXSwapperMockTarget(); + swapper = _createSwapper(address(swapperTarget), address(cluster), address(this)); + + address _owner = address(this); + // create Stargate v2 strategy + strat = new StargateV2Strategy( + StargateV2Strategy.StargateV2InitData( + IYieldBox(address(yieldBox)), + ICluster(address(cluster)), + address(tUsdc), + address(pool), + address(farm), + ITapiocaOracle(address(stgOracle)), + "0x", + ITapiocaOracle(address(arbOracle)), + "0x", + ITapiocaOracle(address(zroOracle)), + "0x", + IZeroXSwapper(address(swapper)), + _owner + ) + ); + vm.label(address(strat), "StrategyV2 Strat"); + + // register strategy on YieldBox + yieldBox.registerAsset(TokenType.ERC20, address(tUsdc), IStrategy(address(strat)), 0); + tUsdcAssetId = yieldBox.ids(TokenType.ERC20, address(tUsdc), IStrategy(address(strat)), 0); + } + + // ***************** // + // *** MODIFIERS *** // + // ***************** // + + function _getToken(address _token, uint256 _amount, address _to) + internal + resetPrank(binanceWalletAddr) + { + IERC20(_token).transfer(_to, _amount); + } + + function _getTokenAndWrap(address _token, address _tToken, uint256 _amount) + internal + whenApprovedViaPearlmit(_token, 0, address(this), _tToken, uint200(_amount), uint48(block.timestamp)) + whenApprovedViaERC20(_token, address(this), address(pearlmit), uint200(_amount)) + resetPrank(address(this)) + { + _getToken(_token, _amount, address(this)); + _resetPrank(address(this)); + + ToftMock(_tToken).wrap(address(this), address(this), _amount); + } + + + function _depositToStrategy(uint256 depositAmount) + internal + whenApprovedViaERC20(address(tUsdc), address(this), address(yieldBox), depositAmount) + { + _getTokenAndWrap(address(usdc), address(tUsdc), depositAmount); + _resetPrank(address(this)); + + vm.expectEmit(true, false, false, false); + emit AmountDeposited(depositAmount); + yieldBox.depositAsset(tUsdcAssetId, address(this), address(this), depositAmount, 0); + } +} diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_claim.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_claim.t.sol new file mode 100644 index 0000000..d80f14d --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_claim.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// external +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// Tapioca +import {StargateV2Strategy} from "tapioca-strategies/StargateV2Strategy/StargateV2Strategy.sol"; + +// tests +import {StargateV2_Shared} from "../../../shared/StargateV2_Shared.t.sol"; + +contract StargateV2Strategy_claim is StargateV2_Shared { + function test_WhenArrayIsEmpty() external { + address[] memory _tokens; + vm.assume(_tokens.length == 0); + vm.expectRevert(); + strat.claim(_tokens); + } + + function test_WhenArrayLengthMoreThanOne() external { + address[] memory _tokens = new address[](2); + _tokens[0] = address(0x1); + _tokens[1] = address(0x2); + // it should revert with TokenNotValid + vm.expectRevert(StargateV2Strategy.TokenNotValid.selector); + strat.claim(_tokens); + } + + function test_WhenItemAtIndexZeroIsNotLpToken() external { + address[] memory _tokens = new address[](1); + _tokens[0] = address(0x1); + // it should revert with TokenNotValid + vm.expectRevert(StargateV2Strategy.TokenNotValid.selector); + strat.claim(_tokens); + } + + function test_WhenItemAtIntexZeroIsLpToken(uint256 depositAmount) + external + whenApprovedViaERC20(address(tUsdc), address(this), address(yieldBox), depositAmount) + { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + + // deposit + _depositToStrategy(depositAmount); + + // advance time to accrue rewards + vm.warp(FUTURE_TIMESTAMP); + + // it should call claim + address[] memory _tokens = new address[](1); + _tokens[0] = pool.lpToken(); + strat.claim(_tokens); + + // assert rewards received + uint256 arbBalance = IERC20(strat.ARB()).balanceOf(address(strat)); + uint256 stgBalance = IERC20(strat.STG()).balanceOf(address(strat)); + assertTrue(arbBalance > 0 || stgBalance > 0); + } +} diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_claim.tree b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_claim.tree new file mode 100644 index 0000000..802f01e --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_claim.tree @@ -0,0 +1,11 @@ +StargateV2Strategy_claim.t.sol +├── when array is empty +│ └── it should revert +└── when array is non-empty + ├── when array length more than one + │ └── it should revert with TokenNotValid + └── when array length is one + ├── when item at index zero is not lpToken + │ └── it should revert with TokenNotValid + └── when item at intex zero is lpToken + └── it should claim \ No newline at end of file diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.t.sol new file mode 100644 index 0000000..1464f73 --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// Tapioca +import {IStargateV2Staking} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Staking.sol"; +import {StargateV2Strategy} from "tapioca-strategies/StargateV2Strategy/StargateV2Strategy.sol"; +import {IStargateV2Pool} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Pool.sol"; +import {ITapiocaOracle} from "tap-utils/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tap-utils/interfaces/periph/IZeroXSwapper.sol"; +import {ICluster} from "tap-utils/interfaces/periph/ICluster.sol"; +import {IYieldBox} from "yieldbox/interfaces/IYieldBox.sol"; + +// tests +import {StargateV2_Shared} from "../../../shared/StargateV2_Shared.t.sol"; + +contract StargateV2Strategy_constructor is StargateV2_Shared { + function test_WhenStrategyIsCreatedWithTheRightParameters() external isArbFork { + // it should have the right pool + assertEq(address(strat.pool()), address(pool)); + // it should have the right farm + assertEq(address(strat.farm()), address(farm)); + // it should have the right inputToken + assertEq(address(strat.inputToken()), address(usdc)); + // it should have the right lpToken + assertEq(address(strat.lpToken()), pool.lpToken()); + // it should have the right stgInputTokenOracle + assertEq(address(strat.stgInputTokenOracle()), address(stgOracle)); + // it should have the right arbInputTokenOracle + assertEq(address(strat.arbInputTokenOracle()), address(arbOracle)); + // it should have the right swapper + assertEq(address(strat.swapper()), address(swapper)); + // it should transfer the ownership to the righ account + assertEq(address(strat.owner()), address(this)); + } + + modifier whenPoolIsNotValid(bool _zero) { + pool = IStargateV2Pool(_zero ? address(0) : address(0x1)); + _; + } + + modifier whenFarmIsNotValid() { + farm = IStargateV2Staking(address(0)); + _; + } + + function test_RevertWhen_PoolIsAddressZero() external whenPoolIsNotValid(true) { + vm.expectRevert(StargateV2Strategy.EmptyAddress.selector); + new StargateV2Strategy( + StargateV2Strategy.StargateV2InitData( + IYieldBox(address(yieldBox)), + ICluster(address(cluster)), + address(tUsdc), + address(pool), + address(farm), + ITapiocaOracle(address(stgOracle)), + "0x", + ITapiocaOracle(address(arbOracle)), + "0x", + ITapiocaOracle(address(zroOracle)), + "0x", + IZeroXSwapper(address(swapper)), + address(this) + ) + ); + } + + function test_RevertWhen_PoolIsNotTheRightType() external whenPoolIsNotValid(false) { + // will revert because pool.lpToken() call will throw an EvmError + vm.expectRevert(); + new StargateV2Strategy( + StargateV2Strategy.StargateV2InitData( + IYieldBox(address(yieldBox)), + ICluster(address(cluster)), + address(tUsdc), + address(pool), + address(farm), + ITapiocaOracle(address(stgOracle)), + "0x", + ITapiocaOracle(address(arbOracle)), + "0x", + ITapiocaOracle(address(zroOracle)), + "0x", + IZeroXSwapper(address(swapper)), + address(this) + ) + ); + } + + function test_RevertWhen_FarmIsNotValid() external whenFarmIsNotValid { + vm.expectRevert(StargateV2Strategy.EmptyAddress.selector); + new StargateV2Strategy( + StargateV2Strategy.StargateV2InitData( + IYieldBox(address(yieldBox)), + ICluster(address(cluster)), + address(tUsdc), + address(pool), + address(farm), + ITapiocaOracle(address(stgOracle)), + "0x", + ITapiocaOracle(address(arbOracle)), + "0x", + ITapiocaOracle(address(zroOracle)), + "0x", + IZeroXSwapper(address(swapper)), + address(this) + ) + ); + } +} diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.tree b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.tree new file mode 100644 index 0000000..364b507 --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.tree @@ -0,0 +1,18 @@ +StargateV2Strategy_constructor.t.sol +├── when strategy is created with the right parameters +│ ├── it should have the right pool +│ ├── it should have the right farm +│ ├── it should have the right inputToken +│ ├── it should have the right lpToken +│ ├── it should have the right stgInputTokenOracle +│ ├── it should have the right arbInputTokenOracle +│ ├── it should have the right swapper +│ └── it should transfer the ownership to the righ account +└── when parameters are wrong + ├── when pool is not valid + │ ├── when pool is address zero + │ │ └── it should revert + │ └── when pool is not the right type + │ └── it should revert + └── when farm is not valid + └── it should revert \ No newline at end of file diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_deposit.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_deposit.t.sol new file mode 100644 index 0000000..a7b981e --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_deposit.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// external +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// Tapioca +import {StargateV2Strategy} from "tapioca-strategies/StargateV2Strategy/StargateV2Strategy.sol"; + +// tests +import {StargateV2_Shared} from "../../../shared/StargateV2_Shared.t.sol"; + +contract StargateV2Strategy_deposit is StargateV2_Shared { + modifier whenDepositsArePaused() { + strat.setPause(true, StargateV2Strategy.PauseType.Deposit); + _; + } + + modifier whenDepositsAreNotPaused() { + strat.setPause(false, StargateV2Strategy.PauseType.Deposit); + _; + } + + function test_WhenDepositsArePaused(uint256 depositAmount) + external + whenDepositsArePaused + whenApprovedViaERC20(address(tUsdc), address(this), address(yieldBox), depositAmount) + { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + + _getTokenAndWrap(address(usdc), address(tUsdc), depositAmount); + _resetPrank(address(this)); + + // it should revert with DepositPaused + vm.expectRevert(StargateV2Strategy.DepositPaused.selector); + yieldBox.depositAsset(tUsdcAssetId, address(this), address(this), depositAmount, 0); + } + + function test_RevertWhen_StrategyDoesNotReceiveTheRequestedAmount(uint256 depositAmount) + external + whenDepositsAreNotPaused + whenApprovedViaERC20(address(tUsdc), address(this), address(yieldBox), type(uint256).max) + { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + // it should revert + vm.expectRevert("BoringERC20: TransferFrom failed"); + yieldBox.depositAsset(tUsdcAssetId, address(this), address(this), depositAmount, 0); + } + + function test_WhenStrategyHasReceivedTheProperAmount(uint256 depositAmount) + external + whenDepositsAreNotPaused + whenApprovedViaERC20(address(tUsdc), address(this), address(yieldBox), depositAmount) + { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + + _depositToStrategy(depositAmount); + + // it should unwrap and increase farm deposit + uint256 farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, depositAmount); + + // it should increase currentBalance + uint256 currentBalance = strat.currentBalance(); + assertEq(currentBalance, depositAmount); + + // it should leave zero approval on inputToken for pool + uint256 inputTokenAllowance = IERC20(usdc).allowance(address(strat), address(pool)); + assertEq(inputTokenAllowance, 0); + + // it should leave zero approval on lpToken for farm + uint256 lpTokenAllowance = IERC20(pool.lpToken()).allowance(address(strat), address(farm)); + assertEq(lpTokenAllowance, 0); + } +} diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_deposit.tree b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_deposit.tree new file mode 100644 index 0000000..14eb1b9 --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_deposit.tree @@ -0,0 +1,11 @@ +StargateV2Strategy_deposit.t.sol +├── when deposits are paused +│ └── it should revert with DepositPaused +└── when deposits are not paused + ├── when strategy does not receive the requested amount + │ └── it should revert + └── when strategy has received the proper amount + ├── it should unwrap and increase farm deposit + ├── it should leave zero approval on inputToken for pool + ├── it should leave zero approval on lpToken for farm + └── it should increase currentBalance \ No newline at end of file diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.t.sol new file mode 100644 index 0000000..cdaded7 --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.t.sol @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// external +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// Tapioca +import {StargateV2Strategy} from "tapioca-strategies/StargateV2Strategy/StargateV2Strategy.sol"; +import {ZeroXSwapper} from "tap-utils/Swapper/ZeroXSwapper.sol"; + +import {ITapiocaOracle} from "tap-utils/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tap-utils/interfaces/periph/IZeroXSwapper.sol"; +import {ICluster} from "tap-utils/interfaces/periph/ICluster.sol"; + +// mocks +import {ZeroXSwapperMockTarget} from "tapioca-strategies/mocks/ZeroXSwapperMockTarget.sol"; + +// tests +import {StargateV2_Shared} from "../../../shared/StargateV2_Shared.t.sol"; + +contract StargateV2Strategy_setters is StargateV2_Shared { + modifier givenContractHasDeposit(uint256 depositAmount) { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + + _approveViaERC20({token: address(tUsdc), from: address(this), operator: address(yieldBox), amount: depositAmount}); + + // deposit + _depositToStrategy(depositAmount); + _; + } + + modifier givenStratIsWhitelisted() { + _resetPrank(address(this)); + cluster.updateContract(0, address(strat), true); + _; + } + + function test_whenRescueEthIsCalled_GivenCallerIsNotOwner() external resetPrank(userA) { + // it should revert with owner error + vm.expectRevert("Ownable: caller is not the owner"); + strat.rescueEth(VALUE_ZERO, ADDRESS_ZERO); + } + + function test_whenRescueEthIsCalled_GivenAmountIsHigherThanAvailable() external { + // it should revert with TransferFailed + vm.expectRevert(StargateV2Strategy.TransferFailed.selector); + strat.rescueEth(LARGE_AMOUNT, address(this)); + } + + function test_whenRescueEthIsCalled_GivenAmountIsAvailable() external { + // add assets + deal(address(strat), LARGE_AMOUNT); + + uint256 balanceBefore = userB.balance; + strat.rescueEth(LARGE_AMOUNT, address(userB)); + + // it should rescue requested amount + assertEq(userB.balance - balanceBefore, LARGE_AMOUNT); + } + + function test_whenEmergencyWithdrawIsCalled_GivenCallerIsNotOwner() external resetPrank(userA) { + // it should revert with owner error + vm.expectRevert("Ownable: caller is not the owner"); + strat.emergencyWithdraw(); + } + + function test_whenEmergencyWithdrawIsCalled_GivenCallerIsOwner(uint256 depositAmount) + external + whenApprovedViaERC20(address(tUsdc), address(this), address(yieldBox), depositAmount) + { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + + // deposit + _depositToStrategy(depositAmount); + + // emergency withdraw + strat.emergencyWithdraw(); + + // it should set depositPaused on true + assertTrue(strat.depositPaused()); + // it should set withdrawPaused on true + assertTrue(strat.withdrawPaused()); + + // it should withdraw the entire amount available on the farm + uint256 farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, 0); + + // it should wrap and leave it on the contract + uint256 stratBalance = tUsdc.balanceOf(address(strat)); + assertEq(stratBalance, depositAmount); + } + + function test_whenSetClusterIsCalled_GivenCallerIsNotOwner() external resetPrank(userA) { + // it should revert with owner error + vm.expectRevert("Ownable: caller is not the owner"); + strat.setCluster(ICluster(address(0x1))); + } + + function test_whenSetClusterIsCalled_GivenClusterIsAddressZero() external { + // it should revert with EmptyAddress + vm.expectRevert(StargateV2Strategy.EmptyAddress.selector); + strat.setCluster(ICluster(ADDRESS_ZERO)); + } + + function test_whenSetClusterIsCalled_GivenClusterAddressIsValid() external { + vm.expectEmit(false, true, false, true); + emit ClusterUpdated(ICluster(ADDRESS_ZERO), ICluster(address(this))); + strat.setCluster(ICluster(address(this))); + } + + + function test_whenSetSwapperIsCalled_GivenCallerIsNotOwner() external resetPrank(userA) { + // it should revert with owner error + vm.expectRevert("Ownable: caller is not the owner"); + strat.setSwapper(IZeroXSwapper(address(0x1))); + } + + function test_whenSetSwapperIsCalled_GivenSwapperIsAddressZero() external { + // it should revert with EmptyAddress + vm.expectRevert(StargateV2Strategy.EmptyAddress.selector); + strat.setSwapper(IZeroXSwapper(ADDRESS_ZERO)); + } + + function test_whenSetSwapperIsCalled_GivenSwapperAddressIsValid() external { + // it should set the new swapper + vm.expectEmit(true, true, true, true); + emit SwapperUpdated(strat.swapper(), IZeroXSwapper(address(this))); + strat.setSwapper(IZeroXSwapper(address(this))); + + // it should set the new swapper + assertEq(address(strat.swapper()), address(this)); + } + + function test_whenSetFarmIsCalled_GivenCallerIsNotOwner() external resetPrank(userA) { + // it should revert with owner error + vm.expectRevert("Ownable: caller is not the owner"); + strat.setFarm(address(0x1)); + } + + function test_whenSetFarmIsCalled_GivenFarmIsAddressZero() external { + // it should revert with EmptyAddress + vm.expectRevert(StargateV2Strategy.EmptyAddress.selector); + strat.setFarm(ADDRESS_ZERO); + } + + function test_whenSetFarmIsCalled_GivenFarmAddressIsValid() external { + // it should set the new farm + vm.expectEmit(true, true, true, true); + emit FarmUpdated(address(strat.farm()), address(this)); + strat.setFarm(address(this)); + + // it should set the new farm + assertEq(address(strat.farm()), address(this)); + } + + + function test_whenSetStgOracleIsCalled_GivenCallerIsNotOwner() external resetPrank(userA) { + // it should revert with owner error + vm.expectRevert("Ownable: caller is not the owner"); + strat.setStgOracle(ITapiocaOracle(address(0x1)), ""); + } + + function test_whenSetStgOracleIsCalled_GivenOracleIsAddressZero() external { + // it should revert with EmptyAddress + vm.expectRevert(StargateV2Strategy.EmptyAddress.selector); + strat.setStgOracle(ITapiocaOracle(ADDRESS_ZERO), ""); + } + + function test_whenSetStgOracleIsCalled_GivenOracleAddressIsValid() external { + vm.expectEmit(true, true, true, true); + emit StgOracleUpdated(address(strat.stgInputTokenOracle()), address(this)); + strat.setStgOracle(ITapiocaOracle(address(this)), ""); + + // it should set the new stgInputTokenOracle + assertEq(address(strat.stgInputTokenOracle()), address(this)); + } + + + function test_whenSetArbOracleIsCalled_GivenCallerIsNotOwner() external resetPrank(userA) { + // it should revert with owner error + vm.expectRevert("Ownable: caller is not the owner"); + strat.setArbOracle(ITapiocaOracle(address(0x1)), ""); + } + + function test_whenSetArbOracleIsCalled_WhenOracleIsAddressZero() external { + // it should revert with EmptyAddress + vm.expectRevert(StargateV2Strategy.EmptyAddress.selector); + strat.setArbOracle(ITapiocaOracle(ADDRESS_ZERO), ""); + } + + function test_whenSetArbOracleIsCalled_WhenOracleAddressIsValid() external { + vm.expectEmit(true, true, true, true); + emit ArbOracleUpdated(address(strat.arbInputTokenOracle()), address(this)); + strat.setArbOracle(ITapiocaOracle(address(this)), ""); + + // it should set the new arbInputTokenOracle + assertEq(address(strat.arbInputTokenOracle()), address(this)); + } + + + function test_whenInvestIsCalled_GivenContractHasNoRewards(uint256 depositAmount) + external + givenContractHasDeposit(depositAmount) + { + // deposit + _depositToStrategy(depositAmount); + + uint256 farmBalanceBefore = farm.balanceOf(address(strat.lpToken()), address(strat)); + + // invest + strat.invest("", "", ""); + + uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); + + // shouldn't change + assertEq(farmBalanceBefore, farmBalanceAfter); + } + + modifier whenContractHasARBAndSTGRewards() { + // deal ARB + _getToken(strat.ARB(), SMALL_AMOUNT, address(strat)); + _getToken(address(usdc), LOW_DECIMALS_SMALL_AMOUNT, address(swapperTarget)); + + + // deal STG + _getToken(strat.STG(), SMALL_AMOUNT, address(strat)); + _getToken(address(usdc), LOW_DECIMALS_SMALL_AMOUNT, address(swapperTarget)); + _; + } + + function test_whenInvestIsCalled_GivenSwapAmountIsLessThanMinAmountOut(uint256 depositAmount) + external + givenContractHasDeposit(depositAmount) + whenContractHasARBAndSTGRewards + givenStratIsWhitelisted + resetPrank(address(this)) + { + //arb swap data + IZeroXSwapper.SZeroXSwapData memory arbZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(strat.ARB()), + buyToken: IERC20(address(usdc)), + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector( + ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), LOW_DECIMALS_SMALL_AMOUNT - 1 + ) + }); + StargateV2Strategy.SSwapData memory arbSwapData = + StargateV2Strategy.SSwapData({minAmountOut: LARGE_AMOUNT, data: arbZeroXSwapData}); + + //stg swap data + IZeroXSwapper.SZeroXSwapData memory stgZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(strat.STG()), + buyToken: IERC20(address(usdc)), + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector( + ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), LOW_DECIMALS_SMALL_AMOUNT - 1 + ) + }); + StargateV2Strategy.SSwapData memory stgSwapData = + StargateV2Strategy.SSwapData({minAmountOut: LOW_DECIMALS_SMALL_AMOUNT, data: stgZeroXSwapData}); + + //zro swap data + IZeroXSwapper.SZeroXSwapData memory zroZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(strat.ZRO()), + buyToken: IERC20(address(usdc)), + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector( + ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), LOW_DECIMALS_SMALL_AMOUNT - 1 + ) + }); + StargateV2Strategy.SSwapData memory zroSwapData = + StargateV2Strategy.SSwapData({minAmountOut: LOW_DECIMALS_SMALL_AMOUNT, data: zroZeroXSwapData}); + + // expect MinSwapFailed error + vm.expectRevert(); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData), abi.encode(zroSwapData)); + } + + function test_whenInvestIsCalled_GivenSwapAmountIsValid(uint256 depositAmount) + external + givenContractHasDeposit(depositAmount) + whenContractHasARBAndSTGRewards + givenStratIsWhitelisted + resetPrank(address(this)) + { + //arb swap data + IZeroXSwapper.SZeroXSwapData memory arbZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(strat.ARB()), + buyToken: IERC20(address(usdc)), + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector( + ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), LOW_DECIMALS_SMALL_AMOUNT + ) + }); + StargateV2Strategy.SSwapData memory arbSwapData = + StargateV2Strategy.SSwapData({minAmountOut: LOW_DECIMALS_SMALL_AMOUNT, data: arbZeroXSwapData}); + + //stg swap data + IZeroXSwapper.SZeroXSwapData memory stgZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(strat.STG()), + buyToken: IERC20(address(usdc)), + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector( + ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), LOW_DECIMALS_SMALL_AMOUNT + ) + }); + StargateV2Strategy.SSwapData memory stgSwapData = + StargateV2Strategy.SSwapData({minAmountOut: LOW_DECIMALS_SMALL_AMOUNT, data: stgZeroXSwapData}); + + //zro swap data + IZeroXSwapper.SZeroXSwapData memory zroZeroXSwapData = IZeroXSwapper.SZeroXSwapData({ + sellToken: IERC20(strat.ZRO()), + buyToken: IERC20(address(usdc)), + swapTarget: payable(swapperTarget), + swapCallData: abi.encodeWithSelector( + ZeroXSwapperMockTarget.transferTokens.selector, address(usdc), LOW_DECIMALS_SMALL_AMOUNT + ) + }); + StargateV2Strategy.SSwapData memory zroSwapData = + StargateV2Strategy.SSwapData({minAmountOut: LOW_DECIMALS_SMALL_AMOUNT, data: zroZeroXSwapData}); + + + // invest + uint256 farmBalanceBefore = farm.balanceOf(address(strat.lpToken()), address(strat)); + + vm.expectEmit(); + emit AmountDeposited(LOW_DECIMALS_SMALL_AMOUNT * 2); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData), abi.encode(zroSwapData)); + uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); + + // it should increase farm balance + assertGt(farmBalanceAfter, farmBalanceBefore); + } +} diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.tree b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.tree new file mode 100644 index 0000000..ae006d3 --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.tree @@ -0,0 +1,60 @@ +StargateV2Strategy_setters.t.sol +├── when rescueEth is called +│ ├── given caller is not owner +│ │ └── it should revert with owner error +│ ├── given amount is higher than available +│ │ └── it should revert with TransferFailed +│ └── given amount is available +│ └── it should rescue requested amount +├── when emergencyWithdraw is called +│ ├── given not owner +│ │ └── it should revert with owner error +│ └── given caller is owner +│ ├── it should set depositPaused on true +│ ├── it should set withdrawPaused on true +│ ├── it should withdraw the entire amount available on the farm +│ └── it should wrap and leave it on the contract +├── when setCluster is called +│ ├── given sender is not owner +│ │ └── it should revert with owner error +│ ├── given cluster is address zero +│ │ └── it should revert with EmptyAddress +│ └── given cluster address is valid +│ └── it should set the new cluster +├── when setSwapper is called +│ ├── given no owner +│ │ └── it should revert with owner error +│ ├── given swapper is address zero +│ │ └── it should revert with EmptyAddress +│ └── given swapper address is valid +│ └── it should set the new swapper +├── when setFarm is called +│ ├── given sender is not the owner +│ │ └── it should revert with owner error +│ ├── given farm is address zero +│ │ └── it should revert with EmptyAddress +│ └── given farm address is valid +│ └── it should set the new farm +├── when setStgOracle is called +│ ├── when caller is different than owner +│ │ └── it should revert with owner error +│ ├── given oracle is address zero +│ │ └── it should revert with EmptyAddress +│ └── given oracle address is valid +│ └── it should set the new stgInputTokenOracle +├── when setArbOracle is called +│ ├── when caller is not owner +│ │ └── it should revert with owner error +│ ├── when oracle is address zero +│ │ └── it should revert with EmptyAddress +│ └── when oracle address is valid +│ └── it should set the new arbInputTokenOracle +└── when invest is called + ├── given contract has no rewards + │ └── it should do nothing + └── given contract has reward + └── when contract has ARB and STG rewards + ├── given swap amount is less than minAmountOut + │ └── it should revert with SwapFailed + └── given swap amount is valid + └── it should increase farm balance \ No newline at end of file diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.t.sol new file mode 100644 index 0000000..6932801 --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// external +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// Tapioca +import {StargateV2Strategy} from "tapioca-strategies/StargateV2Strategy/StargateV2Strategy.sol"; + +// tests +import {StargateV2RewarderMock} from "tapioca-strategies/mocks/StargateV2RewarderMock.sol"; +import {StargateV2_Shared} from "../../../shared/StargateV2_Shared.t.sol"; + +contract StargateV2Strategy_view is StargateV2_Shared { + StargateV2RewarderMock rewarderMock; + + function setUp() public virtual override { + super.setUp(); + rewarderMock = new StargateV2RewarderMock(); + + arbOracle.set(ARB_USDC_RATE); + stgOracle.set(STG_USDC_RATE); + } + + modifier whenMockRewarder() { + address farmOwner = farm.owner(); + _resetPrank(farmOwner); + farm.setPool(pool.lpToken(), address(rewarderMock)); + _; + } + + function test_WhenNameIsCalled() external view { + // it should return value which length is not zero + string memory name = strat.name(); + assertGt(bytes(name).length, 0); + } + + function test_WhenDescriptionIsCalled() external view { + // it should return value which length is not zero + string memory description = strat.description(); + assertGt(bytes(description).length, 0); + } + + function test_WhenOracleReturnsAValidRate(uint256 depositAmount) + external + whenApprovedViaERC20(address(tUsdc), address(this), address(yieldBox), depositAmount) + + { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_LARGE_AMOUNT); + + // deposit for rewards accrual + _depositToStrategy(depositAmount); + + // advance time to check pending rewards + vm.warp(FUTURE_TIMESTAMP); + + assertGt(strat.pendingRewards(), 0); + + } + + function test_WhenOracleReturnsAnInvalidRateAndDoesNotRevert(uint256 depositAmount) + external + whenApprovedViaERC20(address(tUsdc), address(this), address(yieldBox), depositAmount) + { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_LARGE_AMOUNT); + + // deposit for rewards accrual + _depositToStrategy(depositAmount); + + // set invalid oracle rates + arbOracle.set(0); + stgOracle.set(0); + + // advance time to check pending rewards + vm.warp(2951684352); + + // it should return 0 + assertEq(strat.pendingRewards(), 0); + } + + + // @dev this shouldn't revert anymore after audit fixes + function test_RevertWhen_ReceivedRewardsAreNotAccepted() + external + whenMockRewarder + { + // create rewards array + address[] memory _tokens = new address[](2); + _tokens[0] = address(0x1); + _tokens[1] = address(0x2); + uint256[] memory _amounts = new uint256[](2); + rewarderMock.setRewards(_tokens, _amounts); + + uint256 rewards = strat.pendingRewards(); + assertEq(rewards, 0); + } + + function test_WhenReceivedRewardsAreAvailableButWithZeroValue() + external + whenMockRewarder + { + // create rewards array + address[] memory _tokens = new address[](2); + _tokens[0] = strat.STG(); + _tokens[1] = strat.ARB(); + uint256[] memory _amounts = new uint256[](2); + _amounts[0] = 0; + _amounts[1] = 0; + rewarderMock.setRewards(_tokens, _amounts); + + // it should return 0 + assertEq(strat.pendingRewards(), 0); + } +} diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.tree b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.tree new file mode 100644 index 0000000..b297a1d --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.tree @@ -0,0 +1,16 @@ +StargateV2Strategy_view.t.sol +├── when name is called +│ └── it should return value which length is not zero +├── when description is called +│ └── it should return value which length is not zero +└── when pendingRewards is called + ├── when rewards are available + │ ├── when oracle returns a valid rate + │ │ └── it should return a non zero value + │ └── when oracle returns an invalid rate and does not revert + │ └── it should return 0 + └── when rewards are not available + ├── when received rewards are not accepted + │ └── it should revert + └── when received rewards are available but with zero value + └── it should return 0 \ No newline at end of file diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_withdraw.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_withdraw.t.sol new file mode 100644 index 0000000..c83f410 --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_withdraw.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// external +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// Tapioca +import {StargateV2Strategy} from "tapioca-strategies/StargateV2Strategy/StargateV2Strategy.sol"; + +// tests +import {StargateV2_Shared} from "../../../shared/StargateV2_Shared.t.sol"; + +contract StargateV2Strategy_withdraw is StargateV2_Shared { + + modifier whenWithdrawIsPaused() { + strat.setPause(true, StargateV2Strategy.PauseType.Withdraw); + _; + } + + modifier whenWithdrawIsNotPaused() { + strat.setPause(false, StargateV2Strategy.PauseType.Withdraw); + _; + } + + modifier whenTheresADepositedPosition(uint256 depositAmount) { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + _depositToStrategy(depositAmount); + _; + } + + function test_WhenWithdrawasArePaused(uint256 depositAmount, uint256 withdrawAmount) external whenWithdrawIsPaused { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + vm.assume(withdrawAmount < depositAmount); + + // deposit + _depositToStrategy(depositAmount); + + // it should revert with WithdrawPaused + vm.expectRevert(StargateV2Strategy.WithdrawPaused.selector); + yieldBox.withdraw(tUsdcAssetId, address(this), address(this), withdrawAmount, 0); + } + + + function test_WhenStrategyHasEnoughUnstakedBalance(uint256 depositAmount, uint256 withdrawAmount, uint256 strategyAmount) + external + whenWithdrawIsNotPaused + { + vm.assume(depositAmount > 0 && depositAmount <= LOW_DECIMALS_SMALL_AMOUNT); + vm.assume(withdrawAmount < depositAmount); + vm.assume(strategyAmount > withdrawAmount && strategyAmount < depositAmount); + + // deposit + _depositToStrategy(depositAmount); + + // assure strategy has enough tokens + _getTokenAndWrap(address(usdc), address(tUsdc), strategyAmount); + tUsdc.transfer(address(strat), strategyAmount); + + // withdraw + yieldBox.withdraw(tUsdcAssetId, address(this), address(this), withdrawAmount, 0); + + // it should transfer to the receiver + assertEq(tUsdc.balanceOf(address(this)), withdrawAmount); + + // it should not decrease farm balance + uint256 farmBalance = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalance, depositAmount); + } + + function test_WhenStrategyHasZeroBalance(uint256 depositAmount, uint256 withdrawAmount) + external + whenWithdrawIsNotPaused + whenTheresADepositedPosition(depositAmount) + { + vm.assume(withdrawAmount < depositAmount); + + uint256 farmBalanceBefore = farm.balanceOf(address(strat.lpToken()), address(strat)); + + // withdraw + yieldBox.withdraw(tUsdcAssetId, address(this), address(this), withdrawAmount, 0); + + // it should wrap & transfer to the receiver + assertEq(tUsdc.balanceOf(address(this)), withdrawAmount); + + // it should redeem the entire amount from farm + uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertEq(farmBalanceBefore - farmBalanceAfter, withdrawAmount); + + // it should leave zero allowance on inputToken for pearlmit + uint256 inputTokenAllowance = IERC20(usdc).allowance(address(strat), address(pearlmit)); + assertEq(inputTokenAllowance, 0); + } + + function test_WhenStrategyHasLessThanTheRequestAmount(uint256 depositAmount, uint256 withdrawAmount, uint256 strategyAmount) + external + whenWithdrawIsNotPaused + whenTheresADepositedPosition(depositAmount) + { + vm.assume(withdrawAmount < depositAmount); + vm.assume(strategyAmount < withdrawAmount && strategyAmount > 0); + + // assure strategy has enough tokens + _getTokenAndWrap(address(usdc), address(tUsdc), strategyAmount); + tUsdc.transfer(address(strat), strategyAmount); + + uint256 farmBalanceBefore = farm.balanceOf(address(strat.lpToken()), address(strat)); + + vm.expectEmit(true, true, true, true); + emit AmountWithdrawn(address(this), withdrawAmount); + yieldBox.withdraw(tUsdcAssetId, address(this), address(this), withdrawAmount, 0); + + // it should wrap & transfer to the receiver + assertEq(tUsdc.balanceOf(address(this)), withdrawAmount); + + // it should redeem a partial amount from farm + uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertLt(farmBalanceBefore - farmBalanceAfter, withdrawAmount); + + // it should leave zero allowance on inputToken for pearlmit + uint256 inputTokenAllowance = IERC20(usdc).allowance(address(strat), address(pearlmit)); + assertEq(inputTokenAllowance, 0); + } + + function test_RevertWhen_TheresNoDeposit(uint256 withdrawAmount) + external + whenWithdrawIsNotPaused + { + vm.assume(withdrawAmount > 0 && withdrawAmount <= LOW_DECIMALS_SMALL_AMOUNT); + // it should revert + vm.expectRevert(); + yieldBox.withdraw(tUsdcAssetId, address(this), address(this), withdrawAmount, 0); + } +} diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_withdraw.tree b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_withdraw.tree new file mode 100644 index 0000000..8a54604 --- /dev/null +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_withdraw.tree @@ -0,0 +1,21 @@ +StargateV2Strategy_withdraw.t.sol +├── when withdrawas are paused +│ └── it should revert with WithdrawPaused +└── when withdrawas are not paused + ├── when strategy has enough unstaked balance + │ ├── it should transfer to the receiver + │ └── it should not decrease farm balance + └── when strategy does not have enough balance + ├── when there's a deposited position + │ ├── when strategy has zero balance + │ │ ├── it should redeem the entire amount from farm + │ │ ├── it should decrease farm balance + │ │ ├── it should wrap & transfer to the receiver + │ │ └── it should leave zero allowance on inputToken for pearlmit + │ └── when strategy has less than the request amount + │ ├── it should redeem a partial amount from farm + │ ├── it should decrease farm balance + │ ├── it should wrap & transfer to the receiver + │ └── it should leave zero allowance on inputToken for pearlmit + └── when there's no deposit + └── it should revert \ No newline at end of file diff --git a/test/btt/utils/Constants.sol b/test/btt/utils/Constants.sol new file mode 100644 index 0000000..6003ed3 --- /dev/null +++ b/test/btt/utils/Constants.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +/// @notice Helper contract containing constants for testing. +abstract contract Constants { + // *************** // + // *** GENERIC *** // + // *************** // + uint256 public constant SMALL_AMOUNT = 1 ether; + uint256 public constant MEDIUM_AMOUNT = 10 ether; + uint256 public constant LARGE_AMOUNT = 100 ether; + + uint256 public constant LOW_DECIMALS_SMALL_AMOUNT = 1_000_000; + uint256 public constant LOW_DECIMALS_MEDIUM_AMOUNT = 10_000_000; + uint256 public constant LOW_DECIMALS_LARGE_AMOUNT = 100_000_000; + + uint256 public constant LOW_DECIMALS = 6; + uint256 public constant DEFAULT_DECIMALS = 18; + + uint256 public constant USER_A_PKEY = 0x1; + uint256 public constant USER_B_PKEY = 0x2; + + address public constant ADDRESS_ZERO = address(0); + uint256 public constant VALUE_ZERO = 0; + + uint256 public constant DEFAULT_ORACLE_RATE = 1 ether; + uint256 public constant FUTURE_TIMESTAMP = 1999999999; // 2033 + uint256 public constant STG_USDC_RATE = 10000000 * 1e10; //0.1 + uint256 public constant ARB_USDC_RATE = 10000000 * 1e10 ; //0.1 + + // **************** // + // *** PEARLMIT *** // + // **************** // + /// @dev Constant value representing the ERC721 token type for signatures and transfer hooks + uint256 constant TOKEN_TYPE_ERC721 = 721; + /// @dev Constant value representing the ERC1155 token type for signatures and transfer hooks + uint256 constant TOKEN_TYPE_ERC1155 = 1155; + /// @dev Constant value representing the ERC20 token type for signatures and transfer hooks + uint256 constant TOKEN_TYPE_ERC20 = 20; + + // ********************* // + // *** ENV VARIABLES *** // + // ********************* // + string constant ENV_BINANCE_WALLET_ADDRESS = "BINANCE_WALLET_ADDRESS"; + string constant ENV_POOL_ADDRESS = "STARGATEV2_POOL"; + string constant ENV_FARM_ADDRESS = "STARGATEV2_FARM"; + string constant ENV_USDC = "USDC"; + string constant ENV_WETH = "WETH"; + string constant RPC_URL = "ARBITRUM_RPC_URL"; + string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; + uint256 ARB_FORK; + + +} diff --git a/test/btt/utils/Events.sol b/test/btt/utils/Events.sol new file mode 100644 index 0000000..63aa8c9 --- /dev/null +++ b/test/btt/utils/Events.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// Tapioca +import {IZeroXSwapper} from "tap-utils/interfaces/periph/IZeroXSwapper.sol"; +import {ICluster} from "tap-utils/interfaces/periph/ICluster.sol"; + + +abstract contract Events { + // **************************** // + // *** STARGATE V2 STRATEGY *** // + // **************************** // + event AmountDeposited(uint256 amount); + event AmountWithdrawn(address indexed to, uint256 amount); + event ClusterUpdated(ICluster indexed oldCluster, ICluster indexed newCluster); + event SwapperUpdated(IZeroXSwapper indexed oldCluster, IZeroXSwapper indexed newCluster); + event PoolUpdated(address indexed oldAddy, address indexed newAddy); + event FarmUpdated(address indexed oldAddy, address indexed newAddy); + event Paused(bool prev, bool crt, bool isDepositType); + event ArbOracleUpdated(address indexed oldAddy, address indexed newAddy); + event StgOracleUpdated(address indexed oldAddy, address indexed newAddy); +} \ No newline at end of file diff --git a/test/btt/utils/Utils.sol b/test/btt/utils/Utils.sol new file mode 100644 index 0000000..b4fbccb --- /dev/null +++ b/test/btt/utils/Utils.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// external +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// utils +import {Constants} from "./Constants.sol"; + +// tapioca +import {IWrappedNative} from "yieldbox/interfaces/IWrappedNative.sol"; +import {Pearlmit, IPearlmit} from "tap-utils/pearlmit/Pearlmit.sol"; +import {YieldBoxURIBuilder} from "yieldbox/YieldBoxURIBuilder.sol"; +import {ZeroXSwapper} from "tap-utils/Swapper/ZeroXSwapper.sol"; +import {Cluster} from "tap-utils/Cluster/Cluster.sol"; +import {YieldBox} from "yieldbox/YieldBox.sol"; + +import {ICluster} from "tap-utils/interfaces/periph/ICluster.sol"; + +// test +import {OracleMock} from "tapioca-strategies/mocks/OracleMock.sol"; +import {MockERC20} from "tapioca-strategies/mocks/MockERC20.sol"; +import {ToftMock} from "tapioca-strategies/mocks/ToftMock.sol"; +import {Test} from "forge-std/Test.sol"; + +abstract contract Utils is Constants, Test { + // ************************ // + // *** GENERAL: HELPERS *** // + // ************************ // + /// @dev Stops the active prank and sets a new one. + function _resetPrank(address msgSender) internal { + vm.stopPrank(); + vm.startPrank(msgSender); + } + + // ********************** // + // *** DEPLOY HELPERS *** // + // ********************** // + // MockERC20 + function _createToken(string memory _name) internal returns (MockERC20) { + MockERC20 _token = new MockERC20(_name, _name); + vm.label(address(_token), _name); + return _token; + } + + // Creates TOFT mock + function _createToft(address _erc20, address _pearlmit) internal returns (ToftMock) { + ToftMock toft = new ToftMock(_erc20, "TOFT", "TOFT", IPearlmit(_pearlmit)); + vm.label(address(toft), "TOFT"); + return toft; + } + + // Creates user from Private key + function _createUser(uint256 _key, string memory _name) internal returns (address) { + address _user = vm.addr(_key); + vm.deal(_user, LARGE_AMOUNT); + vm.label(_user, _name); + return _user; + } + + // Creates real Cluster + function _createCluster(address _owner) internal returns (Cluster) { + Cluster cluster = new Cluster(0, _owner); + vm.label(address(cluster), "Cluster Test"); + return cluster; + } + + // Creates real Pearlmit + function _createPearlmit(address _owner) internal returns (Pearlmit) { + Pearlmit pearlmit = new Pearlmit("Pearlmit Test", "1", _owner, 0); + vm.label(address(pearlmit), "Pearlmit Test"); + return pearlmit; + } + + // Creates real YieldBox + function _createYieldBox(address _owner, Pearlmit _pearlmit) internal returns (YieldBox) { + YieldBoxURIBuilder uriBuilder = new YieldBoxURIBuilder(); + YieldBox yieldBox = new YieldBox(IWrappedNative(address(0)), uriBuilder, _pearlmit, _owner); + return yieldBox; + } + + // OracleMock; allows changing the current rate to simulate multiple situations + function _createOracle(string memory _name) internal returns (OracleMock) { + OracleMock _oracle = new OracleMock(_name, _name, DEFAULT_ORACLE_RATE); + vm.label(address(_oracle), _name); + return _oracle; + } + + // Creates real 0xSwapper + function _createSwapper(address _target, address _cluster, address _owner) internal returns (ZeroXSwapper) { + ZeroXSwapper swapper = new ZeroXSwapper(_target, ICluster(_cluster), _owner); + vm.label(address(swapper), "Swapper"); + return swapper; + } + + // ************************ // + // *** APPROVAL HELPERS *** // + // ************************ // + function _approveViaERC20(address token, address from, address operator, uint256 amount) internal { + _resetPrank({msgSender: from}); + IERC20(token).approve(address(operator), amount); + } + + function _approveViaPearlmit( + address token, + IPearlmit pearlmit, + address from, + address operator, + uint256 amount, + uint256 expiration, + uint256 tokenId + ) internal { + // Reset prank + _resetPrank({msgSender: from}); + + // Set approvals to pearlmit + IERC20(token).approve(address(pearlmit), amount); + + // Approve via pearlmit + pearlmit.approve(TOKEN_TYPE_ERC20, token, tokenId, operator, uint200(amount), uint48(expiration)); + } + + function _approveYieldBoxAssetId(YieldBox yieldBox, address from, address operator, uint256 assetId) internal { + _resetPrank({msgSender: from}); + yieldBox.setApprovalForAsset(operator, assetId, true); + } + + function _approveYieldBoxForAll(YieldBox yieldBox, address from, address operator) internal { + _resetPrank({msgSender: from}); + yieldBox.setApprovalForAll(operator, true); + } +} diff --git a/test/deprecated/GlpStrategy.t.sol.txt b/test/deprecated/GlpStrategy.t.sol.txt new file mode 100644 index 0000000..e4c7277 --- /dev/null +++ b/test/deprecated/GlpStrategy.t.sol.txt @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +// External +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Tapioca +import {IYieldBox, YieldBox, YieldBoxURIBuilder, IWrappedNative, TokenType, IStrategy} from "yieldbox/YieldBox.sol"; +import {IGmxRewardRouterV2} from "tapioca-strategies/interfaces/gmx/IGmxRewardRouter.sol"; +import {ITOFT} from "tapioca-periph/interfaces/oft/ITOFT.sol"; +import {ITapiocaOracle} from "tapioca-periph/interfaces/periph/ITapiocaOracle.sol"; +import {IGlpManager} from "tapioca-strategies/interfaces/gmx/IGlpManager.sol"; +import {IGmxVault} from "tapioca-strategies/interfaces/gmx/IGmxVault.sol"; +import {GlpStrategy} from "tapioca-strategies/glp/GlpStrategy.sol"; +import {ToftMock} from "tapioca-strategies/mocks/ToftMock.sol"; +import {OracleMock} from "tapioca-mocks/OracleMock.sol"; + +import "forge-std/Test.sol"; + +contract GlpStrategyTest is Test { + /** + * ENV KEY + */ + string constant ENV_BINANCE_WALLET_ADDRESS = "BINANCE_WALLET_ADDRESS"; + string constant ENV_GLP_REWARD_ROUTER = "GLP_REWARD_ROUTER"; + string constant ENV_GMX_REWARD_ROUTER = "GMX_REWARD_ROUTER"; + string constant ENV_STAKED_GLP = "STAKED_GLP"; + string constant ENV_GMX_VAULT = "GMX_VAULT"; + string constant RPC_URL = "RPC_URL"; + string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; + uint256 ARB_FORK; + + /** + * Contract loading + */ + IGmxRewardRouterV2 glpRewardRouter; + OracleMock public wethOracleMock; + GlpStrategy public glpStrategy; + IGlpManager public glpManager; + YieldBox public yieldBox; + IGmxVault gmxVault; + ToftMock tsGLP; + IERC20 sGLP; + + /** + * Vars + */ + uint256 public glpStratAssetId; + address public binanceWalletAddr; + address public weth; + + /** + * Modifiers + */ + modifier isArbFork() { + vm.selectFork(ARB_FORK); + _; + } + + modifier prankBinance() { + vm.startPrank(binanceWalletAddr); + _; + } + + /** + * Setup + */ + function setUp() public { + string memory rpcUrl = vm.envString(RPC_URL); + uint256 forkingBlockNumber = vm.envUint(FORKING_BLOCK_NUMBER); + ARB_FORK = vm.createSelectFork(rpcUrl, forkingBlockNumber); + + // Load env + binanceWalletAddr = vm.envAddress(ENV_BINANCE_WALLET_ADDRESS); + vm.label(binanceWalletAddr, "binanceWalletAddr"); + address glpRewardRouterAddr = vm.envAddress(ENV_GLP_REWARD_ROUTER); + vm.label(glpRewardRouterAddr, "glpRewardRouterAddr"); + address gmxRewardRouterAddr = vm.envAddress(ENV_GMX_REWARD_ROUTER); + vm.label(gmxRewardRouterAddr, "gmxRewardRouterAddr"); + address stakedGlpAddr = vm.envAddress(ENV_STAKED_GLP); + vm.label(stakedGlpAddr, "sGLP"); + address gmxVaultAddr = vm.envAddress(ENV_GMX_VAULT); + vm.label(gmxVaultAddr, "gmxVaultAddr"); + + // Get GMX contracts + glpRewardRouter = IGmxRewardRouterV2(glpRewardRouterAddr); + IGmxRewardRouterV2 gmxRewardRouter = IGmxRewardRouterV2(gmxRewardRouterAddr); + sGLP = IERC20(stakedGlpAddr); + gmxVault = IGmxVault(gmxVaultAddr); + weth = address(gmxRewardRouter.weth()); + glpManager = IGlpManager(glpRewardRouter.glpManager()); + vm.label(address(glpManager), "glpManager"); + + // Deploy contracts + tsGLP = new ToftMock(address(sGLP), "Toft", "TOFT"); + vm.label(address(tsGLP), "tsGLP"); + yieldBox = new YieldBox(IWrappedNative(weth), new YieldBoxURIBuilder()); + vm.label(address(yieldBox), "yieldBox"); + wethOracleMock = new OracleMock("wethOracleMock", "WOM", 1e18); + + // Deploy strategy + glpStrategy = new GlpStrategy( + IYieldBox(address(yieldBox)), + gmxRewardRouter, + glpRewardRouter, + ITOFT(address(tsGLP)), + ITapiocaOracle(address(wethOracleMock)), + "0x", + address(this) + ); + vm.label(address(glpStrategy), "glpStrategy"); + yieldBox.registerAsset(TokenType.ERC20, address(tsGLP), IStrategy(address(glpStrategy)), 0); + glpStratAssetId = yieldBox.ids(TokenType.ERC20, address(tsGLP), IStrategy(address(glpStrategy)), 0); + } + + /** + * Tests + */ + function test_constructor() public isArbFork { + uint256 glpPrice = glpManager.getPrice(true); + assertLe(glpPrice, 1041055094190371419655569666477); + + uint256 wethPrice = gmxVault.getMaxPrice(weth) / 1e12; + assertApproxEqAbs(wethPrice, 1805 * 1e18, 2 * 1e18); + } + + function test_compound_harvest() public isArbFork prankBinance { + uint256 glpPrice = glpManager.getPrice(true) / 1e12; + uint256 wethPrice = gmxVault.getMaxPrice(weth) / 1e12; + + // Get GLP and stake + { + uint256 ethBuyin = 1 ether; + uint256 minUsdg = ((wethPrice * ethBuyin) / 1e18 * 99) / 100; // 1% slippage + uint256 minGlp = (minUsdg * 1e18) / glpPrice; + glpRewardRouter.mintAndStakeGlpETH{value: ethBuyin}(minUsdg, minGlp); + + uint256 glpBal = sGLP.balanceOf(address(binanceWalletAddr)); + assertGe(glpBal, minGlp, "GLP out"); + } + + // Wrap sGLP and deposit into YieldBox/Strategy + uint256 glpBefore = sGLP.balanceOf(binanceWalletAddr); + { + // Wrap sGLP + sGLP.approve(address(tsGLP), glpBefore); + tsGLP.wrap(binanceWalletAddr, binanceWalletAddr, glpBefore); + + // Deposit into YieldBox + tsGLP.approve(address(yieldBox), glpBefore); + yieldBox.depositAsset(glpStratAssetId, binanceWalletAddr, binanceWalletAddr, glpBefore, 0); + } + + // Compound and withdraw + { + uint256 shares = yieldBox.balanceOf(binanceWalletAddr, glpStratAssetId); + compound((86400 * 365) / 10, 6); // Compound 6 times in 1 year (lol) + yieldBox.withdraw(glpStratAssetId, binanceWalletAddr, binanceWalletAddr, 0, shares); + uint256 glpBalAfter = tsGLP.balanceOf(binanceWalletAddr); + assertGt(glpBalAfter, glpBefore, "GLP compound out"); + } + } + + /** + * Utils + */ + function compound(uint256 t, uint256 n) internal { + glpStrategy.harvest(); + + uint256 r = t % n; + uint256 interval = (t - r) / n; + for (uint256 i; i < r; i++) { + vm.warp(block.timestamp + interval + 1); + glpStrategy.harvest(); + } + for (uint256 i; i < n; i++) { + vm.warp(block.timestamp + interval); + glpStrategy.harvest(); + } + } +} diff --git a/test/external/StargateMultiRewarder.sol b/test/external/StargateMultiRewarder.sol new file mode 100644 index 0000000..68ff814 --- /dev/null +++ b/test/external/StargateMultiRewarder.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +/// @dev creation bytecode fetched via arbiscan: https://arbiscan.io/address/0x3da4f8E456AC648c489c286B99Ca37B666be7C4C#readContract +// Deploy via creation bytecode due usage of immutable values (can't use etch) and to prevent to import entire Stargate codebase for one contract +library StargateMultiRewarder { + function creationCode() internal pure returns (bytes memory) { + return hex"60a06040523480156200001157600080fd5b5060405162002edc38038062002edc8339810160408190526200003491620000a1565b6200003f3362000051565b6001600160a01b0316608052620000d3565b600080546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b600060208284031215620000b457600080fd5b81516001600160a01b0381168114620000cc57600080fd5b9392505050565b608051612dca62000112600039600081816101140152818161076e01528181610816015281816109f101528181610f1d0152611a790152612dca6000f3fe6080604052600436106100e85760003560e01c8063aed457c81161008a578063cf17240311610059578063cf17240314610285578063ee8f931b14610399578063f2fde38b146103b9578063f42395f2146103d957600080fd5b8063aed457c814610210578063aeefd1fc14610230578063c1ef636414610250578063c2b18aa01461026357600080fd5b8063715018a6116100c6578063715018a614610181578063779bcb9b1461019657806380520969146101c45780638da5cb5b146101f257600080fd5b8063406b15cd146100ed5780634cf088d9146101025780635a564e8614610153575b600080fd5b6101006100fb36600461268f565b6103f9565b005b34801561010e57600080fd5b506101367f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b34801561015f57600080fd5b5061017361016e3660046126bb565b610593565b60405161014a929190612756565b34801561018d57600080fd5b506101006105aa565b3480156101a257600080fd5b506101b66101b1366004612784565b6105e4565b60405161014a9291906127bd565b3480156101d057600080fd5b506101e46101df3660046126bb565b6108c3565b60405161014a929190612814565b3480156101fe57600080fd5b506000546001600160a01b0316610136565b34801561021c57600080fd5b5061010061022b366004612879565b6108d1565b34801561023c57600080fd5b5061010061024b3660046128c4565b6109e6565b61010061025e366004612930565b610c8c565b34801561026f57600080fd5b50610278610f01565b60405161014a919061297f565b34801561029157600080fd5b5061033e6102a03660046126bb565b6040805160a081018252600080825260208201819052918101829052606081018290526080810191909152506001600160a01b03908116600090815260066020908152604091829020825160a0810184528154815260018201549485169281019290925265ffffffffffff600160a01b8504811693830193909352600160d01b909304909116606082015260029091015460ff161515608082015290565b60405161014a9190600060a082019050825182526001600160a01b036020840151166020830152604083015165ffffffffffff8082166040850152806060860151166060850152505060808301511515608083015292915050565b3480156103a557600080fd5b506101006103b43660046126bb565b610f12565b3480156103c557600080fd5b506101006103d43660046126bb565b611020565b3480156103e557600080fd5b506101006103f43660046129d7565b6110b0565b610401611132565b6001600160a01b0382166000908152600660205260409020600281015460ff16610467576040517fc3367e760000000000000000000000000000000000000000000000000000000081526001600160a01b03841660048201526024015b60405180910390fd5b600181015442600160d01b90910465ffffffffffff1610156104c0576040517f0fc659c20000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260240161045e565b80546104d5906104d09084612a89565b61118e565b600182018054601a906104f8908490600160d01b900465ffffffffffff16612ac4565b92506101000a81548165ffffffffffff021916908365ffffffffffff160217905550826001600160a01b03167f71d0060f3c1f2f0de478aca435ab612f7550b9415f91c6beac671faaca34bb688383600101601a9054906101000a900465ffffffffffff1660405161057c92919091825265ffffffffffff16602082015260400190565b60405180910390a261058e8383611210565b505050565b6060806105a1600184611279565b91509150915091565b6105b2611132565b6040517f20e02be700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b0382166000908152600460205260408120606091829161060a90611430565b90506000815167ffffffffffffffff81111561062857610628612ae3565b604051908082528060200260200182016040528015610651578160200160208202803683370190505b5090506000825167ffffffffffffffff81111561067057610670612ae3565b604051908082528060200260200182016040528015610699578160200160208202803683370190505b50905060005b83518110156108b45760006001800160008684815181106106c2576106c2612b12565b6020908102919091018101518252818101929092526040908101600090812060018101546001600160a01b031680835260069094529190208651919350919086908590811061071357610713612b12565b6001600160a01b03928316602091820292909201015260028301546040517ff7888aec00000000000000000000000000000000000000000000000000000000815290821660048201528a8216602482015261088d9183918c917f0000000000000000000000000000000000000000000000000000000000000000169063f7888aec90604401602060405180830381865afa1580156107b5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107d99190612b41565b60028601546040517fe4dc2aa40000000000000000000000000000000000000000000000000000000081526001600160a01b0391821660048201527f00000000000000000000000000000000000000000000000000000000000000009091169063e4dc2aa490602401602060405180830381865afa15801561085f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906108839190612b41565b8693929190611444565b84848151811061089f5761089f612b12565b6020908102919091010152505060010161069f565b509093509150505b9250929050565b6060806105a16001846114a7565b6108d9611132565b6108e4600184611655565b80156109925760006001600160a01b03841615610981576040517f70a082310000000000000000000000000000000000000000000000000000000081523060048201526001600160a01b038516906370a0823190602401602060405180830381865afa158015610958573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061097c9190612b41565b610983565b475b9050610990838583611816565b505b816001600160a01b0316836001600160a01b03167ff842ed66d8fd158c4e8a71051d6493bff2e7f1994ea49e9713600c9aaa9cc4a7836040516109d9911515815260200190565b60405180910390a3505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a4a576040517f9f49538400000000000000000000000000000000000000000000000000000000815233600482015260240161045e565b6001600160a01b0385166000908152600460205260408120610a6b90611430565b90506000815167ffffffffffffffff811115610a8957610a89612ae3565b604051908082528060200260200182016040528015610ab2578160200160208202803683370190505b5090506000825167ffffffffffffffff811115610ad157610ad1612ae3565b604051908082528060200260200182016040528015610afa578160200160208202803683370190505b50905060005b8351811015610bca576000600180016000868481518110610b2357610b23612b12565b60200260200101518152602001908152602001600020905060008160010160009054906101000a90046001600160a01b0316905080858481518110610b6a57610b6a612b12565b6001600160a01b039283166020918202929092018101919091529082166000908152600690915260409020610ba39083908c8c8c6118dd565b848481518110610bb557610bb5612b12565b60209081029190910101525050600101610b00565b50866001600160a01b03167fc53cb8bc1a7200a84d0b66a538905a245c4915aace7f1ce5dc4a0ba107ebc15c8383604051610c069291906127bd565b60405180910390a260005b8351811015610c81576000828281518110610c2e57610c2e612b12565b60200260200101511115610c7957610c7988848381518110610c5257610c52612b12565b6020026020010151848481518110610c6c57610c6c612b12565b6020026020010151611816565b600101610c11565b505050505050505050565b610c94611132565b428265ffffffffffff161015610ce0576040517f53c1289f00000000000000000000000000000000000000000000000000000000815265ffffffffffff8316600482015260240161045e565b8065ffffffffffff16600003610d22576040517f95cf0dc400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000610d2f6001866118f9565b9050610d3a856119bc565b60018101548490600160d01b900465ffffffffffff16421015610dcc57600182015460009042600160a01b90910465ffffffffffff1611610d7b5742610d90565b6001830154600160a01b900465ffffffffffff165b6001840154909150610db2908290600160d01b900465ffffffffffff16612b5a565b8354610dbe9190612b6d565b610dc89083612b84565b9150505b6000610de065ffffffffffff851683612a89565b905080600003610e1c576040517f6e9377ee00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001830180547fffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffff16600160a01b65ffffffffffff881602179055610e608486612ac4565b60018401805465ffffffffffff928316600160d01b0279ffffffffffffffffffffffffffffffffffffffffffffffffffff9091161790558184556040805188815260208101859052878316818301529186166060830152516001600160a01b038916917fa57b91f8b94eace9e74d336f5f3202d0eb4cb489f646fc322c7ed0f00fcd99fd919081900360800190a2610ef88787611210565b50505050505050565b6060610f0d6007611430565b905090565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610f76576040517f9f49538400000000000000000000000000000000000000000000000000000000815233600482015260240161045e565b6001600160a01b03811660009081526009602052604090205460ff1615610fd4576040517f8575f3a60000000000000000000000000000000000000000000000000000000081526001600160a01b038216600482015260240161045e565b6001600160a01b038116600081815260096020526040808220805460ff19166001179055517fa351e5ceb90bd585957b38958d172682cb48fcff0320a4395c976ccdb40e44539190a250565b611028611132565b6001600160a01b0381166110a45760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f6464726573730000000000000000000000000000000000000000000000000000606482015260840161045e565b6110ad81611af8565b50565b6110b8611132565b6110c1856119bc565b6110d060018686868686611b60565b83836040516110e0929190612b97565b6040518091039020856001600160a01b03167fa8f10febbe8be4d24be81ee9f81f1a380ac400eb0cfc898b153b15dbaa70ae748484604051611123929190612bd9565b60405180910390a35050505050565b6000546001600160a01b0316331461118c5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161045e565b565b600065ffffffffffff82111561120c5760405162461bcd60e51b815260206004820152602660248201527f53616665436173743a2076616c756520646f65736e27742066697420696e203460448201527f3820626974730000000000000000000000000000000000000000000000000000606482015260840161045e565b5090565b6001600160a01b03821661126457803414611260576040517f8b00479e0000000000000000000000000000000000000000000000000000000081526004810182905234602482015260440161045e565b5050565b6112606001600160a01b038316333084611d01565b6001600160a01b0381166000908152600383016020526040812060609182916112a190611430565b9050805167ffffffffffffffff8111156112bd576112bd612ae3565b6040519080825280602002602001820160405280156112e6578160200160208202803683370190505b509250805167ffffffffffffffff81111561130357611303612ae3565b60405190808252806020026020018201604052801561132c578160200160208202803683370190505b50915060005b81518110156114275785600101600083838151811061135357611353612b12565b6020026020010151815260200190815260200160002060010160009054906101000a90046001600160a01b031684828151811061139257611392612b12565b60200260200101906001600160a01b031690816001600160a01b0316815250508560010160008383815181106113ca576113ca612b12565b60200260200101518152602001908152602001600020600101601a9054906101000a900465ffffffffffff1683828151811061140857611408612b12565b65ffffffffffff90921660209283029190910190910152600101611332565b50509250929050565b6060600061143d83611dd0565b9392505050565b600080611452878785611e2c565b6001600160a01b038616600090815260038901602052604090205490915069d3c21bcecceda10000009085906114889084612b5a565b6114929190612b6d565b61149c9190612a89565b979650505050505050565b6001600160a01b0381166000908152600283016020526040812060609182916114cf90611430565b9050805167ffffffffffffffff8111156114eb576114eb612ae3565b604051908082528060200260200182016040528015611514578160200160208202803683370190505b509250805167ffffffffffffffff81111561153157611531612ae3565b60405190808252806020026020018201604052801561155a578160200160208202803683370190505b50915060005b81518110156114275785600101600083838151811061158157611581612b12565b6020026020010151815260200190815260200160002060020160009054906101000a90046001600160a01b03168482815181106115c0576115c0612b12565b60200260200101906001600160a01b031690816001600160a01b0316815250508560010160008383815181106115f8576115f8612b12565b60200260200101518152602001908152602001600020600101601a9054906101000a900465ffffffffffff1683828151811061163657611636612b12565b65ffffffffffff90921660209283029190910190910152600101611560565b6001600160a01b038116600090815260058301602052604090206002015460ff166116b7576040517fc3367e760000000000000000000000000000000000000000000000000000000081526001600160a01b038216600482015260240161045e565b6001600160a01b038116600090815260028301602052604081206116da90611430565b905060005b81518110156117d45760008282815181106116fc576116fc612b12565b60209081029190910181015160008181526001880183526040808220600201546001600160a01b031680835260038a01909452902090925061173e9083611f7d565b506001600160a01b038516600090815260028701602052604090206117639083611f7d565b506001600160a01b0380861660009081526004880160209081526040808320949093168252928352818120819055928352600180880190925290912060020180547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff16600160a01b179055016116df565b506117e26006840183611f89565b50506001600160a01b031660009081526005909101602052604081208181556001810191909155600201805460ff19169055565b6001600160a01b0382166118c9576000836001600160a01b03168260405160006040518083038185875af1925050503d8060008114611871576040519150601f19603f3d011682016040523d82523d6000602084013e611876565b606091505b50509050806118c3576040517f5c4c2a250000000000000000000000000000000000000000000000000000000081526001600160a01b03841660048201526024810183905260440161045e565b50505050565b61058e6001600160a01b0383168483611f9e565b6000806118eb878785611fe7565b905061149c8786868461203a565b6001600160a01b03811660009081526005830160205260409020600281015460ff166119b657606461192d846006016120ac565b10611964576040517f290e96a500000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60028101805460ff1916600117905561198060068401836120b6565b506040516001600160a01b038316907f2967504cad2094d65ef2dcb85e4074f4f7455a846798fcc90657d6f33c4125ea90600090a25b92915050565b6001600160a01b03811660009081526003602052604081206119dd906120ac565b905060005b8181101561058e576001600160a01b03831660009081526003602052604081206002908290611a1190856120cb565b8152602080820192909252604090810160009081206001600160a01b038881168352600690945290829020600282015492517fe4dc2aa40000000000000000000000000000000000000000000000000000000081529284166004840152909350611aee9290917f00000000000000000000000000000000000000000000000000000000000000009091169063e4dc2aa490602401602060405180830381865afa158015611ac2573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ae69190612b41565b839190611fe7565b50506001016119e2565b600080546001600160a01b038381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b6000611b6c87876118f9565b905060008085815b81811015611c94576000611baf8c8c8c8c86818110611b9557611b95612b12565b9050602002016020810190611baa91906126bb565b6120d7565b600081815260018e810160205260409091200154909150611bdf90600160d01b900465ffffffffffff1686612c24565b9450878783818110611bf357611bf3612b12565b9050602002016020810190611c089190612c44565b611c1a9065ffffffffffff1685612c24565b9350878783818110611c2e57611c2e612b12565b9050602002016020810190611c439190612c44565b60009182526001808e0160205260409092208201805465ffffffffffff92909216600160d01b0279ffffffffffffffffffffffffffffffffffffffffffffffffffff90921691909117905501611b74565b5060018401548390611cb09084906001600160a01b0316612c24565b611cba9190612c5f565b60019490940180547fffffffffffffffffffffffff0000000000000000000000000000000000000000166001600160a01b0390951694909417909355505050505050505050565b6040516001600160a01b03808516602483015283166044820152606481018290526118c39085907f23b872dd00000000000000000000000000000000000000000000000000000000906084015b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529190526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff0000000000000000000000000000000000000000000000000000000090931692909217909152612300565b606081600001805480602002602001604051908101604052809291908181526020018280548015611e2057602002820191906000526020600020905b815481526020019060010190808311611e0c575b50505050509050919050565b60018084015490830154600091829165ffffffffffff600160a01b928390048116929091041611611e70576001850154600160a01b900465ffffffffffff16611e85565b6001840154600160a01b900465ffffffffffff165b600185015465ffffffffffff918216925060009142600160d01b9092041610611eae5742611ec3565b6001850154600160d01b900465ffffffffffff165b90508082101580611ed2575083155b80611ee8575060018501546001600160a01b0316155b15611ef85750508354905061143d565b85546001868101549088015486916001600160a01b03169069d3c21bcecceda100000090600160d01b900465ffffffffffff16611f358787612b5a565b8a54611f419190612b6d565b611f4b9190612b6d565b611f559190612b6d565b611f5f9190612a89565b611f699190612a89565b611f739190612b84565b9695505050505050565b600061143d83836123e8565b600061143d836001600160a01b0384166123e8565b6040516001600160a01b03831660248201526044810182905261058e9084907fa9059cbb0000000000000000000000000000000000000000000000000000000090606401611d4e565b6000611ff4848484611e2c565b808555600190940180547fffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffff16600160a01b4265ffffffffffff1602179055509192915050565b6001600160a01b0383166000908152600385016020526040812054819069d3c21bcecceda100000090859061206f9086612b5a565b6120799190612b6d565b6120839190612a89565b6001600160a01b038616600090815260038801602052604090208490559150505b949350505050565b60006119b6825490565b600061143d836001600160a01b0384166124e2565b600061143d8383612531565b6001600160a01b03808316600090815260048501602090815260408083209385168352929052908120549081900361143d576001600160a01b0383166000908152600285016020526040902060649061212f906120ac565b10612166576040517f2fee563000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6001600160a01b038216600090815260088501602052604090205460ff166121c5576040517fff9864e50000000000000000000000000000000000000000000000000000000081526001600160a01b038316600482015260240161045e565b83600001600081546121d690612c7f565b91829055506001600160a01b03808516600081815260048801602090815260408083209488168352938152838220859055918152600288019091522090915061221f908261255b565b506001600160a01b03821660009081526003850160205260409020612244908261255b565b506000818152600185810160205260408083209182018054600290930180547fffffffffffffffffffffffff0000000000000000000000000000000000000000166001600160a01b038881169182179092559088167fffffffffffff00000000000000000000000000000000000000000000000000009094168417600160a01b4265ffffffffffff160217909155905190927f26f4b31b7240e7422a9fe2ba5ce7684500302a536166d0ed481d7ad653ff25ab91a39392505050565b6000612355826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b03166125679092919063ffffffff16565b90508051600014806123765750808060200190518101906123769190612cb7565b61058e5760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e60448201527f6f74207375636365656400000000000000000000000000000000000000000000606482015260840161045e565b600081815260018301602052604081205480156124d157600061240c600183612b5a565b855490915060009061242090600190612b5a565b905081811461248557600086600001828154811061244057612440612b12565b906000526020600020015490508087600001848154811061246357612463612b12565b6000918252602080832090910192909255918252600188019052604090208390555b855486908061249657612496612cd4565b6001900381819060005260206000200160009055905585600101600086815260200190815260200160002060009055600193505050506119b6565b60009150506119b6565b5092915050565b6000818152600183016020526040812054612529575081546001818101845560008481526020808220909301849055845484825282860190935260409020919091556119b6565b5060006119b6565b600082600001828154811061254857612548612b12565b9060005260206000200154905092915050565b600061143d83836124e2565b60606120a4848460008585600080866001600160a01b0316858760405161258e9190612d27565b60006040518083038185875af1925050503d80600081146125cb576040519150601f19603f3d011682016040523d82523d6000602084013e6125d0565b606091505b509150915061149c878383876060831561264b578251600003612644576001600160a01b0385163b6126445760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e7472616374000000604482015260640161045e565b50816120a4565b6120a483838151156126605781518083602001fd5b8060405162461bcd60e51b815260040161045e9190612d43565b6001600160a01b03811681146110ad57600080fd5b600080604083850312156126a257600080fd5b82356126ad8161267a565b946020939093013593505050565b6000602082840312156126cd57600080fd5b813561143d8161267a565b60008151808452602080850194506020840160005b838110156127125781516001600160a01b0316875295820195908201906001016126ed565b509495945050505050565b60008151808452602080850194506020840160005b8381101561271257815165ffffffffffff1687529582019590820190600101612732565b60408152600061276960408301856126d8565b828103602084015261277b818561271d565b95945050505050565b6000806040838503121561279757600080fd5b82356127a28161267a565b915060208301356127b28161267a565b809150509250929050565b6040815260006127d060408301856126d8565b82810360208481019190915284518083528582019282019060005b81811015612807578451835293830193918301916001016127eb565b5090979650505050505050565b604080825283519082018190526000906020906060840190828701845b828110156128565781516001600160a01b031684529284019290840190600101612831565b5050508381036020850152611f73818661271d565b80151581146110ad57600080fd5b60008060006060848603121561288e57600080fd5b83356128998161267a565b925060208401356128a98161267a565b915060408401356128b98161286b565b809150509250925092565b600080600080600060a086880312156128dc57600080fd5b85356128e78161267a565b945060208601356128f78161267a565b94979496505050506040830135926060810135926080909101359150565b803565ffffffffffff8116811461292b57600080fd5b919050565b6000806000806080858703121561294657600080fd5b84356129518161267a565b93506020850135925061296660408601612915565b915061297460608601612915565b905092959194509250565b60208152600061143d60208301846126d8565b60008083601f8401126129a457600080fd5b50813567ffffffffffffffff8111156129bc57600080fd5b6020830191508360208260051b85010111156108bc57600080fd5b6000806000806000606086880312156129ef57600080fd5b85356129fa8161267a565b9450602086013567ffffffffffffffff80821115612a1757600080fd5b612a2389838a01612992565b90965094506040880135915080821115612a3c57600080fd5b50612a4988828901612992565b969995985093965092949392505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600082612abf577f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b500490565b65ffffffffffff8181168382160190808211156124db576124db612a5a565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600060208284031215612b5357600080fd5b5051919050565b818103818111156119b6576119b6612a5a565b80820281158282048414176119b6576119b6612a5a565b808201808211156119b6576119b6612a5a565b60008184825b85811015612bce578135612bb08161267a565b6001600160a01b031683526020928301929190910190600101612b9d565b509095945050505050565b60208082528181018390526000908460408401835b86811015612c195765ffffffffffff612c0684612915565b1682529183019190830190600101612bee565b509695505050505050565b6001600160a01b038181168382160190808211156124db576124db612a5a565b600060208284031215612c5657600080fd5b61143d82612915565b6001600160a01b038281168282160390808211156124db576124db612a5a565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203612cb057612cb0612a5a565b5060010190565b600060208284031215612cc957600080fd5b815161143d8161286b565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b60005b83811015612d1e578181015183820152602001612d06565b50506000910152565b60008251612d39818460208701612d03565b9190910192915050565b6020815260008251806020840152612d62816040850160208701612d03565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016919091016040019291505056fea264697066735822122067296dd182c1bc08332182b8a32d7058a27d9c387da1735d2f5e912941bdb99f64736f6c63430008160033"; + } +} \ No newline at end of file diff --git a/test/glpStrategy/GlpStrategy.t.sol b/test/glpStrategy/GlpStrategy.t.sol index 92fc913..4045aab 100644 --- a/test/glpStrategy/GlpStrategy.t.sol +++ b/test/glpStrategy/GlpStrategy.t.sol @@ -34,7 +34,7 @@ contract GlpStrategyTest is Test { string constant ENV_STAKED_GLP = "STAKED_GLP"; string constant ENV_GMX_VAULT = "GMX_VAULT"; string constant RPC_URL = "ARBITRUM_RPC_URL"; - string constant FORKING_BLOCK_NUMBER = "FORKING_ARBITRUM_BLOCK_NUMBER"; + string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; uint256 ARB_FORK; /** diff --git a/test/sDaiStrategy/sDaiStrategy.t.sol b/test/sDaiStrategy/sDaiStrategy.t.sol index 4c159ec..58f2787 100644 --- a/test/sDaiStrategy/sDaiStrategy.t.sol +++ b/test/sDaiStrategy/sDaiStrategy.t.sol @@ -19,7 +19,7 @@ contract SDaiStrategyTest is Test { string constant ENV_BINANCE_WALLET_ADDRESS = "BINANCE_WALLET_ADDRESS"; string constant ENV_DAI_ADDRESS = "DAI_ADDRESS"; string constant ENV_SAVINGS_DAI_ADDRESS = "SDAI"; - string constant ENV_WETH_ADDRESS = "WETH_ADDRESS"; + string constant ENV_WETH_ADDRESS = "WETH"; string constant RPC_URL = "RPC_URL"; string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; uint256 MAINNET_FORK; diff --git a/test_hardhat/fork/stargateV2Strategy-fork.test.ts b/test_hardhat/fork/stargateV2Strategy-fork.test.ts new file mode 100644 index 0000000..28117c9 --- /dev/null +++ b/test_hardhat/fork/stargateV2Strategy-fork.test.ts @@ -0,0 +1,105 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { expect } from 'chai'; +import hre, { ethers } from 'hardhat'; +import { loadNetworkFork, registerFork } from '../test.utils'; + +async function become(address: string) { + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [address], + }); + return ethers.getSigner(address); +} + + +let BINANCE_WALLET_ADDRESS: string, + WETH: string, + USDC: string, + STARGATEV2_POOL: string, + STARGATEV2_FARM: string; +describe('stargateV2Strategy-fork test', () => { + before(function () { + if (process.env.NETWORK != 'arbitrum') { + this.skip(); + } + loadNetworkFork(); + + ({ + BINANCE_WALLET_ADDRESS, + STARGATEV2_POOL, + STARGATEV2_FARM, + WETH, + USDC, + } = process.env); + }); + + async function setUp() { + const me = await become(BINANCE_WALLET_ADDRESS); + const deployer = (await ethers.getSigners())[0]; + + const weth = ( + await ethers.getContractAt( + 'IWETHToken', + WETH, + ) + ).connect(me); + + const usdc = ( + await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', + USDC, + ) + ).connect(me); + + // Deploy YieldBox + const uriBuilder = await ( + await ethers.getContractFactory('YieldBoxURIBuilder') + ).deploy(); + await uriBuilder.deployed(); + let yieldBox = await ( + await ethers.getContractFactory('YieldBox') + ).deploy(weth.address, uriBuilder.address); + await yieldBox.deployed(); + yieldBox = yieldBox.connect(me); + + const stargateV2Pool = ( + await ethers.getContractAt( + 'tapioca-strategies/interfaces/stargatev2/IStargateV2Pool.sol:IStargateV2Pool', + STARGATEV2_POOL, + ) + ).connect(me); + + const stargateV2Farm = ( + await ethers.getContractAt( + 'tapioca-strategies/interfaces/stargatev2/IStargateV2Staking.sol:IStargateV2Staking', + STARGATEV2_FARM, + ) + ).connect(me); + + return { + me, + deployer, + weth, + usdc, + yieldBox, + stargateV2Pool, + stargateV2Farm + }; + } + + it.only('Should set up the strategy', async () => { + const { + me, + deployer, + weth, + usdc, + yieldBox, + stargateV2Pool, + stargateV2Farm, + } = await loadFixture(setUp); + + // 10 USDC + const amount = ethers.BigNumber.from((1e8).toString()).mul(10); + await usdc.connect(BINANCE_WALLET_ADDRESS).transfer(deployer.address, amount); + }) +}); \ No newline at end of file