diff --git a/contracts/contracts/interfaces/IChildLiquidityGaugeFactory.sol b/contracts/contracts/interfaces/IChildLiquidityGaugeFactory.sol new file mode 100644 index 0000000000..47616a96bb --- /dev/null +++ b/contracts/contracts/interfaces/IChildLiquidityGaugeFactory.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface IChildLiquidityGaugeFactory { + event DeployedGauge( + address indexed _implementation, + address indexed _lp_token, + address indexed _deployer, + bytes32 _salt, + address _gauge + ); + event Minted( + address indexed _user, + address indexed _gauge, + uint256 _new_total + ); + event TransferOwnership(address _old_owner, address _new_owner); + event UpdateCallProxy(address _old_call_proxy, address _new_call_proxy); + event UpdateImplementation( + address _old_implementation, + address _new_implementation + ); + event UpdateManager(address _manager); + event UpdateMirrored(address indexed _gauge, bool _mirrored); + event UpdateRoot(address _factory, address _implementation); + event UpdateVotingEscrow( + address _old_voting_escrow, + address _new_voting_escrow + ); + + function accept_transfer_ownership() external; + + function call_proxy() external view returns (address); + + function commit_transfer_ownership(address _future_owner) external; + + function crv() external view returns (address); + + function deploy_gauge(address _lp_token, bytes32 _salt) + external + returns (address); + + function deploy_gauge( + address _lp_token, + bytes32 _salt, + address _manager + ) external returns (address); + + function future_owner() external view returns (address); + + function gauge_data(address arg0) external view returns (uint256); + + function get_gauge(uint256 arg0) external view returns (address); + + function get_gauge_count() external view returns (uint256); + + function get_gauge_from_lp_token(address arg0) + external + view + returns (address); + + function get_implementation() external view returns (address); + + function is_mirrored(address _gauge) external view returns (bool); + + function is_valid_gauge(address _gauge) external view returns (bool); + + function last_request(address _gauge) external view returns (uint256); + + function manager() external view returns (address); + + function mint(address _gauge) external; + + function mint_many(address[32] memory _gauges) external; + + function minted(address arg0, address arg1) external view returns (uint256); + + function owner() external view returns (address); + + function root_factory() external view returns (address); + + function root_implementation() external view returns (address); + + function set_call_proxy(address _new_call_proxy) external; + + function set_crv(address _crv) external; + + function set_implementation(address _implementation) external; + + function set_manager(address _new_manager) external; + + function set_mirrored(address _gauge, bool _mirrored) external; + + function set_root(address _factory, address _implementation) external; + + function set_voting_escrow(address _voting_escrow) external; + + function version() external view returns (string memory); + + function voting_escrow() external view returns (address); +} diff --git a/contracts/contracts/interfaces/ICurveStableSwapNG.sol b/contracts/contracts/interfaces/ICurveStableSwapNG.sol new file mode 100644 index 0000000000..95dbe29834 --- /dev/null +++ b/contracts/contracts/interfaces/ICurveStableSwapNG.sol @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ICurveStableSwapNG { + event AddLiquidity( + address indexed provider, + uint256[] token_amounts, + uint256[] fees, + uint256 invariant, + uint256 token_supply + ); + event ApplyNewFee(uint256 fee, uint256 offpeg_fee_multiplier); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + event RampA( + uint256 old_A, + uint256 new_A, + uint256 initial_time, + uint256 future_time + ); + event RemoveLiquidity( + address indexed provider, + uint256[] token_amounts, + uint256[] fees, + uint256 token_supply + ); + event RemoveLiquidityImbalance( + address indexed provider, + uint256[] token_amounts, + uint256[] fees, + uint256 invariant, + uint256 token_supply + ); + event RemoveLiquidityOne( + address indexed provider, + int128 token_id, + uint256 token_amount, + uint256 coin_amount, + uint256 token_supply + ); + event SetNewMATime(uint256 ma_exp_time, uint256 D_ma_time); + event StopRampA(uint256 A, uint256 t); + event TokenExchange( + address indexed buyer, + int128 sold_id, + uint256 tokens_sold, + int128 bought_id, + uint256 tokens_bought + ); + event TokenExchangeUnderlying( + 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 D_ma_time() external view returns (uint256); + + function D_oracle() external view returns (uint256); + + function N_COINS() external view returns (uint256); + + function add_liquidity(uint256[] memory _amounts, uint256 _min_mint_amount) + external + returns (uint256); + + function add_liquidity( + uint256[] memory _amounts, + uint256 _min_mint_amount, + address _receiver + ) external returns (uint256); + + function admin_balances(uint256 arg0) external view returns (uint256); + + function admin_fee() external view returns (uint256); + + function allowance(address arg0, address arg1) + external + view + returns (uint256); + + function approve(address _spender, uint256 _value) external returns (bool); + + function balanceOf(address arg0) external view returns (uint256); + + function balances(uint256 i) external view returns (uint256); + + function calc_token_amount(uint256[] 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 decimals() external view returns (uint8); + + function dynamic_fee(int128 i, int128 j) external view returns (uint256); + + function ema_price(uint256 i) external view returns (uint256); + + function exchange( + int128 i, + int128 j, + uint256 _dx, + uint256 _min_dy + ) external returns (uint256); + + function exchange( + int128 i, + int128 j, + uint256 _dx, + uint256 _min_dy, + address _receiver + ) external returns (uint256); + + function exchange_received( + int128 i, + int128 j, + uint256 _dx, + uint256 _min_dy + ) external returns (uint256); + + function exchange_received( + int128 i, + int128 j, + uint256 _dx, + uint256 _min_dy, + address _receiver + ) external returns (uint256); + + function fee() external view returns (uint256); + + function future_A() external view returns (uint256); + + function future_A_time() external view returns (uint256); + + function get_balances() external view returns (uint256[] memory); + + function get_dx( + int128 i, + int128 j, + uint256 dy + ) external view returns (uint256); + + function get_dy( + int128 i, + int128 j, + uint256 dx + ) external view returns (uint256); + + function get_p(uint256 i) 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 last_price(uint256 i) 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 offpeg_fee_multiplier() 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(uint256 i) external view returns (uint256); + + function ramp_A(uint256 _future_A, uint256 _future_time) external; + + function remove_liquidity( + uint256 _burn_amount, + uint256[] memory _min_amounts + ) external returns (uint256[] memory); + + function remove_liquidity( + uint256 _burn_amount, + uint256[] memory _min_amounts, + address _receiver + ) external returns (uint256[] memory); + + function remove_liquidity( + uint256 _burn_amount, + uint256[] memory _min_amounts, + address _receiver, + bool _claim_admin_fees + ) external returns (uint256[] memory); + + function remove_liquidity_imbalance( + uint256[] memory _amounts, + uint256 _max_burn_amount + ) external returns (uint256); + + function remove_liquidity_imbalance( + uint256[] 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 salt() external view returns (bytes32); + + function set_ma_exp_time(uint256 _ma_exp_time, uint256 _D_ma_time) external; + + function set_new_fee(uint256 _new_fee, uint256 _new_offpeg_fee_multiplier) + external; + + function stop_ramp_A() external; + + function stored_rates() external view returns (uint256[] memory); + + 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/contracts/interfaces/ICurveXChainLiquidityGauge.sol b/contracts/contracts/interfaces/ICurveXChainLiquidityGauge.sol new file mode 100644 index 0000000000..debb7886e2 --- /dev/null +++ b/contracts/contracts/interfaces/ICurveXChainLiquidityGauge.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +interface ICurveXChainLiquidityGauge { + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); + event Deposit(address indexed provider, uint256 value); + event SetGaugeManager(address _gauge_manager); + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event UpdateLiquidityLimit( + address indexed user, + uint256 original_balance, + uint256 original_supply, + uint256 working_balance, + uint256 working_supply + ); + event Withdraw(address indexed provider, uint256 value); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function add_reward(address _reward_token, address _distributor) external; + + function allowance(address arg0, address arg1) + external + view + returns (uint256); + + function approve(address _spender, uint256 _value) external returns (bool); + + function balanceOf(address arg0) external view returns (uint256); + + function claim_rewards() external; + + function claim_rewards(address _addr) external; + + function claim_rewards(address _addr, address _receiver) external; + + function claimable_reward(address _user, address _reward_token) + external + view + returns (uint256); + + function claimable_tokens(address addr) external returns (uint256); + + function claimed_reward(address _addr, address _token) + external + view + returns (uint256); + + function decimals() external view returns (uint256); + + function decreaseAllowance(address _spender, uint256 _subtracted_value) + external + returns (bool); + + function deposit(uint256 _value) external; + + function deposit(uint256 _value, address _addr) external; + + function deposit( + uint256 _value, + address _addr, + bool _claim_rewards + ) external; + + function deposit_reward_token(address _reward_token, uint256 _amount) + external; + + function deposit_reward_token( + address _reward_token, + uint256 _amount, + uint256 _epoch + ) external; + + function factory() external view returns (address); + + function increaseAllowance(address _spender, uint256 _added_value) + external + returns (bool); + + function inflation_rate(uint256 arg0) external view returns (uint256); + + function initialize( + address _lp_token, + address _root, + address _manager + ) external; + + function integrate_checkpoint() external view returns (uint256); + + function integrate_checkpoint_of(address arg0) + external + view + returns (uint256); + + function integrate_fraction(address arg0) external view returns (uint256); + + function integrate_inv_supply(int128 arg0) external view returns (uint256); + + function integrate_inv_supply_of(address arg0) + external + view + returns (uint256); + + function is_killed() external view returns (bool); + + function lp_token() external view returns (address); + + function manager() external view returns (address); + + function name() external view returns (string memory); + + function nonces(address arg0) external view returns (uint256); + + function period() external view returns (int128); + + function period_timestamp(int128 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 recover_remaining(address _reward_token) external; + + function reward_count() external view returns (uint256); + + function reward_integral_for(address arg0, address arg1) + external + view + returns (uint256); + + function reward_remaining(address arg0) external view returns (uint256); + + function reward_tokens(uint256 arg0) external view returns (address); + + function rewards_receiver(address arg0) external view returns (address); + + function root_gauge() external view returns (address); + + function set_gauge_manager(address _gauge_manager) external; + + function set_killed(bool _is_killed) external; + + function set_manager(address _gauge_manager) external; + + function set_reward_distributor(address _reward_token, address _distributor) + external; + + function set_rewards_receiver(address _receiver) external; + + function set_root_gauge(address _root) 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 update_voting_escrow() external; + + function user_checkpoint(address addr) external returns (bool); + + function version() external view returns (string memory); + + function voting_escrow() external view returns (address); + + function withdraw(uint256 _value) external; + + function withdraw(uint256 _value, bool _claim_rewards) external; + + function withdraw( + uint256 _value, + bool _claim_rewards, + address _receiver + ) external; + + function working_balances(address arg0) external view returns (uint256); + + function working_supply() external view returns (uint256); +} diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 583df1fd3c..93f2c24869 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -365,3 +365,5 @@ contract MorphoGauntletPrimeUSDTStrategyProxy is contract OETHFixedRateDripperProxy is InitializeGovernedUpgradeabilityProxy { } + +contract OETHBaseCurveAMOProxy is InitializeGovernedUpgradeabilityProxy {} diff --git a/contracts/contracts/strategies/BaseCurveAMOStrategy.sol b/contracts/contracts/strategies/BaseCurveAMOStrategy.sol new file mode 100644 index 0000000000..3f88852891 --- /dev/null +++ b/contracts/contracts/strategies/BaseCurveAMOStrategy.sol @@ -0,0 +1,618 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title Curve Automated Market Maker (AMO) Strategy + * @notice AMO strategy for the Curve OETH/WETH pool + * @author Origin Protocol Inc + */ +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import { IERC20, InitializableAbstractStrategy } from "../utils/InitializableAbstractStrategy.sol"; +import { StableMath } from "../utils/StableMath.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import { IWETH9 } from "../interfaces/IWETH9.sol"; +import { ICurveStableSwapNG } from "../interfaces/ICurveStableSwapNG.sol"; +import { ICurveXChainLiquidityGauge } from "../interfaces/ICurveXChainLiquidityGauge.sol"; +import { IChildLiquidityGaugeFactory } from "../interfaces/IChildLiquidityGaugeFactory.sol"; + +contract BaseCurveAMOStrategy is InitializableAbstractStrategy { + using StableMath for uint256; + + /** + * @dev a threshold under which the contract no longer allows for the protocol to manually rebalance. + * Guarding against a strategist / guardian being taken over and with multiple transactions + * draining the protocol funds. + */ + uint256 public constant SOLVENCY_THRESHOLD = 0.998 ether; + + uint256 public constant MAX_SLIPPAGE = 1e16; // 1%, same as the Curve UI + + // New immutable variables that must be set in the constructor + IChildLiquidityGaugeFactory public immutable gaugeFactory; + ICurveXChainLiquidityGauge public immutable gauge; + ICurveStableSwapNG public immutable curvePool; + IERC20 public immutable lpToken; + IERC20 public immutable oeth; + IWETH9 public immutable weth; + + // Ordered list of pool assets + uint128 public constant oethCoinIndex = 1; + uint128 public constant ethCoinIndex = 0; + + /** + * @dev Verifies that the caller is the Strategist. + */ + modifier onlyStrategist() { + require( + msg.sender == IVault(vaultAddress).strategistAddr(), + "Caller is not the Strategist" + ); + _; + } + + /** + * @dev Checks the Curve pool's balances have improved and the balances + * have not tipped to the other side. + * This modifier only works on functions that do a single sided add or remove. + * The standard deposit function adds to both sides of the pool in a way that + * the pool's balance is not worsened. + * Withdrawals are proportional so doesn't change the pools asset balance. + */ + modifier improvePoolBalance() { + // Get the asset and OToken balances in the Curve pool + uint256[] memory balancesBefore = curvePool.get_balances(); + // diff = ETH balance - OETH balance + int256 diffBefore = int256(balancesBefore[ethCoinIndex]) - + int256(balancesBefore[oethCoinIndex]); + + _; + + // Get the asset and OToken balances in the Curve pool + uint256[] memory balancesAfter = curvePool.get_balances(); + // diff = ETH balance - OETH balance + int256 diffAfter = int256(balancesAfter[ethCoinIndex]) - + int256(balancesAfter[oethCoinIndex]); + + if (diffBefore <= 0) { + // If the pool was originally imbalanced in favor of OETH, then + // we want to check that the pool is now more balanced + require(diffAfter <= 0, "OTokens overshot peg"); + require(diffBefore < diffAfter, "OTokens balance worse"); + } + if (diffBefore >= 0) { + // If the pool was originally imbalanced in favor of ETH, then + // we want to check that the pool is now more balanced + require(diffAfter >= 0, "Assets overshot peg"); + require(diffAfter < diffBefore, "Assets balance worse"); + } + } + + constructor( + BaseStrategyConfig memory _baseConfig, + address _oeth, + address _weth, + address _gauge, + address _gaugeFactory + ) InitializableAbstractStrategy(_baseConfig) { + lpToken = IERC20(_baseConfig.platformAddress); + curvePool = ICurveStableSwapNG(_baseConfig.platformAddress); + + oeth = IERC20(_oeth); + weth = IWETH9(_weth); + gauge = ICurveXChainLiquidityGauge(_gauge); + gaugeFactory = IChildLiquidityGaugeFactory(_gaugeFactory); + } + + /** + * 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 + * @param _assets Addresses of supported assets. eg WETH + */ + function initialize( + address[] calldata _rewardTokenAddresses, // CRV + address[] calldata _assets // WETH + ) external onlyGovernor initializer { + require(_assets.length == 1, "Must have exactly one asset"); + require(_assets[0] == address(weth), "Asset not WETH"); + + address[] memory pTokens = new address[](1); + pTokens[0] = address(curvePool); + + InitializableAbstractStrategy._initialize( + _rewardTokenAddresses, + _assets, + pTokens + ); + + _approveBase(); + } + + /*************************************** + Deposit + ****************************************/ + + /** + * @notice Deposit WETH into the Curve pool + * @param _weth Address of Wrapped ETH (WETH) contract. + * @param _amount Amount of WETH to deposit. + */ + function deposit(address _weth, uint256 _amount) + external + override + onlyVault + nonReentrant + { + _deposit(_weth, _amount); + } + + function _deposit(address _weth, uint256 _wethAmount) internal { + require(_wethAmount > 0, "Must deposit something"); + require(_weth == address(weth), "Can only deposit WETH"); + + emit Deposit(_weth, address(lpToken), _wethAmount); + + // Get the asset and OToken balances in the Curve pool + uint256[] memory balances = curvePool.get_balances(); + // safe to cast since min value is at least 0 + uint256 oethToAdd = uint256( + _max( + 0, + int256(balances[ethCoinIndex]) + + int256(_wethAmount) - + int256(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); + + emit Deposit(address(oeth), address(lpToken), oethToAdd); + + uint256[] memory _amounts = new uint256[](2); + _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 the Curve pool + uint256 lpDeposited = curvePool.add_liquidity(_amounts, minMintAmount); + + // Deposit the Curve pool's LP tokens into the Curve gauge + gauge.deposit(lpDeposited); + + // Ensure solvency of the vault + _solvencyAssert(); + } + + /** + * @notice Deposit the strategy's entire balance of WETH into the Curve pool + */ + function depositAll() external override onlyVault nonReentrant { + uint256 balance = weth.balanceOf(address(this)); + if (balance > 0) { + _deposit(address(weth), balance); + } + } + + /*************************************** + Withdraw + ****************************************/ + + /** + * @notice Withdraw ETH and OETH from the Curve pool, burn the OETH, + * convert the ETH to WETH and transfer to the recipient. + * @param _recipient Address to receive withdrawn asset which is normally the Vault. + * @param _weth Address of the Wrapped ETH (WETH) contract. + * @param _amount Amount of WETH to withdraw. + */ + function withdraw( + address _recipient, + address _weth, + uint256 _amount + ) external override onlyVault nonReentrant { + require(_amount > 0, "Must withdraw something"); + 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[] memory _minWithdrawalAmounts = new uint256[](2); + _minWithdrawalAmounts[ethCoinIndex] = _amount; + // slither-disable-next-line unused-return + curvePool.remove_liquidity(requiredLpTokens, _minWithdrawalAmounts); + + // Burn all the removed OETH and any that was left in the strategy + uint256 oethToBurn = oeth.balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(oethToBurn); + + emit Withdrawal(address(oeth), address(lpToken), oethToBurn); + + // Transfer WETH to the recipient + require( + weth.transfer(_recipient, _amount), + "Transfer of WETH not successful" + ); + + // Ensure solvency of the vault + _solvencyAssert(); + } + + 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; + } + + /** + * @notice Remove all ETH and OETH from the Curve pool, burn the OETH, + * convert the ETH to WETH and transfer to the Vault contract. + */ + function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + uint256 gaugeTokens = gauge.balanceOf(address(this)); + _lpWithdraw(gaugeTokens); + + // Withdraws are proportional to assets held by 3Pool + uint256[] memory minWithdrawAmounts = new uint256[](2); + + // Remove liquidity + // slither-disable-next-line unused-return + curvePool.remove_liquidity( + lpToken.balanceOf(address(this)), + minWithdrawAmounts + ); + + // Burn all OETH + uint256 oethToBurn = oeth.balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(oethToBurn); + + // Get the strategy contract's WETH balance. + // This includes all that was removed from the Curve pool and + // any ether that was sitting in the strategy contract before the removal. + uint256 ethBalance = weth.balanceOf(address(this)); + require( + weth.transfer(vaultAddress, ethBalance), + "Transfer of WETH not successful" + ); + + emit Withdrawal(address(weth), address(lpToken), ethBalance); + emit Withdrawal(address(oeth), address(lpToken), oethToBurn); + } + + /*************************************** + Curve pool Rebalancing + ****************************************/ + + /** + * @notice Mint OTokens and one-sided add to the Curve pool. + * This is used when the Curve pool does not have enough OTokens and too many ETH. + * The OToken/Asset, eg OETH/ETH, price with increase. + * The amount of assets in the vault is unchanged. + * The total supply of OTokens is increased. + * The asset value of the strategy and vault is increased. + * @param _oTokens The amount of OTokens to be minted and added to the pool. + */ + function mintAndAddOTokens(uint256 _oTokens) + external + onlyStrategist + nonReentrant + improvePoolBalance + { + IVault(vaultAddress).mintForStrategy(_oTokens); + + uint256[] memory amounts = new uint256[](2); + amounts[oethCoinIndex] = _oTokens; + + // Convert OETH to Curve pool LP tokens + uint256 valueInLpTokens = (_oTokens).divPrecisely( + curvePool.get_virtual_price() + ); + // Apply slippage to LP tokens + uint256 minMintAmount = valueInLpTokens.mulTruncate( + uint256(1e18) - MAX_SLIPPAGE + ); + + // Add the minted OTokens to the Curve pool + uint256 lpDeposited = curvePool.add_liquidity(amounts, minMintAmount); + + // Deposit the Curve pool LP tokens to the Curve gauge + gauge.deposit(lpDeposited); + + // Ensure solvency of the vault + _solvencyAssert(); + + emit Deposit(address(oeth), address(lpToken), _oTokens); + } + + /** + * @notice One-sided remove of OTokens from the Curve pool which are then burned. + * This is used when the Curve pool has too many OTokens and not enough ETH. + * The amount of assets in the vault is unchanged. + * The total supply of OTokens is reduced. + * The asset value of the strategy and vault is reduced. + * @param _lpTokens The amount of Curve pool LP tokens to be burned for OTokens. + */ + function removeAndBurnOTokens(uint256 _lpTokens) + external + onlyStrategist + nonReentrant + improvePoolBalance + { + // Withdraw Curve pool LP tokens from Convex and remove OTokens from the Curve pool + uint256 oethToBurn = _withdrawAndRemoveFromPool( + _lpTokens, + oethCoinIndex + ); + + // The vault burns the OTokens from this strategy + IVault(vaultAddress).burnForStrategy(oethToBurn); + + // Ensure solvency of the vault + _solvencyAssert(); + + emit Withdrawal(address(oeth), address(lpToken), oethToBurn); + } + + /** + * @notice One-sided remove of ETH from the Curve pool, convert to WETH + * and transfer to the vault. + * This is used when the Curve pool does not have enough OTokens and too many ETH. + * The OToken/Asset, eg OETH/ETH, price with decrease. + * The amount of assets in the vault increases. + * The total supply of OTokens does not change. + * The asset value of the strategy reduces. + * The asset value of the vault should be close to the same. + * @param _lpTokens The amount of Curve pool LP tokens to be burned for ETH. + * @dev Curve pool LP tokens is used rather than WETH assets as Curve does not + * have a way to accurately calculate the amount of LP tokens for a required + * amount of ETH. Curve's `calc_token_amount` functioun does not include fees. + * A 3rd party libary can be used that takes into account the fees, but this + * is a gas intensive process. It's easier for the trusted strategist to + * caclulate the amount of Curve pool LP tokens required off-chain. + */ + function removeOnlyAssets(uint256 _lpTokens) + external + onlyStrategist + nonReentrant + improvePoolBalance + { + // Withdraw Curve pool LP tokens from Curve gauge and remove ETH from the Curve pool + uint256 ethAmount = _withdrawAndRemoveFromPool(_lpTokens, ethCoinIndex); + + // Transfer WETH to the vault + require( + weth.transfer(vaultAddress, ethAmount), + "Transfer of WETH not successful" + ); + + // Ensure solvency of the vault + _solvencyAssert(); + + emit Withdrawal(address(weth), address(lpToken), ethAmount); + } + + /** + * @dev Remove Curve pool LP tokens from the Convex pool and + * do a one-sided remove of ETH or OETH from the Curve pool. + * @param _lpTokens The amount of Curve pool LP tokens to be removed from the Convex pool. + * @param coinIndex The index of the coin to be removed from the Curve pool. 0 = ETH, 1 = OETH. + * @return coinsRemoved The amount of ETH or OETH removed from the Curve pool. + */ + function _withdrawAndRemoveFromPool(uint256 _lpTokens, uint128 coinIndex) + internal + returns (uint256 coinsRemoved) + { + // Withdraw Curve pool LP tokens from Curve gauge + _lpWithdraw(_lpTokens); + + // Convert Curve pool LP tokens to ETH value + uint256 valueInEth = _lpTokens.mulTruncate( + curvePool.get_virtual_price() + ); + // Apply slippage to ETH value + uint256 minAmount = valueInEth.mulTruncate( + uint256(1e18) - MAX_SLIPPAGE + ); + + // Remove just the ETH from the Curve pool + coinsRemoved = curvePool.remove_liquidity_one_coin( + _lpTokens, + int128(coinIndex), + minAmount, + address(this) + ); + } + + /** + * Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can + * keep rebalancing the pool in both directions making the protocol lose a tiny amount of + * funds each time. + * + * Protocol must be at least SOLVENCY_THRESHOLD (99,8 %) backed in order for the rebalances to + * function. + */ + function _solvencyAssert() internal view { + uint256 _totalVaultValue = IVault(vaultAddress).totalValue(); + uint256 _totalOethbSupply = oeth.totalSupply(); + + if ( + _totalVaultValue.divPrecisely(_totalOethbSupply) < + SOLVENCY_THRESHOLD + ) { + revert("Protocol insolvent"); + } + } + + /*************************************** + Assets and Rewards + ****************************************/ + + /** + * @notice Collect accumulated CRV (and other) rewards and send to the Harvester. + */ + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + // CRV rewards flow. + //--- + // CRV inflation: + // Gauge receive CRV rewards from inflation. + // Each checkpoint on the gauge send this CRV inflation to gauge factory. + // This strategy should call mint on the gauge factory to collect the CRV rewards. + // --- + // Extra rewards: + // Calling claim_rewards on the gauge will only claim extra rewards (outside of CRV). + // --- + + // Mint CRV on Child Liquidity gauge factory + gaugeFactory.mint(address(gauge)); + // Collect extra gauge rewards (outside of CRV) + gauge.claim_rewards(); + + _collectRewardTokens(); + } + + function _lpWithdraw(uint256 _lpAmount) internal { + // withdraw lp tokens from the gauge without claiming rewards + gauge.withdraw(_lpAmount); + } + + /** + * @notice 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"); + + // WETH balance needed here for the balance check that happens from vault during depositing. + balance = weth.balanceOf(address(this)); + uint256 lpTokens = gauge.balanceOf(address(this)); + if (lpTokens > 0) { + balance += (lpTokens * curvePool.get_virtual_price()) / 1e18; + } + } + + /** + * @notice Returns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + */ + function supportsAsset(address _asset) public view override returns (bool) { + return _asset == address(weth); + } + + /*************************************** + Approvals + ****************************************/ + + /** + * @notice Approve the spending of all assets by their corresponding pool tokens, + * if for some reason is it necessary. + */ + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + { + _approveBase(); + } + + /** + * @notice Accept unwrapped WETH + */ + receive() external payable {} + + /** + * @dev Since we are unwrapping WETH before depositing it to Curve + * there is no need to set an approval for WETH on the Curve + * pool + * @param _asset Address of the asset + * @param _pToken Address of the Curve LP token + */ + // solhint-disable-next-line no-unused-vars + function _abstractSetPToken(address _asset, address _pToken) + internal + override + {} + + function _approveBase() internal { + // Approve Curve pool for OETH (required for adding liquidity) + // slither-disable-next-line unused-return + oeth.approve(platformAddress, type(uint256).max); + + // Approve Curve pool for WETH (required for adding liquidity) + // slither-disable-next-line unused-return + weth.approve(platformAddress, type(uint256).max); + + // Approve Curve gauge contract to transfer Curve pool LP tokens + // This is needed for deposits if Curve pool LP tokens into the Curve gauge. + // slither-disable-next-line unused-return + lpToken.approve(address(gauge), type(uint256).max); + } + + /** + * @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/deploy/base/022_base_curve_amo.js b/contracts/deploy/base/022_base_curve_amo.js new file mode 100644 index 0000000000..b27cc4e292 --- /dev/null +++ b/contracts/deploy/base/022_base_curve_amo.js @@ -0,0 +1,81 @@ +const { deployOnBaseWithGuardian } = require("../../utils/deploy-l2"); +const { + deployWithConfirmation, + withConfirmation, +} = require("../../utils/deploy"); +const addresses = require("../../utils/addresses"); + +module.exports = deployOnBaseWithGuardian( + { + deployName: "022_base_curve_amo", + }, + async ({ ethers }) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + // Deploy Base Curve AMO proxy + const cOETHbProxy = await ethers.getContract("OETHBaseProxy"); + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cOETHbVaultAdmin = await ethers.getContractAt( + "OETHBaseVaultAdmin", + cOETHbVaultProxy.address + ); + + const dOETHBaseCurveAMOProxy = await deployWithConfirmation( + "OETHBaseCurveAMOProxy", + [] + ); + + const cOETHBaseCurveAMOProxy = await ethers.getContract( + "OETHBaseCurveAMOProxy" + ); + + // Deploy Base Curve AMO implementation + const dOETHBaseCurveAMO = await deployWithConfirmation( + "BaseCurveAMOStrategy", + [ + [addresses.base.OETHb_WETH.pool, cOETHbVaultProxy.address], + cOETHbProxy.address, + addresses.base.WETH, + addresses.base.OETHb_WETH.gauge, + addresses.base.childLiquidityGaugeFactory, + ] + ); + const cOETHBaseCurveAMO = await ethers.getContractAt( + "BaseCurveAMOStrategy", + dOETHBaseCurveAMOProxy.address + ); + + // Initialize Base Curve AMO implementation + const initData = cOETHBaseCurveAMO.interface.encodeFunctionData( + "initialize(address[],address[])", + [[addresses.base.CRV], [addresses.base.WETH]] + ); + await withConfirmation( + // prettier-ignore + cOETHBaseCurveAMOProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dOETHBaseCurveAMO.address, + addresses.base.timelock, + initData + ) + ); + + return { + actions: [ + // Approve strategy on vault + { + contract: cOETHbVaultAdmin, + signature: "approveStrategy(address)", + args: [cOETHBaseCurveAMOProxy.address], + }, + // Add strategyb to mint whitelist + { + contract: cOETHbVaultAdmin, + signature: "addStrategyToMintWhitelist(address)", + args: [cOETHBaseCurveAMOProxy.address], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index dfbd292fb2..4397bb94fb 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -14,6 +14,9 @@ const log = require("../utils/logger")("test:fixtures-base"); const aeroSwapRouterAbi = require("./abi/aerodromeSwapRouter.json"); const aeroNonfungiblePositionManagerAbi = require("./abi/aerodromeNonfungiblePositionManager.json"); const aerodromeSugarAbi = require("./abi/aerodromeSugarHelper.json"); +const curveXChainLiquidityGaugeAbi = require("./abi/curveXChainLiquidityGauge.json"); +const curveStableSwapNGAbi = require("./abi/curveStableSwapNG.json"); +const curveChildLiquidityGaugeFactoryAbi = require("./abi/curveChildLiquidityGaugeFactory.json"); const MINTER_ROLE = "0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6"; @@ -62,7 +65,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { oethbVaultProxy.address ); - let aerodromeAmoStrategy, dripper, harvester, quoter, sugar; + let aerodromeAmoStrategy, dripper, harvester, quoter, sugar, curveAMOStrategy; if (isFork) { // Aerodrome AMO Strategy const aerodromeAmoStrategyProxy = await ethers.getContract( @@ -99,6 +102,12 @@ const defaultBaseFixture = deployments.createFixture(async () => { "FixedRateDripper", dripperProxy.address ); + + const curveAMOProxy = await ethers.getContract("OETHBaseCurveAMOProxy"); + curveAMOStrategy = await ethers.getContractAt( + "BaseCurveAMOStrategy", + curveAMOProxy.address + ); } // Bridged wOETH @@ -195,6 +204,23 @@ const defaultBaseFixture = deployments.createFixture(async () => { addresses.base.nonFungiblePositionManager ); + const curvePoolOEthbWeth = await ethers.getContractAt( + curveStableSwapNGAbi, + addresses.base.OETHb_WETH.pool + ); + + const curveGaugeOETHbWETH = await ethers.getContractAt( + curveXChainLiquidityGaugeAbi, + addresses.base.OETHb_WETH.gauge + ); + + const curveChildLiquidityGaugeFactory = await ethers.getContractAt( + curveChildLiquidityGaugeFactoryAbi, + addresses.base.childLiquidityGaugeFactory + ); + + const crv = await ethers.getContractAt(erc20Abi, addresses.base.CRV); + return { // Aerodrome aeroSwapRouter, @@ -202,6 +228,12 @@ const defaultBaseFixture = deployments.createFixture(async () => { aeroClGauge, aero, + // Curve + crv, + curvePoolOEthbWeth, + curveGaugeOETHbWETH, + curveChildLiquidityGaugeFactory, + // OETHb oethb, oethbVault, @@ -218,6 +250,7 @@ const defaultBaseFixture = deployments.createFixture(async () => { // Strategies aerodromeAmoStrategy, + curveAMOStrategy, // WETH weth, diff --git a/contracts/test/abi/curveChildLiquidityGaugeFactory.json b/contracts/test/abi/curveChildLiquidityGaugeFactory.json new file mode 100644 index 0000000000..acc771d8a8 --- /dev/null +++ b/contracts/test/abi/curveChildLiquidityGaugeFactory.json @@ -0,0 +1,633 @@ +[ + { + "name": "DeployedGauge", + "inputs": [ + { + "name": "_implementation", + "type": "address", + "indexed": true + }, + { + "name": "_lp_token", + "type": "address", + "indexed": true + }, + { + "name": "_deployer", + "type": "address", + "indexed": true + }, + { + "name": "_salt", + "type": "bytes32", + "indexed": false + }, + { + "name": "_gauge", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "Minted", + "inputs": [ + { + "name": "_user", + "type": "address", + "indexed": true + }, + { + "name": "_gauge", + "type": "address", + "indexed": true + }, + { + "name": "_new_total", + "type": "uint256", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateImplementation", + "inputs": [ + { + "name": "_old_implementation", + "type": "address", + "indexed": false + }, + { + "name": "_new_implementation", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateVotingEscrow", + "inputs": [ + { + "name": "_old_voting_escrow", + "type": "address", + "indexed": false + }, + { + "name": "_new_voting_escrow", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateRoot", + "inputs": [ + { + "name": "_factory", + "type": "address", + "indexed": false + }, + { + "name": "_implementation", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateManager", + "inputs": [ + { + "name": "_manager", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateCallProxy", + "inputs": [ + { + "name": "_old_call_proxy", + "type": "address", + "indexed": false + }, + { + "name": "_new_call_proxy", + "type": "address", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "UpdateMirrored", + "inputs": [ + { + "name": "_gauge", + "type": "address", + "indexed": true + }, + { + "name": "_mirrored", + "type": "bool", + "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": "_call_proxy", + "type": "address" + }, + { + "name": "_root_factory", + "type": "address" + }, + { + "name": "_root_impl", + "type": "address" + }, + { + "name": "_crv", + "type": "address" + }, + { + "name": "_owner", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "mint", + "inputs": [ + { + "name": "_gauge", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "mint_many", + "inputs": [ + { + "name": "_gauges", + "type": "address[32]" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + { + "name": "_lp_token", + "type": "address" + }, + { + "name": "_salt", + "type": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + { + "name": "_lp_token", + "type": "address" + }, + { + "name": "_salt", + "type": "bytes32" + }, + { + "name": "_manager", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_crv", + "inputs": [ + { + "name": "_crv", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_root", + "inputs": [ + { + "name": "_factory", + "type": "address" + }, + { + "name": "_implementation", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_voting_escrow", + "inputs": [ + { + "name": "_voting_escrow", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_implementation", + "inputs": [ + { + "name": "_implementation", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_mirrored", + "inputs": [ + { + "name": "_gauge", + "type": "address" + }, + { + "name": "_mirrored", + "type": "bool" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_call_proxy", + "inputs": [ + { + "name": "_new_call_proxy", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_manager", + "inputs": [ + { + "name": "_new_manager", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "commit_transfer_ownership", + "inputs": [ + { + "name": "_future_owner", + "type": "address" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "accept_transfer_ownership", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "is_valid_gauge", + "inputs": [ + { + "name": "_gauge", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "is_mirrored", + "inputs": [ + { + "name": "_gauge", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "last_request", + "inputs": [ + { + "name": "_gauge", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "version", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "crv", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_implementation", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "voting_escrow", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "future_owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "manager", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "root_factory", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "root_implementation", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "call_proxy", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "gauge_data", + "inputs": [ + { + "name": "arg0", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "minted", + "inputs": [ + { + "name": "arg0", + "type": "address" + }, + { + "name": "arg1", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_gauge_from_lp_token", + "inputs": [ + { + "name": "arg0", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_gauge_count", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_gauge", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + } +] \ No newline at end of file diff --git a/contracts/test/abi/curveStableSwapNG.json b/contracts/test/abi/curveStableSwapNG.json new file mode 100644 index 0000000000..6f07d65211 --- /dev/null +++ b/contracts/test/abi/curveStableSwapNG.json @@ -0,0 +1,1452 @@ +[ + { + "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": "TokenExchangeUnderlying", + "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[]", + "indexed": false + }, + { + "name": "fees", + "type": "uint256[]", + "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[]", + "indexed": false + }, + { + "name": "fees", + "type": "uint256[]", + "indexed": false + }, + { + "name": "token_supply", + "type": "uint256", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "RemoveLiquidityOne", + "inputs": [ + { + "name": "provider", + "type": "address", + "indexed": true + }, + { + "name": "token_id", + "type": "int128", + "indexed": false + }, + { + "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[]", + "indexed": false + }, + { + "name": "fees", + "type": "uint256[]", + "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": "ApplyNewFee", + "inputs": [ + { + "name": "fee", + "type": "uint256", + "indexed": false + }, + { + "name": "offpeg_fee_multiplier", + "type": "uint256", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "name": "SetNewMATime", + "inputs": [ + { + "name": "ma_exp_time", + "type": "uint256", + "indexed": false + }, + { + "name": "D_ma_time", + "type": "uint256", + "indexed": false + } + ], + "anonymous": false, + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "constructor", + "inputs": [ + { + "name": "_name", + "type": "string" + }, + { + "name": "_symbol", + "type": "string" + }, + { + "name": "_A", + "type": "uint256" + }, + { + "name": "_fee", + "type": "uint256" + }, + { + "name": "_offpeg_fee_multiplier", + "type": "uint256" + }, + { + "name": "_ma_exp_time", + "type": "uint256" + }, + { + "name": "_coins", + "type": "address[]" + }, + { + "name": "_rate_multipliers", + "type": "uint256[]" + }, + { + "name": "_asset_types", + "type": "uint8[]" + }, + { + "name": "_method_ids", + "type": "bytes4[]" + }, + { + "name": "_oracles", + "type": "address[]" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "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": "nonpayable", + "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": "exchange_received", + "inputs": [ + { + "name": "i", + "type": "int128" + }, + { + "name": "j", + "type": "int128" + }, + { + "name": "_dx", + "type": "uint256" + }, + { + "name": "_min_dy", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "exchange_received", + "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": "add_liquidity", + "inputs": [ + { + "name": "_amounts", + "type": "uint256[]" + }, + { + "name": "_min_mint_amount", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "add_liquidity", + "inputs": [ + { + "name": "_amounts", + "type": "uint256[]" + }, + { + "name": "_min_mint_amount", + "type": "uint256" + }, + { + "name": "_receiver", + "type": "address" + } + ], + "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": "remove_liquidity_imbalance", + "inputs": [ + { + "name": "_amounts", + "type": "uint256[]" + }, + { + "name": "_max_burn_amount", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "remove_liquidity_imbalance", + "inputs": [ + { + "name": "_amounts", + "type": "uint256[]" + }, + { + "name": "_max_burn_amount", + "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[]" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[]" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "remove_liquidity", + "inputs": [ + { + "name": "_burn_amount", + "type": "uint256" + }, + { + "name": "_min_amounts", + "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[]" + }, + { + "name": "_receiver", + "type": "address" + }, + { + "name": "_claim_admin_fees", + "type": "bool" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256[]" + } + ] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "withdraw_admin_fees", + "inputs": [], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "last_price", + "inputs": [ + { + "name": "i", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "ema_price", + "inputs": [ + { + "name": "i", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_p", + "inputs": [ + { + "name": "i", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "price_oracle", + "inputs": [ + { + "name": "i", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "D_oracle", + "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": "DOMAIN_SEPARATOR", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_dx", + "inputs": [ + { + "name": "i", + "type": "int128" + }, + { + "name": "j", + "type": "int128" + }, + { + "name": "dy", + "type": "uint256" + } + ], + "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": "view", + "type": "function", + "name": "calc_withdraw_one_coin", + "inputs": [ + { + "name": "_burn_amount", + "type": "uint256" + }, + { + "name": "i", + "type": "int128" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "totalSupply", + "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[]" + }, + { + "name": "_is_deposit", + "type": "bool" + } + ], + "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": "balances", + "inputs": [ + { + "name": "i", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "get_balances", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256[]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "stored_rates", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256[]" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "dynamic_fee", + "inputs": [ + { + "name": "i", + "type": "int128" + }, + { + "name": "j", + "type": "int128" + } + ], + "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": "nonpayable", + "type": "function", + "name": "set_new_fee", + "inputs": [ + { + "name": "_new_fee", + "type": "uint256" + }, + { + "name": "_new_offpeg_fee_multiplier", + "type": "uint256" + } + ], + "outputs": [] + }, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "set_ma_exp_time", + "inputs": [ + { + "name": "_ma_exp_time", + "type": "uint256" + }, + { + "name": "_D_ma_time", + "type": "uint256" + } + ], + "outputs": [] + }, + { + "stateMutability": "view", + "type": "function", + "name": "N_COINS", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "coins", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "fee", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "offpeg_fee_multiplier", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "admin_fee", + "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": "admin_balances", + "inputs": [ + { + "name": "arg0", + "type": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "ma_exp_time", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "D_ma_time", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "ma_last_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": "decimals", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint8" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "version", + "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": "nonces", + "inputs": [ + { + "name": "arg0", + "type": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256" + } + ] + }, + { + "stateMutability": "view", + "type": "function", + "name": "salt", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ] + } +] \ No newline at end of file diff --git a/contracts/test/abi/curveXChainLiquidityGauge.json b/contracts/test/abi/curveXChainLiquidityGauge.json new file mode 100644 index 0000000000..81bd900b7b --- /dev/null +++ b/contracts/test/abi/curveXChainLiquidityGauge.json @@ -0,0 +1 @@ +[{"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":true},{"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":"SetGaugeManager","inputs":[{"name":"_gauge_manager","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":[{"name":"_factory","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"initialize","inputs":[{"name":"_lp_token","type":"address"},{"name":"_root","type":"address"},{"name":"_manager","type":"address"}],"outputs":[]},{"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":"withdraw","inputs":[{"name":"_value","type":"uint256"},{"name":"_claim_rewards","type":"bool"},{"name":"_receiver","type":"address"}],"outputs":[]},{"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":"transferFrom","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"transfer","inputs":[{"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":"nonpayable","type":"function","name":"increaseAllowance","inputs":[{"name":"_spender","type":"address"},{"name":"_added_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"decreaseAllowance","inputs":[{"name":"_spender","type":"address"},{"name":"_subtracted_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"user_checkpoint","inputs":[{"name":"addr","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"set_rewards_receiver","inputs":[{"name":"_receiver","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_gauge_manager","inputs":[{"name":"_gauge_manager","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_manager","inputs":[{"name":"_gauge_manager","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"deposit_reward_token","inputs":[{"name":"_reward_token","type":"address"},{"name":"_amount","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"deposit_reward_token","inputs":[{"name":"_reward_token","type":"address"},{"name":"_amount","type":"uint256"},{"name":"_epoch","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"recover_remaining","inputs":[{"name":"_reward_token","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"add_reward","inputs":[{"name":"_reward_token","type":"address"},{"name":"_distributor","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_reward_distributor","inputs":[{"name":"_reward_token","type":"address"},{"name":"_distributor","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_killed","inputs":[{"name":"_is_killed","type":"bool"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_root_gauge","inputs":[{"name":"_root","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"update_voting_escrow","inputs":[],"outputs":[]},{"stateMutability":"view","type":"function","name":"claimed_reward","inputs":[{"name":"_addr","type":"address"},{"name":"_token","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"claimable_reward","inputs":[{"name":"_user","type":"address"},{"name":"_reward_token","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"claimable_tokens","inputs":[{"name":"addr","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_checkpoint","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"version","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"factory","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"voting_escrow","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"balanceOf","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"totalSupply","inputs":[],"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":"name","inputs":[],"outputs":[{"name":"","type":"string"}]},{"stateMutability":"view","type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string"}]},{"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":"manager","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"lp_token","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"is_killed","inputs":[],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"inflation_rate","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"reward_count","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"reward_data","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"distributor","type":"address"},{"name":"period_finish","type":"uint256"},{"name":"rate","type":"uint256"},{"name":"last_update","type":"uint256"},{"name":"integral","type":"uint256"}]}]},{"stateMutability":"view","type":"function","name":"reward_remaining","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"rewards_receiver","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"reward_integral_for","inputs":[{"name":"arg0","type":"address"},{"name":"arg1","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"working_balances","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"working_supply","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_inv_supply_of","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_checkpoint_of","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_fraction","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"period","inputs":[],"outputs":[{"name":"","type":"int128"}]},{"stateMutability":"view","type":"function","name":"reward_tokens","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"period_timestamp","inputs":[{"name":"arg0","type":"int128"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"integrate_inv_supply","inputs":[{"name":"arg0","type":"int128"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"root_gauge","inputs":[],"outputs":[{"name":"","type":"address"}]}] \ No newline at end of file diff --git a/contracts/test/behaviour/strategy.js b/contracts/test/behaviour/strategy.js index 382f1eed9a..685345124b 100644 --- a/contracts/test/behaviour/strategy.js +++ b/contracts/test/behaviour/strategy.js @@ -1,5 +1,7 @@ const { expect } = require("chai"); const { Wallet } = require("ethers"); +const hre = require("hardhat"); +const { setERC20TokenBalance } = require("../_fund"); const { units } = require("../helpers"); const { impersonateAndFund } = require("../../utils/signers"); @@ -89,13 +91,17 @@ const shouldBehaveLikeStrategy = (context) => { it("Should be able to deposit each asset", async () => { const { assets, valueAssets, strategy, vault } = await context(); - const strategySigner = await impersonateAndFund(strategy.address); const vaultSigner = await impersonateAndFund(vault.address); for (const asset of assets) { const depositAmount = await units("1000", asset); // mint some test assets directly into the strategy contract - await asset.connect(strategySigner).mint(depositAmount); + await setERC20TokenBalance( + strategy.address, + asset, + depositAmount, + hre + ); const tx = await strategy .connect(vaultSigner) @@ -132,13 +138,17 @@ const shouldBehaveLikeStrategy = (context) => { it("Should be able to deposit all asset together", async () => { const { assets, strategy, vault } = await context(); - const strategySigner = await impersonateAndFund(strategy.address); const vaultSigner = await impersonateAndFund(vault.address); for (const [i, asset] of assets.entries()) { const depositAmount = await units("1000", asset); // mint some test assets directly into the strategy contract - await asset.connect(strategySigner).mint(depositAmount.mul(i + 1)); + await setERC20TokenBalance( + strategy.address, + asset, + depositAmount.mul(i + 1), + hre + ); } const tx = await strategy.connect(vaultSigner).depositAll(); @@ -204,7 +214,13 @@ const shouldBehaveLikeStrategy = (context) => { } }); it("Should be able to call withdraw all by vault", async () => { - const { strategy, vault } = await context(); + const { strategy, vault, curveAMOStrategy } = await context(); + + // If strategy is Curve Base AMO, withdrawAll cannot work if there are no assets in the strategy. + // As it will try to remove 0 LPs from the gauge, which is not permitted by Curve gauge. + if (curveAMOStrategy != undefined && curveAMOStrategy == strategy) + return; + const vaultSigner = await impersonateAndFund(vault.address); const tx = await strategy.connect(vaultSigner).withdrawAll(); @@ -212,8 +228,12 @@ const shouldBehaveLikeStrategy = (context) => { await expect(tx).to.not.emit(strategy, "Withdrawal"); }); it("Should be able to call withdraw all by governor", async () => { - const { strategy, governor } = await context(); + const { strategy, governor, curveAMOStrategy } = await context(); + // If strategy is Curve Base AMO, withdrawAll cannot work if there are no assets in the strategy. + // As it will try to remove 0 LPs from the gauge, which is not permitted by Curve gauge. + if (curveAMOStrategy != undefined && curveAMOStrategy == strategy) + return; const tx = await strategy.connect(governor).withdrawAll(); await expect(tx).to.not.emit(strategy, "Withdrawal"); @@ -232,14 +252,18 @@ const shouldBehaveLikeStrategy = (context) => { describe("with assets in the strategy", () => { beforeEach(async () => { const { assets, strategy, vault } = await context(); - const strategySigner = await impersonateAndFund(strategy.address); const vaultSigner = await impersonateAndFund(vault.address); // deposit some assets into the strategy so we can withdraw them for (const [i, asset] of assets.entries()) { const depositAmount = await units("10000", asset); // mint some test assets directly into the strategy contract - await asset.connect(strategySigner).mint(depositAmount.mul(i + 1)); + await setERC20TokenBalance( + strategy.address, + asset, + depositAmount.mul(i + 1), + hre + ); } await strategy.connect(vaultSigner).depositAll(); }); @@ -275,9 +299,15 @@ const shouldBehaveLikeStrategy = (context) => { await expect(tx) .to.emit(strategy, "Withdrawal") .withArgs(asset.address, platformAddress, withdrawAmount); + // the transfer does not have to come from the strategy. It can come directly from the platform + // Need to handle WETH which has different named args in the Transfer event + const erc20Asset = await ethers.getContractAt( + "IERC20", + asset.address + ); await expect(tx) - .to.emit(asset, "Transfer") + .to.emit(erc20Asset, "Transfer") .withNamedArgs({ to: vault.address, value: withdrawAmount }); } }); @@ -289,6 +319,7 @@ const shouldBehaveLikeStrategy = (context) => { vault, fraxEthStrategy, sfrxETH, + curveAMOStrategy, } = await context(); const vaultSigner = await impersonateAndFund(vault.address); @@ -310,6 +341,13 @@ const shouldBehaveLikeStrategy = (context) => { vault.address, withdrawAmount.mul(3) ); + } else if ( + curveAMOStrategy != undefined && + curveAMOStrategy == strategy + ) { + // Didn't managed to get this work with args. + await expect(tx).to.emit(strategy, "Withdrawal"); + await expect(tx).to.emit(asset, "Transfer"); } else { await expect(tx) .to.emit(strategy, "Withdrawal") @@ -330,14 +368,13 @@ const shouldBehaveLikeStrategy = (context) => { }); }); it("Should allow transfer of arbitrary token by Governor", async () => { - const { governor, anna, crv, strategy, crvMinter } = context(); + const { governor, crv, strategy } = context(); const governorDaiBalanceBefore = await crv.balanceOf(governor.address); const strategyDaiBalanceBefore = await crv.balanceOf(strategy.address); // Anna accidentally sends CRV to strategy - await crvMinter.connect(governor).mint(anna.address); const recoveryAmount = parseUnits("2"); - await crv.connect(anna).transfer(strategy.address, recoveryAmount); + await setERC20TokenBalance(strategy.address, crv, recoveryAmount, hre); // Anna asks Governor for help const tx = await strategy diff --git a/contracts/test/strategies/base/curve-amo.base.fork-test.js b/contracts/test/strategies/base/curve-amo.base.fork-test.js new file mode 100644 index 0000000000..a7caf18d82 --- /dev/null +++ b/contracts/test/strategies/base/curve-amo.base.fork-test.js @@ -0,0 +1,622 @@ +const { createFixtureLoader } = require("../../_fixture"); +const { defaultBaseFixture } = require("../../_fixture-base"); +const { expect } = require("chai"); +const { oethUnits } = require("../../helpers"); +const addresses = require("../../../utils/addresses"); +const { impersonateAndFund } = require("../../../utils/signers"); +const { setERC20TokenBalance } = require("../../_fund"); +const hre = require("hardhat"); +const { advanceTime } = require("../../helpers"); +const { shouldBehaveLikeGovernable } = require("../../behaviour/governable"); +const { shouldBehaveLikeHarvestable } = require("../../behaviour/harvestable"); +const { shouldBehaveLikeStrategy } = require("../../behaviour/strategy"); + +const baseFixture = createFixtureLoader(defaultBaseFixture); + +describe("Curve AMO strategy", function () { + let fixture, + oethbVault, + curveAMOStrategy, + oethb, + weth, + nick, + clement, + rafael, + governor, + timelock; + + let curvePool, + curveGauge, + impersonatedVaultSigner, + impersonatedStrategist, + impersonatedHarvester, + impersonatedCurveGaugeFactory, + impersonatedAMOGovernor, + impersonatedCurveStrategy, + curveChildLiquidityGaugeFactory, + impersonatedTimelock, + crv, + harvester; + + let defaultDepositor; + + const defaultDeposit = oethUnits("5"); + + beforeEach(async () => { + fixture = await baseFixture(); + oethbVault = fixture.oethbVault; + curveAMOStrategy = fixture.curveAMOStrategy; + oethb = fixture.oethb; + weth = fixture.weth; + nick = fixture.nick; + rafael = fixture.rafael; + clement = fixture.clement; + governor = fixture.governor; + timelock = fixture.timelock; + curvePool = fixture.curvePoolOEthbWeth; + curveGauge = fixture.curveGaugeOETHbWETH; + curveChildLiquidityGaugeFactory = fixture.curveChildLiquidityGaugeFactory; + crv = fixture.crv; + harvester = fixture.harvester; + + defaultDepositor = rafael; + + impersonatedVaultSigner = await impersonateAndFund(oethbVault.address); + impersonatedStrategist = await impersonateAndFund( + await oethbVault.strategistAddr() + ); + impersonatedHarvester = await impersonateAndFund(harvester.address); + impersonatedCurveGaugeFactory = await impersonateAndFund( + curveChildLiquidityGaugeFactory.address + ); + impersonatedAMOGovernor = await impersonateAndFund( + await curveAMOStrategy.governor() + ); + impersonatedTimelock = await impersonateAndFund(timelock.address); + impersonatedCurveStrategy = await impersonateAndFund( + curveAMOStrategy.address + ); + + // Set vaultBuffer to 100% + await oethbVault + .connect(impersonatedTimelock) + .setVaultBuffer(oethUnits("1")); + + await curveAMOStrategy + .connect(impersonatedAMOGovernor) + .setHarvesterAddress(harvester.address); + }); + + describe("Initial paramaters", () => { + it("Should have correct parameters after deployment", async () => { + const { curveAMOStrategy, oethbVault, oethb, weth } = fixture; + expect(await curveAMOStrategy.platformAddress()).to.equal( + addresses.base.OETHb_WETH.pool + ); + expect(await curveAMOStrategy.vaultAddress()).to.equal( + oethbVault.address + ); + expect(await curveAMOStrategy.gauge()).to.equal( + addresses.base.OETHb_WETH.gauge + ); + expect(await curveAMOStrategy.curvePool()).to.equal( + addresses.base.OETHb_WETH.pool + ); + expect(await curveAMOStrategy.lpToken()).to.equal( + addresses.base.OETHb_WETH.pool + ); + expect(await curveAMOStrategy.oeth()).to.equal(oethb.address); + expect(await curveAMOStrategy.weth()).to.equal(weth.address); + expect(await curveAMOStrategy.governor()).to.equal( + addresses.base.timelock + ); + expect(await curveAMOStrategy.rewardTokenAddresses(0)).to.equal( + addresses.base.CRV + ); + }); + + it("Should deposit to strategy", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + expect( + await curveAMOStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit.mul(2)); + expect(await oethb.balanceOf(defaultDepositor.address)).to.equal( + defaultDeposit + ); + expect(await weth.balanceOf(curveAMOStrategy.address)).to.equal(0); + }); + + it("Should withdraw from strategy", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + const impersonatedVaultSigner = await impersonateAndFund( + oethbVault.address + ); + + await curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(oethbVault.address, weth.address, oethUnits("1")); + + expect( + await curveAMOStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance(defaultDeposit.sub(oethUnits("1")).mul(2)); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit.sub(oethUnits("1")).mul(2)); + expect(await oethb.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await weth.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + }); + + it("Should withdraw all from strategy", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + await curveAMOStrategy.connect(impersonatedVaultSigner).withdrawAll(); + + expect( + await curveAMOStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance(0); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(0); + expect(await oethb.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await weth.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + }); + + it("Should mintAndAddOToken", async () => { + await unbalancePool({ + balancedBefore: true, + wethbAmount: defaultDeposit, + }); + + await curveAMOStrategy + .connect(impersonatedStrategist) + .mintAndAddOTokens(defaultDeposit); + + expect( + await curveAMOStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance(defaultDeposit); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit); + expect(await oethb.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await weth.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + }); + + it("Should removeAndBurnOToken", async () => { + await balancePool(); + await mintAndDepositToStrategy({ + userOverride: false, + amount: defaultDeposit.mul(2), + returnTransaction: false, + }); + await unbalancePool({ + balancedBefore: true, + oethbAmount: defaultDeposit.mul(2), + }); + + await curveAMOStrategy + .connect(impersonatedStrategist) + .removeAndBurnOTokens(defaultDeposit); + + expect( + await curveAMOStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); + expect(await oethb.balanceOf(curveAMOStrategy.address)).to.equal(0); + expect(await weth.balanceOf(curveAMOStrategy.address)).to.equal( + oethUnits("0") + ); + }); + + it("Should removeOnlyAssets", async () => { + await balancePool(); + await mintAndDepositToStrategy({ + userOverride: false, + amount: defaultDeposit.mul(2), + returnTransaction: false, + }); + await unbalancePool({ + balancedBefore: true, + wethbAmount: defaultDeposit.mul(2), + }); + + const vaultETHBalanceBefore = await weth.balanceOf(oethbVault.address); + + await curveAMOStrategy + .connect(impersonatedStrategist) + .removeOnlyAssets(defaultDeposit); + + expect( + await curveAMOStrategy.checkBalance(weth.address) + ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); + expect( + await curveGauge.balanceOf(curveAMOStrategy.address) + ).to.approxEqualTolerance(defaultDeposit.mul(4).sub(defaultDeposit)); + expect(await weth.balanceOf(oethbVault.address)).to.approxEqualTolerance( + vaultETHBalanceBefore.add(defaultDeposit) + ); + }); + + it("Should collectRewardTokens", async () => { + await mintAndDepositToStrategy(); + await simulateCRVInflation({ + amount: oethUnits("1000000"), + timejump: 60, + checkpoint: true, + }); + + const balanceCRVHarvesterBefore = await crv.balanceOf(harvester.address); + await curveAMOStrategy + .connect(impersonatedHarvester) + .collectRewardTokens(); + const balanceCRVHarvesterAfter = await crv.balanceOf(harvester.address); + + expect(balanceCRVHarvesterAfter).to.be.gt(balanceCRVHarvesterBefore); + expect(await crv.balanceOf(curveGauge.address)).to.equal(0); + }); + }); + + describe("Should revert when", () => { + it("Deposit: Must deposit something", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .deposit(weth.address, 0) + ).to.be.revertedWith("Must deposit something"); + }); + it("Deposit: Can only deposit WETH", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .deposit(oethb.address, defaultDeposit) + ).to.be.revertedWith("Can only deposit WETH"); + }); + it("Deposit: Caller is not the Vault", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .deposit(weth.address, defaultDeposit) + ).to.be.revertedWith("Caller is not the Vault"); + }); + it("Deposit: Protocol is insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + + // Make protocol insolvent by minting a lot of OETH + // This is a cheat. + // prettier-ignore + await oethbVault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + + await expect( + mintAndDepositToStrategy({ returnTransaction: true }) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Withdraw: Must withdraw something", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(oethbVault.address, weth.address, 0) + ).to.be.revertedWith("Must withdraw something"); + }); + it("Withdraw: Can only withdraw WETH", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(oethbVault.address, oethb.address, defaultDeposit) + ).to.be.revertedWith("Can only withdraw WETH"); + }); + it("Withdraw: Caller is not the vault", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .withdraw(oethbVault.address, weth.address, defaultDeposit) + ).to.be.revertedWith("Caller is not the Vault"); + }); + it("Withdraw: Amount is greater than balance", async () => { + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(oethbVault.address, weth.address, oethUnits("1000000")) + ).to.be.revertedWith(""); + }); + it("Withdraw: Protocol is insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); + + // Make protocol insolvent by minting a lot of OETH and send them + // Otherwise they will be burned and the protocol will not be insolvent. + // This is a cheat. + // prettier-ignore + await oethbVault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + await oethb + .connect(impersonatedCurveStrategy) + .transfer(oethbVault.address, oethUnits("1000000")); + + await expect( + curveAMOStrategy + .connect(impersonatedVaultSigner) + .withdraw(oethbVault.address, weth.address, defaultDeposit) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Mint OToken: Asset overshot peg", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + await unbalancePool({ wethbAmount: defaultDeposit }); // +5 WETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .mintAndAddOTokens(defaultDeposit.mul(2)) + ).to.be.revertedWith("Assets overshot peg"); + }); + it("Mint OToken: OTokens balance worse", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + await unbalancePool({ oethbAmount: defaultDeposit.mul(2) }); // +10 OETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .mintAndAddOTokens(defaultDeposit) + ).to.be.revertedWith("OTokens balance worse"); + }); + it("Mint OToken: Protocol insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + // prettier-ignore + await oethbVault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .mintAndAddOTokens(defaultDeposit) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Burn OToken: Asset balance worse", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + await unbalancePool({ wethbAmount: defaultDeposit.mul(2) }); // +10 WETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeAndBurnOTokens(defaultDeposit) + ).to.be.revertedWith("Assets balance worse"); + }); + it("Burn OToken: OTokens overshot peg", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + await unbalancePool({ oethbAmount: defaultDeposit }); // +5 OETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeAndBurnOTokens(defaultDeposit) + ).to.be.revertedWith("OTokens overshot peg"); + }); + it("Burn OToken: Protocol insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy(); + // prettier-ignore + await oethbVault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeAndBurnOTokens(defaultDeposit) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Remove only assets: Asset overshot peg", async () => { + await balancePool(); + await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); + await unbalancePool({ wethbAmount: defaultDeposit.mul(2) }); // +10 WETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeOnlyAssets(defaultDeposit.mul(3)) + ).to.be.revertedWith("Assets overshot peg"); + }); + it("Remove only assets: OTokens balance worse", async () => { + await balancePool(); + await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); + await unbalancePool({ oethbAmount: defaultDeposit.mul(2) }); // +10 OETH in the pool + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeOnlyAssets(defaultDeposit) + ).to.be.revertedWith("OTokens balance worse"); + }); + it("Remove only assets: Protocol insolvent", async () => { + await balancePool(); + await mintAndDepositToStrategy({ amount: defaultDeposit.mul(2) }); + // prettier-ignore + await oethbVault + .connect(impersonatedCurveStrategy)["mintForStrategy(uint256)"](oethUnits("1000000")); + await expect( + curveAMOStrategy + .connect(impersonatedStrategist) + .removeOnlyAssets(defaultDeposit) + ).to.be.revertedWith("Protocol insolvent"); + }); + it("Check balance: Unsupported asset", async () => { + await expect( + curveAMOStrategy.checkBalance(oethb.address) + ).to.be.revertedWith("Unsupported asset"); + }); + }); + + shouldBehaveLikeGovernable(() => ({ + ...fixture, + anna: rafael, + josh: nick, + matt: clement, + dai: crv, + strategy: curveAMOStrategy, + })); + + shouldBehaveLikeHarvestable(() => ({ + ...fixture, + anna: rafael, + strategy: curveAMOStrategy, + harvester: harvester, + oeth: oethb, + })); + + shouldBehaveLikeStrategy(() => ({ + ...fixture, + // Contracts + strategy: curveAMOStrategy, + vault: oethbVault, + assets: [weth], + timelock: timelock, + governor: governor, + strategist: rafael, + harvester: harvester, + // As we don't have this on base fixture, we use CRV + usdt: crv, + usdc: crv, + dai: crv, + weth: weth, + reth: crv, + stETH: crv, + frxETH: crv, + cvx: crv, + comp: crv, + bal: crv, + // Users + anna: rafael, + matt: clement, + josh: nick, + })); + + const mintAndDepositToStrategy = async ({ + userOverride, + amount, + returnTransaction, + } = {}) => { + const user = userOverride || defaultDepositor; + amount = amount || defaultDeposit; + + const balance = weth.balanceOf(user.address); + if (balance < amount) { + await setERC20TokenBalance(user.address, weth, amount + balance, hre); + } + await weth.connect(user).approve(oethbVault.address, amount); + await oethbVault.connect(user).mint(weth.address, amount, amount); + + const gov = await oethbVault.governor(); + const tx = await oethbVault + .connect(await impersonateAndFund(gov)) + .depositToStrategy(curveAMOStrategy.address, [weth.address], [amount]); + + if (returnTransaction) { + return tx; + } + + await expect(tx).to.emit(curveAMOStrategy, "Deposit"); + }; + + const balancePool = async () => { + let balances = await curvePool.get_balances(); + const balanceWETH = balances[0]; + const balanceOETH = balances[1]; + + if (balanceWETH > balanceOETH) { + const amount = balanceWETH.sub(balanceOETH); + const balance = weth.balanceOf(nick.address); + if (balance < amount) { + await setERC20TokenBalance(nick.address, weth, amount + balance, hre); + } + await weth.connect(nick).approve(oethbVault.address, amount); + await oethbVault.connect(nick).mint(weth.address, amount, amount); + await oethb.connect(nick).approve(curvePool.address, amount); + // prettier-ignore + await curvePool + .connect(nick)["add_liquidity(uint256[],uint256)"]([0, amount], 0); + } else if (balanceWETH < balanceOETH) { + const amount = balanceOETH.sub(balanceWETH); + const balance = weth.balanceOf(nick.address); + if (balance < amount) { + await setERC20TokenBalance(nick.address, weth, amount + balance, hre); + } + await weth.connect(nick).approve(curvePool.address, amount); + // prettier-ignore + await curvePool + .connect(nick)["add_liquidity(uint256[],uint256)"]([amount, 0], 0); + } + + balances = await curvePool.get_balances(); + expect(balances[0]).to.approxEqualTolerance(balances[1]); + }; + + const unbalancePool = async ({ + balancedBefore, + wethbAmount, + oethbAmount, + } = {}) => { + if (balancedBefore) { + await balancePool(); + } + + if (wethbAmount) { + const balance = weth.balanceOf(nick.address); + if (balance < wethbAmount) { + await setERC20TokenBalance( + nick.address, + weth, + wethbAmount + balance, + hre + ); + } + await weth.connect(nick).approve(curvePool.address, wethbAmount); + // prettier-ignore + await curvePool + .connect(nick)["add_liquidity(uint256[],uint256)"]([wethbAmount, 0], 0); + } else { + const balance = weth.balanceOf(nick.address); + if (balance < oethbAmount) { + await setERC20TokenBalance( + nick.address, + weth, + oethbAmount + balance, + hre + ); + } + await weth.connect(nick).approve(oethbVault.address, oethbAmount); + await oethbVault + .connect(nick) + .mint(weth.address, oethbAmount, oethbAmount); + await oethb.connect(nick).approve(curvePool.address, oethbAmount); + // prettier-ignore + await curvePool + .connect(nick)["add_liquidity(uint256[],uint256)"]([0, oethbAmount], 0); + } + }; + + const simulateCRVInflation = async ({ + amount, + timejump, + checkpoint, + } = {}) => { + await setERC20TokenBalance(curveGauge.address, crv, amount, hre); + await advanceTime(timejump); + if (checkpoint) { + curveGauge + .connect(impersonatedCurveGaugeFactory) + .user_checkpoint(curveAMOStrategy.address); + } + }; +}); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index e8473375e0..1b28c6ff00 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -337,6 +337,14 @@ addresses.base.oethbBribesContract = addresses.base.OZRelayerAddress = "0xc0D6fa24D135c006dE5B8b2955935466A03D920a"; +// Base Curve +addresses.base.CRV = "0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415"; +addresses.base.OETHb_WETH = {}; +addresses.base.OETHb_WETH.pool = "0x302A94E3C28c290EAF2a4605FC52e11Eb915f378"; +addresses.base.OETHb_WETH.gauge = "0x9da8420dbEEBDFc4902B356017610259ef7eeDD8"; +addresses.base.childLiquidityGaugeFactory = + "0xe35A879E5EfB4F1Bb7F70dCF3250f2e19f096bd8"; + // Sonic addresses.sonic = {}; addresses.sonic.wS = "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38";