From 6f7500f97dbdef5c4b9ea0beabcd272aa4ee90b1 Mon Sep 17 00:00:00 2001 From: jordan Date: Mon, 19 Sep 2022 16:03:10 +0400 Subject: [PATCH 1/2] test for beefy strategy no swaps --- hub/remappings.txt | 2 + .../strategy/strategies/base/BaseStrategy.sol | 201 ++++++++++++++++++ .../strategies/beefy/BeefyVelodrome.sol | 97 +++++++++ .../interfaces/IBeefyUniV2ZapSolidly.sol | 53 +++++ .../beefy/interfaces/IBeefyVaultV6.sol | 97 +++++++++ .../strategy/{ => xchain}/BaseStrategy.sol | 9 +- .../strategy/{ => xchain}/XChainStrategy.sol | 4 +- .../{ => xchain}/XChainStrategyEvents.sol | 0 hub/test/XChainStrategy.t.sol | 6 +- hub/test/mocks/MockStrategy.sol | 2 +- hub/test/strategies/beefy/Beefy.t.sol | 137 ++++++++++++ 11 files changed, 595 insertions(+), 13 deletions(-) create mode 100644 hub/src/strategy/strategies/base/BaseStrategy.sol create mode 100644 hub/src/strategy/strategies/beefy/BeefyVelodrome.sol create mode 100644 hub/src/strategy/strategies/beefy/interfaces/IBeefyUniV2ZapSolidly.sol create mode 100644 hub/src/strategy/strategies/beefy/interfaces/IBeefyVaultV6.sol rename hub/src/strategy/{ => xchain}/BaseStrategy.sol (95%) rename hub/src/strategy/{ => xchain}/XChainStrategy.sol (98%) rename hub/src/strategy/{ => xchain}/XChainStrategyEvents.sol (100%) create mode 100644 hub/test/strategies/beefy/Beefy.t.sol diff --git a/hub/remappings.txt b/hub/remappings.txt index d4c8905..74ead18 100644 --- a/hub/remappings.txt +++ b/hub/remappings.txt @@ -6,6 +6,7 @@ openzeppelin/=lib/openzeppelin-contracts/contracts/ @interfaces/=src/interfaces/ @vault-interfaces/=src/vaults/interfaces/ +@vaults/=src/vaults/ @std/=lib/forge-std/src @@ -13,6 +14,7 @@ openzeppelin/=lib/openzeppelin-contracts/contracts/ @hub-test/=test/ @hub-upgradeable/=src/upgradeable/ +@strategies/=src/strategy/strategies/ @mocks/=test/mocks/ @prb/test/=lib/prb-test/src/ \ No newline at end of file diff --git a/hub/src/strategy/strategies/base/BaseStrategy.sol b/hub/src/strategy/strategies/base/BaseStrategy.sol new file mode 100644 index 0000000..2642148 --- /dev/null +++ b/hub/src/strategy/strategies/base/BaseStrategy.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.10; + +import {ERC20Upgradeable as ERC20} from "@oz-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {OwnableUpgradeable as Ownable} from "@oz-upgradeable/access/OwnableUpgradeable.sol"; +import {AccessControlUpgradeable as AccessControl} from "@oz-upgradeable/access/AccessControlUpgradeable.sol"; +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@oz-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {AddressUpgradeable as Address} from "@oz-upgradeable/utils/AddressUpgradeable.sol"; + +import {Initializable} from "@oz-upgradeable/proxy/utils/Initializable.sol"; +import {IERC20MetadataUpgradeable as IERC20} from "@oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "@oz-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import {IVault} from "@interfaces/IVault.sol"; + +abstract contract BaseStrategy is Initializable { + using SafeERC20 for IERC20; + + /*/////////////////////////////////////////////////////////////// + IMMUTABLES + //////////////////////////////////////////////////////////////*/ + + /// @notice Success return value. + /// @dev This is returned in case of success. + uint8 public constant SUCCESS = 0; + + /// @notice Error return value. + /// @dev This is returned when the strategy has not enough underlying to pull. + uint8 public constant NOT_ENOUGH_UNDERLYING = 1; + + /*/////////////////////////////////////////////////////////////// + STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice The Strategy name. + string public name; + + /// @notice The underlying token the strategy accepts. + IERC20 public underlying; + + /// @notice The Vault managing this strategy. + IVault public vault; + + /// @notice Deposited underlying. + uint256 depositedUnderlying; + + /// @notice The strategy manager. + address public manager; + + /// @notice The strategist. + address public strategist; + + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Event emitted when a new manager is set for this strategy. + event UpdateManager(address indexed manager); + + /// @notice Event emitted when a new strategist is set for this strategy. + event UpdateStrategist(address indexed strategist); + + /// @notice Event emitted when rewards are sold. + event RewardsHarvested( + address indexed reward, + uint256 rewards, + uint256 underlying + ); + + /// @notice Event emitted when underlying is deposited in this strategy. + event Deposit(IVault indexed vault, uint256 amount); + + /// @notice Event emitted when underlying is withdrawn from this strategy. + event Withdraw(IVault indexed vault, uint256 amount); + + /// @notice Event emitted when underlying is deployed. + event DepositUnderlying(uint256 deposited); + + /// @notice Event emitted when underlying is removed from other contracts and returned to the strategy. + event WithdrawUnderlying(uint256 amount); + + /// @notice Event emitted when tokens are sweeped from this strategy. + event Sweep(IERC20 indexed asset, uint256 amount); + + /*/////////////////////////////////////////////////////////////// + INITIALIZE + //////////////////////////////////////////////////////////////*/ + + function __initialize( + IVault vault_, + IERC20 underlying_, + address manager_, + address strategist_, + string memory name_ + ) internal virtual initializer { + name = name_; + vault = vault_; + manager = manager_; + strategist = strategist_; + underlying = underlying_; + } + + /*/////////////////////////////////////////////////////////////// + MANAGER/STRATEGIST + //////////////////////////////////////////////////////////////*/ + + /// @notice Change strategist address. + /// @param strategist_ The new strategist address. + function setStrategist(address strategist_) external { + require(msg.sender == manager); + strategist = strategist_; + + emit UpdateStrategist(manager); + } + + /// @notice Change manager address. + /// @param manager_ The new manager address. + function setManager(address manager_) external { + require(msg.sender == manager); + manager = manager_; + + emit UpdateManager(manager_); + } + + /*/////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAW + //////////////////////////////////////////////////////////////*/ + + /// @notice Deposit a specific amount of underlying tokens. + /// @param amount The amount of underlying tokens to deposit. + function deposit(uint256 amount) external virtual returns (uint8 success) { + require(msg.sender == address(vault), "deposit::NOT_VAULT"); + + depositedUnderlying += amount; + underlying.safeTransferFrom(msg.sender, address(this), amount); + + emit Deposit(IVault(msg.sender), amount); + + success = SUCCESS; + } + + /// @notice Withdraw a specific amount of underlying tokens. + /// @param amount The amount of underlying to withdraw. + function withdraw(uint256 amount) external virtual returns (uint8 success) { + require(msg.sender == address(vault), "withdraw::NOT_VAULT"); + + /// underflow should not stop vault from withdrawing + uint256 depositedUnderlying_ = depositedUnderlying; + if (depositedUnderlying_ >= amount) { + unchecked { + depositedUnderlying = depositedUnderlying_ - amount; + } + } + + if (float() < amount) { + success = NOT_ENOUGH_UNDERLYING; + } else { + underlying.transfer(msg.sender, amount); + + emit Withdraw(IVault(msg.sender), amount); + } + } + + /*/////////////////////////////////////////////////////////////// + DEPOSIT/WITHDRAW UNDERLYING + //////////////////////////////////////////////////////////////*/ + + /// @notice Deposit underlying in strategy's yielding option. + /// @param amount The amount to deposit. + function depositUnderlying(uint256 amount) external virtual; + + /// @notice Withdraw underlying from strategy's yielding option. + /// @param amount The amount to withdraw. + function withdrawUnderlying(uint256 amount) external virtual; + + /*/////////////////////////////////////////////////////////////// + ACCOUNTING + //////////////////////////////////////////////////////////////*/ + + /// @notice Float amount of underlying tokens. + function float() public view returns (uint256) { + return underlying.balanceOf(address(this)); + } + + /// @notice An estimate amount of underlying managed by the strategy. + function estimatedUnderlying() external view virtual returns (uint256); + + /*/////////////////////////////////////////////////////////////// + EMERGENCY/ASSETS RECOVERY + //////////////////////////////////////////////////////////////*/ + + /// @notice Sweep tokens not equals to the underlying asset. + /// @dev Can be used to transfer non-desired assets from the strategy. + function sweep(IERC20 asset, uint256 amount) external { + require(msg.sender == manager, "sweep::NOT_MANAGER"); + require(asset != underlying, "sweep:SAME_AS_UNDERLYING"); + asset.safeTransfer(msg.sender, amount); + + emit Sweep(asset, amount); + } +} diff --git a/hub/src/strategy/strategies/beefy/BeefyVelodrome.sol b/hub/src/strategy/strategies/beefy/BeefyVelodrome.sol new file mode 100644 index 0000000..6aeab84 --- /dev/null +++ b/hub/src/strategy/strategies/beefy/BeefyVelodrome.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.10; + +import {ERC20Upgradeable as ERC20} from "@oz-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {OwnableUpgradeable as Ownable} from "@oz-upgradeable/access/OwnableUpgradeable.sol"; +import {AccessControlUpgradeable as AccessControl} from "@oz-upgradeable/access/AccessControlUpgradeable.sol"; +import {ReentrancyGuardUpgradeable as ReentrancyGuard} from "@oz-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {AddressUpgradeable as Address} from "@oz-upgradeable/utils/AddressUpgradeable.sol"; + +import {Initializable} from "@oz-upgradeable/proxy/utils/Initializable.sol"; +import {IERC20MetadataUpgradeable as IERC20} from "@oz-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {SafeERC20Upgradeable as SafeERC20} from "@oz-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import {IVault} from "@interfaces/IVault.sol"; +import {BaseStrategy} from "@strategies/base/BaseStrategy.sol"; + +import {IBeefyVaultV6} from "./interfaces/IBeefyVaultV6.sol"; +import {IBeefyUniV2ZapSolidly} from "./interfaces/IBeefyUniV2ZapSolidly.sol"; + +/// @notice deposits USDC into the beefy vault +contract BeefyVelodromeStrategy is BaseStrategy { + using SafeERC20 for IERC20; + + // --- todo: check upgrade + IBeefyVaultV6 public constant BEEFY_VAULT_USDC_MAI = + IBeefyVaultV6(0x01D9cfB8a9D43013a1FdC925640412D8d2D900F0); + IBeefyUniV2ZapSolidly public constant BEEFY_UNI_V2_ZAP = + IBeefyUniV2ZapSolidly( + payable(0x9b50B06B81f033ca86D70F0a44F30BD7E0155737) + ); + + function initialize( + IVault _vault, + address _manager, + address _strategist + ) external initializer { + __initialize( + _vault, + IERC20(address(_vault.underlying())), + _manager, + _strategist, + "BeefyUSDC" + ); + } + + function depositUnderlying(uint256 amount) external override { + require( + msg.sender == manager, + "BeefyVelodromeStrategy::depositUnderling:NOT MANAGER" + ); + + underlying.approve(address(BEEFY_UNI_V2_ZAP), amount); + + BEEFY_UNI_V2_ZAP.beefIn( + address(BEEFY_VAULT_USDC_MAI), + (amount * 995) / 1000, + address(underlying), + amount + ); + } + + function withdrawUnderlying(uint256 amount) external override { + require( + msg.sender == manager, + "BeefyVelodromeStrategy::depositUnderling:NOT MANAGER" + ); + BEEFY_VAULT_USDC_MAI.approve(address(BEEFY_UNI_V2_ZAP), amount); + + BEEFY_UNI_V2_ZAP.beefOut(address(BEEFY_VAULT_USDC_MAI), amount); + } + + function estimatedUnderlying() external view override returns (uint256) { + return float() + beefyBalance(); + } + + function depositedIntoVault() public view returns (uint256) { + return depositedUnderlying - float(); + } + + /// @notice computes the estimated balance in underlying + + /// we need to approximate the value of the beefy vault in whatever unerlying token we have + function beefyBalance() public view returns (uint256) { + bytes memory sig = abi.encodeWithSignature( + "beefOutAndSwap(address,uint256,address,uint256)", + address(BEEFY_VAULT_USDC_MAI), + (depositedIntoVault() * 995) / 1000, + address(underlying), + depositedIntoVault() + ); + + (bool success, bytes memory data) = address(BEEFY_UNI_V2_ZAP) + .staticcall(sig); + require(success, "Call failed"); + return 100; + } +} diff --git a/hub/src/strategy/strategies/beefy/interfaces/IBeefyUniV2ZapSolidly.sol b/hub/src/strategy/strategies/beefy/interfaces/IBeefyUniV2ZapSolidly.sol new file mode 100644 index 0000000..e9b91ba --- /dev/null +++ b/hub/src/strategy/strategies/beefy/interfaces/IBeefyUniV2ZapSolidly.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.6.6. SEE SOURCE BELOW. !! +pragma solidity >=0.7.0 <0.9.0; + +interface IBeefyUniV2ZapSolidly { + function WETH() external view returns (address); + + function beefIn( + address beefyVault, + uint256 tokenAmountOutMin, + address tokenIn, + uint256 tokenInAmount + ) external; + + function beefInETH(address beefyVault, uint256 tokenAmountOutMin) + external + payable; + + function beefOut(address beefyVault, uint256 withdrawAmount) external; + + function beefOutAndSwap( + address beefyVault, + uint256 withdrawAmount, + address desiredToken, + uint256 desiredTokenOutMin + ) external; + + function checkWETH() external view returns (bool isValid); + + function estimateSwap( + address beefyVault, + address tokenIn, + uint256 fullInvestmentIn + ) + external + view + returns ( + uint256 swapAmountIn, + uint256 swapAmountOut, + address swapTokenOut + ); + + function minimumAmount() external view returns (uint256); + + function router() external view returns (address); + + receive() external payable; +} + +// THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: +/* +[{"inputs":[{"internalType":"address","name":"_router","type":"address"},{"internalType":"address","name":"_WETH","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"beefyVault","type":"address"},{"internalType":"uint256","name":"tokenAmountOutMin","type":"uint256"},{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"uint256","name":"tokenInAmount","type":"uint256"}],"name":"beefIn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"beefyVault","type":"address"},{"internalType":"uint256","name":"tokenAmountOutMin","type":"uint256"}],"name":"beefInETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"beefyVault","type":"address"},{"internalType":"uint256","name":"withdrawAmount","type":"uint256"}],"name":"beefOut","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"beefyVault","type":"address"},{"internalType":"uint256","name":"withdrawAmount","type":"uint256"},{"internalType":"address","name":"desiredToken","type":"address"},{"internalType":"uint256","name":"desiredTokenOutMin","type":"uint256"}],"name":"beefOutAndSwap","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"checkWETH","outputs":[{"internalType":"bool","name":"isValid","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"beefyVault","type":"address"},{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"uint256","name":"fullInvestmentIn","type":"uint256"}],"name":"estimateSwap","outputs":[{"internalType":"uint256","name":"swapAmountIn","type":"uint256"},{"internalType":"uint256","name":"swapAmountOut","type":"uint256"},{"internalType":"address","name":"swapTokenOut","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minimumAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"router","outputs":[{"internalType":"contract IUniswapRouterSolidly","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}] +*/ diff --git a/hub/src/strategy/strategies/beefy/interfaces/IBeefyVaultV6.sol b/hub/src/strategy/strategies/beefy/interfaces/IBeefyVaultV6.sol new file mode 100644 index 0000000..6732d50 --- /dev/null +++ b/hub/src/strategy/strategies/beefy/interfaces/IBeefyVaultV6.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.6.6. SEE SOURCE BELOW. !! +pragma solidity >=0.7.0 <0.9.0; + +interface IBeefyVaultV6 { + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + event NewStratCandidate(address implementation); + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + event Transfer(address indexed from, address indexed to, uint256 value); + event UpgradeStrat(address implementation); + + function allowance(address owner, address spender) + external + view + returns (uint256); + + function approvalDelay() external view returns (uint256); + + function approve(address spender, uint256 amount) external returns (bool); + + function available() external view returns (uint256); + + function balance() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function decimals() external view returns (uint8); + + function decreaseAllowance(address spender, uint256 subtractedValue) + external + returns (bool); + + function deposit(uint256 _amount) external; + + function depositAll() external; + + function earn() external; + + function getPricePerFullShare() external view returns (uint256); + + function inCaseTokensGetStuck(address _token) external; + + function increaseAllowance(address spender, uint256 addedValue) + external + returns (bool); + + function name() external view returns (string memory); + + function owner() external view returns (address); + + function proposeStrat(address _implementation) external; + + function renounceOwnership() external; + + function stratCandidate() + external + view + returns (address implementation, uint256 proposedTime); + + function strategy() external view returns (address); + + function symbol() external view returns (string memory); + + function totalSupply() external view returns (uint256); + + function transfer(address recipient, uint256 amount) + external + returns (bool); + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + + function transferOwnership(address newOwner) external; + + function upgradeStrat() external; + + function want() external view returns (address); + + function withdraw(uint256 _shares) external; + + function withdrawAll() external; +} + +// THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: +/* +[{"inputs":[{"internalType":"contract IStrategy","name":"_strategy","type":"address"},{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint256","name":"_approvalDelay","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"implementation","type":"address"}],"name":"NewStratCandidate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"implementation","type":"address"}],"name":"UpgradeStrat","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"approvalDelay","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"available","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"balance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"depositAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"earn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getPricePerFullShare","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"inCaseTokensGetStuck","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_implementation","type":"address"}],"name":"proposeStrat","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stratCandidate","outputs":[{"internalType":"address","name":"implementation","type":"address"},{"internalType":"uint256","name":"proposedTime","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"strategy","outputs":[{"internalType":"contract IStrategy","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"upgradeStrat","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"want","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_shares","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawAll","outputs":[],"stateMutability":"nonpayable","type":"function"}] +*/ diff --git a/hub/src/strategy/BaseStrategy.sol b/hub/src/strategy/xchain/BaseStrategy.sol similarity index 95% rename from hub/src/strategy/BaseStrategy.sol rename to hub/src/strategy/xchain/BaseStrategy.sol index 2d9e21b..d4fbb56 100644 --- a/hub/src/strategy/BaseStrategy.sol +++ b/hub/src/strategy/xchain/BaseStrategy.sol @@ -14,12 +14,9 @@ pragma solidity ^0.8.12; import {IVault} from "@interfaces/IVault.sol"; - -import {Address} from "openzeppelin/utils/Address.sol"; -import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; -import {AccessControl} from "openzeppelin/access/AccessControl.sol"; -import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; -import {ReentrancyGuard} from "openzeppelin/security/ReentrancyGuard.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {ReentrancyGuard} from "@oz/security/ReentrancyGuard.sol"; abstract contract BaseStrategy is ReentrancyGuard { using SafeERC20 for IERC20; diff --git a/hub/src/strategy/XChainStrategy.sol b/hub/src/strategy/xchain/XChainStrategy.sol similarity index 98% rename from hub/src/strategy/XChainStrategy.sol rename to hub/src/strategy/xchain/XChainStrategy.sol index 2cc60fa..ea29ac7 100644 --- a/hub/src/strategy/XChainStrategy.sol +++ b/hub/src/strategy/xchain/XChainStrategy.sol @@ -14,8 +14,8 @@ pragma solidity ^0.8.12; import {XChainHub} from "@hub/XChainHub.sol"; -import {BaseStrategy} from "@hub/strategy/BaseStrategy.sol"; -import {XChainStrategyEvents} from "@hub/strategy/XChainStrategyEvents.sol"; +import {BaseStrategy} from "@hub/strategy/xchain/BaseStrategy.sol"; +import {XChainStrategyEvents} from "@hub/strategy/xchain/XChainStrategyEvents.sol"; import {CallFacet} from "@hub/CallFacet.sol"; import {IVault} from "@interfaces/IVault.sol"; diff --git a/hub/src/strategy/XChainStrategyEvents.sol b/hub/src/strategy/xchain/XChainStrategyEvents.sol similarity index 100% rename from hub/src/strategy/XChainStrategyEvents.sol rename to hub/src/strategy/xchain/XChainStrategyEvents.sol diff --git a/hub/test/XChainStrategy.t.sol b/hub/test/XChainStrategy.t.sol index 390a70d..b3fd024 100644 --- a/hub/test/XChainStrategy.t.sol +++ b/hub/test/XChainStrategy.t.sol @@ -1,16 +1,14 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.12; -pragma abicoder v2; - import "@oz/token/ERC20/ERC20.sol"; import "@std/console.sol"; import {PRBTest} from "@prb/test/PRBTest.sol"; -import {XChainStrategy} from "@hub/strategy/XChainStrategy.sol"; +import {XChainStrategy} from "@hub/strategy/xchain/XChainStrategy.sol"; import {XChainHub} from "@hub/XChainHub.sol"; -import {XChainStrategyEvents} from "@hub/strategy/XChainStrategyEvents.sol"; +import {XChainStrategyEvents} from "@hub/strategy/xchain/XChainStrategyEvents.sol"; import {AuxoTest} from "@hub-test/mocks/MockERC20.sol"; import {MockVault} from "@hub-test/mocks/MockVault.sol"; diff --git a/hub/test/mocks/MockStrategy.sol b/hub/test/mocks/MockStrategy.sol index e940e76..d923dff 100644 --- a/hub/test/mocks/MockStrategy.sol +++ b/hub/test/mocks/MockStrategy.sol @@ -5,7 +5,7 @@ pragma abicoder v2; import "@oz/token/ERC20/ERC20.sol"; import "@interfaces/IStargateReceiver.sol"; -import "@hub/strategy/XChainStrategy.sol"; +import "@hub/strategy/xchain/XChainStrategy.sol"; contract MockStrat { ERC20 public underlying; diff --git a/hub/test/strategies/beefy/Beefy.t.sol b/hub/test/strategies/beefy/Beefy.t.sol new file mode 100644 index 0000000..fe17de3 --- /dev/null +++ b/hub/test/strategies/beefy/Beefy.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.10; + +import "@oz/token/ERC20/ERC20.sol"; +import "@std/console.sol"; +import {PRBTest} from "@prb/test/PRBTest.sol"; + +import {IVault} from "@interfaces/IVault.sol"; +import {IHubPayload} from "@interfaces/IHubPayload.sol"; +import {XChainHub} from "@hub/XChainHub.sol"; + +import {LZEndpointMock} from "@hub-test/mocks/MockLayerZeroEndpoint.sol"; +import {XChainHubEvents} from "@hub/XChainHubEvents.sol"; +import {MockStrat} from "@hub-test/mocks/MockStrategy.sol"; +import {AuxoTest} from "@hub-test/mocks/MockERC20.sol"; +import {StargateRouterMock} from "@hub-test/mocks/MockStargateRouter.sol"; + +import {TransparentUpgradeableProxy} from "@oz/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@oz/proxy/transparent/ProxyAdmin.sol"; + +import {BeefyVelodromeStrategy} from "@strategies/beefy/BeefyVelodrome.sol"; +import {IBeefyVaultV6} from "@strategies/beefy/interfaces/IBeefyVaultV6.sol"; + +// for testing strategies, we don't need the full vault functionality +contract SimpleMockVault { + IERC20 public underlying; + + constructor(IERC20 _underlying) { + underlying = _underlying; + } +} + +// usdc minting on optimism is controlled by the circle bridge +interface IERC20Mintable is IERC20 { + function mint(address account, uint256 amount) external; + + function l2Bridge() external returns (address); +} + +/// @dev IMPORTANT run this test against a fork of the optimism network +contract TestBeefyVelodromeStrategy is PRBTest { + IVault vault; + BeefyVelodromeStrategy beefyStrategy; + + IERC20Mintable constant usdc_optimism = + IERC20Mintable(0x7F5c764cBc14f9669B88837ca1490cCa17c31607); + + address constant manager = 0xBEeFbeefbEefbeEFbeEfbEEfBEeFbeEfBeEfBeef; + address constant strategist = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; + ProxyAdmin admin; + + function _setupVault(IERC20 _token) internal returns (IVault) { + SimpleMockVault mockVault = new SimpleMockVault(_token); + return IVault(address(mockVault)); + } + + function initializeBeefyContractProxy(IVault _vault, ProxyAdmin _admin) + internal + returns (BeefyVelodromeStrategy) + { + address implementation = address(new BeefyVelodromeStrategy()); + + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + implementation, + address(_admin), + abi.encodeWithSelector( + BeefyVelodromeStrategy.initialize.selector, + _vault, + manager, + strategist + ) + ); + return BeefyVelodromeStrategy(address(proxy)); + } + + modifier asManager() { + vm.startPrank(manager); + _; + vm.stopPrank(); + } + + function setUp() public { + vault = _setupVault(usdc_optimism); + admin = new ProxyAdmin(); + + vm.startPrank(usdc_optimism.l2Bridge()); + usdc_optimism.mint(address(vault), 1e18); + vm.stopPrank(); + + beefyStrategy = initializeBeefyContractProxy(vault, admin); + + vm.startPrank(address(vault)); + + usdc_optimism.approve(address(beefyStrategy), 1e18); + beefyStrategy.deposit(1e10); + + vm.stopPrank(); + } + + function testDepositIntoStrategy() public { + assertEq(beefyStrategy.float(), 1e10); + } + + // tests to see if deposit successful + function testDepositToVault() public asManager { + beefyStrategy.depositUnderlying(1e10); + + // check no underlying left in the strat + assertEq(beefyStrategy.float(), 0); + assertEq(usdc_optimism.balanceOf(address(beefyStrategy)), 0); + + // check that the balance of the strat in beefy tokens has increased + IBeefyVaultV6 beefyVault = beefyStrategy.BEEFY_VAULT_USDC_MAI(); + assertNotEq(beefyVault.balanceOf(address(beefyStrategy)), 0); + } + + // tests to see if deposit successful + function testWithdrawFromVault() public asManager { + beefyStrategy.depositUnderlying(1e10); + IBeefyVaultV6 beefyVault = beefyStrategy.BEEFY_VAULT_USDC_MAI(); + uint256 balanceBefore = beefyVault.balanceOf(address(beefyStrategy)); + + require(balanceBefore >= 1e10, "balancetoo low"); + + beefyStrategy.withdrawUnderlying(1e10); + + // check no underlying left in the strat + assertNotEq(beefyStrategy.float(), 0); + assertNotEq(usdc_optimism.balanceOf(address(beefyStrategy)), 0); + + // check that the balance of the strat in beefy tokens has increased + assertNotEq( + beefyVault.balanceOf(address(beefyStrategy)), + balanceBefore + ); + } +} From fcbbadf0500389ce412f86f56c2268c1ff881280 Mon Sep 17 00:00:00 2001 From: jordan Date: Tue, 20 Sep 2022 20:52:04 +0400 Subject: [PATCH 2/2] working beefy vault --- .../strategies/beefy/BeefyVelodrome.sol | 146 ++++++++++++++---- .../beefy/interfaces/IUniswapV2Pair.sol | 115 ++++++++++++++ .../beefy/interfaces/IUniswapV2Router01.sol | 107 +++++++++++++ hub/test/strategies/beefy/Beefy.t.sol | 141 +++++++++++++---- 4 files changed, 452 insertions(+), 57 deletions(-) create mode 100644 hub/src/strategy/strategies/beefy/interfaces/IUniswapV2Pair.sol create mode 100644 hub/src/strategy/strategies/beefy/interfaces/IUniswapV2Router01.sol diff --git a/hub/src/strategy/strategies/beefy/BeefyVelodrome.sol b/hub/src/strategy/strategies/beefy/BeefyVelodrome.sol index 6aeab84..5b82fbb 100644 --- a/hub/src/strategy/strategies/beefy/BeefyVelodrome.sol +++ b/hub/src/strategy/strategies/beefy/BeefyVelodrome.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.10; +pragma solidity ^0.8.12; import {ERC20Upgradeable as ERC20} from "@oz-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {OwnableUpgradeable as Ownable} from "@oz-upgradeable/access/OwnableUpgradeable.sol"; @@ -16,19 +16,39 @@ import {BaseStrategy} from "@strategies/base/BaseStrategy.sol"; import {IBeefyVaultV6} from "./interfaces/IBeefyVaultV6.sol"; import {IBeefyUniV2ZapSolidly} from "./interfaces/IBeefyUniV2ZapSolidly.sol"; +import {IUniswapV2Pair} from "./interfaces/IUniswapV2Pair.sol"; +import {IUniswapRouterSolidly} from "./interfaces/IUniswapV2Router01.sol"; /// @notice deposits USDC into the beefy vault -contract BeefyVelodromeStrategy is BaseStrategy { +/// @dev this is for USDC on Optimism +contract BeefyVelodromeStrategyUSDC_MAI is BaseStrategy { using SafeERC20 for IERC20; - // --- todo: check upgrade + // ---------------- + // Variables + // ---------------- + + /// @notice address of the Beefy Vault on optimism IBeefyVaultV6 public constant BEEFY_VAULT_USDC_MAI = IBeefyVaultV6(0x01D9cfB8a9D43013a1FdC925640412D8d2D900F0); + + /// @notice address of the Uniswap Beefy Zap LP contract IBeefyUniV2ZapSolidly public constant BEEFY_UNI_V2_ZAP = IBeefyUniV2ZapSolidly( payable(0x9b50B06B81f033ca86D70F0a44F30BD7E0155737) ); + /// @notice the accepted percent slippage in whole percent from trades + /// @dev this can be set before calling the depositUnderlying or withdrawUnderlying functions + uint8 public slippagePercentage; + + // ---------------- + // Initializer + // ---------------- + + /// @param _vault the address of the Auxo Vault that will deposit into this strategy + /// @param _manager the address of the manager who can administrate this strategy + /// @param _strategist address of the strategist who has limited but elevated access function initialize( IVault _vault, address _manager, @@ -41,57 +61,131 @@ contract BeefyVelodromeStrategy is BaseStrategy { _strategist, "BeefyUSDC" ); + slippagePercentage = 3; } - function depositUnderlying(uint256 amount) external override { + // ---------------- + // State Changing Functions + // ---------------- + + /// @notice updates the slippage percentage that is permitted in swaps + /// @dev slippage is calculated (x * (100 - slippagePercentage) / 100) + /// @param _slippagePercentage the new slippage percentage + function setSlippage(uint8 _slippagePercentage) external { require( msg.sender == manager, - "BeefyVelodromeStrategy::depositUnderling:NOT MANAGER" + "BeefyVelodromeStrategy::setSlippage:NOT MANAGER" + ); + require( + _slippagePercentage < 100, + "BeefyVelodromeStrategy::setSlippage:INVALID SLIPPAGE" + ); + slippagePercentage = _slippagePercentage; + } + + /// @notice deposits underlying tokens held in the strategy into the beefy vault + /// @param _amount of the underlying token to deposit to the beefy vault + function depositUnderlying(uint256 _amount) external override { + require( + msg.sender == manager, + "BeefyVelodromeStrategy::depositUnderlying:NOT MANAGER" ); - underlying.approve(address(BEEFY_UNI_V2_ZAP), amount); + underlying.approve(address(BEEFY_UNI_V2_ZAP), _amount); BEEFY_UNI_V2_ZAP.beefIn( address(BEEFY_VAULT_USDC_MAI), - (amount * 995) / 1000, + (_amount * (100 - slippagePercentage)) / 100, address(underlying), - amount + _amount ); + emit DepositUnderlying(_amount); } - function withdrawUnderlying(uint256 amount) external override { + /// @notice withdraws underlying tokens from the vault into the strategy + /// @param _amountShares the number of beefy vault shares to burn + function withdrawUnderlying(uint256 _amountShares) external override { require( msg.sender == manager, - "BeefyVelodromeStrategy::depositUnderling:NOT MANAGER" + "BeefyVelodromeStrategy::withdrawUnderlying:NOT MANAGER" ); - BEEFY_VAULT_USDC_MAI.approve(address(BEEFY_UNI_V2_ZAP), amount); + BEEFY_VAULT_USDC_MAI.approve(address(BEEFY_UNI_V2_ZAP), _amountShares); + + // Min quantity of underlying to accept from swap (assuming 50:50 pool) + uint256 minUnderlyingFromPool = (sharesToUnderlying(_amountShares / 2) * + (100 - slippagePercentage)) / 100; - BEEFY_UNI_V2_ZAP.beefOut(address(BEEFY_VAULT_USDC_MAI), amount); + BEEFY_UNI_V2_ZAP.beefOutAndSwap( + address(BEEFY_VAULT_USDC_MAI), + _amountShares, + address(underlying), + minUnderlyingFromPool + ); + emit WithdrawUnderlying(_amountShares); } + // ---------------- + // View Functions + // ---------------- + + /// @notice estimates the value of the underlying holdings function estimatedUnderlying() external view override returns (uint256) { return float() + beefyBalance(); } - function depositedIntoVault() public view returns (uint256) { - return depositedUnderlying - float(); + /// @notice estimates the value of beefy vault shares in underlying tokens + /// @dev takes shares * price and gets the amount of tokens returned from uni pair + /// @dev then gets a quote to swap the pair token we don't need + /// @param _shares the number of shares to estimate a value for + function sharesToUnderlying(uint256 _shares) public view returns (uint256) { + // price * qty of shares is the total liqudity we want to quote for + uint256 liquidityToRemove = (_shares * + BEEFY_VAULT_USDC_MAI.getPricePerFullShare()) / + 10**BEEFY_VAULT_USDC_MAI.decimals(); + + // grab the pair from the vault + IUniswapV2Pair pair = IUniswapV2Pair(BEEFY_VAULT_USDC_MAI.want()); + address token0 = pair.token0(); + address token1 = pair.token1(); + + // set the swap token to the pair value we are not interested in + address swapToken = (token0 == address(underlying)) ? token1 : token0; + + IUniswapRouterSolidly router = IUniswapRouterSolidly( + BEEFY_UNI_V2_ZAP.router() + ); + + // we have vault balance and price, we now want to actually get the quotes + ( + uint256 receiveAmountSwapToken, + uint256 receiveAmountUnderlying + ) = router.quoteRemoveLiquidity( + swapToken, + address(underlying), + true, + liquidityToRemove + ); + + // we only want a single underlying token + // so quote for swapping the other pair token for it + (uint256 swappedAmount, ) = router.getAmountOut( + receiveAmountSwapToken, + swapToken, + address(underlying) + ); + return swappedAmount + receiveAmountUnderlying; } /// @notice computes the estimated balance in underlying - /// we need to approximate the value of the beefy vault in whatever unerlying token we have function beefyBalance() public view returns (uint256) { - bytes memory sig = abi.encodeWithSignature( - "beefOutAndSwap(address,uint256,address,uint256)", - address(BEEFY_VAULT_USDC_MAI), - (depositedIntoVault() * 995) / 1000, - address(underlying), - depositedIntoVault() - ); + uint256 balanceStrat = BEEFY_VAULT_USDC_MAI.balanceOf(address(this)); + return sharesToUnderlying(balanceStrat); + } - (bool success, bytes memory data) = address(BEEFY_UNI_V2_ZAP) - .staticcall(sig); - require(success, "Call failed"); - return 100; + /// @dev beefy zaps can return trace amounts of unwanted token to the contract + /// @param _token the address of the token pair that is not underlying + function _residualBalance(address _token) internal view returns (uint256) { + return IERC20(_token).balanceOf(address(this)); } } diff --git a/hub/src/strategy/strategies/beefy/interfaces/IUniswapV2Pair.sol b/hub/src/strategy/strategies/beefy/interfaces/IUniswapV2Pair.sol new file mode 100644 index 0000000..97662bd --- /dev/null +++ b/hub/src/strategy/strategies/beefy/interfaces/IUniswapV2Pair.sol @@ -0,0 +1,115 @@ +interface IUniswapV2Pair { + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + event Transfer(address indexed from, address indexed to, uint256 value); + + function name() external pure returns (string memory); + + function symbol() external pure returns (string memory); + + function decimals() external pure returns (uint8); + + function totalSupply() external view returns (uint256); + + function balanceOf(address owner) external view returns (uint256); + + function allowance(address owner, address spender) + external + view + returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + + function transfer(address to, uint256 value) external returns (bool); + + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function PERMIT_TYPEHASH() external pure returns (bytes32); + + function nonces(address owner) external view returns (uint256); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn( + address indexed sender, + uint256 amount0, + uint256 amount1, + address indexed to + ); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint256); + + function factory() external view returns (address); + + function token0() external view returns (address); + + function token1() external view returns (address); + + function getReserves() + external + view + returns ( + uint112 reserve0, + uint112 reserve1, + uint32 blockTimestampLast + ); + + function price0CumulativeLast() external view returns (uint256); + + function price1CumulativeLast() external view returns (uint256); + + function kLast() external view returns (uint256); + + function mint(address to) external returns (uint256 liquidity); + + function burn(address to) + external + returns (uint256 amount0, uint256 amount1); + + function swap( + uint256 amount0Out, + uint256 amount1Out, + address to, + bytes calldata data + ) external; + + function skim(address to) external; + + function sync() external; + + function stable() external view returns (bool); + + function getAmountOut(uint256 amountIn, address tokenIn) + external + view + returns (uint256); + + function initialize(address, address) external; +} diff --git a/hub/src/strategy/strategies/beefy/interfaces/IUniswapV2Router01.sol b/hub/src/strategy/strategies/beefy/interfaces/IUniswapV2Router01.sol new file mode 100644 index 0000000..e2ec30d --- /dev/null +++ b/hub/src/strategy/strategies/beefy/interfaces/IUniswapV2Router01.sol @@ -0,0 +1,107 @@ +pragma solidity >=0.6.0 <0.9.0; + +interface IUniswapRouterSolidly { + function addLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) + external + returns ( + uint256 amountA, + uint256 amountB, + uint256 liquidity + ); + + function addLiquidityETH( + address token, + bool stable, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) + external + payable + returns ( + uint256 amountToken, + uint256 amountETH, + uint256 liquidity + ); + + function removeLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + + function removeLiquidityETH( + address token, + bool stable, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + + function swapExactTokensForTokensSimple( + uint256 amountIn, + uint256 amountOutMin, + address tokenFrom, + address tokenTo, + bool stable, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function getAmountOut( + uint256 amountIn, + address tokenIn, + address tokenOut + ) external view returns (uint256 amount, bool stable); + + function quoteAddLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 amountADesired, + uint256 amountBDesired + ) + external + view + returns ( + uint256 amountA, + uint256 amountB, + uint256 liquidity + ); + + function quoteRemoveLiquidity( + address tokenA, + address tokenB, + bool stable, + uint256 liquidity + ) external view returns (uint256 amountA, uint256 amountB); + + function quoteLiquidity( + uint256 amountA, + uint256 reserveA, + uint256 reserveB + ) external view returns (uint256 amountB); + + function factory() external view returns (address); + + function weth() external view returns (address); +} diff --git a/hub/test/strategies/beefy/Beefy.t.sol b/hub/test/strategies/beefy/Beefy.t.sol index fe17de3..1c1e517 100644 --- a/hub/test/strategies/beefy/Beefy.t.sol +++ b/hub/test/strategies/beefy/Beefy.t.sol @@ -18,7 +18,7 @@ import {StargateRouterMock} from "@hub-test/mocks/MockStargateRouter.sol"; import {TransparentUpgradeableProxy} from "@oz/proxy/transparent/TransparentUpgradeableProxy.sol"; import {ProxyAdmin} from "@oz/proxy/transparent/ProxyAdmin.sol"; -import {BeefyVelodromeStrategy} from "@strategies/beefy/BeefyVelodrome.sol"; +import {BeefyVelodromeStrategyUSDC_MAI} from "@strategies/beefy/BeefyVelodrome.sol"; import {IBeefyVaultV6} from "@strategies/beefy/interfaces/IBeefyVaultV6.sol"; // for testing strategies, we don't need the full vault functionality @@ -39,16 +39,27 @@ interface IERC20Mintable is IERC20 { /// @dev IMPORTANT run this test against a fork of the optimism network contract TestBeefyVelodromeStrategy is PRBTest { + bool DEBUG = true; + IVault vault; - BeefyVelodromeStrategy beefyStrategy; + BeefyVelodromeStrategyUSDC_MAI beefyStrategy; IERC20Mintable constant usdc_optimism = IERC20Mintable(0x7F5c764cBc14f9669B88837ca1490cCa17c31607); + IERC20 constant mai_optimism = + IERC20(0xdFA46478F9e5EA86d57387849598dbFB2e964b02); + address constant manager = 0xBEeFbeefbEefbeEFbeEfbEEfBEeFbeEfBeEfBeef; address constant strategist = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; ProxyAdmin admin; + modifier asManager() { + vm.startPrank(manager); + _; + vm.stopPrank(); + } + function _setupVault(IERC20 _token) internal returns (IVault) { SimpleMockVault mockVault = new SimpleMockVault(_token); return IVault(address(mockVault)); @@ -56,30 +67,26 @@ contract TestBeefyVelodromeStrategy is PRBTest { function initializeBeefyContractProxy(IVault _vault, ProxyAdmin _admin) internal - returns (BeefyVelodromeStrategy) + returns (BeefyVelodromeStrategyUSDC_MAI) { - address implementation = address(new BeefyVelodromeStrategy()); + address implementation = address(new BeefyVelodromeStrategyUSDC_MAI()); TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( implementation, address(_admin), abi.encodeWithSelector( - BeefyVelodromeStrategy.initialize.selector, + BeefyVelodromeStrategyUSDC_MAI.initialize.selector, _vault, manager, strategist ) ); - return BeefyVelodromeStrategy(address(proxy)); - } - - modifier asManager() { - vm.startPrank(manager); - _; - vm.stopPrank(); + return BeefyVelodromeStrategyUSDC_MAI(address(proxy)); } function setUp() public { + if (DEBUG) console.log("---- DEBUG MODE IS ON ----"); + vault = _setupVault(usdc_optimism); admin = new ProxyAdmin(); @@ -92,46 +99,118 @@ contract TestBeefyVelodromeStrategy is PRBTest { vm.startPrank(address(vault)); usdc_optimism.approve(address(beefyStrategy), 1e18); - beefyStrategy.deposit(1e10); + beefyStrategy.deposit(1e18); vm.stopPrank(); } function testDepositIntoStrategy() public { - assertEq(beefyStrategy.float(), 1e10); + assertEq(beefyStrategy.float(), 1e18); + assertAlmostEq(beefyStrategy.estimatedUnderlying(), 1e18, 1e12); } // tests to see if deposit successful - function testDepositToVault() public asManager { - beefyStrategy.depositUnderlying(1e10); + function testDepositToVault(uint256 _amt) public asManager { + vm.assume(_amt < 1e12 && _amt > 1000); + logTokenBalances(address(beefyStrategy)); + + beefyStrategy.depositUnderlying(_amt); // check no underlying left in the strat - assertEq(beefyStrategy.float(), 0); - assertEq(usdc_optimism.balanceOf(address(beefyStrategy)), 0); + logTokenBalances(address(beefyStrategy)); + + // rounding error in the beef in: allow 0.001c error max + assertAlmostEq(beefyStrategy.float(), 1e18 - _amt, 1e3); // check that the balance of the strat in beefy tokens has increased IBeefyVaultV6 beefyVault = beefyStrategy.BEEFY_VAULT_USDC_MAI(); - assertNotEq(beefyVault.balanceOf(address(beefyStrategy)), 0); + + // beefy has 18 decimals, so 1e12 is 1 millionth of a token + assertAlmostEq( + beefyStrategy.sharesToUnderlying( + beefyVault.balanceOf(address(beefyStrategy)) + ), + _amt, + 1e12 + ); + + // underlying not changed + assertAlmostEq(beefyStrategy.estimatedUnderlying(), 1e18, 1e12); } - // tests to see if deposit successful - function testWithdrawFromVault() public asManager { - beefyStrategy.depositUnderlying(1e10); + // useful for debugging + function logTokenBalances(address _who) internal view { + if (DEBUG) { + IBeefyVaultV6 beefyVault = beefyStrategy.BEEFY_VAULT_USDC_MAI(); + + console.log("---- Token Balances -----"); + console.log("Beefy Vault", beefyVault.balanceOf(_who)); + console.log("USDC", usdc_optimism.balanceOf(_who) / 10**6); + console.log("MAI", mai_optimism.balanceOf(_who) / 10**18); + } + } + + function testWithdrawFromVault(uint8 _withdrawPc) public asManager { + vm.assume(_withdrawPc <= 100 && _withdrawPc > 0); IBeefyVaultV6 beefyVault = beefyStrategy.BEEFY_VAULT_USDC_MAI(); + + logTokenBalances(address(beefyStrategy)); + + beefyStrategy.depositUnderlying(1_000_000); + uint256 balanceBefore = beefyVault.balanceOf(address(beefyStrategy)); + uint256 approxValueBefore = beefyStrategy.sharesToUnderlying( + balanceBefore + ); - require(balanceBefore >= 1e10, "balancetoo low"); + logTokenBalances(address(beefyStrategy)); - beefyStrategy.withdrawUnderlying(1e10); + uint256 sharesWithdraw = (balanceBefore * _withdrawPc) / 100; - // check no underlying left in the strat - assertNotEq(beefyStrategy.float(), 0); - assertNotEq(usdc_optimism.balanceOf(address(beefyStrategy)), 0); + beefyStrategy.withdrawUnderlying(sharesWithdraw); - // check that the balance of the strat in beefy tokens has increased - assertNotEq( - beefyVault.balanceOf(address(beefyStrategy)), - balanceBefore + logTokenBalances(address(beefyStrategy)); + + assertAlmostEq(beefyStrategy.estimatedUnderlying(), 1e18, 1e12); + + uint256 balanceAfter = beefyVault.balanceOf(address(beefyStrategy)); + uint256 approxValueAfter = beefyStrategy.sharesToUnderlying( + balanceAfter ); + + assertAlmostEq( + approxValueBefore - approxValueAfter, + beefyStrategy.sharesToUnderlying(sharesWithdraw), + 1e12 + ); + } + + function testSlippage(uint8 _slippagePercentage) public asManager { + if (_slippagePercentage < 100) { + beefyStrategy.setSlippage(_slippagePercentage); + assertEq(beefyStrategy.slippagePercentage(), _slippagePercentage); + } else { + vm.expectRevert( + "BeefyVelodromeStrategy::setSlippage:INVALID SLIPPAGE" + ); + beefyStrategy.setSlippage(_slippagePercentage); + } + } + + function testManagerialFunctions(address _notManager) public { + vm.assume(_notManager != manager); + + vm.expectRevert( + "BeefyVelodromeStrategy::withdrawUnderlying:NOT MANAGER" + ); + beefyStrategy.withdrawUnderlying(100); + + vm.expectRevert( + "BeefyVelodromeStrategy::depositUnderlying:NOT MANAGER" + ); + beefyStrategy.depositUnderlying(100); + + vm.expectRevert("BeefyVelodromeStrategy::setSlippage:NOT MANAGER"); + beefyStrategy.setSlippage(100); } }