diff --git a/brownie/addresses.py b/brownie/addresses.py index 647bb93055..4c4a4e5d11 100644 --- a/brownie/addresses.py +++ b/brownie/addresses.py @@ -15,6 +15,7 @@ # OUSD Contracts OUSD = '0x2A8e1E676Ec238d8A992307B495b45B3fEAa5e86' +OETH = '0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3' AAVE_STRAT = '0x5e3646A1Db86993f73E6b74A57D8640B69F7e259' COMP_STRAT = '0x9c459eeb3FA179a40329b81C1635525e9A0Ef094' CONVEX_STRAT = '0xEA2Ef2e2E5A749D4A66b41Db9aD85a38Aa264cb3' @@ -27,6 +28,7 @@ HARVESTER = '0x21fb5812d70b3396880d30e90d9e5c1202266c89' DRIPPER = '0x80c898ae5e56f888365e235ceb8cea3eb726cb58' VAULT_PROXY_ADDRESS = '0xE75D77B1865Ae93c7eaa3040B038D7aA7BC02F70' +VAULT_OETH_PROXY_ADDRESS = '0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab' VAULT_ADMIN_IMPL = '0x1eF0553FEb80e6f133cAe3092e38F0b23dA6452b' VAULT_CORE_IMPL = '0xf00d4b19458c594d4ea9d0b9861edfd2c444fa9a' VAULT_VALUE_CHECKER = '0xEEcD72c99749A1FC977704AB900a05e8300F4318' @@ -46,6 +48,7 @@ CVX = '0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B' FRAX = '0x853d955aCEf822Db058eb8505911ED77F175b99e' BUSD = '0x4Fabb145d64652a948d72533023f6E7A623C7C53' +WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' THREEPOOL_LP = '0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490' OUSD_METAPOOL = '0x87650D7bbfC3A9F10587d7778206671719d9910D' diff --git a/brownie/world.py b/brownie/world.py index d842acefad..b9707fe3e2 100644 --- a/brownie/world.py +++ b/brownie/world.py @@ -27,7 +27,9 @@ def load_contract(name, address): frax = load_contract('ERC20', FRAX) busd = load_contract('ERC20', BUSD) +weth = load_contract('ERC20', WETH) ousd = load_contract('ousd', OUSD) +oeth = load_contract('ousd', OETH) usdt = load_contract('usdt', USDT) usdc = load_contract('usdc', USDC) dai = load_contract('dai', DAI) @@ -38,6 +40,8 @@ def load_contract(name, address): veogv = load_contract('veogv', VEOGV) vault_admin = load_contract('vault_admin', VAULT_PROXY_ADDRESS) vault_core = load_contract('vault_core', VAULT_PROXY_ADDRESS) +vault_oeth_admin = load_contract('vault_admin', VAULT_OETH_PROXY_ADDRESS) +vault_oeth_core = load_contract('vault_core', VAULT_OETH_PROXY_ADDRESS) vault_value_checker = load_contract('vault_value_checker', VAULT_VALUE_CHECKER) dripper = load_contract('dripper', DRIPPER) harvester = load_contract('harvester', HARVESTER) diff --git a/contracts/contracts/harvest/BaseHarvester.sol b/contracts/contracts/harvest/BaseHarvester.sol new file mode 100644 index 0000000000..3ec67f16db --- /dev/null +++ b/contracts/contracts/harvest/BaseHarvester.sol @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import { StableMath } from "../utils/StableMath.sol"; +import { Governable } from "../governance/Governable.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import { IOracle } from "../interfaces/IOracle.sol"; +import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IUniswapV2Router } from "../interfaces/uniswap/IUniswapV2Router02.sol"; +import "../utils/Helpers.sol"; + +abstract contract BaseHarvester is Governable { + using SafeERC20 for IERC20; + using SafeMath for uint256; + using StableMath for uint256; + + event UniswapUpdated(address _address); + event SupportedStrategyUpdate(address _address, bool _isSupported); + event RewardTokenConfigUpdated( + address _tokenAddress, + uint16 _allowedSlippageBps, + uint16 _harvestRewardBps, + address _uniswapV2CompatibleAddr, + uint256 _liquidationLimit, + bool _doSwapRewardToken + ); + + // Configuration properties for harvesting logic of reward tokens + struct RewardTokenConfig { + // Max allowed slippage when swapping reward token for a stablecoin denominated in basis points. + uint16 allowedSlippageBps; + // Reward when calling a harvest function denominated in basis points. + uint16 harvestRewardBps; + /* Address of Uniswap V2 compatible exchange (Uniswap V2, SushiSwap). + */ + address uniswapV2CompatibleAddr; + /* When true the reward token is being swapped. In a need of (temporarily) disabling the swapping of + * a reward token this needs to be set to false. + */ + bool doSwapRewardToken; + /* How much token can be sold per one harvest call. If the balance of rewards tokens + * exceeds that limit multiple harvest calls are required to harvest all of the tokens. + * Set it to MAX_INT to effectively disable the limit. + */ + uint256 liquidationLimit; + } + + mapping(address => RewardTokenConfig) public rewardTokenConfigs; + mapping(address => bool) public supportedStrategies; + + address public immutable vaultAddress; + + /** + * Address receiving rewards proceeds. Initially the Vault contract later will possibly + * be replaced by another contract that eases out rewards distribution. + */ + address public rewardProceedsAddress; + + /** + * @dev Constructor to set up initial internal state + * @param _vaultAddress Address of the Vault + */ + constructor(address _vaultAddress) { + require(address(_vaultAddress) != address(0)); + vaultAddress = _vaultAddress; + } + + /*************************************** + Configuration + ****************************************/ + + /** + * @dev Throws if called by any address other than the Vault. + */ + modifier onlyVaultOrGovernor() { + require( + msg.sender == vaultAddress || isGovernor(), + "Caller is not the Vault or Governor" + ); + _; + } + + /** + * Set the Address receiving rewards proceeds. + * @param _rewardProceedsAddress Address of the reward token + */ + function setRewardsProceedsAddress(address _rewardProceedsAddress) + external + onlyGovernor + { + require( + _rewardProceedsAddress != address(0), + "Rewards proceeds address should be a non zero address" + ); + + rewardProceedsAddress = _rewardProceedsAddress; + } + + /** + * @dev Add/update a reward token configuration that holds harvesting config variables + * @param _tokenAddress Address of the reward token + * @param _allowedSlippageBps uint16 maximum allowed slippage denominated in basis points. + * Example: 300 == 3% slippage + * @param _harvestRewardBps uint16 amount of reward tokens the caller of the function is rewarded. + * Example: 100 == 1% + * @param _uniswapV2CompatibleAddr Address Address of a UniswapV2 compatible contract to perform + * the exchange from reward tokens to stablecoin (currently hard-coded to USDT) + * @param _liquidationLimit uint256 Maximum amount of token to be sold per one swap function call. + * When value is 0 there is no limit. + * @param _doSwapRewardToken bool When true the reward token is being swapped. In a need of (temporarily) + * disabling the swapping of a reward token this needs to be set to false. + */ + function setRewardTokenConfig( + address _tokenAddress, + uint16 _allowedSlippageBps, + uint16 _harvestRewardBps, + address _uniswapV2CompatibleAddr, + uint256 _liquidationLimit, + bool _doSwapRewardToken + ) external onlyGovernor { + require( + _allowedSlippageBps <= 1000, + "Allowed slippage should not be over 10%" + ); + require( + _harvestRewardBps <= 1000, + "Harvest reward fee should not be over 10%" + ); + require( + _uniswapV2CompatibleAddr != address(0), + "Uniswap compatible address should be non zero address" + ); + + RewardTokenConfig memory tokenConfig = RewardTokenConfig({ + allowedSlippageBps: _allowedSlippageBps, + harvestRewardBps: _harvestRewardBps, + uniswapV2CompatibleAddr: _uniswapV2CompatibleAddr, + doSwapRewardToken: _doSwapRewardToken, + liquidationLimit: _liquidationLimit + }); + + address oldUniswapAddress = rewardTokenConfigs[_tokenAddress] + .uniswapV2CompatibleAddr; + rewardTokenConfigs[_tokenAddress] = tokenConfig; + + IERC20 token = IERC20(_tokenAddress); + + address priceProvider = IVault(vaultAddress).priceProvider(); + + // Revert if feed does not exist + // slither-disable-next-line unused-return + IOracle(priceProvider).price(_tokenAddress); + + // if changing token swap provider cancel existing allowance + if ( + /* oldUniswapAddress == address(0) when there is no pre-existing + * configuration for said rewards token + */ + oldUniswapAddress != address(0) && + oldUniswapAddress != _uniswapV2CompatibleAddr + ) { + token.safeApprove(oldUniswapAddress, 0); + } + + // Give Uniswap infinite approval when needed + if (oldUniswapAddress != _uniswapV2CompatibleAddr) { + token.safeApprove(_uniswapV2CompatibleAddr, 0); + token.safeApprove(_uniswapV2CompatibleAddr, type(uint256).max); + } + + emit RewardTokenConfigUpdated( + _tokenAddress, + _allowedSlippageBps, + _harvestRewardBps, + _uniswapV2CompatibleAddr, + _liquidationLimit, + _doSwapRewardToken + ); + } + + /** + * @dev Flags a strategy as supported or not supported one + * @param _strategyAddress Address of the strategy + * @param _isSupported Bool marking strategy as supported or not supported + */ + function setSupportedStrategy(address _strategyAddress, bool _isSupported) + external + onlyVaultOrGovernor + { + supportedStrategies[_strategyAddress] = _isSupported; + emit SupportedStrategyUpdate(_strategyAddress, _isSupported); + } + + /*************************************** + Rewards + ****************************************/ + + /** + * @dev Transfer token to governor. Intended for recovering tokens stuck in + * contract, i.e. mistaken sends. + * @param _asset Address for the asset + * @param _amount Amount of the asset to transfer + */ + function transferToken(address _asset, uint256 _amount) + external + onlyGovernor + { + IERC20(_asset).safeTransfer(governor(), _amount); + } + + /** + * @dev Collect reward tokens from all strategies + */ + function harvest() external onlyGovernor nonReentrant { + _harvest(); + } + + /** + * @dev Swap all supported swap tokens for stablecoins via Uniswap. + */ + function swap() external onlyGovernor nonReentrant { + _swap(rewardProceedsAddress); + } + + /* + * @dev Collect reward tokens from all strategies and swap for supported + * stablecoin via Uniswap + */ + function harvestAndSwap() external onlyGovernor nonReentrant { + _harvest(); + _swap(rewardProceedsAddress); + } + + /** + * @dev Collect reward tokens for a specific strategy. + * @param _strategyAddr Address of the strategy to collect rewards from + */ + function harvest(address _strategyAddr) external onlyGovernor nonReentrant { + _harvest(_strategyAddr); + } + + /** + * @dev Collect reward tokens for a specific strategy and swap for supported + * stablecoin via Uniswap. Can be called by anyone. Rewards incentivizing + * the caller are sent to the caller of this function. + * @param _strategyAddr Address of the strategy to collect rewards from + */ + function harvestAndSwap(address _strategyAddr) external nonReentrant { + // Remember _harvest function checks for the validity of _strategyAddr + _harvestAndSwap(_strategyAddr, msg.sender); + } + + /** + * @dev Collect reward tokens for a specific strategy and swap for supported + * stablecoin via Uniswap. Can be called by anyone. + * @param _strategyAddr Address of the strategy to collect rewards from + * @param _rewardTo Address where to send a share of harvest rewards to as an incentive + * for executing this function + */ + function harvestAndSwap(address _strategyAddr, address _rewardTo) + external + nonReentrant + { + // Remember _harvest function checks for the validity of _strategyAddr + _harvestAndSwap(_strategyAddr, _rewardTo); + } + + /** + * @dev Governance convenience function to swap a specific _rewardToken and send + * rewards to the vault. + * @param _swapToken Address of the token to swap. + */ + function swapRewardToken(address _swapToken) + external + onlyGovernor + nonReentrant + { + _swap(_swapToken, rewardProceedsAddress); + } + + /** + * @dev Collect reward tokens from all strategies + */ + function _harvest() internal { + address[] memory allStrategies = IVault(vaultAddress) + .getAllStrategies(); + for (uint256 i = 0; i < allStrategies.length; i++) { + _harvest(allStrategies[i]); + } + } + + /** + * @dev Collect reward tokens for a specific strategy and swap for supported + * stablecoin via Uniswap. + * @param _strategyAddr Address of the strategy to collect rewards from + * @param _rewardTo Address where to send a share of harvest rewards to as an incentive + * for executing this function + */ + function _harvestAndSwap(address _strategyAddr, address _rewardTo) + internal + { + _harvest(_strategyAddr); + IStrategy strategy = IStrategy(_strategyAddr); + address[] memory rewardTokens = strategy.getRewardTokenAddresses(); + for (uint256 i = 0; i < rewardTokens.length; i++) { + _swap(rewardTokens[i], _rewardTo); + } + } + + /** + * @dev Collect reward tokens from a single strategy and swap them for a + * supported stablecoin via Uniswap + * @param _strategyAddr Address of the strategy to collect rewards from. + */ + function _harvest(address _strategyAddr) internal { + require( + supportedStrategies[_strategyAddr], + "Not a valid strategy address" + ); + + IStrategy strategy = IStrategy(_strategyAddr); + strategy.collectRewardTokens(); + } + + /** + * @dev Swap all supported swap tokens for stablecoins via Uniswap. And send the incentive part + * of the rewards to _rewardTo address. + * @param _rewardTo Address where to send a share of harvest rewards to as an incentive + * for executing this function + */ + function _swap(address _rewardTo) internal { + address[] memory allStrategies = IVault(vaultAddress) + .getAllStrategies(); + + for (uint256 i = 0; i < allStrategies.length; i++) { + IStrategy strategy = IStrategy(allStrategies[i]); + address[] memory rewardTokenAddresses = strategy + .getRewardTokenAddresses(); + + for (uint256 j = 0; j < rewardTokenAddresses.length; j++) { + _swap(rewardTokenAddresses[j], _rewardTo); + } + } + } + + /** + * @dev Swap a reward token for stablecoins on Uniswap. The token must have + * a registered price feed with the price provider. + * @param _swapToken Address of the token to swap. + * @param _rewardTo Address where to send the share of harvest rewards to + */ + function _swap(address _swapToken, address _rewardTo) internal virtual; +} diff --git a/contracts/contracts/harvest/Harvester.sol b/contracts/contracts/harvest/Harvester.sol index f64bf9f065..21a3f8e8dd 100644 --- a/contracts/contracts/harvest/Harvester.sol +++ b/contracts/contracts/harvest/Harvester.sol @@ -10,355 +10,35 @@ import { StableMath } from "../utils/StableMath.sol"; import { Governable } from "../governance/Governable.sol"; import { IVault } from "../interfaces/IVault.sol"; import { IOracle } from "../interfaces/IOracle.sol"; +import { BaseHarvester } from "./BaseHarvester.sol"; import { IStrategy } from "../interfaces/IStrategy.sol"; import { IUniswapV2Router } from "../interfaces/uniswap/IUniswapV2Router02.sol"; import "../utils/Helpers.sol"; -contract Harvester is Governable { +contract Harvester is BaseHarvester { using SafeERC20 for IERC20; using SafeMath for uint256; using StableMath for uint256; - event UniswapUpdated(address _address); - event SupportedStrategyUpdate(address _address, bool _isSupported); - event RewardTokenConfigUpdated( - address _tokenAddress, - uint16 _allowedSlippageBps, - uint16 _harvestRewardBps, - address _uniswapV2CompatibleAddr, - uint256 _liquidationLimit, - bool _doSwapRewardToken - ); - - // Configuration properties for harvesting logic of reward tokens - struct RewardTokenConfig { - // Max allowed slippage when swapping reward token for a stablecoin denominated in basis points. - uint16 allowedSlippageBps; - // Reward when calling a harvest function denominated in basis points. - uint16 harvestRewardBps; - /* Address of Uniswap V2 compatible exchange (Uniswap V2, SushiSwap). - */ - address uniswapV2CompatibleAddr; - /* When true the reward token is being swapped. In a need of (temporarily) disabling the swapping of - * a reward token this needs to be set to false. - */ - bool doSwapRewardToken; - /* How much token can be sold per one harvest call. If the balance of rewards tokens - * exceeds that limit multiple harvest calls are required to harvest all of the tokens. - * Set it to MAX_INT to effectively disable the limit. - */ - uint256 liquidationLimit; - } - - mapping(address => RewardTokenConfig) public rewardTokenConfigs; - mapping(address => bool) public supportedStrategies; - - address public immutable vaultAddress; address public immutable usdtAddress; - /** - * Address receiving rewards proceeds. Initially the Vault contract later will possibly - * be replaced by another contract that eases out rewards distribution. - */ - address public rewardProceedsAddress; - /** * @dev Constructor to set up initial internal state - * @param _vaultAddress Address of the Vault + * @param _vault Address of the Vault * @param _usdtAddress Address of Tether */ - constructor(address _vaultAddress, address _usdtAddress) { - require(address(_vaultAddress) != address(0)); + constructor(address _vault, address _usdtAddress) BaseHarvester(_vault) { require(address(_usdtAddress) != address(0)); - vaultAddress = _vaultAddress; usdtAddress = _usdtAddress; } - /*************************************** - Configuration - ****************************************/ - - /** - * @dev Throws if called by any address other than the Vault. - */ - modifier onlyVaultOrGovernor() { - require( - msg.sender == vaultAddress || isGovernor(), - "Caller is not the Vault or Governor" - ); - _; - } - - /** - * Set the Address receiving rewards proceeds. - * @param _rewardProceedsAddress Address of the reward token - */ - function setRewardsProceedsAddress(address _rewardProceedsAddress) - external - onlyGovernor - { - require( - _rewardProceedsAddress != address(0), - "Rewards proceeds address should be a non zero address" - ); - - rewardProceedsAddress = _rewardProceedsAddress; - } - - /** - * @dev Add/update a reward token configuration that holds harvesting config variables - * @param _tokenAddress Address of the reward token - * @param _allowedSlippageBps uint16 maximum allowed slippage denominated in basis points. - * Example: 300 == 3% slippage - * @param _harvestRewardBps uint16 amount of reward tokens the caller of the function is rewarded. - * Example: 100 == 1% - * @param _uniswapV2CompatibleAddr Address Address of a UniswapV2 compatible contract to perform - * the exchange from reward tokens to stablecoin (currently hard-coded to USDT) - * @param _liquidationLimit uint256 Maximum amount of token to be sold per one swap function call. - * When value is 0 there is no limit. - * @param _doSwapRewardToken bool When true the reward token is being swapped. In a need of (temporarily) - * disabling the swapping of a reward token this needs to be set to false. - */ - function setRewardTokenConfig( - address _tokenAddress, - uint16 _allowedSlippageBps, - uint16 _harvestRewardBps, - address _uniswapV2CompatibleAddr, - uint256 _liquidationLimit, - bool _doSwapRewardToken - ) external onlyGovernor { - require( - _allowedSlippageBps <= 1000, - "Allowed slippage should not be over 10%" - ); - require( - _harvestRewardBps <= 1000, - "Harvest reward fee should not be over 10%" - ); - require( - _uniswapV2CompatibleAddr != address(0), - "Uniswap compatible address should be non zero address" - ); - - RewardTokenConfig memory tokenConfig = RewardTokenConfig({ - allowedSlippageBps: _allowedSlippageBps, - harvestRewardBps: _harvestRewardBps, - uniswapV2CompatibleAddr: _uniswapV2CompatibleAddr, - doSwapRewardToken: _doSwapRewardToken, - liquidationLimit: _liquidationLimit - }); - - address oldUniswapAddress = rewardTokenConfigs[_tokenAddress] - .uniswapV2CompatibleAddr; - rewardTokenConfigs[_tokenAddress] = tokenConfig; - - IERC20 token = IERC20(_tokenAddress); - - address priceProvider = IVault(vaultAddress).priceProvider(); - - // Revert if feed does not exist - // slither-disable-next-line unused-return - IOracle(priceProvider).price(_tokenAddress); - - // if changing token swap provider cancel existing allowance - if ( - /* oldUniswapAddress == address(0) when there is no pre-existing - * configuration for said rewards token - */ - oldUniswapAddress != address(0) && - oldUniswapAddress != _uniswapV2CompatibleAddr - ) { - token.safeApprove(oldUniswapAddress, 0); - } - - // Give Uniswap infinite approval when needed - if (oldUniswapAddress != _uniswapV2CompatibleAddr) { - token.safeApprove(_uniswapV2CompatibleAddr, 0); - token.safeApprove(_uniswapV2CompatibleAddr, type(uint256).max); - } - - emit RewardTokenConfigUpdated( - _tokenAddress, - _allowedSlippageBps, - _harvestRewardBps, - _uniswapV2CompatibleAddr, - _liquidationLimit, - _doSwapRewardToken - ); - } - - /** - * @dev Flags a strategy as supported or not supported one - * @param _strategyAddress Address of the strategy - * @param _isSupported Bool marking strategy as supported or not supported - */ - function setSupportedStrategy(address _strategyAddress, bool _isSupported) - external - onlyVaultOrGovernor - { - supportedStrategies[_strategyAddress] = _isSupported; - emit SupportedStrategyUpdate(_strategyAddress, _isSupported); - } - - /*************************************** - Rewards - ****************************************/ - - /** - * @dev Transfer token to governor. Intended for recovering tokens stuck in - * contract, i.e. mistaken sends. - * @param _asset Address for the asset - * @param _amount Amount of the asset to transfer - */ - function transferToken(address _asset, uint256 _amount) - external - onlyGovernor - { - IERC20(_asset).safeTransfer(governor(), _amount); - } - - /** - * @dev Collect reward tokens from all strategies - */ - function harvest() external onlyGovernor nonReentrant { - _harvest(); - } - - /** - * @dev Swap all supported swap tokens for stablecoins via Uniswap. - */ - function swap() external onlyGovernor nonReentrant { - _swap(rewardProceedsAddress); - } - - /* - * @dev Collect reward tokens from all strategies and swap for supported - * stablecoin via Uniswap - */ - function harvestAndSwap() external onlyGovernor nonReentrant { - _harvest(); - _swap(rewardProceedsAddress); - } - - /** - * @dev Collect reward tokens for a specific strategy. - * @param _strategyAddr Address of the strategy to collect rewards from - */ - function harvest(address _strategyAddr) external onlyGovernor nonReentrant { - _harvest(_strategyAddr); - } - - /** - * @dev Collect reward tokens for a specific strategy and swap for supported - * stablecoin via Uniswap. Can be called by anyone. Rewards incentivizing - * the caller are sent to the caller of this function. - * @param _strategyAddr Address of the strategy to collect rewards from - */ - function harvestAndSwap(address _strategyAddr) external nonReentrant { - // Remember _harvest function checks for the validity of _strategyAddr - _harvestAndSwap(_strategyAddr, msg.sender); - } - - /** - * @dev Collect reward tokens for a specific strategy and swap for supported - * stablecoin via Uniswap. Can be called by anyone. - * @param _strategyAddr Address of the strategy to collect rewards from - * @param _rewardTo Address where to send a share of harvest rewards to as an incentive - * for executing this function - */ - function harvestAndSwap(address _strategyAddr, address _rewardTo) - external - nonReentrant - { - // Remember _harvest function checks for the validity of _strategyAddr - _harvestAndSwap(_strategyAddr, _rewardTo); - } - - /** - * @dev Governance convenience function to swap a specific _rewardToken and send - * rewards to the vault. - * @param _swapToken Address of the token to swap. - */ - function swapRewardToken(address _swapToken) - external - onlyGovernor - nonReentrant - { - _swap(_swapToken, rewardProceedsAddress); - } - - /** - * @dev Collect reward tokens from all strategies - */ - function _harvest() internal { - address[] memory allStrategies = IVault(vaultAddress) - .getAllStrategies(); - for (uint256 i = 0; i < allStrategies.length; i++) { - _harvest(allStrategies[i]); - } - } - - /** - * @dev Collect reward tokens for a specific strategy and swap for supported - * stablecoin via Uniswap. - * @param _strategyAddr Address of the strategy to collect rewards from - * @param _rewardTo Address where to send a share of harvest rewards to as an incentive - * for executing this function - */ - function _harvestAndSwap(address _strategyAddr, address _rewardTo) - internal - { - _harvest(_strategyAddr); - IStrategy strategy = IStrategy(_strategyAddr); - address[] memory rewardTokens = strategy.getRewardTokenAddresses(); - for (uint256 i = 0; i < rewardTokens.length; i++) { - _swap(rewardTokens[i], _rewardTo); - } - } - - /** - * @dev Collect reward tokens from a single strategy and swap them for a - * supported stablecoin via Uniswap - * @param _strategyAddr Address of the strategy to collect rewards from. - */ - function _harvest(address _strategyAddr) internal { - require( - supportedStrategies[_strategyAddr], - "Not a valid strategy address" - ); - - IStrategy strategy = IStrategy(_strategyAddr); - strategy.collectRewardTokens(); - } - - /** - * @dev Swap all supported swap tokens for stablecoins via Uniswap. And send the incentive part - * of the rewards to _rewardTo address. - * @param _rewardTo Address where to send a share of harvest rewards to as an incentive - * for executing this function - */ - function _swap(address _rewardTo) internal { - address[] memory allStrategies = IVault(vaultAddress) - .getAllStrategies(); - - for (uint256 i = 0; i < allStrategies.length; i++) { - IStrategy strategy = IStrategy(allStrategies[i]); - address[] memory rewardTokenAddresses = strategy - .getRewardTokenAddresses(); - - for (uint256 j = 0; j < rewardTokenAddresses.length; j++) { - _swap(rewardTokenAddresses[j], _rewardTo); - } - } - } - /** * @dev Swap a reward token for stablecoins on Uniswap. The token must have * a registered price feed with the price provider. * @param _swapToken Address of the token to swap. * @param _rewardTo Address where to send the share of harvest rewards to */ - function _swap(address _swapToken, address _rewardTo) internal { + function _swap(address _swapToken, address _rewardTo) internal override { RewardTokenConfig memory tokenConfig = rewardTokenConfigs[_swapToken]; /* This will trigger a return when reward token configuration has not yet been set diff --git a/contracts/contracts/harvest/OETHHarvester.sol b/contracts/contracts/harvest/OETHHarvester.sol new file mode 100644 index 0000000000..9ba51032af --- /dev/null +++ b/contracts/contracts/harvest/OETHHarvester.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import { StableMath } from "../utils/StableMath.sol"; +import { Governable } from "../governance/Governable.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import { IOracle } from "../interfaces/IOracle.sol"; +import { BaseHarvester } from "./BaseHarvester.sol"; +import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IUniswapV2Router } from "../interfaces/uniswap/IUniswapV2Router02.sol"; +import "../utils/Helpers.sol"; + +contract OETHHarvester is BaseHarvester { + using SafeERC20 for IERC20; + using SafeMath for uint256; + using StableMath for uint256; + + // "_usdtAddress" is set to Vault's address, but is really not used + constructor(address _vault) BaseHarvester(_vault) {} + + /** + * @dev Swap a reward token for stablecoins on Uniswap. The token must have + * a registered price feed with the price provider. + * @param _swapToken Address of the token to swap. + * @param _rewardTo Address where to send the share of harvest rewards to + */ + function _swap(address _swapToken, address _rewardTo) internal override { + RewardTokenConfig memory tokenConfig = rewardTokenConfigs[_swapToken]; + + /* This will trigger a return when reward token configuration has not yet been set + * or we have temporarily disabled swapping of specific reward token via setting + * doSwapRewardToken to false. + */ + if (!tokenConfig.doSwapRewardToken) { + return; + } + + address priceProvider = IVault(vaultAddress).priceProvider(); + + IERC20 swapToken = IERC20(_swapToken); + uint256 balance = swapToken.balanceOf(address(this)); + + if (balance == 0) { + return; + } + + uint256 balanceToSwap = Math.min(balance, tokenConfig.liquidationLimit); + + // Find reward token price feed paired with (W)ETH + uint256 oraclePrice = IOracle(priceProvider).price(_swapToken); + + // Oracle price is in 18 digits, WETH decimals are in 1e18 + uint256 minExpected = (balanceToSwap * + (1e4 - tokenConfig.allowedSlippageBps) * // max allowed slippage + oraclePrice).scaleBy(18, Helpers.getDecimals(_swapToken)) / + 1e4 / // fix the max slippage decimal position + 1e18; // and oracle price decimals position + + address wethAddress = IUniswapV2Router( + tokenConfig.uniswapV2CompatibleAddr + ).WETH(); + + // Uniswap redemption path + address[] memory path = new address[](2); + path[0] = _swapToken; + path[1] = wethAddress; + + // slither-disable-next-line unused-return + IUniswapV2Router(tokenConfig.uniswapV2CompatibleAddr) + .swapExactTokensForTokens( + balanceToSwap, + minExpected, + path, + address(this), + block.timestamp + ); + + IERC20 wethErc20 = IERC20(wethAddress); + uint256 wethBalance = wethErc20.balanceOf(address(this)); + + uint256 vaultBps = 1e4 - tokenConfig.harvestRewardBps; + uint256 rewardsProceedsShare = (wethBalance * vaultBps) / 1e4; + + require( + vaultBps > tokenConfig.harvestRewardBps, + "Address receiving harvest incentive is receiving more rewards than the rewards proceeds address" + ); + + wethErc20.safeTransfer(rewardProceedsAddress, rewardsProceedsShare); + wethErc20.safeTransfer( + _rewardTo, + wethBalance - rewardsProceedsShare // remaining share of the rewards + ); + } +} diff --git a/contracts/contracts/interfaces/IOracle.sol b/contracts/contracts/interfaces/IOracle.sol index 490eab39fa..610580486d 100644 --- a/contracts/contracts/interfaces/IOracle.sol +++ b/contracts/contracts/interfaces/IOracle.sol @@ -3,7 +3,9 @@ pragma solidity ^0.8.0; interface IOracle { /** - * @dev returns the asset price in USD, 8 decimal digits. + * @dev returns the asset price in USD, in 8 decimal digits. + * + * The version of priceProvider deployed for OETH has 18 decimal digits */ function price(address asset) external view returns (uint256); } diff --git a/contracts/contracts/oracle/OracleRouter.sol b/contracts/contracts/oracle/OracleRouter.sol index daeac71307..149af098f1 100644 --- a/contracts/contracts/oracle/OracleRouter.sol +++ b/contracts/contracts/oracle/OracleRouter.sol @@ -36,35 +36,36 @@ abstract contract OracleRouterBase is IOracle { address _feed = feed(asset); require(_feed != address(0), "Asset not available"); require(_feed != FIXED_PRICE, "Fixed price feeds not supported"); + (, int256 _iprice, , , ) = AggregatorV3Interface(_feed) .latestRoundData(); - uint8 decimals = getDecimals(asset); + uint8 decimals = getDecimals(_feed); uint256 _price = uint256(_iprice).scaleBy(18, decimals); - if (isStablecoin(asset)) { + if (shouldBePegged(asset)) { require(_price <= MAX_DRIFT, "Oracle: Price exceeds max"); require(_price >= MIN_DRIFT, "Oracle: Price under min"); } return uint256(_price); } - function getDecimals(address _asset) internal view virtual returns (uint8) { - uint8 decimals = decimalsCache[_asset]; + function getDecimals(address _feed) internal view virtual returns (uint8) { + uint8 decimals = decimalsCache[_feed]; require(decimals > 0, "Oracle: Decimals not cached"); return decimals; } - function cacheDecimals(address _asset) external returns (uint8) { - address _feed = feed(_asset); + function cacheDecimals(address asset) external returns (uint8) { + address _feed = feed(asset); require(_feed != address(0), "Asset not available"); require(_feed != FIXED_PRICE, "Fixed price feeds not supported"); uint8 decimals = AggregatorV3Interface(_feed).decimals(); - decimalsCache[_asset] = decimals; + decimalsCache[_feed] = decimals; return decimals; } - function isStablecoin(address _asset) internal view returns (bool) { + function shouldBePegged(address _asset) internal view returns (bool) { string memory symbol = Helpers.getSymbol(_asset); bytes32 symbolHash = keccak256(abi.encodePacked(symbol)); return @@ -74,12 +75,20 @@ abstract contract OracleRouterBase is IOracle { } } +/* Oracle Router that denominates all prices in USD + */ contract OracleRouter is OracleRouterBase { /** * @dev The price feed contract to use for a particular asset. * @param asset address of the asset */ - function feed(address asset) internal pure override returns (address) { + function feed(address asset) + internal + pure + virtual + override + returns (address) + { if (asset == 0x6B175474E89094C44Da98b954EedeAC495271d0F) { // Chainlink: DAI/USD return 0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9; @@ -101,6 +110,57 @@ contract OracleRouter is OracleRouterBase { } else if (asset == 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B) { // Chainlink: CVX/USD return 0xd962fC30A72A84cE50161031391756Bf2876Af5D; + } else { + revert("Asset not available"); + } + } +} + +/* Oracle Router that denominates all prices in ETH + */ +contract OETHOracleRouter is OracleRouter { + using StableMath for uint256; + + /** + * @notice Returns the total price in 18 digit units for a given asset. + * This implementation does not (!) do range checks as the + * parent OracleRouter does. + * @param asset address of the asset + * @return uint256 unit price for 1 asset unit, in 18 decimal fixed + */ + function price(address asset) + external + view + virtual + override + returns (uint256) + { + address _feed = feed(asset); + if (_feed == FIXED_PRICE) { + return 1e18; + } + require(_feed != address(0), "Asset not available"); + + (, int256 _iprice, , , ) = AggregatorV3Interface(_feed) + .latestRoundData(); + + uint8 decimals = getDecimals(_feed); + uint256 _price = uint256(_iprice).scaleBy(18, decimals); + return _price; + } + + /** + * @dev The price feed contract to use for a particular asset paired with ETH + * @param asset address of the asset + * @return address address of the price feed for the asset paired with ETH + */ + function feed(address asset) internal pure override returns (address) { + if (asset == 0xD533a949740bb3306d119CC777fa900bA034cd52) { + // Chainlink: CRV/ETH + return 0x8a12Be339B0cD1829b91Adc01977caa5E9ac121e; + } else if (asset == 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B) { + // Chainlink: CVX/ETH + return 0xC9CbF687f43176B302F03f5e58470b77D07c61c6; } else if (asset == 0xae78736Cd615f374D3085123A210448E74Fc6393) { // Chainlink: rETH/ETH return 0x536218f9E9Eb48863970252233c8F271f554C2d0; @@ -163,13 +223,7 @@ contract OracleRouterDev is OracleRouterBase { /* * The dev version of the Oracle doesn't need to gas optimize and cache the decimals */ - function getDecimals(address _asset) - internal - view - override - returns (uint8) - { - address _feed = feed(_asset); + function getDecimals(address _feed) internal view override returns (uint8) { require(_feed != address(0), "Asset not available"); require(_feed != FIXED_PRICE, "Fixed price feeds not supported"); diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index e36eacb876..08351646e1 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -123,12 +123,26 @@ contract OETHDripperProxy is InitializeGovernedUpgradeabilityProxy { } /** - * @notice FraxETHStrategyProxy delegates calls to a FraxETHStrategy implementation + * @notice OETHHarvesterProxy delegates calls to a Harvester implementation + */ +contract OETHHarvesterProxy is InitializeGovernedUpgradeabilityProxy { + +} + +/** + * @notice FraxETHStrategyProxy delegates calls to a Generalized4626Strategy implementation */ contract FraxETHStrategyProxy is InitializeGovernedUpgradeabilityProxy { } +/** + * @notice CurveEthStrategyProxy delegates calls to a CurveEthStrategy implementation + */ +contract ConvexEthMetaStrategyProxy is InitializeGovernedUpgradeabilityProxy { + +} + /** * @notice BuybackProxy delegates calls to Buyback implementation */ diff --git a/contracts/contracts/strategies/ConvexEthMetaStrategy.sol b/contracts/contracts/strategies/ConvexEthMetaStrategy.sol new file mode 100644 index 0000000000..c55c25c80e --- /dev/null +++ b/contracts/contracts/strategies/ConvexEthMetaStrategy.sol @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title Curve 3Pool Strategy + * @notice Investment strategy for investing stablecoins via Curve 3Pool + * @author Origin Protocol Inc + */ +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import { ICurveETHPoolV1 } from "./ICurveETHPoolV1.sol"; +import { IERC20, InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol"; +import { StableMath } from "../utils/StableMath.sol"; +import { Helpers } from "../utils/Helpers.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import { IWETH9 } from "../interfaces/IWETH9.sol"; +import { IConvexDeposits } from "./IConvexDeposits.sol"; +import { IRewardStaking } from "./IRewardStaking.sol"; + +contract ConvexEthMetaStrategy is InitializableAbstractStrategy { + using StableMath for uint256; + using SafeERC20 for IERC20; + + uint256 internal constant MAX_SLIPPAGE = 1e16; // 1%, same as the Curve UI + address internal constant ETH_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address internal cvxDepositorAddress; + IRewardStaking public cvxRewardStaker; + uint256 internal cvxDepositorPTokenId; + ICurveETHPoolV1 internal curvePool; + IERC20 internal lpToken; + IERC20 internal oeth; + IWETH9 internal weth; + // Ordered list of pool assets + uint128 internal oethCoinIndex; + uint128 internal ethCoinIndex; + + // used to circumvent the stack too deep issue + struct InitialiseConfig { + address curvePoolAddress; //Address of the Curve pool + address vaultAddress; //Address of the vault + address cvxDepositorAddress; //Address of the Convex depositor(AKA booster) for this pool + address oethAddress; //Address of OETH token + address wethAddress; //Address of WETH token + address cvxRewardStakerAddress; //Address of the CVX rewards staker + address curvePoolLpToken; //Address of metapool LP token + uint256 cvxDepositorPTokenId; //Pid of the pool referred to by Depositor and staker + } + + /** + * Initializer for setting up strategy internal state. This overrides the + * InitializableAbstractStrategy initializer as Curve strategies don't fit + * well within that abstraction. + * @param _rewardTokenAddresses Address of CRV & CVX + * @param _assets Addresses of supported assets. MUST be passed in the same + * order as returned by coins on the pool contract, i.e. + * WETH + * @param initConfig Various addresses and info for initialization state + */ + function initialize( + address[] calldata _rewardTokenAddresses, // CRV + CVX + address[] calldata _assets, + address[] calldata _pTokens, + InitialiseConfig calldata initConfig + ) external onlyGovernor initializer { + require(_assets.length == 1, "Must have exactly one asset"); + // Should be set prior to abstract initialize call otherwise + // abstractSetPToken calls will fail + cvxDepositorAddress = initConfig.cvxDepositorAddress; + cvxRewardStaker = IRewardStaking(initConfig.cvxRewardStakerAddress); + cvxDepositorPTokenId = initConfig.cvxDepositorPTokenId; + lpToken = IERC20(initConfig.curvePoolLpToken); + curvePool = ICurveETHPoolV1(initConfig.curvePoolAddress); + oeth = IERC20(initConfig.oethAddress); + weth = IWETH9(initConfig.wethAddress); + ethCoinIndex = uint128(_getCoinIndex(ETH_ADDRESS)); + oethCoinIndex = uint128(_getCoinIndex(initConfig.oethAddress)); + + super._initialize( + initConfig.curvePoolAddress, + initConfig.vaultAddress, + _rewardTokenAddresses, + _assets, + _pTokens + ); + + /* needs to be called after super._initialize so that the platformAddress + * is correctly set + */ + _approveBase(); + } + + /** + * @dev Deposit asset into the Curve ETH pool + * @param _weth Address of WETH + * @param _amount Amount of asset to deposit + */ + function deposit(address _weth, uint256 _amount) + external + override + onlyVault + nonReentrant + { + _deposit(_weth, _amount); + } + + // slither-disable-next-line arbitrary-send-eth + function _deposit(address _weth, uint256 _wethAmount) internal { + require(_wethAmount > 0, "Must deposit something"); + require(_weth == address(weth), "Can only deposit WETH"); + weth.withdraw(_wethAmount); + + emit Deposit(_weth, address(lpToken), _wethAmount); + + // safe to cast since min value is at least 0 + uint256 oethToAdd = uint256( + _max( + 0, + int256(curvePool.balances(ethCoinIndex)) + + int256(_wethAmount) - + int256(curvePool.balances(oethCoinIndex)) + ) + ); + + /* Add so much OETH so that the pool ends up being balanced. And at minimum + * add as much OETH as WETH and at maximum twice as much OETH. + */ + oethToAdd = Math.max(oethToAdd, _wethAmount); + oethToAdd = Math.min(oethToAdd, _wethAmount * 2); + + /* Mint OETH with a strategy that attempts to contribute to stability of OETH/WETH pool. Try + * to mint so much OETH that after deployment of liquidity pool ends up being balanced. + * + * To manage unpredictability minimal OETH minted will always be at least equal or greater + * to WETH amount deployed. And never larger than twice the WETH amount deployed even if + * it would have a further beneficial effect on pool stability. + */ + IVault(vaultAddress).mintForStrategy(oethToAdd); + + uint256[2] memory _amounts; + _amounts[ethCoinIndex] = _wethAmount; + _amounts[oethCoinIndex] = oethToAdd; + + uint256 valueInLpTokens = (_wethAmount + oethToAdd).divPrecisely( + curvePool.get_virtual_price() + ); + uint256 minMintAmount = valueInLpTokens.mulTruncate( + uint256(1e18) - MAX_SLIPPAGE + ); + + // Do the deposit to Curve ETH pool + // slither-disable-next-line arbitrary-send-eth + uint256 lpDeposited = curvePool.add_liquidity{ value: _wethAmount }( + _amounts, + minMintAmount + ); + + require( + // slither-disable-next-line arbitrary-send-eth + IConvexDeposits(cvxDepositorAddress).deposit( + cvxDepositorPTokenId, + lpDeposited, + true // Deposit with staking + ), + "Depositing LP to Convex not successful" + ); + } + + /** + * @dev Deposit the entire balance of any supported asset into the Curve 3pool + */ + function depositAll() external override onlyVault nonReentrant { + uint256 balance = weth.balanceOf(address(this)); + if (balance > 0) { + _deposit(address(weth), balance); + } + } + + /** + * @dev Withdraw asset from Curve ETH pool + * @param _recipient Address to receive withdrawn asset + * @param _weth Address of asset to withdraw + * @param _amount Amount of asset to withdraw + */ + function withdraw( + address _recipient, + address _weth, + uint256 _amount + ) external override onlyVault nonReentrant { + require(_amount > 0, "Invalid amount"); + require(_weth == address(weth), "Can only withdraw WETH"); + + emit Withdrawal(_weth, address(lpToken), _amount); + + uint256 requiredLpTokens = calcTokenToBurn(_amount); + + _lpWithdraw(requiredLpTokens); + + /* math in requiredLpTokens should correctly calculate the amount of LP to remove + * in that the strategy receives enough WETH on balanced removal + */ + uint256[2] memory _minWithdrawalAmounts = [uint256(0), uint256(0)]; + _minWithdrawalAmounts[ethCoinIndex] = _amount; + // slither-disable-next-line unused-return + curvePool.remove_liquidity(requiredLpTokens, _minWithdrawalAmounts); + + // Burn OETH + IVault(vaultAddress).burnForStrategy(oeth.balanceOf(address(this))); + // Transfer WETH + weth.deposit{ value: _amount }(); + require( + weth.transfer(_recipient, _amount), + "Transfer of WETH not successful" + ); + } + + function calcTokenToBurn(uint256 _wethAmount) + internal + view + returns (uint256 lpToBurn) + { + /* The rate between coins in the pool determines the rate at which pool returns + * tokens when doing balanced removal (remove_liquidity call). And by knowing how much WETH + * we want we can determine how much of OETH we receive by removing liquidity. + * + * Because we are doing balanced removal we should be making profit when removing liquidity in a + * pool tilted to either side. + * + * Important: A downside is that the Strategist / Governor needs to be + * cognisant of not removing too much liquidity. And while the proposal to remove liquidity + * is being voted on the pool tilt might change so much that the proposal that has been valid while + * created is no longer valid. + */ + + uint256 poolWETHBalance = curvePool.balances(ethCoinIndex); + /* K is multiplied by 1e36 which is used for higher precision calculation of required + * pool LP tokens. Without it the end value can have rounding errors up to precision of + * 10 digits. This way we move the decimal point by 36 places when doing the calculation + * and again by 36 places when we are done with it. + */ + uint256 k = (1e36 * lpToken.totalSupply()) / poolWETHBalance; + // prettier-ignore + // slither-disable-next-line divide-before-multiply + uint256 diff = (_wethAmount + 1) * k; + lpToBurn = diff / 1e36; + } + + /** + * @dev Remove all assets from platform and send them to Vault contract. + */ + function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + uint256 gaugeTokens = cvxRewardStaker.balanceOf(address(this)); + _lpWithdraw(gaugeTokens); + + // Withdraws are proportional to assets held by 3Pool + uint256[2] memory minWithdrawAmounts = [uint256(0), uint256(0)]; + + // Remove liquidity + // slither-disable-next-line unused-return + curvePool.remove_liquidity( + lpToken.balanceOf(address(this)), + minWithdrawAmounts + ); + + // Burn all OETH + uint256 oethBalance = oeth.balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(oethBalance); + + // Send all ETH and WETH on the contract, including extra + weth.deposit{ value: address(this).balance }(); + require( + weth.transfer(vaultAddress, weth.balanceOf(address(this))), + "Transfer of WETH not successful" + ); + } + + /** + * @dev Collect accumulated CRV and CVX and send to Harvester. + */ + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + // Collect CRV and CVX + cvxRewardStaker.getReward(); + _collectRewardTokens(); + } + + function _lpWithdraw(uint256 _wethAmount) internal { + // withdraw and unwrap with claim takes back the lpTokens + // and also collects the rewards for deposit + cvxRewardStaker.withdrawAndUnwrap(_wethAmount, true); + } + + /** + * @dev Get the total asset value held in the platform + * @param _asset Address of the asset + * @return balance Total value of the asset in the platform + */ + function checkBalance(address _asset) + public + view + override + returns (uint256 balance) + { + require(_asset == address(weth), "Unsupported asset"); + + // Eth balance needed here for the balance check that happens from vault during depositing. + balance += address(this).balance; + uint256 lpTokens = cvxRewardStaker.balanceOf(address(this)); + if (lpTokens > 0) { + balance += (lpTokens * curvePool.get_virtual_price()) / 1e18; + } + } + + /** + * @dev Retuns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + */ + function supportsAsset(address _asset) + external + view + override + returns (bool) + { + return _asset == address(weth); + } + + /** + * @dev Approve the spending of all assets by their corresponding pool tokens, + * if for some reason is it necessary. + */ + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + { + _approveAsset(address(weth)); + _approveAsset(address(oeth)); + } + + /** + * @dev Accept unwrapped WETH + */ + receive() external payable {} + + /** + * @dev Call the necessary approvals for the Curve pool and gauge + * @param _asset Address of the asset + */ + // solhint-disable-next-line no-unused-vars + function _abstractSetPToken(address _asset, address _pToken) + internal + override + { + _approveAsset(_asset); + } + + function _approveAsset(address _asset) internal { + // approve curve pool for asset (required for adding liquidity) + IERC20(_asset).safeApprove(platformAddress, type(uint256).max); + } + + function _approveBase() internal { + // WETH was approved as a supported asset, + // so we need seperate OETH approve + _approveAsset(address(oeth)); + lpToken.safeApprove(cvxDepositorAddress, type(uint256).max); + } + + /** + * @dev Get the index of the coin + */ + function _getCoinIndex(address _asset) internal view returns (uint256) { + for (uint256 i = 0; i < 2; i++) { + if (curvePool.coins(i) == _asset) return i; + } + revert("Invalid curve pool asset"); + } + + /** + * @dev Returns the largest of two numbers int256 version + */ + function _max(int256 a, int256 b) internal pure returns (int256) { + return a >= b ? a : b; + } +} diff --git a/contracts/contracts/strategies/ICurveETHPoolV1.sol b/contracts/contracts/strategies/ICurveETHPoolV1.sol new file mode 100644 index 0000000000..5aa04ae745 --- /dev/null +++ b/contracts/contracts/strategies/ICurveETHPoolV1.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ICurveETHPoolV1 { + event AddLiquidity( + address indexed provider, + uint256[2] token_amounts, + uint256[2] fees, + uint256 invariant, + uint256 token_supply + ); + event ApplyNewFee(uint256 fee); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + event CommitNewFee(uint256 new_fee); + event RampA( + uint256 old_A, + uint256 new_A, + uint256 initial_time, + uint256 future_time + ); + event RemoveLiquidity( + address indexed provider, + uint256[2] token_amounts, + uint256[2] fees, + uint256 token_supply + ); + event RemoveLiquidityImbalance( + address indexed provider, + uint256[2] token_amounts, + uint256[2] fees, + uint256 invariant, + uint256 token_supply + ); + event RemoveLiquidityOne( + address indexed provider, + uint256 token_amount, + uint256 coin_amount, + uint256 token_supply + ); + event StopRampA(uint256 A, uint256 t); + event TokenExchange( + address indexed buyer, + int128 sold_id, + uint256 tokens_sold, + int128 bought_id, + uint256 tokens_bought + ); + event Transfer( + address indexed sender, + address indexed receiver, + uint256 value + ); + + function A() external view returns (uint256); + + function A_precise() external view returns (uint256); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function add_liquidity(uint256[2] memory _amounts, uint256 _min_mint_amount) + external + payable + returns (uint256); + + function add_liquidity( + uint256[2] memory _amounts, + uint256 _min_mint_amount, + address _receiver + ) external payable returns (uint256); + + function admin_action_deadline() external view returns (uint256); + + function admin_balances(uint256 i) external view returns (uint256); + + function admin_fee() external view returns (uint256); + + function allowance(address arg0, address arg1) + external + view + returns (uint256); + + function apply_new_fee() external; + + function approve(address _spender, uint256 _value) external returns (bool); + + function balanceOf(address arg0) external view returns (uint256); + + function balances(uint256 arg0) external view returns (uint256); + + function calc_token_amount(uint256[2] memory _amounts, bool _is_deposit) + external + view + returns (uint256); + + function calc_withdraw_one_coin(uint256 _burn_amount, int128 i) + external + view + returns (uint256); + + function coins(uint256 arg0) external view returns (address); + + function commit_new_fee(uint256 _new_fee) external; + + function decimals() external view returns (uint256); + + function ema_price() external view returns (uint256); + + function exchange( + int128 i, + int128 j, + uint256 _dx, + uint256 _min_dy + ) external payable returns (uint256); + + function exchange( + int128 i, + int128 j, + uint256 _dx, + uint256 _min_dy, + address _receiver + ) external payable returns (uint256); + + function fee() external view returns (uint256); + + function future_A() external view returns (uint256); + + function future_A_time() external view returns (uint256); + + function future_fee() external view returns (uint256); + + function get_balances() external view returns (uint256[2] memory); + + function get_dy( + int128 i, + int128 j, + uint256 dx + ) external view returns (uint256); + + function get_p() external view returns (uint256); + + function get_virtual_price() external view returns (uint256); + + function initial_A() external view returns (uint256); + + function initial_A_time() external view returns (uint256); + + function initialize( + string memory _name, + string memory _symbol, + address[4] memory _coins, + uint256[4] memory _rate_multipliers, + uint256 _A, + uint256 _fee + ) external; + + function last_price() external view returns (uint256); + + function ma_exp_time() external view returns (uint256); + + function ma_last_time() external view returns (uint256); + + function name() external view returns (string memory); + + function nonces(address arg0) external view returns (uint256); + + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external returns (bool); + + function price_oracle() external view returns (uint256); + + function ramp_A(uint256 _future_A, uint256 _future_time) external; + + function remove_liquidity( + uint256 _burn_amount, + uint256[2] memory _min_amounts + ) external returns (uint256[2] memory); + + function remove_liquidity( + uint256 _burn_amount, + uint256[2] memory _min_amounts, + address _receiver + ) external returns (uint256[2] memory); + + function remove_liquidity_imbalance( + uint256[2] memory _amounts, + uint256 _max_burn_amount + ) external returns (uint256); + + function remove_liquidity_imbalance( + uint256[2] memory _amounts, + uint256 _max_burn_amount, + address _receiver + ) external returns (uint256); + + function remove_liquidity_one_coin( + uint256 _burn_amount, + int128 i, + uint256 _min_received + ) external returns (uint256); + + function remove_liquidity_one_coin( + uint256 _burn_amount, + int128 i, + uint256 _min_received, + address _receiver + ) external returns (uint256); + + function set_ma_exp_time(uint256 _ma_exp_time) external; + + function stop_ramp_A() external; + + function symbol() external view returns (string memory); + + function totalSupply() external view returns (uint256); + + function transfer(address _to, uint256 _value) external returns (bool); + + function transferFrom( + address _from, + address _to, + uint256 _value + ) external returns (bool); + + function version() external view returns (string memory); + + function withdraw_admin_fees() external; +} diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js index aeda615e80..edc2573b16 100644 --- a/contracts/deploy/001_core.js +++ b/contracts/deploy/001_core.js @@ -682,73 +682,46 @@ const deployOracles = async () => { .connect(sDeployer) .setFeed(assetAddresses.DAI, oracleAddresses.chainlink.DAI_USD) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.DAI) - ); await withConfirmation( oracleRouter .connect(sDeployer) .setFeed(assetAddresses.USDC, oracleAddresses.chainlink.USDC_USD) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.USDC) - ); await withConfirmation( oracleRouter .connect(sDeployer) .setFeed(assetAddresses.USDT, oracleAddresses.chainlink.USDT_USD) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.USDT) - ); await withConfirmation( oracleRouter .connect(sDeployer) .setFeed(assetAddresses.TUSD, oracleAddresses.chainlink.TUSD_USD) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.TUSD) - ); await withConfirmation( oracleRouter .connect(sDeployer) .setFeed(assetAddresses.COMP, oracleAddresses.chainlink.COMP_USD) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.COMP) - ); await withConfirmation( oracleRouter .connect(sDeployer) .setFeed(assetAddresses.AAVE, oracleAddresses.chainlink.AAVE_USD) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.AAVE) - ); await withConfirmation( oracleRouter .connect(sDeployer) .setFeed(assetAddresses.CRV, oracleAddresses.chainlink.CRV_USD) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.CRV) - ); await withConfirmation( oracleRouter .connect(sDeployer) .setFeed(assetAddresses.CVX, oracleAddresses.chainlink.CVX_USD) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.CVX) - ); await withConfirmation( oracleRouter .connect(sDeployer) .setFeed(assetAddresses.RETH, oracleAddresses.chainlink.RETH_ETH) ); - await withConfirmation( - oracleRouter.connect(sDeployer).cacheDecimals(assetAddresses.RETH) - ); await withConfirmation( oracleRouter .connect(sDeployer) diff --git a/contracts/deploy/052_decimal_cache.js b/contracts/deploy/052_decimal_cache.js index 0440ae1269..e763479ef6 100644 --- a/contracts/deploy/052_decimal_cache.js +++ b/contracts/deploy/052_decimal_cache.js @@ -3,6 +3,11 @@ const addresses = require("../utils/addresses"); const { isMainnet } = require("../test/helpers.js"); module.exports = deploymentWithGovernanceProposal( + /* IMPORTANT (!) + * + * Once this gets deployed undo the `skip` in the `vault.fork-test.js` under + * the "Should have correct Price Oracle address set" scenario. + */ { deployName: "052_decimal_cache", forceDeploy: false, @@ -18,14 +23,19 @@ module.exports = deploymentWithGovernanceProposal( withConfirmation, }) => { const { deployerAddr, governorAddr } = await getNamedAccounts(); + + if (isMainnet) { + throw new Error("Delete once sure to update OUSD contracts"); + } + // Current contracts const cVaultProxy = await ethers.getContract("VaultProxy"); const dVaultAdmin = await deployWithConfirmation("VaultAdmin"); const dOracleRouter = await deployWithConfirmation("OracleRouter"); + const dVaultCore = await deployWithConfirmation("VaultCore"); const cVault = await ethers.getContractAt("Vault", cVaultProxy.address); const cOracleRouter = await ethers.getContract("OracleRouter"); - await cOracleRouter.cacheDecimals(addresses.mainnet.rETH); await cOracleRouter.cacheDecimals(addresses.mainnet.DAI); await cOracleRouter.cacheDecimals(addresses.mainnet.USDC); await cOracleRouter.cacheDecimals(addresses.mainnet.USDT); @@ -104,6 +114,12 @@ module.exports = deploymentWithGovernanceProposal( signature: "upgradeTo(address)", args: [dHarvester.address], }, + { + // Set new implementation + contract: cVaultProxy, + signature: "upgradeTo(address)", + args: [dVaultCore.address], + }, ], }; } diff --git a/contracts/deploy/053_oeth.js b/contracts/deploy/053_oeth.js index 5c0a85aa86..d90f86c0a8 100644 --- a/contracts/deploy/053_oeth.js +++ b/contracts/deploy/053_oeth.js @@ -25,7 +25,7 @@ module.exports = deploymentWithGuardianGovernor( ethers, }); - // await deployDripper({ deployWithConfirmation, withConfirmation, ethers }); + await deployDripper({ deployWithConfirmation, withConfirmation, ethers }); await deployZapper({ deployWithConfirmation, @@ -44,7 +44,7 @@ module.exports = deploymentWithGuardianGovernor( // Governance Actions // ---------------- return { - name: "Deploy OETH Vault, Token, Strategies, Harvester and the Dripper", + name: "Deploy OETH Vault, Token, Strategies and the Dripper", actions, }; } diff --git a/contracts/deploy/055_curve_amo.js b/contracts/deploy/055_curve_amo.js new file mode 100644 index 0000000000..f57f6b3c21 --- /dev/null +++ b/contracts/deploy/055_curve_amo.js @@ -0,0 +1,479 @@ +const { + deploymentWithGuardianGovernor, + impersonateAccount, + sleep, +} = require("../utils/deploy"); +const addresses = require("../utils/addresses"); +const hre = require("hardhat"); +const { BigNumber, utils, Contract } = require("ethers"); +const { + getAssetAddresses, + getOracleAddresses, + isMainnet, + isFork, + isMainnetOrFork, +} = require("../test/helpers.js"); +const { MAX_UINT256, oethPoolLpPID } = require("../utils/constants"); +const crvRewards = "0x24b65DC1cf053A8D96872c323d29e86ec43eB33A"; +const gaugeAddress = "0xd03BE91b1932715709e18021734fcB91BB431715"; +const poolAddress = "0x94b17476a93b3262d87b9a326965d1e91f9c13e7"; +const tokenAddress = "0x94b17476a93b3262d87b9a326965d1e91f9c13e7"; + +// 5/8 multisig +const guardianAddr = addresses.mainnet.Guardian; + +module.exports = deploymentWithGuardianGovernor( + { deployName: "055_curve_amo" }, + async ({ deployWithConfirmation, ethers, getTxOpts, withConfirmation }) => { + const { deployerAddr, governorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + let actions = []; + // No need to Deploy Curve since it is already live on the mainnet + + // let { + // actions + // } = await deployCurve({ + // deployWithConfirmation, + // withConfirmation, + // ethers, + // }); + + const { cHarvester, actions: harvesterActions } = + await deployHarvesterAndOracleRouter({ + deployWithConfirmation, + withConfirmation, + ethers, + }); + + actions = actions.concat(harvesterActions); + + // actions = actions.concat(await reDeployOETH({ + // deployWithConfirmation, + // withConfirmation, + // ethers + // })); + + actions = actions.concat( + await configureDripper({ + deployWithConfirmation, + withConfirmation, + ethers, + cHarvester, + }) + ); + + actions = actions.concat( + await deployConvexETHMetaStrategy({ + deployWithConfirmation, + withConfirmation, + ethers, + cHarvester, + }) + ); + + // Governance Actions + // ---------------- + return { + name: "Deploy Curve AMO, Harvester, Dripper and Oracle Router", + actions, + }; + } +); + +const configureDripper = async ({ + deployWithConfirmation, + withConfirmation, + ethers, + cHarvester, +}) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const cVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cDripperProxy = await ethers.getContract("OETHDripperProxy"); + const cDripper = await ethers.getContractAt( + "OETHDripper", + cDripperProxy.address + ); + + return [ + { + // 1. Configure Dripper to three days + contract: cDripper, + signature: "setDripDuration(uint256)", + args: [3 * 24 * 60 * 60], + }, + { + contract: cHarvester, + signature: "setRewardsProceedsAddress(address)", + args: [cDripper.address], + }, + ]; +}; + +/** + * Deploy Convex ETH Strategy + */ +const deployConvexETHMetaStrategy = async ({ + deployWithConfirmation, + withConfirmation, + ethers, + cHarvester, +}) => { + const assetAddresses = await getAssetAddresses(hre.deployments); + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + const cVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cVault = await ethers.getContractAt("OETHVault", cVaultProxy.address); + + const dConvexEthMetaStrategyProxy = await deployWithConfirmation( + "ConvexEthMetaStrategyProxy" + ); + const cConvexEthMetaStrategyProxy = await ethers.getContract( + "ConvexEthMetaStrategyProxy" + ); + const dConvexETHMetaStrategy = await deployWithConfirmation( + "ConvexEthMetaStrategy" + ); + const cConvexETHMetaStrategy = await ethers.getContractAt( + "ConvexEthMetaStrategy", + dConvexEthMetaStrategyProxy.address + ); + await withConfirmation( + cConvexEthMetaStrategyProxy + .connect(sDeployer) + ["initialize(address,address,bytes)"]( + dConvexETHMetaStrategy.address, + deployerAddr, + [] + ) + ); + + console.log("Initialized ConvexETHMetaStrategyProxy"); + const initFunction = + "initialize(address[],address[],address[],(address,address,address,address,address,address,address,uint256))"; + + await withConfirmation( + cConvexETHMetaStrategy + .connect(sDeployer) + [initFunction]( + [assetAddresses.CVX, assetAddresses.CRV], + [addresses.mainnet.WETH], + [tokenAddress], + [ + poolAddress, + cVaultProxy.address, + addresses.mainnet.CVXBooster, + addresses.mainnet.OETHProxy, + addresses.mainnet.WETH, + crvRewards, + tokenAddress, + oethPoolLpPID, + ] + ) + ); + console.log("Initialized ConvexETHMetaStrategy"); + await withConfirmation( + cConvexETHMetaStrategy.connect(sDeployer).transferGovernance(guardianAddr) + ); + console.log( + `ConvexETHMetaStrategy transferGovernance(${guardianAddr} called` + ); + + return [ + { + // Claim Vault governance + contract: cConvexETHMetaStrategy, + signature: "claimGovernance()", + args: [], + }, + { + // Approve strategy + contract: cVault, + signature: "approveStrategy(address)", + args: [cConvexETHMetaStrategy.address], + }, + { + // Set ConvexEthMeta strategy as the Vault strategy that can print OETH + contract: cVault, + signature: "setOusdMetaStrategy(address)", + args: [cConvexETHMetaStrategy.address], + }, + { + // Set OETH mint threshold to 25k + contract: cVault, + signature: "setNetOusdMintForStrategyThreshold(uint256)", + args: [utils.parseUnits("25", 21)], + }, + { + // Set harvester address + contract: cConvexETHMetaStrategy, + signature: "setHarvesterAddress(address)", + args: [cHarvester.address], + }, + // set ConvexEth strategy as supported one by harvester + { + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cConvexETHMetaStrategy.address, true], + }, + // Set reward token config + { + contract: cHarvester, + signature: + "setRewardTokenConfig(address,uint16,uint16,address,uint256,bool)", + args: [ + assetAddresses.CRV, // tokenAddress + 300, // allowedSlippageBps + 100, // harvestRewardBps + assetAddresses.sushiswapRouter, // uniswapV2CompatibleAddr + MAX_UINT256, // liquidationLimit + true, // doSwapRewardToken + ], + }, + // Set reward token config + { + contract: cHarvester, + signature: + "setRewardTokenConfig(address,uint16,uint16,address,uint256,bool)", + args: [ + assetAddresses.CVX, // tokenAddress + 300, // allowedSlippageBps + 100, // harvestRewardBps + assetAddresses.sushiswapRouter, // uniswapV2CompatibleAddr + MAX_UINT256, // liquidationLimit + true, // doSwapRewardToken + ], + }, + ]; +}; + +const deployHarvesterAndOracleRouter = async ({ + deployWithConfirmation, + withConfirmation, + ethers, +}) => { + await deployWithConfirmation("OETHOracleRouter"); + + const { deployerAddr, governorAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + const dHarvesterProxy = await deployWithConfirmation("OETHHarvesterProxy"); + const cVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cVault = await ethers.getContractAt("OETHVault", cVaultProxy.address); + const cOETHOracleRouter = await ethers.getContract("OETHOracleRouter"); + console.log(`Harvester proxy deployed at: ${dHarvesterProxy.address}`); + + const cHarvesterProxy = await ethers.getContractAt( + "OETHHarvesterProxy", + dHarvesterProxy.address + ); + + const dHarvester = await deployWithConfirmation("OETHHarvester", [ + cVaultProxy.address, + ]); + + await withConfirmation( + cHarvesterProxy.connect(sDeployer)[ + // eslint-disable-next-line + "initialize(address,address,bytes)" + ](dHarvester.address, deployerAddr, []) + ); + + const cHarvester = await ethers.getContractAt( + "OETHHarvester", + cHarvesterProxy.address + ); + + await withConfirmation( + // CRV/ETH + cOETHOracleRouter.cacheDecimals(addresses.mainnet.CRV) + ); + + await withConfirmation( + // CVX/ETH + cOETHOracleRouter.cacheDecimals(addresses.mainnet.CVX) + ); + + await withConfirmation( + // rETH/ETH + cOETHOracleRouter.cacheDecimals(addresses.mainnet.rETH) + ); + + await withConfirmation( + // stETH/ETH + cOETHOracleRouter.cacheDecimals(addresses.mainnet.stETH) + ); + + await withConfirmation( + cHarvester.connect(sDeployer).transferGovernance(guardianAddr) + ); + + // Some of the harvester governance actions are executed when deploying Curve + // strategy + return { + actions: [ + { + contract: cHarvester, + signature: "claimGovernance()", + args: [], + }, + { + contract: cVault, + signature: "setPriceProvider(address)", + args: [cOETHOracleRouter.address], + }, + ], + cHarvester, + }; +}; + +// const reDeployOETH = async ({ +// deployWithConfirmation, +// withConfirmation, +// ethers, +// }) => { +// const cOETHProxy = await ethers.getContract("OETHProxy"); +// const dOETH = await deployWithConfirmation("OETH"); +// +// return [ +// { +// // Upgrade OETH proxy +// contract: cOETHProxy, +// signature: "upgradeTo(address)", +// args: [dOETH.address], +// } +// ]; +// }; + +const deployCurve = async ({ + deployWithConfirmation, + withConfirmation, + ethers, +}) => { + if (isMainnet) { + throw new Error("This shouldn't happen on the mainnet"); + } + + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + const gaugeControllerAdmin = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968"; + await impersonateAccount(gaugeControllerAdmin); + const sGaugeControllerAdmin = await ethers.provider.getSigner( + gaugeControllerAdmin + ); + const cVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cVault = await ethers.getContractAt( + "OETHVaultCore", + cVaultProxy.address + ); + + // prettier-ignore + const curveFactoryAbi = [{name: "CryptoPoolDeployed",inputs: [{ name: "token", type: "address", indexed: false },{ name: "coins", type: "address[2]", indexed: false },{ name: "A", type: "uint256", indexed: false },{ name: "gamma", type: "uint256", indexed: false },{ name: "mid_fee", type: "uint256", indexed: false },{ name: "out_fee", type: "uint256", indexed: false },{ name: "allowed_extra_profit", type: "uint256", indexed: false },{ name: "fee_gamma", type: "uint256", indexed: false },{ name: "adjustment_step", type: "uint256", indexed: false },{ name: "admin_fee", type: "uint256", indexed: false },{ name: "ma_half_time", type: "uint256", indexed: false },{ name: "initial_price", type: "uint256", indexed: false },{ name: "deployer", type: "address", indexed: false },],anonymous: false,type: "event",},{name: "LiquidityGaugeDeployed",inputs: [{ name: "pool", type: "address", indexed: false },{ name: "token", type: "address", indexed: false },{ name: "gauge", type: "address", indexed: false },],anonymous: false,type: "event",},{name: "UpdateFeeReceiver",inputs: [{ name: "_old_fee_receiver", type: "address", indexed: false },{ name: "_new_fee_receiver", type: "address", indexed: false },],anonymous: false,type: "event",},{name: "UpdatePoolImplementation",inputs: [{ name: "_old_pool_implementation", type: "address", indexed: false },{ name: "_new_pool_implementation", type: "address", indexed: false },],anonymous: false,type: "event",},{name: "UpdateTokenImplementation",inputs: [{ name: "_old_token_implementation", type: "address", indexed: false },{ name: "_new_token_implementation", type: "address", indexed: false },],anonymous: false,type: "event",},{name: "UpdateGaugeImplementation",inputs: [{ name: "_old_gauge_implementation", type: "address", indexed: false },{ name: "_new_gauge_implementation", type: "address", indexed: false },],anonymous: false,type: "event",},{name: "TransferOwnership",inputs: [{ name: "_old_owner", type: "address", indexed: false },{ name: "_new_owner", type: "address", indexed: false },],anonymous: false,type: "event",},{stateMutability: "nonpayable",type: "constructor",inputs: [{ name: "_fee_receiver", type: "address" },{ name: "_pool_implementation", type: "address" },{ name: "_token_implementation", type: "address" },{ name: "_gauge_implementation", type: "address" },{ name: "_weth", type: "address" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "deploy_pool",inputs: [{ name: "_name", type: "string" },{ name: "_symbol", type: "string" },{ name: "_coins", type: "address[2]" },{ name: "A", type: "uint256" },{ name: "gamma", type: "uint256" },{ name: "mid_fee", type: "uint256" },{ name: "out_fee", type: "uint256" },{ name: "allowed_extra_profit", type: "uint256" },{ name: "fee_gamma", type: "uint256" },{ name: "adjustment_step", type: "uint256" },{ name: "admin_fee", type: "uint256" },{ name: "ma_half_time", type: "uint256" },{ name: "initial_price", type: "uint256" },],outputs: [{ name: "", type: "address" }],},{stateMutability: "nonpayable",type: "function",name: "deploy_gauge",inputs: [{ name: "_pool", type: "address" }],outputs: [{ name: "", type: "address" }],},{stateMutability: "nonpayable",type: "function",name: "set_fee_receiver",inputs: [{ name: "_fee_receiver", type: "address" }],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "set_pool_implementation",inputs: [{ name: "_pool_implementation", type: "address" }],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "set_token_implementation",inputs: [{ name: "_token_implementation", type: "address" }],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "set_gauge_implementation",inputs: [{ name: "_gauge_implementation", type: "address" }],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "commit_transfer_ownership",inputs: [{ name: "_addr", type: "address" }],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "accept_transfer_ownership",inputs: [],outputs: [],},{stateMutability: "view",type: "function",name: "find_pool_for_coins",inputs: [{ name: "_from", type: "address" },{ name: "_to", type: "address" },],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "find_pool_for_coins",inputs: [{ name: "_from", type: "address" },{ name: "_to", type: "address" },{ name: "i", type: "uint256" },],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "get_coins",inputs: [{ name: "_pool", type: "address" }],outputs: [{ name: "", type: "address[2]" }],},{stateMutability: "view",type: "function",name: "get_decimals",inputs: [{ name: "_pool", type: "address" }],outputs: [{ name: "", type: "uint256[2]" }],},{stateMutability: "view",type: "function",name: "get_balances",inputs: [{ name: "_pool", type: "address" }],outputs: [{ name: "", type: "uint256[2]" }],},{stateMutability: "view",type: "function",name: "get_coin_indices",inputs: [{ name: "_pool", type: "address" },{ name: "_from", type: "address" },{ name: "_to", type: "address" },],outputs: [{ name: "", type: "uint256" },{ name: "", type: "uint256" },],},{stateMutability: "view",type: "function",name: "get_gauge",inputs: [{ name: "_pool", type: "address" }],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "get_eth_index",inputs: [{ name: "_pool", type: "address" }],outputs: [{ name: "", type: "uint256" }],},{stateMutability: "view",type: "function",name: "get_token",inputs: [{ name: "_pool", type: "address" }],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "admin",inputs: [],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "future_admin",inputs: [],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "fee_receiver",inputs: [],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "pool_implementation",inputs: [],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "token_implementation",inputs: [],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "gauge_implementation",inputs: [],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "pool_count",inputs: [],outputs: [{ name: "", type: "uint256" }],},{stateMutability: "view",type: "function",name: "pool_list",inputs: [{ name: "arg0", type: "uint256" }],outputs: [{ name: "", type: "address" }]}]; + // prettier-ignore + const convexPoolManagerAbi = [{inputs: [{ internalType: "address", name: "_pools", type: "address" }],stateMutability: "nonpayable",type: "constructor",},{inputs: [{ internalType: "address", name: "_gauge", type: "address" },{ internalType: "uint256", name: "_stashVersion", type: "uint256" },],name: "addPool",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_gauge", type: "address" }],name: "addPool",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "", type: "address" }],name: "blockList",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "address", name: "_lptoken", type: "address" },{ internalType: "address", name: "_gauge", type: "address" },{ internalType: "uint256", name: "_stashVersion", type: "uint256" },],name: "forceAddPool",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "gaugeController",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "operator",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "pools",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "postAddHook",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "address", name: "_operator", type: "address" }],name: "setOperator",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_hook", type: "address" }],name: "setPostAddHook",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" }],name: "shutdownPool",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function"}]; + // prettier-ignore + const curveGaugeFactoryAbi = [{name: "SetManager",inputs: [{ name: "_manager", type: "address", indexed: true }],anonymous: false,type: "event",},{name: "SetGaugeManager",inputs: [{ name: "_gauge", type: "address", indexed: true },{ name: "_gauge_manager", type: "address", indexed: true },],anonymous: false,type: "event",},{stateMutability: "nonpayable",type: "constructor",inputs: [{ name: "_factory", type: "address" },{ name: "_manager", type: "address" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "add_reward",inputs: [{ name: "_gauge", type: "address" },{ name: "_reward_token", type: "address" },{ name: "_distributor", type: "address" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "set_reward_distributor",inputs: [{ name: "_gauge", type: "address" },{ name: "_reward_token", type: "address" },{ name: "_distributor", type: "address" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "deploy_gauge",inputs: [{ name: "_pool", type: "address" }],outputs: [{ name: "", type: "address" }],},{stateMutability: "nonpayable",type: "function",name: "deploy_gauge",inputs: [{ name: "_pool", type: "address" },{ name: "_gauge_manager", type: "address" },],outputs: [{ name: "", type: "address" }],},{stateMutability: "nonpayable",type: "function",name: "set_gauge_manager",inputs: [{ name: "_gauge", type: "address" },{ name: "_gauge_manager", type: "address" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "set_manager",inputs: [{ name: "_manager", type: "address" }],outputs: [],},{stateMutability: "pure",type: "function",name: "factory",inputs: [],outputs: [{ name: "", type: "address" }],},{stateMutability: "pure",type: "function",name: "owner_proxy",inputs: [],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "gauge_manager",inputs: [{ name: "arg0", type: "address" }],outputs: [{ name: "", type: "address" }],},{stateMutability: "view",type: "function",name: "manager",inputs: [],outputs: [{ name: "", type: "address" }]}]; + // prettier-ignore + const curveGaugeAbi = [{name: "Deposit",inputs: [{ name: "provider", type: "address", indexed: true },{ name: "value", type: "uint256", indexed: false },],anonymous: false,type: "event",},{name: "Withdraw",inputs: [{ name: "provider", type: "address", indexed: true },{ name: "value", type: "uint256", indexed: false },],anonymous: false,type: "event",},{name: "UpdateLiquidityLimit",inputs: [{ name: "user", type: "address", indexed: false },{ name: "original_balance", type: "uint256", indexed: false },{ name: "original_supply", type: "uint256", indexed: false },{ name: "working_balance", type: "uint256", indexed: false },{ name: "working_supply", type: "uint256", indexed: false },],anonymous: false,type: "event",},{name: "CommitOwnership",inputs: [{ name: "admin", type: "address", indexed: false }],anonymous: false,type: "event",},{name: "ApplyOwnership",inputs: [{ name: "admin", type: "address", indexed: false }],anonymous: false,type: "event",},{name: "Transfer",inputs: [{ name: "_from", type: "address", indexed: true },{ name: "_to", type: "address", indexed: true },{ name: "_value", type: "uint256", indexed: false },],anonymous: false,type: "event",},{name: "Approval",inputs: [{ name: "_owner", type: "address", indexed: true },{ name: "_spender", type: "address", indexed: true },{ name: "_value", type: "uint256", indexed: false },],anonymous: false,type: "event",},{stateMutability: "nonpayable",type: "constructor",inputs: [],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "initialize",inputs: [{ name: "_lp_token", type: "address" }],outputs: [],gas: 374587,},{stateMutability: "view",type: "function",name: "decimals",inputs: [],outputs: [{ name: "", type: "uint256" }],gas: 318,},{stateMutability: "view",type: "function",name: "integrate_checkpoint",inputs: [],outputs: [{ name: "", type: "uint256" }],gas: 4590,},{stateMutability: "nonpayable",type: "function",name: "user_checkpoint",inputs: [{ name: "addr", type: "address" }],outputs: [{ name: "", type: "bool" }],gas: 3123886,},{stateMutability: "nonpayable",type: "function",name: "claimable_tokens",inputs: [{ name: "addr", type: "address" }],outputs: [{ name: "", type: "uint256" }],gas: 3038676,},{stateMutability: "view",type: "function",name: "claimed_reward",inputs: [{ name: "_addr", type: "address" },{ name: "_token", type: "address" },],outputs: [{ name: "", type: "uint256" }],gas: 3036,},{stateMutability: "view",type: "function",name: "claimable_reward",inputs: [{ name: "_user", type: "address" },{ name: "_reward_token", type: "address" },],outputs: [{ name: "", type: "uint256" }],gas: 20255,},{stateMutability: "nonpayable",type: "function",name: "set_rewards_receiver",inputs: [{ name: "_receiver", type: "address" }],outputs: [],gas: 35673,},{stateMutability: "nonpayable",type: "function",name: "claim_rewards",inputs: [],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "claim_rewards",inputs: [{ name: "_addr", type: "address" }],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "claim_rewards",inputs: [{ name: "_addr", type: "address" },{ name: "_receiver", type: "address" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "kick",inputs: [{ name: "addr", type: "address" }],outputs: [],gas: 3137977,},{stateMutability: "nonpayable",type: "function",name: "deposit",inputs: [{ name: "_value", type: "uint256" }],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "deposit",inputs: [{ name: "_value", type: "uint256" },{ name: "_addr", type: "address" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "deposit",inputs: [{ name: "_value", type: "uint256" },{ name: "_addr", type: "address" },{ name: "_claim_rewards", type: "bool" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "withdraw",inputs: [{ name: "_value", type: "uint256" }],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "withdraw",inputs: [{ name: "_value", type: "uint256" },{ name: "_claim_rewards", type: "bool" },],outputs: [],},{stateMutability: "nonpayable",type: "function",name: "transfer",inputs: [{ name: "_to", type: "address" },{ name: "_value", type: "uint256" },],outputs: [{ name: "", type: "bool" }],gas: 18062826,},{stateMutability: "nonpayable",type: "function",name: "transferFrom",inputs: [{ name: "_from", type: "address" },{ name: "_to", type: "address" },{ name: "_value", type: "uint256" },],outputs: [{ name: "", type: "bool" }],gas: 18100776,},{stateMutability: "nonpayable",type: "function",name: "approve",inputs: [{ name: "_spender", type: "address" },{ name: "_value", type: "uint256" },],outputs: [{ name: "", type: "bool" }],gas: 38151,},{stateMutability: "nonpayable",type: "function",name: "increaseAllowance",inputs: [{ name: "_spender", type: "address" },{ name: "_added_value", type: "uint256" },],outputs: [{ name: "", type: "bool" }],gas: 40695,},{stateMutability: "nonpayable",type: "function",name: "decreaseAllowance",inputs: [{ name: "_spender", type: "address" },{ name: "_subtracted_value", type: "uint256" },],outputs: [{ name: "", type: "bool" }],gas: 40719,},{stateMutability: "nonpayable",type: "function",name: "add_reward",inputs: [{ name: "_reward_token", type: "address" },{ name: "_distributor", type: "address" },],outputs: [],gas: 115414,},{stateMutability: "nonpayable",type: "function",name: "set_reward_distributor",inputs: [{ name: "_reward_token", type: "address" },{ name: "_distributor", type: "address" },],outputs: [],gas: 43179,},{stateMutability: "nonpayable",type: "function",name: "deposit_reward_token",inputs: [{ name: "_reward_token", type: "address" },{ name: "_amount", type: "uint256" },],outputs: [],gas: 1540067,},{stateMutability: "nonpayable",type: "function",name: "set_killed",inputs: [{ name: "_is_killed", type: "bool" }],outputs: [],gas: 40529,},{stateMutability: "view",type: "function",name: "lp_token",inputs: [],outputs: [{ name: "", type: "address" }],gas: 3018,},{stateMutability: "view",type: "function",name: "future_epoch_time",inputs: [],outputs: [{ name: "", type: "uint256" }],gas: 3048,},{stateMutability: "view",type: "function",name: "balanceOf",inputs: [{ name: "arg0", type: "address" }],outputs: [{ name: "", type: "uint256" }],gas: 3293,},{stateMutability: "view",type: "function",name: "totalSupply",inputs: [],outputs: [{ name: "", type: "uint256" }],gas: 3108,},{stateMutability: "view",type: "function",name: "allowance",inputs: [{ name: "arg0", type: "address" },{ name: "arg1", type: "address" },],outputs: [{ name: "", type: "uint256" }],gas: 3568,},{stateMutability: "view",type: "function",name: "name",inputs: [],outputs: [{ name: "", type: "string" }],gas: 13398,},{stateMutability: "view",type: "function",name: "symbol",inputs: [],outputs: [{ name: "", type: "string" }],gas: 11151,},{stateMutability: "view",type: "function",name: "working_balances",inputs: [{ name: "arg0", type: "address" }],outputs: [{ name: "", type: "uint256" }],gas: 3443,},{stateMutability: "view",type: "function",name: "working_supply",inputs: [],outputs: [{ name: "", type: "uint256" }],gas: 3258,},{stateMutability: "view",type: "function",name: "period",inputs: [],outputs: [{ name: "", type: "int128" }],gas: 3288,},{stateMutability: "view",type: "function",name: "period_timestamp",inputs: [{ name: "arg0", type: "uint256" }],outputs: [{ name: "", type: "uint256" }],gas: 3363,},{stateMutability: "view",type: "function",name: "integrate_inv_supply",inputs: [{ name: "arg0", type: "uint256" }],outputs: [{ name: "", type: "uint256" }],gas: 3393,},{stateMutability: "view",type: "function",name: "integrate_inv_supply_of",inputs: [{ name: "arg0", type: "address" }],outputs: [{ name: "", type: "uint256" }],gas: 3593,},{stateMutability: "view",type: "function",name: "integrate_checkpoint_of",inputs: [{ name: "arg0", type: "address" }],outputs: [{ name: "", type: "uint256" }],gas: 3623,},{stateMutability: "view",type: "function",name: "integrate_fraction",inputs: [{ name: "arg0", type: "address" }],outputs: [{ name: "", type: "uint256" }],gas: 3653,},{stateMutability: "view",type: "function",name: "inflation_rate",inputs: [],outputs: [{ name: "", type: "uint256" }],gas: 3468,},{stateMutability: "view",type: "function",name: "reward_count",inputs: [],outputs: [{ name: "", type: "uint256" }],gas: 3498,},{stateMutability: "view",type: "function",name: "reward_tokens",inputs: [{ name: "arg0", type: "uint256" }],outputs: [{ name: "", type: "address" }],gas: 3573,},{stateMutability: "view",type: "function",name: "reward_data",inputs: [{ name: "arg0", type: "address" }],outputs: [{ name: "token", type: "address" },{ name: "distributor", type: "address" },{ name: "period_finish", type: "uint256" },{ name: "rate", type: "uint256" },{ name: "last_update", type: "uint256" },{ name: "integral", type: "uint256" },],gas: 15003,},{stateMutability: "view",type: "function",name: "rewards_receiver",inputs: [{ name: "arg0", type: "address" }],outputs: [{ name: "", type: "address" }],gas: 3803,},{stateMutability: "view",type: "function",name: "reward_integral_for",inputs: [{ name: "arg0", type: "address" },{ name: "arg1", type: "address" },],outputs: [{ name: "", type: "uint256" }],gas: 4048,},{stateMutability: "view",type: "function",name: "is_killed",inputs: [],outputs: [{ name: "", type: "bool" }],gas: 3648,},{stateMutability: "view",type: "function",name: "factory",inputs: [],outputs: [{ name: "", type: "address" }],gas: 3678,},]; + // prettier-ignore + const gaugeControllerAbi = [{name: "CommitOwnership",inputs: [{ type: "address", name: "admin", indexed: false }],anonymous: false,type: "event",},{name: "ApplyOwnership",inputs: [{ type: "address", name: "admin", indexed: false }],anonymous: false,type: "event",},{name: "AddType",inputs: [{ type: "string", name: "name", indexed: false },{ type: "int128", name: "type_id", indexed: false },],anonymous: false,type: "event",},{name: "NewTypeWeight",inputs: [{ type: "int128", name: "type_id", indexed: false },{ type: "uint256", name: "time", indexed: false },{ type: "uint256", name: "weight", indexed: false },{ type: "uint256", name: "total_weight", indexed: false },],anonymous: false,type: "event",},{name: "NewGaugeWeight",inputs: [{ type: "address", name: "gauge_address", indexed: false },{ type: "uint256", name: "time", indexed: false },{ type: "uint256", name: "weight", indexed: false },{ type: "uint256", name: "total_weight", indexed: false },],anonymous: false,type: "event",},{name: "VoteForGauge",inputs: [{ type: "uint256", name: "time", indexed: false },{ type: "address", name: "user", indexed: false },{ type: "address", name: "gauge_addr", indexed: false },{ type: "uint256", name: "weight", indexed: false },],anonymous: false,type: "event",},{name: "NewGauge",inputs: [{ type: "address", name: "addr", indexed: false },{ type: "int128", name: "gauge_type", indexed: false },{ type: "uint256", name: "weight", indexed: false },],anonymous: false,type: "event",},{outputs: [],inputs: [{ type: "address", name: "_token" },{ type: "address", name: "_voting_escrow" },],stateMutability: "nonpayable",type: "constructor",},{name: "commit_transfer_ownership",outputs: [],inputs: [{ type: "address", name: "addr" }],stateMutability: "nonpayable",type: "function",gas: 37597,},{name: "apply_transfer_ownership",outputs: [],inputs: [],stateMutability: "nonpayable",type: "function",gas: 38497,},{name: "gauge_types",outputs: [{ type: "int128", name: "" }],inputs: [{ type: "address", name: "_addr" }],stateMutability: "view",type: "function",gas: 1625,},{name: "add_gauge",outputs: [],inputs: [{ type: "address", name: "addr" },{ type: "int128", name: "gauge_type" },],stateMutability: "nonpayable",type: "function",},{name: "add_gauge",outputs: [],inputs: [{ type: "address", name: "addr" },{ type: "int128", name: "gauge_type" },{ type: "uint256", name: "weight" },],stateMutability: "nonpayable",type: "function",},{name: "checkpoint",outputs: [],inputs: [],stateMutability: "nonpayable",type: "function",gas: 18033784416,},{name: "checkpoint_gauge",outputs: [],inputs: [{ type: "address", name: "addr" }],stateMutability: "nonpayable",type: "function",gas: 18087678795,},{name: "gauge_relative_weight",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "address", name: "addr" }],stateMutability: "view",type: "function",},{name: "gauge_relative_weight",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "address", name: "addr" },{ type: "uint256", name: "time" },],stateMutability: "view",type: "function",},{name: "gauge_relative_weight_write",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "address", name: "addr" }],stateMutability: "nonpayable",type: "function",},{name: "gauge_relative_weight_write",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "address", name: "addr" },{ type: "uint256", name: "time" },],stateMutability: "nonpayable",type: "function",},{name: "add_type",outputs: [],inputs: [{ type: "string", name: "_name" }],stateMutability: "nonpayable",type: "function",},{name: "add_type",outputs: [],inputs: [{ type: "string", name: "_name" },{ type: "uint256", name: "weight" },],stateMutability: "nonpayable",type: "function",},{name: "change_type_weight",outputs: [],inputs: [{ type: "int128", name: "type_id" },{ type: "uint256", name: "weight" },],stateMutability: "nonpayable",type: "function",gas: 36246310050,},{name: "change_gauge_weight",outputs: [],inputs: [{ type: "address", name: "addr" },{ type: "uint256", name: "weight" },],stateMutability: "nonpayable",type: "function",gas: 36354170809,},{name: "vote_for_gauge_weights",outputs: [],inputs: [{ type: "address", name: "_gauge_addr" },{ type: "uint256", name: "_user_weight" },],stateMutability: "nonpayable",type: "function",gas: 18142052127,},{name: "get_gauge_weight",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "address", name: "addr" }],stateMutability: "view",type: "function",gas: 2974,},{name: "get_type_weight",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "int128", name: "type_id" }],stateMutability: "view",type: "function",gas: 2977,},{name: "get_total_weight",outputs: [{ type: "uint256", name: "" }],inputs: [],stateMutability: "view",type: "function",gas: 2693,},{name: "get_weights_sum_per_type",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "int128", name: "type_id" }],stateMutability: "view",type: "function",gas: 3109,},{name: "admin",outputs: [{ type: "address", name: "" }],inputs: [],stateMutability: "view",type: "function",gas: 1841,},{name: "future_admin",outputs: [{ type: "address", name: "" }],inputs: [],stateMutability: "view",type: "function",gas: 1871,},{name: "token",outputs: [{ type: "address", name: "" }],inputs: [],stateMutability: "view",type: "function",gas: 1901,},{name: "voting_escrow",outputs: [{ type: "address", name: "" }],inputs: [],stateMutability: "view",type: "function",gas: 1931,},{name: "n_gauge_types",outputs: [{ type: "int128", name: "" }],inputs: [],stateMutability: "view",type: "function",gas: 1961,},{name: "n_gauges",outputs: [{ type: "int128", name: "" }],inputs: [],stateMutability: "view",type: "function",gas: 1991,},{name: "gauge_type_names",outputs: [{ type: "string", name: "" }],inputs: [{ type: "int128", name: "arg0" }],stateMutability: "view",type: "function",gas: 8628,},{name: "gauges",outputs: [{ type: "address", name: "" }],inputs: [{ type: "uint256", name: "arg0" }],stateMutability: "view",type: "function",gas: 2160,},{name: "vote_user_slopes",outputs: [{ type: "uint256", name: "slope" },{ type: "uint256", name: "power" },{ type: "uint256", name: "end" },],inputs: [{ type: "address", name: "arg0" },{ type: "address", name: "arg1" },],stateMutability: "view",type: "function",gas: 5020,},{name: "vote_user_power",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "address", name: "arg0" }],stateMutability: "view",type: "function",gas: 2265,},{name: "last_user_vote",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "address", name: "arg0" },{ type: "address", name: "arg1" },],stateMutability: "view",type: "function",gas: 2449,},{name: "points_weight",outputs: [{ type: "uint256", name: "bias" },{ type: "uint256", name: "slope" },],inputs: [{ type: "address", name: "arg0" },{ type: "uint256", name: "arg1" },],stateMutability: "view",type: "function",gas: 3859,},{name: "time_weight",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "address", name: "arg0" }],stateMutability: "view",type: "function",gas: 2355,},{name: "points_sum",outputs: [{ type: "uint256", name: "bias" },{ type: "uint256", name: "slope" },],inputs: [{ type: "int128", name: "arg0" },{ type: "uint256", name: "arg1" },],stateMutability: "view",type: "function",gas: 3970,},{name: "time_sum",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "uint256", name: "arg0" }],stateMutability: "view",type: "function",gas: 2370,},{name: "points_total",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "uint256", name: "arg0" }],stateMutability: "view",type: "function",gas: 2406,},{name: "time_total",outputs: [{ type: "uint256", name: "" }],inputs: [],stateMutability: "view",type: "function",gas: 2321,},{name: "points_type_weight",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "int128", name: "arg0" },{ type: "uint256", name: "arg1" },],stateMutability: "view",type: "function",gas: 2671,},{name: "time_type_weight",outputs: [{ type: "uint256", name: "" }],inputs: [{ type: "uint256", name: "arg0" }],stateMutability: "view",type: "function",gas: 2490,},]; + // prettier-ignore + const erc20Abi = [{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: 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",},{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: [{ 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: [{ internalType: "address", name: "account", type: "address" }],name: "balanceOf",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],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",},]; + // prettier-ignore + const curvePoolAbi = [{"name":"Transfer","inputs":[{"name":"sender","type":"address","indexed":true},{"name":"receiver","type":"address","indexed":true},{"name":"value","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true},{"name":"spender","type":"address","indexed":true},{"name":"value","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"TokenExchange","inputs":[{"name":"buyer","type":"address","indexed":true},{"name":"sold_id","type":"int128","indexed":false},{"name":"tokens_sold","type":"uint256","indexed":false},{"name":"bought_id","type":"int128","indexed":false},{"name":"tokens_bought","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"AddLiquidity","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"token_amounts","type":"uint256[2]","indexed":false},{"name":"fees","type":"uint256[2]","indexed":false},{"name":"invariant","type":"uint256","indexed":false},{"name":"token_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RemoveLiquidity","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"token_amounts","type":"uint256[2]","indexed":false},{"name":"fees","type":"uint256[2]","indexed":false},{"name":"token_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RemoveLiquidityOne","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"token_amount","type":"uint256","indexed":false},{"name":"coin_amount","type":"uint256","indexed":false},{"name":"token_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RemoveLiquidityImbalance","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"token_amounts","type":"uint256[2]","indexed":false},{"name":"fees","type":"uint256[2]","indexed":false},{"name":"invariant","type":"uint256","indexed":false},{"name":"token_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RampA","inputs":[{"name":"old_A","type":"uint256","indexed":false},{"name":"new_A","type":"uint256","indexed":false},{"name":"initial_time","type":"uint256","indexed":false},{"name":"future_time","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"StopRampA","inputs":[{"name":"A","type":"uint256","indexed":false},{"name":"t","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"CommitNewFee","inputs":[{"name":"new_fee","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"ApplyNewFee","inputs":[{"name":"fee","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"initialize","inputs":[{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_coins","type":"address[4]"},{"name":"_rate_multipliers","type":"uint256[4]"},{"name":"_A","type":"uint256"},{"name":"_fee","type":"uint256"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"transfer","inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"transferFrom","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"approve","inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"permit","inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_deadline","type":"uint256"},{"name":"_v","type":"uint8"},{"name":"_r","type":"bytes32"},{"name":"_s","type":"bytes32"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"last_price","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"ema_price","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_balances","inputs":[],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"view","type":"function","name":"admin_fee","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"A","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"A_precise","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_p","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"price_oracle","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_virtual_price","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"calc_token_amount","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_is_deposit","type":"bool"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"add_liquidity","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_min_mint_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"add_liquidity","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_min_mint_amount","type":"uint256"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_dy","inputs":[{"name":"i","type":"int128"},{"name":"j","type":"int128"},{"name":"dx","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"exchange","inputs":[{"name":"i","type":"int128"},{"name":"j","type":"int128"},{"name":"_dx","type":"uint256"},{"name":"_min_dy","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"payable","type":"function","name":"exchange","inputs":[{"name":"i","type":"int128"},{"name":"j","type":"int128"},{"name":"_dx","type":"uint256"},{"name":"_min_dy","type":"uint256"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"_min_amounts","type":"uint256[2]"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"_min_amounts","type":"uint256[2]"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity_imbalance","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_max_burn_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity_imbalance","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_max_burn_amount","type":"uint256"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"calc_withdraw_one_coin","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"i","type":"int128"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity_one_coin","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"i","type":"int128"},{"name":"_min_received","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity_one_coin","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"i","type":"int128"},{"name":"_min_received","type":"uint256"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"ramp_A","inputs":[{"name":"_future_A","type":"uint256"},{"name":"_future_time","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"stop_ramp_A","inputs":[],"outputs":[]},{"stateMutability":"view","type":"function","name":"admin_balances","inputs":[{"name":"i","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"withdraw_admin_fees","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"commit_new_fee","inputs":[{"name":"_new_fee","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"apply_new_fee","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_ma_exp_time","inputs":[{"name":"_ma_exp_time","type":"uint256"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"version","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"coins","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"balances","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"fee","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"future_fee","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"admin_action_deadline","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"initial_A","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"future_A","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"initial_A_time","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"future_A_time","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"balanceOf","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"allowance","inputs":[{"name":"arg0","type":"address"},{"name":"arg1","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"DOMAIN_SEPARATOR","inputs":[],"outputs":[{"name":"","type":"bytes32"}]},{"stateMutability":"view","type":"function","name":"nonces","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"ma_exp_time","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"ma_last_time","inputs":[],"outputs":[{"name":"","type":"uint256"}]}]; + + const cCurveGaugeFactory = new Contract( + "0x9f99FDe2ED3997EAfE52b78E3981b349fD2Eb8C9", + curveGaugeFactoryAbi, + sDeployer + ); + const cConvexPoolManager = new Contract( + "0xc461E1CE3795Ee30bA2EC59843d5fAe14d5782D5", + convexPoolManagerAbi, + sDeployer + ); + const gaugeController = new Contract( + "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB", + gaugeControllerAbi + ); + + const gaugeTx = await withConfirmation( + cCurveGaugeFactory.connect(sDeployer)["deploy_gauge(address)"](poolAddress) + ); + + const gaugeAddress = + "0x" + gaugeTx.receipt.logs[0].data.substr(2 + 64 * 2 + 24, 40); + + console.log("Gauge deployed to address: ", gaugeAddress); + + const gaugeControllerTx = await withConfirmation( + gaugeController + .connect(sGaugeControllerAdmin) + ["add_gauge(address,int128)"](gaugeAddress, 0) + ); + + const gaugeControllerTx2 = await withConfirmation( + gaugeController + .connect(sGaugeControllerAdmin) + .change_gauge_weight(gaugeAddress, 100, { gasLimit: 2000000 }) + ); + + const convexTx = await withConfirmation( + cConvexPoolManager.connect(sDeployer)["addPool(address)"](gaugeAddress) + ); + + // add liquidity to Curve pool otherwise multiple functions fail when called + const oeth = new Contract(addresses.mainnet.OETHProxy, erc20Abi); + const weth = new Contract(addresses.mainnet.WETH, erc20Abi); + const reth = new Contract(addresses.mainnet.rETH, erc20Abi); + const curvePool = new Contract(poolAddress, curvePoolAbi); + const weth_whale = "0x44cc771fbe10dea3836f37918cf89368589b6316"; + const reth_whale = "0x5313b39bf226ced2332C81eB97BB28c6fD50d1a3"; + + await impersonateAccount(weth_whale); + const sWethWhale = await ethers.provider.getSigner(weth_whale); + await impersonateAccount(reth_whale); + const sRethWhale = await ethers.provider.getSigner(reth_whale); + + await reth + .connect(sRethWhale) + .approve(cVault.address, utils.parseUnits("1", 50)); + + // mint a bunch of OETH so the tests don't trigger the 3% maxSupplyDiff on redeem + const oethToMint = utils.parseUnits("4000", 18); + await cVault.connect(sRethWhale).mint(reth.address, oethToMint, 0); + await oeth.connect(sRethWhale).transfer(weth_whale, oethToMint); + await oeth + .connect(sRethWhale) + .approve(sRethWhale.address, utils.parseUnits("1", 50)); + await oeth + .connect(sRethWhale) + .transferFrom(sRethWhale.address, weth_whale, oethToMint); + + await weth + .connect(sWethWhale) + .approve(poolAddress, utils.parseUnits("1", 50)); + await oeth + .connect(sWethWhale) + .approve(poolAddress, utils.parseUnits("1", 50)); + + const depositValue = utils.parseUnits("50", 18); + await curvePool + .connect(sWethWhale) + ["add_liquidity(uint256[2],uint256)"]([depositValue, depositValue], 0, { + value: depositValue, + }); + + const tokenContract = new Contract(tokenAddress, erc20Abi); + // console.log("LP RECEIVED:", (await tokenContract.connect(sWethWhale).balanceOf(weth_whale)).toString()); + + // find out the CVX booster PID + // prettier-ignore + const cvxBoosterABI = [{inputs: [{ internalType: "address", name: "_staker", type: "address" },{ internalType: "address", name: "_minter", type: "address" },],stateMutability: "nonpayable",type: "constructor",},{anonymous: false,inputs: [{indexed: true,internalType: "address",name: "user",type: "address",},{indexed: true,internalType: "uint256",name: "poolid",type: "uint256",},{indexed: false,internalType: "uint256",name: "amount",type: "uint256",},],name: "Deposited",type: "event",},{anonymous: false,inputs: [{indexed: true,internalType: "address",name: "user",type: "address",},{indexed: true,internalType: "uint256",name: "poolid",type: "uint256",},{indexed: false,internalType: "uint256",name: "amount",type: "uint256",},],name: "Withdrawn",type: "event",},{inputs: [],name: "FEE_DENOMINATOR",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],stateMutability: "view",type: "function",},{inputs: [],name: "MaxFees",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "address", name: "_lptoken", type: "address" },{ internalType: "address", name: "_gauge", type: "address" },{ internalType: "uint256", name: "_stashVersion", type: "uint256" },],name: "addPool",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" },{ internalType: "address", name: "_gauge", type: "address" },],name: "claimRewards",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "crv",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" },{ internalType: "uint256", name: "_amount", type: "uint256" },{ internalType: "bool", name: "_stake", type: "bool" },],name: "deposit",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" },{ internalType: "bool", name: "_stake", type: "bool" },],name: "depositAll",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "distributionAddressId",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],stateMutability: "view",type: "function",},{inputs: [],name: "earmarkFees",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "earmarkIncentive",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" }],name: "earmarkRewards",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "feeDistro",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "feeManager",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "feeToken",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "address", name: "", type: "address" }],name: "gaugeMap",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "view",type: "function",},{inputs: [],name: "isShutdown",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "view",type: "function",},{inputs: [],name: "lockFees",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "lockIncentive",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],stateMutability: "view",type: "function",},{inputs: [],name: "lockRewards",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "minter",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "owner",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "platformFee",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "uint256", name: "", type: "uint256" }],name: "poolInfo",outputs: [{ internalType: "address", name: "lptoken", type: "address" },{ internalType: "address", name: "token", type: "address" },{ internalType: "address", name: "gauge", type: "address" },{ internalType: "address", name: "crvRewards", type: "address" },{ internalType: "address", name: "stash", type: "address" },{ internalType: "bool", name: "shutdown", type: "bool" },],stateMutability: "view",type: "function",},{inputs: [],name: "poolLength",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],stateMutability: "view",type: "function",},{inputs: [],name: "poolManager",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "registry",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "rewardArbitrator",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" },{ internalType: "address", name: "_address", type: "address" },{ internalType: "uint256", name: "_amount", type: "uint256" },],name: "rewardClaimed",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "rewardFactory",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "address", name: "_arb", type: "address" }],name: "setArbitrator",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_rfactory", type: "address" },{ internalType: "address", name: "_sfactory", type: "address" },{ internalType: "address", name: "_tfactory", type: "address" },],name: "setFactories",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "setFeeInfo",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_feeM", type: "address" }],name: "setFeeManager",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "uint256", name: "_lockFees", type: "uint256" },{ internalType: "uint256", name: "_stakerFees", type: "uint256" },{ internalType: "uint256", name: "_callerFees", type: "uint256" },{ internalType: "uint256", name: "_platform", type: "uint256" },],name: "setFees",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" }],name: "setGaugeRedirect",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_owner", type: "address" }],name: "setOwner",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_poolM", type: "address" }],name: "setPoolManager",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_rewards", type: "address" },{ internalType: "address", name: "_stakerRewards", type: "address" },],name: "setRewardContracts",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_treasury", type: "address" }],name: "setTreasury",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "address", name: "_voteDelegate", type: "address" },],name: "setVoteDelegate",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" }],name: "shutdownPool",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "shutdownSystem",outputs: [],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "staker",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "stakerIncentive",outputs: [{ internalType: "uint256", name: "", type: "uint256" }],stateMutability: "view",type: "function",},{inputs: [],name: "stakerRewards",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "stashFactory",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "tokenFactory",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "treasury",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "uint256", name: "_voteId", type: "uint256" },{ internalType: "address", name: "_votingAddress", type: "address" },{ internalType: "bool", name: "_support", type: "bool" },],name: "vote",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "voteDelegate",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "address[]", name: "_gauge", type: "address[]" },{ internalType: "uint256[]", name: "_weight", type: "uint256[]" },],name: "voteGaugeWeight",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [],name: "voteOwnership",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [],name: "voteParameter",outputs: [{ internalType: "address", name: "", type: "address" }],stateMutability: "view",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" },{ internalType: "uint256", name: "_amount", type: "uint256" },],name: "withdraw",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" }],name: "withdrawAll",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},{inputs: [{ internalType: "uint256", name: "_pid", type: "uint256" },{ internalType: "uint256", name: "_amount", type: "uint256" },{ internalType: "address", name: "_to", type: "address" },],name: "withdrawTo",outputs: [{ internalType: "bool", name: "", type: "bool" }],stateMutability: "nonpayable",type: "function",},]; + const cvxBooster = new Contract(addresses.mainnet.CVXBooster, cvxBoosterABI); + + return { + actions: [], + }; +}; diff --git a/contracts/fork-test.sh b/contracts/fork-test.sh index f6d3d14868..e975f88f45 100755 --- a/contracts/fork-test.sh +++ b/contracts/fork-test.sh @@ -69,12 +69,15 @@ main() cp -r deployments/localhost deployments/hardhat fi - if [ -z "$1" ]; then - # Run all files with `.fork-test.js` suffix when no param is given - params+="test/**/*.fork-test.js" + #if [ -z "$1" ] || [ "$1" = --* ]; then + if [ -z "$1" ] || [[ $1 == --* ]]; then + # Run all files with `.fork-test.js` suffix when no file name param is given + # pass all other params along + params+="test/**/*.fork-test.js $@" else # Run specifc files when a param is given params+=($1) + params+=" $@" fi FORK=true IS_TEST=true npx --no-install hardhat test ${params[@]} diff --git a/contracts/package.json b/contracts/package.json index 14ab5963f7..a3b6edd48c 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -17,6 +17,7 @@ "prettier:sol": "prettier --write \"contracts/**/*.sol\"", "test": "IS_TEST=true npx hardhat test", "test:fork": "./fork-test.sh", + "test:fork:w_trace": "./fork-test.sh --trace", "fund": "FORK=true npx hardhat fund --network localhost", "copy-interface-artifacts": "mkdir -p ../dapp/abis && cp artifacts/contracts/interfaces/IVault.sol/IVault.json ../dapp/abis/IVault.json && cp artifacts/contracts/liquidity/LiquidityReward.sol/LiquidityReward.json ../dapp/abis/LiquidityReward.json && cp artifacts/contracts/interfaces/uniswap/IUniswapV2Pair.sol/IUniswapV2Pair.json ../dapp/abis/IUniswapV2Pair.json && cp artifacts/contracts/staking/SingleAssetStaking.sol/SingleAssetStaking.json ../dapp/abis/SingleAssetStaking.json && cp artifacts/contracts/compensation/CompensationClaims.sol/CompensationClaims.json ../dapp/abis/CompensationClaims.json && cp artifacts/contracts/flipper/Flipper.sol/Flipper.json ../dapp/abis/Flipper.json && cp artifacts/contracts/flipper/Flipper.sol/Flipper.json ../dapp/abis/Flipper.json", "echidna": "yarn run clean && echidna-test . --contract PropertiesOUSDTransferable --config contracts/crytic/TestOUSDTransferable.yaml", @@ -63,5 +64,8 @@ "hooks": { "pre-push": "yarn run prettier:check" } + }, + "dependencies": { + "hardhat-tracer": "^2.2.2" } } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 5990703688..65fee2fbf2 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -39,7 +39,9 @@ async function defaultFixture() { const ousdProxy = await ethers.getContract("OUSDProxy"); const vaultProxy = await ethers.getContract("VaultProxy"); + const harvesterProxy = await ethers.getContract("HarvesterProxy"); + const compoundStrategyProxy = await ethers.getContract( "CompoundStrategyProxy" ); @@ -129,6 +131,7 @@ async function defaultFixture() { dai, tusd, usdc, + weth, ogn, ogv, rewardsSource, @@ -176,13 +179,18 @@ async function defaultFixture() { cvxBooster, cvxRewardPool, LUSDMetaStrategyProxy, - LUSDMetaStrategy; + LUSDMetaStrategy, + oethHarvester, + oethDripper, + ConvexEthMetaStrategyProxy, + ConvexEthMetaStrategy; if (isFork) { usdt = await ethers.getContractAt(usdtAbi, addresses.mainnet.USDT); dai = await ethers.getContractAt(daiAbi, addresses.mainnet.DAI); tusd = await ethers.getContractAt(erc20Abi, addresses.mainnet.TUSD); usdc = await ethers.getContractAt(erc20Abi, addresses.mainnet.USDC); + weth = await ethers.getContractAt(erc20Abi, addresses.mainnet.WETH); cusdt = await ethers.getContractAt(erc20Abi, addresses.mainnet.cUSDT); cdai = await ethers.getContractAt(erc20Abi, addresses.mainnet.cDAI); cusdc = await ethers.getContractAt(erc20Abi, addresses.mainnet.cUSDC); @@ -244,11 +252,32 @@ async function defaultFixture() { "Generalized4626Strategy", fraxEthStrategyProxy.address ); + + const oethHarvesterProxy = await ethers.getContract("OETHHarvesterProxy"); + oethHarvester = await ethers.getContractAt( + "OETHHarvester", + oethHarvesterProxy.address + ); + + ConvexEthMetaStrategyProxy = await ethers.getContract( + "ConvexEthMetaStrategyProxy" + ); + ConvexEthMetaStrategy = await ethers.getContractAt( + "ConvexEthMetaStrategy", + ConvexEthMetaStrategyProxy.address + ); + + const oethDripperProxy = await ethers.getContract("OETHDripperProxy"); + oethDripper = await ethers.getContractAt( + "OETHDripper", + oethDripperProxy.address + ); } else { usdt = await ethers.getContract("MockUSDT"); dai = await ethers.getContract("MockDAI"); tusd = await ethers.getContract("MockTUSD"); usdc = await ethers.getContract("MockUSDC"); + weth = await ethers.getContract("MockWETH"); ogn = await ethers.getContract("MockOGN"); LUSD = await ethers.getContract("MockLUSD"); ogv = await ethers.getContract("MockOGV"); @@ -324,6 +353,7 @@ async function defaultFixture() { LUSDMetaStrategyProxy.address ); } + if (!isFork) { const assetAddresses = await getAssetAddresses(deployments); @@ -390,7 +420,9 @@ async function defaultFixture() { ousd, vault, harvester, + oethHarvester, dripper, + oethDripper, mockNonRebasing, mockNonRebasingTwo, // Oracle @@ -409,6 +441,7 @@ async function defaultFixture() { usdc, ogn, LUSD, + weth, ogv, reth, rewardsSource, @@ -438,6 +471,7 @@ async function defaultFixture() { convexStrategy, OUSDmetaStrategy, LUSDMetaStrategy, + ConvexEthMetaStrategy, morphoCompoundStrategy, morphoAaveStrategy, cvx, @@ -1005,6 +1039,58 @@ async function convexLUSDMetaVaultFixture() { fixture.usdc.address, fixture.LUSDMetaStrategy.address ); + + return fixture; +} + +/** + * Configure a Vault with only the OETH/(W)ETH Curve Metastrategy. + */ +async function convexOETHMetaVaultFixture() { + const fixture = await loadFixture(defaultFixture); + const { guardianAddr } = await getNamedAccounts(); + const sGuardian = await ethers.provider.getSigner(guardianAddr); + + await impersonateAndFundAddress( + fixture.weth.address, + [ + "0x7373BD8512d17FC53e9b39c9655A95c9813A0aB1", + "0x821A96fbD4465D02726EDbAa936A0d6d1032dE46", + "0x4b7fEcEffE3b14fFD522e72b711B087f08BD98Ab", + "0x204bcc7A3da640EF95cB01a15c63938C6B878e9e", + ], + // Josh is loaded with weth + fixture.josh.getAddress() + ); + + // Get some 3CRV from most loaded contracts/wallets + await impersonateAndFundAddress( + addresses.mainnet.CRV, + [ + "0x0A2634885B47F15064fB2B33A86733C614c9950A", + "0x34ea4138580435B5A521E460035edb19Df1938c1", + "0x28C6c06298d514Db089934071355E5743bf21d60", + "0xa6a4d3218BBf0E81B38390396f9EA7eb8B9c9820", + "0xb73D8dCE603155e231aAd4381a2F20071Ca4D55c", + ], + // Josh is loaded with CRV + fixture.josh.getAddress() + ); + + // Add Convex Meta strategy + await fixture.oethVault + .connect(sGuardian) + .setAssetDefaultStrategy( + fixture.weth.address, + fixture.ConvexEthMetaStrategy.address + ); + + // TODO: hardcode this once deployed to the mainnet + fixture.cvxRewardPool = await ethers.getContractAt( + "IRewardStaking", + await fixture.ConvexEthMetaStrategy.cvxRewardStaker() + ); + return fixture; } @@ -1196,7 +1282,6 @@ async function hackedVaultFixture() { }); const evilDAI = await ethers.getContract("MockEvilDAI"); - await oracleRouter.setFeed( evilDAI.address, oracleAddresses.chainlink.DAI_USD @@ -1261,6 +1346,7 @@ module.exports = { threepoolVaultFixture, convexVaultFixture, convexMetaVaultFixture, + convexOETHMetaVaultFixture, convexGeneralizedMetaForkedFixture, convexLUSDMetaVaultFixture, morphoCompoundFixture, diff --git a/contracts/test/_metastrategies-fixtures.js b/contracts/test/_metastrategies-fixtures.js index e153698263..c4ac487ee1 100644 --- a/contracts/test/_metastrategies-fixtures.js +++ b/contracts/test/_metastrategies-fixtures.js @@ -7,6 +7,7 @@ const { resetAllowance, impersonateAndFundContract, } = require("./_fixture"); +const addresses = require("../utils/addresses"); const erc20Abi = require("./abi/erc20.json"); // NOTE: This can cause a change in setup from mainnet. @@ -26,6 +27,11 @@ async function withDefaultOUSDMetapoolStrategiesSet() { .connect(timelock) .setAssetDefaultStrategy(usdc.address, OUSDmetaStrategy.address); + fixture.cvxRewardPool = await ethers.getContractAt( + "IRewardStaking", + addresses.mainnet.CVXRewardsPool + ); + return fixture; } diff --git a/contracts/test/helpers.js b/contracts/test/helpers.js index adc7d7e921..1a936e3b2b 100644 --- a/contracts/test/helpers.js +++ b/contracts/test/helpers.js @@ -1,7 +1,7 @@ const hre = require("hardhat"); const chai = require("chai"); const mocha = require("mocha"); -const { parseUnits, formatUnits } = require("ethers").utils; +const { parseUnits, formatUnits, parseEther } = require("ethers").utils; const BigNumber = require("ethers").BigNumber; const { createFixtureLoader } = require("ethereum-waffle"); @@ -187,6 +187,7 @@ const loadFixture = createFixtureLoader( ); const advanceTime = async (seconds) => { + seconds = Math.floor(seconds); await hre.ethers.provider.send("evm_increaseTime", [seconds]); await hre.ethers.provider.send("evm_mine"); }; @@ -399,6 +400,13 @@ const getAssetAddresses = async (deployments) => { } }; +async function fundAccount(address, balance = "1000") { + await hre.network.provider.send("hardhat_setBalance", [ + address, + parseEther(balance).toHexString(), + ]); +} + async function changeInBalance( functionChangingBalance, balanceChangeContract, @@ -620,4 +628,5 @@ module.exports = { differenceInErc20TokenBalance, differenceInErc20TokenBalances, differenceInStrategyBalance, + fundAccount, }; diff --git a/contracts/test/strategies/3pool.js b/contracts/test/strategies/3pool.js index 62de617e96..0b57d1c8f0 100644 --- a/contracts/test/strategies/3pool.js +++ b/contracts/test/strategies/3pool.js @@ -170,7 +170,7 @@ describe("3Pool Strategy", function () { it("Should collect reward tokens and swap via Uniswap", async () => { const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize([crv.address], [usdt.address]); + await mockUniswapRouter.initialize([crv.address], [usdt.address]); await harvester.connect(governor).setRewardTokenConfig( crv.address, // reward token 300, // max slippage bps diff --git a/contracts/test/strategies/convex.js b/contracts/test/strategies/convex.js index e05cf0d357..d82de48c1b 100644 --- a/contracts/test/strategies/convex.js +++ b/contracts/test/strategies/convex.js @@ -214,7 +214,7 @@ describe("Convex Strategy", function () { it("Should collect reward tokens and swap via Uniswap", async () => { const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize( + await mockUniswapRouter.initialize( [crv.address, cvx.address], [usdt.address, usdt.address] ); @@ -279,7 +279,7 @@ describe("Convex Strategy", function () { const harvestAndSwapTokens = async (callAsGovernor) => { const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize( + await mockUniswapRouter.initialize( [crv.address, cvx.address], [usdt.address, usdt.address] ); diff --git a/contracts/test/strategies/oeth-metapool.fork-test.js b/contracts/test/strategies/oeth-metapool.fork-test.js new file mode 100644 index 0000000000..3d9ebd92dd --- /dev/null +++ b/contracts/test/strategies/oeth-metapool.fork-test.js @@ -0,0 +1,182 @@ +const { expect } = require("chai"); + +const { loadFixture } = require("ethereum-waffle"); +const { units, oethUnits, forkOnlyDescribe } = require("../helpers"); +const { + convexOETHMetaVaultFixture, + impersonateAndFundContract, +} = require("../_fixture"); + +forkOnlyDescribe("ForkTest: OETH Curve Metapool Strategy", function () { + this.timeout(0); + // due to hardhat forked mode timeouts - retry failed tests up to 3 times + this.retries(3); + + it("Should stake WETH in Curve guage via metapool", async function () { + // TODO: should have differently balanced metapools + const fixture = await loadFixture(convexOETHMetaVaultFixture); + + const { josh, weth } = fixture; + await mintTest(fixture, josh, weth, "5"); + }); + + it("Should be able to withdraw all", async () => { + const { oethVault, oeth, weth, josh, ConvexEthMetaStrategy } = + await loadFixture(convexOETHMetaVaultFixture); + + await oethVault.connect(josh).allocate(); + const supplyBeforeMint = await oeth.totalSupply(); + const amount = "10"; + const unitAmount = oethUnits(amount); + + await weth.connect(josh).approve(oethVault.address, unitAmount); + await oethVault.connect(josh).mint(weth.address, unitAmount, 0); + await oethVault.connect(josh).allocate(); + + // mul by 2 because the other 50% is represented by the OETH balance + const strategyBalance = ( + await ConvexEthMetaStrategy.checkBalance(weth.address) + ).mul(2); + + // 10 WETH + 10 (printed) OETH + await expect(strategyBalance).to.be.gte(oethUnits("20")); + + const currentSupply = await oeth.totalSupply(); + const supplyAdded = currentSupply.sub(supplyBeforeMint); + // 10 OETH to josh for minting. And 10 printed into the strategy + expect(supplyAdded).to.be.gte(oethUnits("19.98")); + + const vaultSigner = await impersonateAndFundContract(oethVault.address); + // Now try to redeem the amount + await ConvexEthMetaStrategy.connect(vaultSigner).withdrawAll(); + + const newSupply = await oeth.totalSupply(); + const supplyDiff = currentSupply.sub(newSupply); + + expect(supplyDiff).to.be.gte(oethUnits("9.95")); + }); + + it("Should redeem", async () => { + const { oethVault, oeth, weth, josh, ConvexEthMetaStrategy } = + await loadFixture(convexOETHMetaVaultFixture); + + await oethVault.connect(josh).allocate(); + const supplyBeforeMint = await oeth.totalSupply(); + const amount = "10"; + const unitAmount = oethUnits(amount); + + await weth.connect(josh).approve(oethVault.address, unitAmount); + await oethVault.connect(josh).mint(weth.address, unitAmount, 0); + await oethVault.connect(josh).allocate(); + + // mul by 2 because the other 50% is represented by the OETH balance + const strategyBalance = ( + await ConvexEthMetaStrategy.checkBalance(weth.address) + ).mul(2); + + // 10 WETH + 10 (printed) OETH + await expect(strategyBalance).to.be.gte(oethUnits("20")); + + const currentSupply = await oeth.totalSupply(); + const supplyAdded = currentSupply.sub(supplyBeforeMint); + // 10 OETH to josh for minting. And 10 printed into the strategy + expect(supplyAdded).to.be.gte(oethUnits("19.98")); + + const currentBalance = await oeth.connect(josh).balanceOf(josh.address); + + // Now try to redeem the amount + await oethVault.connect(josh).redeem(oethUnits("8"), 0); + + // User balance should be down by 8 eth + const newBalance = await oeth.connect(josh).balanceOf(josh.address); + expect(newBalance).to.approxEqualTolerance( + currentBalance.sub(oethUnits("8")), + 1 + ); + + const newSupply = await oeth.totalSupply(); + const supplyDiff = currentSupply.sub(newSupply); + + expect(supplyDiff).to.be.gte(oethUnits("7.95")); + }); + + it("Should be able to harvest the rewards", async function () { + const fixture = await loadFixture(convexOETHMetaVaultFixture); + + const { + josh, + weth, + oethHarvester, + oethDripper, + oethVault, + ConvexEthMetaStrategy, + crv, + } = fixture; + await mintTest(fixture, josh, weth, "5"); + + // send some CRV to the strategy to partly simulate reward harvesting + await crv + .connect(josh) + .transfer(ConvexEthMetaStrategy.address, oethUnits("1000")); + + const wethBefore = await weth.balanceOf(oethDripper.address); + + // prettier-ignore + await oethHarvester + .connect(josh)["harvestAndSwap(address)"](ConvexEthMetaStrategy.address); + + const wethDiff = (await weth.balanceOf(oethDripper.address)).sub( + wethBefore + ); + await oethVault.connect(josh).rebase(); + + await expect(wethDiff).to.be.gte(oethUnits("0.3")); + }); +}); + +async function mintTest(fixture, user, asset, amount = "3") { + const { oethVault, oeth, ConvexEthMetaStrategy, cvxRewardPool } = fixture; + + const unitAmount = await units(amount, asset); + + await oethVault.connect(user).rebase(); + await oethVault.connect(user).allocate(); + + const currentSupply = await oeth.totalSupply(); + const currentBalance = await oeth.connect(user).balanceOf(user.address); + + const currentRewardPoolBalance = await cvxRewardPool + .connect(user) + .balanceOf(ConvexEthMetaStrategy.address); + + // Mint OUSD w/ asset + await asset.connect(user).approve(oethVault.address, unitAmount); + await oethVault.connect(user).mint(asset.address, unitAmount, 0); + await oethVault.connect(user).allocate(); + + // Ensure user has correct balance (w/ 1% slippage tolerance) + const newBalance = await oeth.connect(user).balanceOf(user.address); + const balanceDiff = newBalance.sub(currentBalance); + expect(balanceDiff).to.approxEqualTolerance(oethUnits(amount), 1); + + // Supply checks + const newSupply = await oeth.totalSupply(); + const supplyDiff = newSupply.sub(currentSupply); + + expect(supplyDiff).to.approxEqualTolerance(oethUnits(amount).mul(2), 5); + + //Ensure some LP tokens got staked under OUSDMetaStrategy address + const newRewardPoolBalance = await cvxRewardPool + .connect(user) + .balanceOf(ConvexEthMetaStrategy.address); + const rewardPoolBalanceDiff = newRewardPoolBalance.sub( + currentRewardPoolBalance + ); + + // multiplied by 2 because the strategy prints corresponding amount of OETH and + // deploys it in the pool + expect(rewardPoolBalanceDiff).to.approxEqualTolerance( + oethUnits(amount).mul(2), + 1 + ); +} diff --git a/contracts/test/vault/compound.js b/contracts/test/vault/compound.js index 76013ef933..c3ecc26564 100644 --- a/contracts/test/vault/compound.js +++ b/contracts/test/vault/compound.js @@ -689,7 +689,7 @@ describe("Vault with Compound strategy", function () { const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize([comp.address], [usdt.address]); + await mockUniswapRouter.initialize([comp.address], [usdt.address]); const compAmount = utils.parseUnits("100", 18); await comp.connect(governor).mint(compAmount); @@ -749,7 +749,7 @@ describe("Vault with Compound strategy", function () { const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize([comp.address], [usdt.address]); + await mockUniswapRouter.initialize([comp.address], [usdt.address]); // Mock router gives 1:1, if we set this to something high there will be // too much slippage @@ -797,7 +797,7 @@ describe("Vault with Compound strategy", function () { const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize([comp.address], [usdt.address]); + await mockUniswapRouter.initialize([comp.address], [usdt.address]); const compAmount = utils.parseUnits("100", 18); await comp.connect(governor).mint(compAmount); diff --git a/contracts/test/vault/harvester.js b/contracts/test/vault/harvester.js index 234df8245c..e3754a2a84 100644 --- a/contracts/test/vault/harvester.js +++ b/contracts/test/vault/harvester.js @@ -274,7 +274,7 @@ describe("Harvester", function () { await sendRewardsToCompStrategy("10", governor, compoundStrategy, comp); const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize([comp.address], [usdt.address]); + await mockUniswapRouter.initialize([comp.address], [usdt.address]); await usdt .connect(josh) .transfer(mockUniswapRouter.address, usdtUnits("100")); @@ -410,7 +410,7 @@ describe("Harvester", function () { await sendRewardsToCompStrategy("10", governor, compoundStrategy, comp); const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize([comp.address], [usdt.address]); + await mockUniswapRouter.initialize([comp.address], [usdt.address]); await usdt .connect(josh) .transfer(mockUniswapRouter.address, usdtUnits("100")); @@ -494,7 +494,7 @@ describe("Harvester", function () { await sendRewardsToCompStrategy("10", governor, compoundStrategy, comp); const mockUniswapRouter = await ethers.getContract("MockUniswapRouter"); - mockUniswapRouter.initialize([comp.address], [usdt.address]); + await mockUniswapRouter.initialize([comp.address], [usdt.address]); await usdt .connect(josh) .transfer(mockUniswapRouter.address, usdtUnits("100")); diff --git a/contracts/test/vault/vault.fork-test.js b/contracts/test/vault/vault.fork-test.js index 1d0507c322..9888e372be 100644 --- a/contracts/test/vault/vault.fork-test.js +++ b/contracts/test/vault/vault.fork-test.js @@ -224,7 +224,12 @@ forkOnlyDescribe("ForkTest: Vault", function () { }); describe("Oracle", () => { - it("Should have correct Price Oracle address set", async () => { + /* NOTICE: update once the address is the updated on the mainnet. + * the fork tests require the 052 deploy to run in order to be + * compatible with the latest codebase -> which is not yet deployed to + * OUSD mainnet. + */ + it.skip("Should have correct Price Oracle address set", async () => { const { vault } = fixture; expect(await vault.priceProvider()).to.equal( "0x7533365d1b0D95380bc4e94D0bdEF5173E43f954" diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index d9868aee69..2738248dd1 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -7,6 +7,7 @@ const addresses = {}; // Utility addresses addresses.zero = "0x0000000000000000000000000000000000000000"; addresses.dead = "0x0000000000000000000000000000000000000001"; +addresses.ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; addresses.mainnet = {}; @@ -50,7 +51,6 @@ addresses.mainnet.ThreePoolToken = "0x6c3f90f043a72fa612cbac8115ee7e52bde6e490"; addresses.mainnet.ThreePoolGauge = "0xbFcF63294aD7105dEa65aA58F8AE5BE2D9d0952A"; // CVX addresses.mainnet.CVX = "0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b"; -addresses.mainnet.CRVRewardsPool = "0x689440f2ff927e1f24c72f1087e1faf471ece1c8"; addresses.mainnet.CVXBooster = "0xF403C135812408BFbE8713b5A23a04b3D48AAE31"; addresses.mainnet.CVXRewardsPool = "0x7D536a737C13561e0D2Decf1152a653B4e615158"; // Open Oracle @@ -107,7 +107,7 @@ addresses.mainnet.chainlinkcbETH_ETH = "0xF017fcB346A1885194689bA23Eff2fE6fA5C483b"; // WETH Token -addresses.mainnet.WETH = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; +addresses.mainnet.WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; // Deployed OUSD contracts addresses.mainnet.Guardian = "0xbe2AB3d3d8F6a32b96414ebbd865dBD276d3d899"; // ERC 20 owner multisig. addresses.mainnet.VaultProxy = "0xe75d77b1865ae93c7eaa3040b038d7aa7bc02f70"; diff --git a/contracts/utils/constants.js b/contracts/utils/constants.js index 7a9e208767..aa2508ea34 100644 --- a/contracts/utils/constants.js +++ b/contracts/utils/constants.js @@ -1,6 +1,7 @@ const threeCRVPid = 9; const metapoolLPCRVPid = 56; const lusdMetapoolLPCRVPid = 33; +const oethPoolLpPID = 174; const { BigNumber } = require("ethers"); const MAX_UINT256 = BigNumber.from( "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" @@ -10,6 +11,7 @@ module.exports = { threeCRVPid, metapoolLPCRVPid, lusdMetapoolLPCRVPid, + oethPoolLpPID, MAX_UINT256, }; diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index 37fe103a76..184ccfcf3e 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -130,6 +130,25 @@ const impersonateGuardian = async (optGuardianAddr = null) => { log(`Impersonated Guardian at ${guardianAddr}`); }; +const impersonateAccount = async (address) => { + if (!isFork) { + throw new Error("impersonateAccount only works on Fork"); + } + const { findBestMainnetTokenHolder } = require("../utils/funding"); + + const bestSigner = await findBestMainnetTokenHolder(null, hre); + await bestSigner.sendTransaction({ + to: address, + value: utils.parseEther("100"), + }); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [address], + }); + log(`Impersonated Account at ${address}`); +}; + /** * Execute a proposal on local test network (including on Fork). * @@ -935,7 +954,7 @@ function deploymentWithGuardianGovernor(opts, fn) { if (isMainnet) { // On Mainnet, only propose. The enqueue and execution are handled manually via multi-sig. - log( + console.log( "Manually create the 5/8 multisig batch transaction with details:", proposal ); @@ -944,6 +963,7 @@ function deploymentWithGuardianGovernor(opts, fn) { await impersonateGuardian(guardianAddr); const sGuardian = await ethers.provider.getSigner(guardianAddr); + console.log("guardianAddr", guardianAddr); const guardianActions = []; for (const action of proposal.actions) { @@ -1007,6 +1027,7 @@ module.exports = { deployWithConfirmation, withConfirmation, impersonateGuardian, + impersonateAccount, executeProposal, executeProposalOnFork, sendProposal, diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 27033f6b19..d075478fc6 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -9556,9 +9556,9 @@ underscore@1.9.1: integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== undici@^5.14.0: - version "5.22.1" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.1.tgz#877d512effef2ac8be65e695f3586922e1a57d7b" - integrity sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw== + version "5.21.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.21.2.tgz#329f628aaea3f1539a28b9325dccc72097d29acd" + integrity sha512-f6pTQ9RF4DQtwoWSaC42P/NKlUjvezVvd9r155ohqkwFNRyBKM3f3pcty3ouusefNRyM25XhIQEbeQ46sZDJfQ== dependencies: busboy "^1.6.0"