From 22efc13276fbcad279fb0606521c5854fb80edf0 Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Thu, 11 Jul 2024 09:17:35 +0300 Subject: [PATCH 01/18] feat(`stargate v2 strategy`): added `StargateV2Strategy` [`86du2qrnp`] --- .../StargateV2Strategy/StargateV2Strategy.sol | 414 ++++++++++++++++++ .../stargatev2/IStargateV2MultiRewarder.sol | 6 + .../interfaces/stargatev2/IStargateV2Pool.sol | 13 + .../stargatev2/IStargateV2Staking.sol | 10 + contracts/mocks/ToftMock.sol | 15 +- contracts/mocks/ZeroXSwapperMockTarget.sol | 33 ++ test/StargateV2Strategy.t.sol | 281 ++++++++++++ .../GlpStrategy.t.sol.txt} | 0 8 files changed, 770 insertions(+), 2 deletions(-) create mode 100644 contracts/StargateV2Strategy/StargateV2Strategy.sol create mode 100644 contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol create mode 100644 contracts/interfaces/stargatev2/IStargateV2Pool.sol create mode 100644 contracts/interfaces/stargatev2/IStargateV2Staking.sol create mode 100644 contracts/mocks/ZeroXSwapperMockTarget.sol create mode 100644 test/StargateV2Strategy.t.sol rename test/{GlpStrategy.t.sol => deprecated/GlpStrategy.t.sol.txt} (100%) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol new file mode 100644 index 0000000..36ca4e7 --- /dev/null +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -0,0 +1,414 @@ +// 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 {IStargateV2Staking} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Staking.sol"; +import {IStargateV2Pool} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Pool.sol"; +import {ITapiocaOracle} from "tapioca-periph/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tapioca-periph/interfaces/periph/IZeroXSwapper.sol"; +import {IPearlmit} from "tapioca-periph/interfaces/periph/IPearlmit.sol"; +import {BaseERC20Strategy} from "yieldbox/strategies/BaseStrategy.sol"; +import {ICluster} from "tapioca-periph/interfaces/periph/ICluster.sol"; +import {ITOFT} from "tapioca-periph/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 pool; + IStargateV2Staking public farm; + IERC20 public inputToken; //erc20 of token.erc20() + IERC20 public lpToken; + IZeroXSwapper public swapper; + + ITapiocaOracle public stgInputTokenOracle; + bytes public stgInputTokenOracleData; + + ITapiocaOracle public arbInputTokenOracle; + bytes public arbInputTokenOracleData; + + ICluster internal cluster; + 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; + + // ************** // + // *** 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 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); + + // ************** // + // *** ERRORS *** // + // ************** // + error TokenNotValid(); + error TransferFailed(); + error DepositPaused(); + error WithdrawPaused(); + error NotEnough(); + error PauserNotAuthorized(); + error EmptyAddress(); + error SwapFailed(); + + constructor( + IYieldBox _yieldBox, + ICluster _cluster, + address _token, + address _pool, + address _farm, + ITapiocaOracle _stgInputTokenOracle, + bytes memory _stgInputTokenOracleData, + ITapiocaOracle _arbInputTokenOracle, + bytes memory _arbInputTokenOracleData, + IZeroXSwapper _swapper, + address _owner + ) BaseERC20Strategy(_yieldBox, _token) { + if (_pool == address(0)) revert EmptyAddress(); + if (_farm == address(0)) revert EmptyAddress(); + + cluster = _cluster; + + pool = IStargateV2Pool(_pool); + farm = IStargateV2Staking(_farm); + inputToken = IERC20(ITOFT(_token).erc20()); + lpToken = IERC20(pool.lpToken()); + + stgInputTokenOracle = _stgInputTokenOracle; + stgInputTokenOracleData = _stgInputTokenOracleData; + + arbInputTokenOracle = _arbInputTokenOracle; + arbInputTokenOracleData = _arbInputTokenOracleData; + + swapper = _swapper; + + transferOwnership(_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)); // TODO: check if lpToken <> token ratio is 1:1 + + // 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); + pearlmit.clearAllowance(address(this), 20, address(inputToken), 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 pool address. + * @dev can only be called by the owner. + * @param _pool the new address. + */ + function setPool(address _pool) external onlyOwner { + if (address(_pool) == address(0)) revert EmptyAddress(); + emit PoolUpdated(address(pool), _pool); + pool = IStargateV2Pool(_pool); + } + + /** + * @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(); + emit FarmUpdated(address(farm), _farm); + farm = IStargateV2Staking(_farm); + } + + /** + * @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 invests currently available STG for compounding interest + */ + function invest(bytes calldata arbData, bytes calldata stgData) external { + IERC20 _stg = IERC20(STG); + IERC20 _arb = IERC20(ARB); + + // should only harvest for the current `lpToken` + uint256 availableStg = _stg.balanceOf(address(this)); + uint256 availableArb = _arb.balanceOf(address(this)); + if (availableStg == 0 && availableArb == 0) return; + + if(availableStg > 0) { + // swap STG to usdc + SSwapData memory swapData = abi.decode(stgData, (SSwapData)); + _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(); + + // _deposit & stake + _depositAndStake(amountOut); + } + + if (availableArb > 0) { + // swap STG to usdc + SSwapData memory swapData = abi.decode(arbData, (SSwapData)); + _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(); + + // _deposit & stake + _depositAndStake(amountOut); + } + } + + // ********************** // + // *** 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 that should be harvested + */ + function pendingRewards() public view returns (uint256 amount) { + address _rewarder = farm.rewarder(address(lpToken)); + (address[] memory tokens, uint256[] memory rewards) = IStargateV2MultiRewarder(_rewarder).getRewards(address(lpToken), address(this)); + + uint256 _index = _findIndex(tokens, STG); + uint256 rewardAmount = rewards[_index]; + if (rewardAmount == 0) return 0; + + (, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); + amount = (rewardAmount * stgPrice) / 1e18; + } + + /** + * @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: wrap fees are not taken into account here because it's 0 + amount = farm.balanceOf(address(lpToken), address(this)); // TODO: check if lpToken <> token ratio is 1:1 + 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); + } + + + 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); + + // send `contractAddress` + IERC20(contractAddress).safeTransfer(to, amount); + } + + // ********************************* // + /* ============ 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; + } + } + revert TokenNotValid(); + } + + 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); + } + +} \ 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..5bd47d4 --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +interface IStargateV2MultiRewarder { + function getRewards(address stakingToken, address user) external view returns (address[] memory, uint256[] memory); +} \ 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..ba6c1b7 --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2Pool.sol @@ -0,0 +1,13 @@ +// 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); +} \ 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..78cb220 --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2Staking.sol @@ -0,0 +1,10 @@ +// 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; +} \ No newline at end of file diff --git a/contracts/mocks/ToftMock.sol b/contracts/mocks/ToftMock.sol index 70e4234..355f10f 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 "tapioca-periph/pearlmit/Pearlmit.sol"; /* ████████╗ █████╗ ██████╗ ██╗ ██████╗ ██████╗ █████╗ @@ -14,19 +15,29 @@ import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract ToftMock is ERC20 { address public erc20; + IPearlmit public pearlmit; + + error FailedToWrap(); constructor(address erc20_, string memory name_, string memory symbol_) ERC20(name_, symbol_) { erc20 = erc20_; } + function setPearlmit(IPearlmit _pearlmit) external { + pearlmit = _pearlmit; + } + function wrap(address from, address to, uint256 amount) external payable returns (uint256 minted) { - IERC20(erc20).transferFrom(from, address(this), amount); + bool isErr = pearlmit.transferFromERC20(from, address(this), erc20, amount); + if (isErr) revert FailedToWrap(); + _mint(to, amount); return amount; } - function unwrap(address to, uint256 amount) external { + 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/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol new file mode 100644 index 0000000..adc8a6d --- /dev/null +++ b/test/StargateV2Strategy.t.sol @@ -0,0 +1,281 @@ +// 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 "tapioca-periph/pearlmit/Pearlmit.sol"; +import {ITapiocaOracle} from "tapioca-periph/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tapioca-periph/interfaces/periph/IZeroXSwapper.sol"; +import {BaseERC20Strategy} from "yieldbox/strategies/BaseStrategy.sol"; +import {ICluster} from "tapioca-periph/interfaces/periph/ICluster.sol"; +import {ZeroXSwapper} from "tapioca-periph/Swapper/ZeroXSwapper.sol"; +import {ITOFT} from "tapioca-periph/interfaces/oft/ITOFT.sol"; +import {IYieldBox} from "yieldbox/interfaces/IYieldBox.sol"; +import {Cluster} from "tapioca-periph/Cluster/Cluster.sol"; +import {OracleMock} from "tapioca-mocks/OracleMock.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 = "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; + 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); + tUsdc = new ToftMock(address(usdc), "Toft", "TOFT"); + tUsdc.setPearlmit(IPearlmit(address(pearlmit))); + yieldBox = new YieldBox(IWrappedNative(address(weth)), new YieldBoxURIBuilder()); + cluster = new Cluster(0, address(this)); + swapperTarget = new ZeroXSwapperMockTarget(); + swapper = new ZeroXSwapper(address(swapperTarget), ICluster(address(cluster)), address(this)); + + strat = new StargateV2Strategy( + IYieldBox(address(yieldBox)), + ICluster(address(cluster)), + address(tUsdc), + address(pool), + address(farm), + ITapiocaOracle(address(stgOracleMock)), + "0x", + ITapiocaOracle(address(arbOracleMock)), + "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 + }); + + uint256 farmBalanceBefore = farm.balanceOf(address(strat.lpToken()), address(strat)); + + cluster.updateContract(0, address(strat), true); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData)); + + uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertGt(farmBalanceAfter, farmBalanceBefore); + } + + 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); + + } +} diff --git a/test/GlpStrategy.t.sol b/test/deprecated/GlpStrategy.t.sol.txt similarity index 100% rename from test/GlpStrategy.t.sol rename to test/deprecated/GlpStrategy.t.sol.txt From dbe4428b8242237f9f7acee7f61e0edb422b65cc Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Thu, 11 Jul 2024 09:58:04 +0300 Subject: [PATCH 02/18] chore: missing onlyOwner and events --- contracts/StargateV2Strategy/StargateV2Strategy.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index 36ca4e7..a51e0bb 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -81,7 +81,6 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { error TransferFailed(); error DepositPaused(); error WithdrawPaused(); - error NotEnough(); error PauserNotAuthorized(); error EmptyAddress(); error SwapFailed(); @@ -154,7 +153,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { depositPaused = true; withdrawPaused = true; - uint256 amount = farm.balanceOf(address(lpToken), address(this)); // TODO: check if lpToken <> token ratio is 1:1 + uint256 amount = farm.balanceOf(address(lpToken), address(this)); // withdraw from farm farm.withdraw(address(lpToken), amount); @@ -250,7 +249,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { /** * @notice invests currently available STG for compounding interest */ - function invest(bytes calldata arbData, bytes calldata stgData) external { + function invest(bytes calldata arbData, bytes calldata stgData) external onlyOwner { IERC20 _stg = IERC20(STG); IERC20 _arb = IERC20(ARB); @@ -331,7 +330,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { // *********************************** // function _currentBalance() internal view override returns (uint256 amount) { /// @dev: wrap fees are not taken into account here because it's 0 - amount = farm.balanceOf(address(lpToken), address(this)); // TODO: check if lpToken <> token ratio is 1:1 + amount = farm.balanceOf(address(lpToken), address(this)); amount += IERC20(contractAddress).balanceOf(address(this)); amount += pendingRewards(); } @@ -385,6 +384,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { // send `contractAddress` IERC20(contractAddress).safeTransfer(to, amount); + emit AmountWithdrawn(to, amount); } // ********************************* // @@ -409,6 +409,8 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { lpToken.safeApprove(address(farm), type(uint256).max); farm.deposit(address(lpToken), lpAmount); lpToken.safeApprove(address(farm), 0); + + event AmountDeposited(uint256 lpAmount); } } \ No newline at end of file From 94834005d91963e2af37de3dc7185c65390a6ee2 Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Thu, 11 Jul 2024 16:16:54 +0300 Subject: [PATCH 03/18] chore: used arb oracle --- contracts/StargateV2Strategy/StargateV2Strategy.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a51e0bb..f13948b 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -309,11 +309,15 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { (address[] memory tokens, uint256[] memory rewards) = IStargateV2MultiRewarder(_rewarder).getRewards(address(lpToken), address(this)); uint256 _index = _findIndex(tokens, STG); - uint256 rewardAmount = rewards[_index]; - if (rewardAmount == 0) return 0; + uint256 stgRewardAmount = rewards[_index]; + _index = _findIndex(tokens, ARB); + uint256 arbRewardAmount = rewards[_index]; + if (stgRewardAmount == 0 && arbRewardAmount == 0) return 0; (, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); - amount = (rewardAmount * stgPrice) / 1e18; + (, uint256 arbPrice) = arbInputTokenOracle.peek(arbInputTokenOracleData); + amount = (stgRewardAmount * stgPrice) / 1e18; + amount += (arbRewardAmount * arbPrice) / 1e18; } /** @@ -410,7 +414,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { farm.deposit(address(lpToken), lpAmount); lpToken.safeApprove(address(farm), 0); - event AmountDeposited(uint256 lpAmount); + emit AmountDeposited(lpAmount); } } \ No newline at end of file From 995a21e8d17091733e5d1738a6081bd76342cc5d Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Thu, 11 Jul 2024 09:17:35 +0300 Subject: [PATCH 04/18] feat(`stargate v2 strategy`): added `StargateV2Strategy` [`86du2qrnp`] --- .../StargateV2Strategy/StargateV2Strategy.sol | 414 ++++++++++++++++++ .../stargatev2/IStargateV2MultiRewarder.sol | 6 + .../interfaces/stargatev2/IStargateV2Pool.sol | 13 + .../stargatev2/IStargateV2Staking.sol | 10 + contracts/mocks/ToftMock.sol | 13 +- contracts/mocks/ZeroXSwapperMockTarget.sol | 33 ++ test/StargateV2Strategy.t.sol | 281 ++++++++++++ 7 files changed, 768 insertions(+), 2 deletions(-) create mode 100644 contracts/StargateV2Strategy/StargateV2Strategy.sol create mode 100644 contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol create mode 100644 contracts/interfaces/stargatev2/IStargateV2Pool.sol create mode 100644 contracts/interfaces/stargatev2/IStargateV2Staking.sol create mode 100644 contracts/mocks/ZeroXSwapperMockTarget.sol create mode 100644 test/StargateV2Strategy.t.sol diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol new file mode 100644 index 0000000..36ca4e7 --- /dev/null +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -0,0 +1,414 @@ +// 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 {IStargateV2Staking} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Staking.sol"; +import {IStargateV2Pool} from "tapioca-strategies/interfaces/stargatev2/IStargateV2Pool.sol"; +import {ITapiocaOracle} from "tapioca-periph/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tapioca-periph/interfaces/periph/IZeroXSwapper.sol"; +import {IPearlmit} from "tapioca-periph/interfaces/periph/IPearlmit.sol"; +import {BaseERC20Strategy} from "yieldbox/strategies/BaseStrategy.sol"; +import {ICluster} from "tapioca-periph/interfaces/periph/ICluster.sol"; +import {ITOFT} from "tapioca-periph/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 pool; + IStargateV2Staking public farm; + IERC20 public inputToken; //erc20 of token.erc20() + IERC20 public lpToken; + IZeroXSwapper public swapper; + + ITapiocaOracle public stgInputTokenOracle; + bytes public stgInputTokenOracleData; + + ITapiocaOracle public arbInputTokenOracle; + bytes public arbInputTokenOracleData; + + ICluster internal cluster; + 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; + + // ************** // + // *** 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 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); + + // ************** // + // *** ERRORS *** // + // ************** // + error TokenNotValid(); + error TransferFailed(); + error DepositPaused(); + error WithdrawPaused(); + error NotEnough(); + error PauserNotAuthorized(); + error EmptyAddress(); + error SwapFailed(); + + constructor( + IYieldBox _yieldBox, + ICluster _cluster, + address _token, + address _pool, + address _farm, + ITapiocaOracle _stgInputTokenOracle, + bytes memory _stgInputTokenOracleData, + ITapiocaOracle _arbInputTokenOracle, + bytes memory _arbInputTokenOracleData, + IZeroXSwapper _swapper, + address _owner + ) BaseERC20Strategy(_yieldBox, _token) { + if (_pool == address(0)) revert EmptyAddress(); + if (_farm == address(0)) revert EmptyAddress(); + + cluster = _cluster; + + pool = IStargateV2Pool(_pool); + farm = IStargateV2Staking(_farm); + inputToken = IERC20(ITOFT(_token).erc20()); + lpToken = IERC20(pool.lpToken()); + + stgInputTokenOracle = _stgInputTokenOracle; + stgInputTokenOracleData = _stgInputTokenOracleData; + + arbInputTokenOracle = _arbInputTokenOracle; + arbInputTokenOracleData = _arbInputTokenOracleData; + + swapper = _swapper; + + transferOwnership(_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)); // TODO: check if lpToken <> token ratio is 1:1 + + // 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); + pearlmit.clearAllowance(address(this), 20, address(inputToken), 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 pool address. + * @dev can only be called by the owner. + * @param _pool the new address. + */ + function setPool(address _pool) external onlyOwner { + if (address(_pool) == address(0)) revert EmptyAddress(); + emit PoolUpdated(address(pool), _pool); + pool = IStargateV2Pool(_pool); + } + + /** + * @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(); + emit FarmUpdated(address(farm), _farm); + farm = IStargateV2Staking(_farm); + } + + /** + * @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 invests currently available STG for compounding interest + */ + function invest(bytes calldata arbData, bytes calldata stgData) external { + IERC20 _stg = IERC20(STG); + IERC20 _arb = IERC20(ARB); + + // should only harvest for the current `lpToken` + uint256 availableStg = _stg.balanceOf(address(this)); + uint256 availableArb = _arb.balanceOf(address(this)); + if (availableStg == 0 && availableArb == 0) return; + + if(availableStg > 0) { + // swap STG to usdc + SSwapData memory swapData = abi.decode(stgData, (SSwapData)); + _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(); + + // _deposit & stake + _depositAndStake(amountOut); + } + + if (availableArb > 0) { + // swap STG to usdc + SSwapData memory swapData = abi.decode(arbData, (SSwapData)); + _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(); + + // _deposit & stake + _depositAndStake(amountOut); + } + } + + // ********************** // + // *** 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 that should be harvested + */ + function pendingRewards() public view returns (uint256 amount) { + address _rewarder = farm.rewarder(address(lpToken)); + (address[] memory tokens, uint256[] memory rewards) = IStargateV2MultiRewarder(_rewarder).getRewards(address(lpToken), address(this)); + + uint256 _index = _findIndex(tokens, STG); + uint256 rewardAmount = rewards[_index]; + if (rewardAmount == 0) return 0; + + (, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); + amount = (rewardAmount * stgPrice) / 1e18; + } + + /** + * @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: wrap fees are not taken into account here because it's 0 + amount = farm.balanceOf(address(lpToken), address(this)); // TODO: check if lpToken <> token ratio is 1:1 + 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); + } + + + 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); + + // send `contractAddress` + IERC20(contractAddress).safeTransfer(to, amount); + } + + // ********************************* // + /* ============ 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; + } + } + revert TokenNotValid(); + } + + 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); + } + +} \ 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..5bd47d4 --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.22; + +interface IStargateV2MultiRewarder { + function getRewards(address stakingToken, address user) external view returns (address[] memory, uint256[] memory); +} \ 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..ba6c1b7 --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2Pool.sol @@ -0,0 +1,13 @@ +// 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); +} \ 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..78cb220 --- /dev/null +++ b/contracts/interfaces/stargatev2/IStargateV2Staking.sol @@ -0,0 +1,10 @@ +// 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; +} \ No newline at end of file diff --git a/contracts/mocks/ToftMock.sol b/contracts/mocks/ToftMock.sol index a78688e..30ceb1e 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 "tapioca-periph/pearlmit/Pearlmit.sol"; // Tapioca import {PearlmitHandler} from "tap-utils/pearlmit/PearlmitHandler.sol"; @@ -20,6 +21,9 @@ contract ToftMock is ERC20, PearlmitHandler { error TOFT_AllowanceNotValid(); address public erc20; + IPearlmit public pearlmit; + + error FailedToWrap(); constructor(address erc20_, string memory name_, string memory symbol_, IPearlmit _pearlmit) ERC20(name_, symbol_) @@ -28,14 +32,19 @@ contract ToftMock is ERC20, PearlmitHandler { erc20 = erc20_; } + function setPearlmit(IPearlmit _pearlmit) external { + pearlmit = _pearlmit; + } + 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/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol new file mode 100644 index 0000000..adc8a6d --- /dev/null +++ b/test/StargateV2Strategy.t.sol @@ -0,0 +1,281 @@ +// 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 "tapioca-periph/pearlmit/Pearlmit.sol"; +import {ITapiocaOracle} from "tapioca-periph/interfaces/periph/ITapiocaOracle.sol"; +import {IZeroXSwapper} from "tapioca-periph/interfaces/periph/IZeroXSwapper.sol"; +import {BaseERC20Strategy} from "yieldbox/strategies/BaseStrategy.sol"; +import {ICluster} from "tapioca-periph/interfaces/periph/ICluster.sol"; +import {ZeroXSwapper} from "tapioca-periph/Swapper/ZeroXSwapper.sol"; +import {ITOFT} from "tapioca-periph/interfaces/oft/ITOFT.sol"; +import {IYieldBox} from "yieldbox/interfaces/IYieldBox.sol"; +import {Cluster} from "tapioca-periph/Cluster/Cluster.sol"; +import {OracleMock} from "tapioca-mocks/OracleMock.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 = "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; + 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); + tUsdc = new ToftMock(address(usdc), "Toft", "TOFT"); + tUsdc.setPearlmit(IPearlmit(address(pearlmit))); + yieldBox = new YieldBox(IWrappedNative(address(weth)), new YieldBoxURIBuilder()); + cluster = new Cluster(0, address(this)); + swapperTarget = new ZeroXSwapperMockTarget(); + swapper = new ZeroXSwapper(address(swapperTarget), ICluster(address(cluster)), address(this)); + + strat = new StargateV2Strategy( + IYieldBox(address(yieldBox)), + ICluster(address(cluster)), + address(tUsdc), + address(pool), + address(farm), + ITapiocaOracle(address(stgOracleMock)), + "0x", + ITapiocaOracle(address(arbOracleMock)), + "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 + }); + + uint256 farmBalanceBefore = farm.balanceOf(address(strat.lpToken()), address(strat)); + + cluster.updateContract(0, address(strat), true); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData)); + + uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); + assertGt(farmBalanceAfter, farmBalanceBefore); + } + + 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); + + } +} From 801ed03bf157f29fead047e0e0205768dfa83765 Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Thu, 11 Jul 2024 09:58:04 +0300 Subject: [PATCH 05/18] chore: missing onlyOwner and events --- contracts/StargateV2Strategy/StargateV2Strategy.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index 36ca4e7..a51e0bb 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -81,7 +81,6 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { error TransferFailed(); error DepositPaused(); error WithdrawPaused(); - error NotEnough(); error PauserNotAuthorized(); error EmptyAddress(); error SwapFailed(); @@ -154,7 +153,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { depositPaused = true; withdrawPaused = true; - uint256 amount = farm.balanceOf(address(lpToken), address(this)); // TODO: check if lpToken <> token ratio is 1:1 + uint256 amount = farm.balanceOf(address(lpToken), address(this)); // withdraw from farm farm.withdraw(address(lpToken), amount); @@ -250,7 +249,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { /** * @notice invests currently available STG for compounding interest */ - function invest(bytes calldata arbData, bytes calldata stgData) external { + function invest(bytes calldata arbData, bytes calldata stgData) external onlyOwner { IERC20 _stg = IERC20(STG); IERC20 _arb = IERC20(ARB); @@ -331,7 +330,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { // *********************************** // function _currentBalance() internal view override returns (uint256 amount) { /// @dev: wrap fees are not taken into account here because it's 0 - amount = farm.balanceOf(address(lpToken), address(this)); // TODO: check if lpToken <> token ratio is 1:1 + amount = farm.balanceOf(address(lpToken), address(this)); amount += IERC20(contractAddress).balanceOf(address(this)); amount += pendingRewards(); } @@ -385,6 +384,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { // send `contractAddress` IERC20(contractAddress).safeTransfer(to, amount); + emit AmountWithdrawn(to, amount); } // ********************************* // @@ -409,6 +409,8 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { lpToken.safeApprove(address(farm), type(uint256).max); farm.deposit(address(lpToken), lpAmount); lpToken.safeApprove(address(farm), 0); + + event AmountDeposited(uint256 lpAmount); } } \ No newline at end of file From 98d805f158bcb6618d525b5b6cfb32454f076b7c Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Thu, 11 Jul 2024 16:16:54 +0300 Subject: [PATCH 06/18] chore: used arb oracle --- contracts/StargateV2Strategy/StargateV2Strategy.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a51e0bb..f13948b 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -309,11 +309,15 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { (address[] memory tokens, uint256[] memory rewards) = IStargateV2MultiRewarder(_rewarder).getRewards(address(lpToken), address(this)); uint256 _index = _findIndex(tokens, STG); - uint256 rewardAmount = rewards[_index]; - if (rewardAmount == 0) return 0; + uint256 stgRewardAmount = rewards[_index]; + _index = _findIndex(tokens, ARB); + uint256 arbRewardAmount = rewards[_index]; + if (stgRewardAmount == 0 && arbRewardAmount == 0) return 0; (, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); - amount = (rewardAmount * stgPrice) / 1e18; + (, uint256 arbPrice) = arbInputTokenOracle.peek(arbInputTokenOracleData); + amount = (stgRewardAmount * stgPrice) / 1e18; + amount += (arbRewardAmount * arbPrice) / 1e18; } /** @@ -410,7 +414,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { farm.deposit(address(lpToken), lpAmount); lpToken.safeApprove(address(farm), 0); - event AmountDeposited(uint256 lpAmount); + emit AmountDeposited(lpAmount); } } \ No newline at end of file From 84bd33b12140398c7f23d08b08cfdba7b1fc3421 Mon Sep 17 00:00:00 2001 From: Elpacos Date: Fri, 2 Aug 2024 13:42:17 +0200 Subject: [PATCH 07/18] chore: rebase dependency refactor --- .env.example | 7 +++++- .gitmodules | 1 - .../StargateV2Strategy/StargateV2Strategy.sol | 10 ++++----- contracts/mocks/ToftMock.sol | 7 +----- lib/forge-std | 2 +- lib/tap-utils | 2 +- lib/tap-yieldbox | 2 +- test/StargateV2Strategy.t.sol | 22 +++++++++---------- 8 files changed, 26 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index 32b79e8..f46a5d0 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 +WETH="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" +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 index f13948b..a2a1e4b 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -11,12 +11,12 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 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 "tapioca-periph/interfaces/periph/ITapiocaOracle.sol"; -import {IZeroXSwapper} from "tapioca-periph/interfaces/periph/IZeroXSwapper.sol"; -import {IPearlmit} from "tapioca-periph/interfaces/periph/IPearlmit.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 "tapioca-periph/interfaces/periph/ICluster.sol"; -import {ITOFT} from "tapioca-periph/interfaces/oft/ITOFT.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"; /* diff --git a/contracts/mocks/ToftMock.sol b/contracts/mocks/ToftMock.sol index 30ceb1e..f58c196 100644 --- a/contracts/mocks/ToftMock.sol +++ b/contracts/mocks/ToftMock.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.22; import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {Pearlmit, IPearlmit, PearlmitHash} from "tapioca-periph/pearlmit/Pearlmit.sol"; +import {Pearlmit, IPearlmit, PearlmitHash} from "tap-utils/pearlmit/Pearlmit.sol"; // Tapioca import {PearlmitHandler} from "tap-utils/pearlmit/PearlmitHandler.sol"; @@ -21,7 +21,6 @@ contract ToftMock is ERC20, PearlmitHandler { error TOFT_AllowanceNotValid(); address public erc20; - IPearlmit public pearlmit; error FailedToWrap(); @@ -32,10 +31,6 @@ contract ToftMock is ERC20, PearlmitHandler { erc20 = erc20_; } - function setPearlmit(IPearlmit _pearlmit) external { - pearlmit = _pearlmit; - } - 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 FailedToWrap(); diff --git a/lib/forge-std b/lib/forge-std index ae570fe..4d63c97 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce +Subproject commit 4d63c978718517fa02d4e330fbe7372dbb06c2f1 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 index adc8a6d..fb9929d 100644 --- a/test/StargateV2Strategy.t.sol +++ b/test/StargateV2Strategy.t.sol @@ -13,16 +13,16 @@ import {YieldBox, YieldBoxURIBuilder, IWrappedNative, TokenType, IStrategy} from 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 "tapioca-periph/pearlmit/Pearlmit.sol"; -import {ITapiocaOracle} from "tapioca-periph/interfaces/periph/ITapiocaOracle.sol"; -import {IZeroXSwapper} from "tapioca-periph/interfaces/periph/IZeroXSwapper.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 "tapioca-periph/interfaces/periph/ICluster.sol"; -import {ZeroXSwapper} from "tapioca-periph/Swapper/ZeroXSwapper.sol"; -import {ITOFT} from "tapioca-periph/interfaces/oft/ITOFT.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 "tapioca-periph/Cluster/Cluster.sol"; -import {OracleMock} from "tapioca-mocks/OracleMock.sol"; +import {Cluster} from "tap-utils/Cluster/Cluster.sol"; +import {OracleMock} from "tapioca-strategies/mocks/OracleMock.sol"; import {ZeroXSwapperMockTarget} from "tapioca-strategies/mocks/ZeroXSwapperMockTarget.sol"; import {ToftMock} from "tapioca-strategies/mocks/ToftMock.sol"; @@ -37,7 +37,7 @@ contract StargateV2StrategyTest is Test { string constant ENV_FARM_ADDRESS = "STARGATEV2_FARM"; string constant ENV_USDC = "USDC"; string constant ENV_WETH = "WETH"; - string constant RPC_URL = "RPC_URL"; + string constant RPC_URL = "ARBITRUM_RPC_URL"; string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; uint256 ARB_FORK; @@ -85,9 +85,9 @@ contract StargateV2StrategyTest is Test { pearlmit = new Pearlmit("Test", "1", address(this), 0); stgOracleMock = new OracleMock("stgOracleMock", "SOM", 1e18); arbOracleMock = new OracleMock("arbOracleMock", "SOM", 1e18); - tUsdc = new ToftMock(address(usdc), "Toft", "TOFT"); + tUsdc = new ToftMock(address(usdc), "Toft", "TOFT", IPearlmit(address(pearlmit))); tUsdc.setPearlmit(IPearlmit(address(pearlmit))); - yieldBox = new YieldBox(IWrappedNative(address(weth)), new YieldBoxURIBuilder()); + 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)); From 86af00a83d0a4173640772d67439d42ca96cc9c0 Mon Sep 17 00:00:00 2001 From: kartojal Date: Tue, 6 Aug 2024 13:00:06 +0000 Subject: [PATCH 08/18] fix: withdrawal amount check --- .env.example | 4 ++-- contracts/StargateV2Strategy/StargateV2Strategy.sol | 9 ++++++--- test/StargateV2Strategy.t.sol | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index f46a5d0..9224f35 100644 --- a/.env.example +++ b/.env.example @@ -25,8 +25,8 @@ export SCAN_API_KEY= # Arbitrum Sepolia API key FORKING_ARBITRUM_BLOCK_NUMBER=238439389 FORKING_BLOCK_NUMBER=238639389 -WETH="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" -USDC="0xaf88d065e77c8cC2239327C5EDb3A432268e5831" +ARB_WETH="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" +ARB_USDC="0xaf88d065e77c8cC2239327C5EDb3A432268e5831" STARGATEV2_POOL="0xe8CDF27AcD73a434D661C84887215F7598e7d0d3" STARGATEV2_FARM="0x3da4f8E456AC648c489c286B99Ca37B666be7C4C" diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a2a1e4b..c337e89 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -350,6 +350,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { } + /// @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(); @@ -385,10 +386,12 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { // - 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, amount); - emit AmountWithdrawn(to, amount); + IERC20(contractAddress).safeTransfer(to, withdrawalAmount); + emit AmountWithdrawn(to, withdrawalAmount); } // ********************************* // diff --git a/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol index fb9929d..234553d 100644 --- a/test/StargateV2Strategy.t.sol +++ b/test/StargateV2Strategy.t.sol @@ -35,10 +35,10 @@ contract StargateV2StrategyTest is Test { 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 ENV_USDC = "ARB_USDC"; + string constant ENV_WETH = "ARB_WETH"; string constant RPC_URL = "ARBITRUM_RPC_URL"; - string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; + string constant FORKING_BLOCK_NUMBER = "FORKING_ARBITRUM_BLOCK_NUMBER"; uint256 ARB_FORK; From 2cde9a6109a235d16419aa88779c88414eb7579e Mon Sep 17 00:00:00 2001 From: kartojal Date: Tue, 6 Aug 2024 18:26:28 +0000 Subject: [PATCH 09/18] fix: remove setPool() function and set pool, inputToken and lpToken as immutable --- .../StargateV2Strategy/StargateV2Strategy.sol | 18 +++--------------- test/StargateV2Strategy.t.sol | 6 +++--- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a2a1e4b..65514b2 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -32,10 +32,10 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; using SafeCast for uint256; - IStargateV2Pool public pool; + IStargateV2Pool public immutable pool; IStargateV2Staking public farm; - IERC20 public inputToken; //erc20 of token.erc20() - IERC20 public lpToken; + IERC20 public immutable inputToken; //erc20 of token.erc20() + IERC20 public immutable lpToken; IZeroXSwapper public swapper; ITapiocaOracle public stgInputTokenOracle; @@ -68,7 +68,6 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { 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); @@ -198,17 +197,6 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { swapper = _swapper; } - /** - * @notice updates the StargateV2 pool address. - * @dev can only be called by the owner. - * @param _pool the new address. - */ - function setPool(address _pool) external onlyOwner { - if (address(_pool) == address(0)) revert EmptyAddress(); - emit PoolUpdated(address(pool), _pool); - pool = IStargateV2Pool(_pool); - } - /** * @notice updates the StargateV2 staking address. * @dev can only be called by the owner. diff --git a/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol index fb9929d..234553d 100644 --- a/test/StargateV2Strategy.t.sol +++ b/test/StargateV2Strategy.t.sol @@ -35,10 +35,10 @@ contract StargateV2StrategyTest is Test { 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 ENV_USDC = "ARB_USDC"; + string constant ENV_WETH = "ARB_WETH"; string constant RPC_URL = "ARBITRUM_RPC_URL"; - string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; + string constant FORKING_BLOCK_NUMBER = "FORKING_ARBITRUM_BLOCK_NUMBER"; uint256 ARB_FORK; From 35dcb788c8bb0bcbb9c7ee2bcf8bf8b07372a911 Mon Sep 17 00:00:00 2001 From: kartojal Date: Wed, 7 Aug 2024 09:33:26 +0000 Subject: [PATCH 10/18] fix: do not revert if ARB or STG reward are disabled --- .../StargateV2Strategy/StargateV2Strategy.sol | 25 +++++++++++-------- test/StargateV2Strategy.t.sol | 6 ++--- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a2a1e4b..1ab3930 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -305,19 +305,23 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { * @return amount The amount of STG 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)); - uint256 _index = _findIndex(tokens, STG); - uint256 stgRewardAmount = rewards[_index]; - _index = _findIndex(tokens, ARB); - uint256 arbRewardAmount = rewards[_index]; - if (stgRewardAmount == 0 && arbRewardAmount == 0) return 0; + tokenIndex = _findIndex(tokens, STG); + if (tokenIndex != 404 ) { + (, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); + amount += (rewards[tokenIndex] * stgPrice) / 1e18; + } + + tokenIndex = _findIndex(tokens, ARB); + if (tokenIndex != 404 ) { + (, uint256 arbPrice) = arbInputTokenOracle.peek(arbInputTokenOracleData); + amount += ( rewards[tokenIndex] * arbPrice) / 1e18; + } - (, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); - (, uint256 arbPrice) = arbInputTokenOracle.peek(arbInputTokenOracleData); - amount = (stgRewardAmount * stgPrice) / 1e18; - amount += (arbRewardAmount * arbPrice) / 1e18; + return amount; } /** @@ -401,7 +405,8 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { return i; } } - revert TokenNotValid(); + // 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 { diff --git a/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol index fb9929d..234553d 100644 --- a/test/StargateV2Strategy.t.sol +++ b/test/StargateV2Strategy.t.sol @@ -35,10 +35,10 @@ contract StargateV2StrategyTest is Test { 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 ENV_USDC = "ARB_USDC"; + string constant ENV_WETH = "ARB_WETH"; string constant RPC_URL = "ARBITRUM_RPC_URL"; - string constant FORKING_BLOCK_NUMBER = "FORKING_BLOCK_NUMBER"; + string constant FORKING_BLOCK_NUMBER = "FORKING_ARBITRUM_BLOCK_NUMBER"; uint256 ARB_FORK; From 08c6293b322c435e138f933daed2f51f22b545e8 Mon Sep 17 00:00:00 2001 From: kartojal Date: Thu, 8 Aug 2024 09:47:44 +0000 Subject: [PATCH 11/18] fix: return expected withdrawal balance with conversion rate --- .../StargateV2Strategy/StargateV2Strategy.sol | 32 +++++++++++++++++-- .../interfaces/stargatev2/IStargateV2Pool.sol | 2 ++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a2a1e4b..29e1baa 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -3,6 +3,7 @@ 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"; @@ -45,6 +46,10 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { bytes public arbInputTokenOracleData; ICluster internal cluster; + + /// @dev StargateBase: The rate between local decimals and shared decimals. + uint256 public immutable stargateConvertRate; + bool public depositPaused; bool public withdrawPaused; @@ -116,6 +121,8 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { swapper = _swapper; + stargateConvertRate = 10 ** (IERC20Metadata(address(inputToken)).decimals() - pool.sharedDecimals()); + transferOwnership(_owner); } @@ -333,8 +340,8 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { /* ============ INTERNAL ============ */ // *********************************** // function _currentBalance() internal view override returns (uint256 amount) { - /// @dev: wrap fees are not taken into account here because it's 0 - amount = farm.balanceOf(address(lpToken), address(this)); + /// @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(); } @@ -391,6 +398,27 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { emit AmountWithdrawn(to, amount); } + /// @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 ============ */ // ********************************* // diff --git a/contracts/interfaces/stargatev2/IStargateV2Pool.sol b/contracts/interfaces/stargatev2/IStargateV2Pool.sol index ba6c1b7..c1b627b 100644 --- a/contracts/interfaces/stargatev2/IStargateV2Pool.sol +++ b/contracts/interfaces/stargatev2/IStargateV2Pool.sol @@ -10,4 +10,6 @@ interface IStargateV2Pool { ) 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 From fc9598ceb7255cc489936162a9b1ed821dba9d6c Mon Sep 17 00:00:00 2001 From: kartojal Date: Thu, 8 Aug 2024 19:21:23 +0000 Subject: [PATCH 12/18] fix: setFarm now claims previous rewards before updating farm/staking contract, add e2e setFarm test --- .../StargateV2Strategy/StargateV2Strategy.sol | 11 +++ .../stargatev2/IStargateV2MultiRewarder.sol | 1 + .../stargatev2/IStargateV2Staking.sol | 2 + test/StargateV2Strategy.t.sol | 67 ++++++++++++++++++- test/external/StargateMultiRewarder.sol | 10 +++ 5 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 test/external/StargateMultiRewarder.sol diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a2a1e4b..f9ded08 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -216,8 +216,19 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { */ 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)); + + lpToken.safeApprove(address(farm), lpBalance); + farm.deposit(address(lpToken), lpBalance); + lpToken.safeApprove(address(farm), 0); } /** diff --git a/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol b/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol index 5bd47d4..78f565f 100644 --- a/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol +++ b/contracts/interfaces/stargatev2/IStargateV2MultiRewarder.sol @@ -3,4 +3,5 @@ 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/IStargateV2Staking.sol b/contracts/interfaces/stargatev2/IStargateV2Staking.sol index 78cb220..a30f477 100644 --- a/contracts/interfaces/stargatev2/IStargateV2Staking.sol +++ b/contracts/interfaces/stargatev2/IStargateV2Staking.sol @@ -7,4 +7,6 @@ interface IStargateV2Staking { 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 setPool(address token, address newRewarder) external; } \ No newline at end of file diff --git a/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol index fb9929d..8403ce8 100644 --- a/test/StargateV2Strategy.t.sol +++ b/test/StargateV2Strategy.t.sol @@ -23,14 +23,18 @@ 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"; +import "forge-std/console.sol"; + +contract StargateV2StrategyTest is Test { + using stdStorage for StdStorage; -contract StargateV2StrategyTest is Test { address owner; string constant ENV_BINANCE_WALLET_ADDRESS = "BINANCE_WALLET_ADDRESS"; string constant ENV_POOL_ADDRESS = "STARGATEV2_POOL"; @@ -278,4 +282,63 @@ contract StargateV2StrategyTest is Test { 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/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 From 5bd1fa9d56033f4a54813c6c132ded1c40ed363a Mon Sep 17 00:00:00 2001 From: kartojal Date: Fri, 9 Aug 2024 09:07:39 +0000 Subject: [PATCH 13/18] fix: check if oracle is active to sum the output amount --- contracts/StargateV2Strategy/StargateV2Strategy.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index 1ab3930..e4d1d53 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -311,14 +311,18 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { tokenIndex = _findIndex(tokens, STG); if (tokenIndex != 404 ) { - (, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); - amount += (rewards[tokenIndex] * stgPrice) / 1e18; + (bool stgOracleActive, uint256 stgPrice) = stgInputTokenOracle.peek(stgInputTokenOracleData); + if (stgOracleActive) { + amount += (rewards[tokenIndex] * stgPrice) / 1e18; + } } tokenIndex = _findIndex(tokens, ARB); if (tokenIndex != 404 ) { - (, uint256 arbPrice) = arbInputTokenOracle.peek(arbInputTokenOracleData); - amount += ( rewards[tokenIndex] * arbPrice) / 1e18; + (bool arbOracleActive, uint256 arbPrice) = arbInputTokenOracle.peek(arbInputTokenOracleData); + if (arbOracleActive) { + amount += ( rewards[tokenIndex] * arbPrice) / 1e18; + } } return amount; From e6b3798568f982ae05d75a71a1fe2988bdf7b2ab Mon Sep 17 00:00:00 2001 From: kartojal Date: Fri, 9 Aug 2024 09:41:20 +0000 Subject: [PATCH 14/18] fix: remove clearAllowance --- contracts/StargateV2Strategy/StargateV2Strategy.sol | 2 -- lib/forge-std | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a2a1e4b..473ee3f 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -173,7 +173,6 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { // - reset approvals inputToken.safeApprove(address(pearlmit), 0); - pearlmit.clearAllowance(address(this), 20, address(inputToken), 0); } /** @@ -384,7 +383,6 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { 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); // send `contractAddress` IERC20(contractAddress).safeTransfer(to, amount); diff --git a/lib/forge-std b/lib/forge-std index 4d63c97..1714bee 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 4d63c978718517fa02d4e330fbe7372dbb06c2f1 +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d From ea46584c93a8f3b177e929e61c6176c72d20ea79 Mon Sep 17 00:00:00 2001 From: kartojal Date: Fri, 9 Aug 2024 11:37:14 +0000 Subject: [PATCH 15/18] fix: detect if output swap token is correct at StargateV2Strategy.invest() --- .../StargateV2Strategy/StargateV2Strategy.sol | 2 ++ test/StargateV2Strategy.t.sol | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index a2a1e4b..2fd1fa6 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -261,6 +261,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { 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); @@ -273,6 +274,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { 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); diff --git a/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol index a1258b5..490dbc2 100644 --- a/test/StargateV2Strategy.t.sol +++ b/test/StargateV2Strategy.t.sol @@ -227,6 +227,34 @@ contract StargateV2StrategyTest is Test { 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; + + // 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)); + } + function test_emergencyWithdraw_stg() public isArbFork { uint256 amount = 10_000_000; // 10 USDC From 87d2e27c0650247739d854871fc1a729400910c9 Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Mon, 19 Aug 2024 16:25:38 +0300 Subject: [PATCH 16/18] patch(`StargateV2Strategy`): minor fix for `setFarm` - check if `lpBalance > 0` --- contracts/StargateV2Strategy/StargateV2Strategy.sol | 9 +++++---- .../concrete/StargateV2/StargateV2Strategy_view.t.sol | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index 0ba067f..7b11dc9 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -212,10 +212,11 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { // Deposit in new farm uint256 lpBalance = lpToken.balanceOf(address(this)); - - lpToken.safeApprove(address(farm), lpBalance); - farm.deposit(address(lpToken), lpBalance); - lpToken.safeApprove(address(farm), 0); + if (lpBalance > 0) { + lpToken.safeApprove(address(farm), lpBalance); + farm.deposit(address(lpToken), lpBalance); + lpToken.safeApprove(address(farm), 0); + } } /** diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.t.sol index c3b8181..6932801 100644 --- a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.t.sol +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_view.t.sol @@ -78,7 +78,8 @@ contract StargateV2Strategy_view is StargateV2_Shared { assertEq(strat.pendingRewards(), 0); } - + + // @dev this shouldn't revert anymore after audit fixes function test_RevertWhen_ReceivedRewardsAreNotAccepted() external whenMockRewarder @@ -90,9 +91,8 @@ contract StargateV2Strategy_view is StargateV2_Shared { uint256[] memory _amounts = new uint256[](2); rewarderMock.setRewards(_tokens, _amounts); - // it should revert - vm.expectRevert(StargateV2Strategy.TokenNotValid.selector); - strat.pendingRewards(); + uint256 rewards = strat.pendingRewards(); + assertEq(rewards, 0); } function test_WhenReceivedRewardsAreAvailableButWithZeroValue() From 30b7d8c2fa12a6644057f39443ac9b5d6e643c0e Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Mon, 19 Aug 2024 16:44:45 +0300 Subject: [PATCH 17/18] chore(`tests`): fixed old StargateV2Strategy `setUp` --- test/StargateV2Strategy.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol index 2586372..790b213 100644 --- a/test/StargateV2Strategy.t.sol +++ b/test/StargateV2Strategy.t.sol @@ -37,10 +37,10 @@ contract StargateV2StrategyTest is Test { 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 = "ARB_USDC"; - string constant ENV_WETH = "ARB_WETH"; + string constant ENV_USDC = "USDC"; + string constant ENV_WETH = "WETH"; 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; address public binanceWalletAddr; From d57152e706cbbdbfdacb8ba463fa32cbc625becc Mon Sep 17 00:00:00 2001 From: cryptotechmaker Date: Fri, 30 Aug 2024 14:25:37 +0300 Subject: [PATCH 18/18] feat: added ZRO rewards to StargateV2Strategy --- .../StargateV2Strategy/StargateV2Strategy.sol | 113 +++++++++++++----- test/StargateV2Strategy.t.sol | 38 +++--- test/btt/shared/StargateV2_Shared.t.sol | 28 +++-- .../StargateV2Strategy_constructor.t.sol | 78 +++++++----- .../StargateV2Strategy_setters.t.sol | 33 ++++- 5 files changed, 197 insertions(+), 93 deletions(-) diff --git a/contracts/StargateV2Strategy/StargateV2Strategy.sol b/contracts/StargateV2Strategy/StargateV2Strategy.sol index 81bbc86..c0a0888 100644 --- a/contracts/StargateV2Strategy/StargateV2Strategy.sol +++ b/contracts/StargateV2Strategy/StargateV2Strategy.sol @@ -45,6 +45,9 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { 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. @@ -65,6 +68,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { address public constant STG = 0x6694340fc020c5E6B96567843da2df01b2CE1eb6; address public constant ARB = 0x912CE59144191C1204E64559FE8253a0e49E6548; + address public constant ZRO = 0x6985884C4392D348587B19cb9eAAf157F13271cd; // ************** // // *** EVENTS *** // @@ -77,6 +81,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { 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 *** // @@ -89,40 +94,48 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { 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( - IYieldBox _yieldBox, - ICluster _cluster, - address _token, - address _pool, - address _farm, - ITapiocaOracle _stgInputTokenOracle, - bytes memory _stgInputTokenOracleData, - ITapiocaOracle _arbInputTokenOracle, - bytes memory _arbInputTokenOracleData, - IZeroXSwapper _swapper, - address _owner - ) BaseERC20Strategy(_yieldBox, _token) { - if (_pool == address(0)) revert EmptyAddress(); - if (_farm == address(0)) revert EmptyAddress(); + StargateV2InitData memory initData + ) BaseERC20Strategy(initData.yieldBox, initData.token) { + if (initData.pool == address(0)) revert EmptyAddress(); + if (initData.farm == address(0)) revert EmptyAddress(); - cluster = _cluster; + cluster = initData.cluster; - pool = IStargateV2Pool(_pool); - farm = IStargateV2Staking(_farm); - inputToken = IERC20(ITOFT(_token).erc20()); + pool = IStargateV2Pool(initData.pool); + farm = IStargateV2Staking(initData.farm); + inputToken = IERC20(ITOFT(initData.token).erc20()); lpToken = IERC20(pool.lpToken()); - stgInputTokenOracle = _stgInputTokenOracle; - stgInputTokenOracleData = _stgInputTokenOracleData; + stgInputTokenOracle = initData.stgInputTokenOracle; + stgInputTokenOracleData = initData.stgInputTokenOracleData; - arbInputTokenOracle = _arbInputTokenOracle; - arbInputTokenOracleData = _arbInputTokenOracleData; + arbInputTokenOracle = initData.arbInputTokenOracle; + arbInputTokenOracleData = initData.arbInputTokenOracleData; - swapper = _swapper; + zroInputTokenOracle = initData.zroInputTokenOracle; + zroInputTokenOracleData = initData.zroInputTokenOracleData; + + swapper = initData.swapper; stargateConvertRate = 10 ** (IERC20Metadata(address(inputToken)).decimals() - pool.sharedDecimals()); - transferOwnership(_owner); + transferOwnership(initData.owner); } // *********************** // @@ -252,18 +265,35 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { 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) external onlyOwner { + 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)); - if (availableStg == 0 && availableArb == 0) return; + 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)); @@ -273,8 +303,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { _stg.safeApprove(address(swapper), 0); if (amountOut < swapData.minAmountOut) revert SwapFailed(); - // _deposit & stake - _depositAndStake(amountOut); + totalAmountOut += amountOut; } if (availableArb > 0) { @@ -286,8 +315,24 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { _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(amountOut); + _depositAndStake(totalAmountOut); } } @@ -310,7 +355,7 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { /** * @notice Returns The estimate the pending rewards. - * @return amount The amount of STG that should be harvested + * @return amount The amount of STG, ARB and ZRO that should be harvested */ function pendingRewards() public view returns (uint256 amount) { uint256 tokenIndex; @@ -333,6 +378,14 @@ contract StargateV2Strategy is BaseERC20Strategy, Ownable, ReentrancyGuard { } } + tokenIndex = _findIndex(tokens, ZRO); + if (tokenIndex != 404 ) { + (bool zroOracleActive, uint256 zroPrice) = zroInputTokenOracle.peek(zroInputTokenOracleData); + if (zroOracleActive) { + amount += ( rewards[tokenIndex] * zroPrice) / 1e18; + } + } + return amount; } diff --git a/test/StargateV2Strategy.t.sol b/test/StargateV2Strategy.t.sol index 790b213..61028f6 100644 --- a/test/StargateV2Strategy.t.sol +++ b/test/StargateV2Strategy.t.sol @@ -29,8 +29,6 @@ import {ZeroXSwapperMockTarget} from "tapioca-strategies/mocks/ZeroXSwapperMockT import {ToftMock} from "tapioca-strategies/mocks/ToftMock.sol"; import "forge-std/Test.sol"; -import "forge-std/console.sol"; - contract StargateV2StrategyTest is Test { address owner; @@ -50,6 +48,7 @@ contract StargateV2StrategyTest is Test { IStargateV2Staking farm; OracleMock public stgOracleMock; OracleMock public arbOracleMock; + OracleMock public zroOracleMock; ToftMock tUsdc; StargateV2Strategy strat; ZeroXSwapperMockTarget swapperTarget; @@ -86,6 +85,7 @@ contract StargateV2StrategyTest is Test { 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)); @@ -94,17 +94,21 @@ contract StargateV2StrategyTest is Test { swapper = new ZeroXSwapper(address(swapperTarget), ICluster(address(cluster)), address(this)); strat = new StargateV2Strategy( - IYieldBox(address(yieldBox)), - ICluster(address(cluster)), - address(tUsdc), - address(pool), - address(farm), - ITapiocaOracle(address(stgOracleMock)), - "0x", - ITapiocaOracle(address(arbOracleMock)), - "0x", - IZeroXSwapper(address(swapper)), - address(this) + 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"); @@ -220,11 +224,14 @@ contract StargateV2StrategyTest is Test { 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)); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData), abi.encode(zroSwapData)); uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); assertGt(farmBalanceAfter, farmBalanceBefore); @@ -248,6 +255,7 @@ contract StargateV2StrategyTest is Test { }); StargateV2Strategy.SSwapData memory stgSwapData; + StargateV2Strategy.SSwapData memory zroSwapData; // Set ARB 1 wei balance to enter condition in strategy deal(arb, address(strat), 1); @@ -255,7 +263,7 @@ contract StargateV2StrategyTest is Test { cluster.updateContract(0, address(strat), true); vm.expectRevert(StargateV2Strategy.TokenNotValid.selector); - strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData)); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData), abi.encode(zroSwapData)); } function test_emergencyWithdraw_stg() public isArbFork { diff --git a/test/btt/shared/StargateV2_Shared.t.sol b/test/btt/shared/StargateV2_Shared.t.sol index 61dc70e..39d0ae3 100644 --- a/test/btt/shared/StargateV2_Shared.t.sol +++ b/test/btt/shared/StargateV2_Shared.t.sol @@ -34,6 +34,7 @@ abstract contract StargateV2_Shared is Base_Test, Events { ZeroXSwapperMockTarget swapperTarget; OracleMock stgOracle; OracleMock arbOracle; + OracleMock zroOracle; ToftMock tUsdc; uint256 tUsdcAssetId; @@ -71,6 +72,7 @@ abstract contract StargateV2_Shared is Base_Test, Events { // create arb > usdc oracle arbOracle = _createOracle("ARBUSDC"); stgOracle = _createOracle("STGUSDC"); + zroOracle = _createOracle("ZROUSDC"); // create 0xSwapper swapperTarget = new ZeroXSwapperMockTarget(); @@ -79,17 +81,21 @@ abstract contract StargateV2_Shared is Base_Test, Events { address _owner = address(this); // create Stargate v2 strategy strat = new StargateV2Strategy( - IYieldBox(address(yieldBox)), - ICluster(address(cluster)), - address(tUsdc), - address(pool), - address(farm), - ITapiocaOracle(address(stgOracle)), - "0x", - ITapiocaOracle(address(arbOracle)), - "0x", - IZeroXSwapper(address(swapper)), - _owner + 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"); diff --git a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.t.sol index 40f3bb7..1464f73 100644 --- a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.t.sol +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_constructor.t.sol @@ -46,17 +46,21 @@ contract StargateV2Strategy_constructor is StargateV2_Shared { function test_RevertWhen_PoolIsAddressZero() external whenPoolIsNotValid(true) { vm.expectRevert(StargateV2Strategy.EmptyAddress.selector); new StargateV2Strategy( - IYieldBox(address(yieldBox)), - ICluster(address(cluster)), - address(tUsdc), - address(pool), - address(farm), - ITapiocaOracle(address(stgOracle)), - "0x", - ITapiocaOracle(address(arbOracle)), - "0x", - IZeroXSwapper(address(swapper)), - address(this) + 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) + ) ); } @@ -64,34 +68,42 @@ contract StargateV2Strategy_constructor is StargateV2_Shared { // will revert because pool.lpToken() call will throw an EvmError vm.expectRevert(); new StargateV2Strategy( - IYieldBox(address(yieldBox)), - ICluster(address(cluster)), - address(tUsdc), - address(pool), - address(farm), - ITapiocaOracle(address(stgOracle)), - "0x", - ITapiocaOracle(address(arbOracle)), - "0x", - IZeroXSwapper(address(swapper)), - address(this) + 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( - IYieldBox(address(yieldBox)), - ICluster(address(cluster)), - address(tUsdc), - address(pool), - address(farm), - ITapiocaOracle(address(stgOracle)), - "0x", - ITapiocaOracle(address(arbOracle)), - "0x", - IZeroXSwapper(address(swapper)), - address(this) + 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_setters.t.sol b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.t.sol index dfa278e..cdaded7 100644 --- a/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.t.sol +++ b/test/btt/unit/concrete/StargateV2/StargateV2Strategy_setters.t.sol @@ -208,7 +208,7 @@ contract StargateV2Strategy_setters is StargateV2_Shared { uint256 farmBalanceBefore = farm.balanceOf(address(strat.lpToken()), address(strat)); // invest - strat.invest("", ""); + strat.invest("", "", ""); uint256 farmBalanceAfter = farm.balanceOf(address(strat.lpToken()), address(strat)); @@ -259,9 +259,21 @@ contract StargateV2Strategy_setters is StargateV2_Shared { 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)); + strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData), abi.encode(zroSwapData)); } function test_whenInvestIsCalled_GivenSwapAmountIsValid(uint256 depositAmount) @@ -295,12 +307,25 @@ contract StargateV2Strategy_setters is StargateV2_Shared { 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); - strat.invest(abi.encode(arbSwapData), abi.encode(stgSwapData)); + 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