diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol
index e6b1668ca9..3beab28cfd 100644
--- a/contracts/contracts/interfaces/IVault.sol
+++ b/contracts/contracts/interfaces/IVault.sol
@@ -25,6 +25,11 @@ interface IVault {
event YieldDistribution(address _to, uint256 _yield, uint256 _fee);
event TrusteeFeeBpsChanged(uint256 _basis);
event TrusteeAddressChanged(address _address);
+ event AMOStrategyUpdated(address _addr, bool _isAMO);
+ event MintForStrategyThresholdChanged(
+ address _strategy,
+ uint256 _threshold
+ );
event SwapperChanged(address _address);
event SwapAllowedUndervalueChanged(uint256 _basis);
event SwapSlippageChanged(address _asset, uint256 _basis);
@@ -79,8 +84,6 @@ interface IVault {
function trusteeFeeBps() external view returns (uint256);
- function ousdMetaStrategy() external view returns (address);
-
function setSwapper(address _swapperAddr) external;
function setSwapAllowedUndervalue(uint16 _percentageBps) external;
@@ -189,13 +192,15 @@ interface IVault {
function getAllStrategies() external view returns (address[] memory);
- function isSupportedAsset(address _asset) external view returns (bool);
-
- function netOusdMintForStrategyThreshold() external view returns (uint256);
+ function getStrategyConfig(address _addr)
+ external
+ view
+ returns (VaultStorage.Strategy memory);
- function setOusdMetaStrategy(address _ousdMetaStrategy) external;
+ function isSupportedAsset(address _asset) external view returns (bool);
- function setNetOusdMintForStrategyThreshold(uint256 _threshold) external;
+ function setAMOStrategy(address _address, bool _isAMO) external;
- function netOusdMintedForStrategy() external view returns (int256);
+ function setMintForStrategyThreshold(address _strategy, uint256 _threshold)
+ external;
}
diff --git a/contracts/contracts/mocks/curve/MockCurveAbstractMetapool.sol b/contracts/contracts/mocks/curve/MockCurveAbstractMetapool.sol
index 647c503f5e..0b2a92b835 100644
--- a/contracts/contracts/mocks/curve/MockCurveAbstractMetapool.sol
+++ b/contracts/contracts/mocks/curve/MockCurveAbstractMetapool.sol
@@ -10,24 +10,36 @@ import "../../utils/Helpers.sol";
abstract contract MockCurveAbstractMetapool is MintableERC20 {
using StableMath for uint256;
+ address constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
+
address[] public coins;
uint256[2] public balances;
+ function get_balances() external view returns (uint256[2] memory) {
+ return balances;
+ }
+
// Returns the same amount of LP tokens in 1e18 decimals
function add_liquidity(uint256[2] calldata _amounts, uint256 _minAmount)
external
+ payable
returns (uint256 lpAmount)
{
for (uint256 i = 0; i < _amounts.length; i++) {
if (_amounts[i] > 0) {
- IERC20(coins[i]).transferFrom(
- msg.sender,
- address(this),
- _amounts[i]
- );
- uint256 assetDecimals = Helpers.getDecimals(coins[i]);
- // Convert to 1e18 and add to sum
- lpAmount += _amounts[i].scaleBy(18, assetDecimals);
+ if (coins[i] == ETH) {
+ require(msg.value == _amounts[i], "no ETH sent");
+ lpAmount += _amounts[i];
+ } else {
+ IERC20(coins[i]).transferFrom(
+ msg.sender,
+ address(this),
+ _amounts[i]
+ );
+ uint256 assetDecimals = Helpers.getDecimals(coins[i]);
+ // Convert to 1e18 and add to sum
+ lpAmount += _amounts[i].scaleBy(18, assetDecimals);
+ }
balances[i] = balances[i] + _amounts[i];
}
}
@@ -39,13 +51,12 @@ abstract contract MockCurveAbstractMetapool is MintableERC20 {
}
// Dumb implementation that returns the same amount
- function calc_withdraw_one_coin(uint256 _amount, int128 _index)
+ function calc_withdraw_one_coin(uint256 _burn_amount, int128)
public
- view
- returns (uint256 lpAmount)
+ pure
+ returns (uint256 coinAmount)
{
- uint256 assetDecimals = Helpers.getDecimals(coins[uint128(_index)]);
- lpAmount = _amount.scaleBy(assetDecimals, 18);
+ coinAmount = _burn_amount;
}
function remove_liquidity_one_coin(
@@ -54,12 +65,32 @@ abstract contract MockCurveAbstractMetapool is MintableERC20 {
// solhint-disable-next-line no-unused-vars
uint256 _minAmount
) external returns (uint256 amount) {
+ amount = remove_liquidity_one_coin(
+ _lpAmount,
+ _index,
+ _minAmount,
+ msg.sender
+ );
+ }
+
+ function remove_liquidity_one_coin(
+ uint256 _lpAmount,
+ int128 _index,
+ // solhint-disable-next-line no-unused-vars
+ uint256 _minAmount,
+ address _receiver
+ ) public returns (uint256 amount) {
_burn(msg.sender, _lpAmount);
uint256[] memory amounts = new uint256[](coins.length);
amounts[uint128(_index)] = _lpAmount;
amount = calc_withdraw_one_coin(_lpAmount, _index);
balances[uint128(_index)] -= amount;
- IERC20(coins[uint128(_index)]).transfer(msg.sender, amount);
+ if (coins[uint128(_index)] == ETH) {
+ require(address(this).balance >= amount, "not enough ETH");
+ payable(_receiver).transfer(amount);
+ } else {
+ IERC20(coins[uint128(_index)]).transfer(_receiver, amount);
+ }
}
function get_virtual_price() external pure returns (uint256) {
@@ -71,15 +102,21 @@ abstract contract MockCurveAbstractMetapool is MintableERC20 {
public
returns (uint256[2] memory amounts)
{
- _burn(msg.sender, _amount);
uint256 totalSupply = totalSupply();
+ if (totalSupply == 0) return amounts;
+
+ _burn(msg.sender, _amount);
for (uint256 i = 0; i < 2; i++) {
- amounts[i] = totalSupply > 0
- ? (_amount * IERC20(coins[i]).balanceOf(address(this))) /
- totalSupply
- : IERC20(coins[i]).balanceOf(address(this));
balances[i] -= amounts[i];
- IERC20(coins[i]).transfer(msg.sender, amounts[i]);
+ if (coins[i] == ETH) {
+ amounts[i] = (_amount * address(this).balance) / totalSupply;
+ payable(msg.sender).transfer(amounts[i]);
+ } else {
+ amounts[i] =
+ (_amount * IERC20(coins[i]).balanceOf(address(this))) /
+ totalSupply;
+ IERC20(coins[i]).transfer(msg.sender, amounts[i]);
+ }
}
}
@@ -118,7 +155,11 @@ abstract contract MockCurveAbstractMetapool is MintableERC20 {
for (uint256 i = 0; i < _amounts.length; i++) {
balances[i] -= _amounts[i];
if (_amounts[i] > 0) {
- IERC20(coins[i]).transfer(_reveiver, _amounts[i]);
+ if (coins[i] == ETH) {
+ payable(msg.sender).transfer(_amounts[i]);
+ } else {
+ IERC20(coins[i]).transfer(_reveiver, _amounts[i]);
+ }
}
}
}
diff --git a/contracts/contracts/mocks/curve/MockCurveFrxEthOethPool.sol b/contracts/contracts/mocks/curve/MockCurveFrxEthOethPool.sol
new file mode 100644
index 0000000000..f27f591932
--- /dev/null
+++ b/contracts/contracts/mocks/curve/MockCurveFrxEthOethPool.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import { MockCurveAbstractMetapool } from "./MockCurveAbstractMetapool.sol";
+import "../MintableERC20.sol";
+
+contract MockCurveFrxEthOethPool is MockCurveAbstractMetapool {
+ constructor(address[2] memory _coins)
+ ERC20("Curve.fi Factory Plain Pool: frxETH/OETH", "frxETHOETH-f")
+ {
+ coins = _coins;
+ }
+}
diff --git a/contracts/contracts/mocks/curve/MockCurveOethEthPool.sol b/contracts/contracts/mocks/curve/MockCurveOethEthPool.sol
new file mode 100644
index 0000000000..a0b428be98
--- /dev/null
+++ b/contracts/contracts/mocks/curve/MockCurveOethEthPool.sol
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import { MockCurveAbstractMetapool } from "./MockCurveAbstractMetapool.sol";
+import "../MintableERC20.sol";
+
+contract MockCurveOethEthPool is MockCurveAbstractMetapool {
+ constructor(address[2] memory _coins)
+ ERC20("Curve.fi Factory Pool: OETH", "OETHCRV-f")
+ {
+ coins = _coins;
+ }
+
+ // TODO need to handle native ETH transfers
+}
diff --git a/contracts/contracts/mocks/curve/MockCurvePool.sol b/contracts/contracts/mocks/curve/MockCurvePool.sol
index f3b6d288e4..093582dc95 100644
--- a/contracts/contracts/mocks/curve/MockCurvePool.sol
+++ b/contracts/contracts/mocks/curve/MockCurvePool.sol
@@ -30,7 +30,7 @@ contract MockCurvePool {
function add_liquidity(uint256[3] calldata _amounts, uint256 _minAmount)
external
{
- uint256 sum = 0;
+ uint256 lpAmount = 0;
for (uint256 i = 0; i < _amounts.length; i++) {
if (_amounts[i] > 0) {
IERC20(coins[i]).transferFrom(
@@ -40,16 +40,16 @@ contract MockCurvePool {
);
uint256 assetDecimals = Helpers.getDecimals(coins[i]);
// Convert to 1e18 and add to sum
- sum += _amounts[i].scaleBy(18, assetDecimals);
+ lpAmount += _amounts[i].scaleBy(18, assetDecimals);
balances[i] = balances[i] + _amounts[i];
}
}
// Hacky way of simulating slippage to check _minAmount
- if (sum == 29000e18) sum = 14500e18;
- require(sum >= _minAmount, "Slippage ruined your day");
+ if (lpAmount == 29000e18) lpAmount = 14500e18;
+ require(lpAmount >= _minAmount, "Slippage ruined your day");
// Send LP token to sender, e.g. 3CRV
- IMintableERC20(lpToken).mint(sum);
- IERC20(lpToken).transfer(msg.sender, sum);
+ IMintableERC20(lpToken).mint(lpAmount);
+ IERC20(lpToken).transfer(msg.sender, lpAmount);
}
// Dumb implementation that returns the same amount
@@ -85,9 +85,9 @@ contract MockCurvePool {
function remove_liquidity(uint256 _lpAmount, uint256[3] memory _min_amounts)
public
{
+ uint256 totalSupply = IERC20(lpToken).totalSupply();
// Burn the Curve LP tokens
IBurnableERC20(lpToken).burnFrom(msg.sender, _lpAmount);
- uint256 totalSupply = IERC20(lpToken).totalSupply();
for (uint256 i = 0; i < 3; i++) {
uint256 coinAmount = totalSupply > 0
? (_lpAmount * IERC20(coins[i]).balanceOf(address(this))) /
diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol
index 327f60d8fa..dc0f09b229 100644
--- a/contracts/contracts/proxies/Proxies.sol
+++ b/contracts/contracts/proxies/Proxies.sol
@@ -204,3 +204,10 @@ contract ConvexFrxEthWethStrategyProxy is
{
}
+
+/**
+ * @notice ConvexFrxETHAMOStrategyProxy delegates calls to a ConvexFrxETHAMOStrategy implementation
+ */
+contract ConvexFrxETHAMOStrategyProxy is InitializeGovernedUpgradeabilityProxy {
+
+}
diff --git a/contracts/contracts/strategies/CompoundStrategy.sol b/contracts/contracts/strategies/CompoundStrategy.sol
index b195c3b9cc..9516c0244d 100644
--- a/contracts/contracts/strategies/CompoundStrategy.sol
+++ b/contracts/contracts/strategies/CompoundStrategy.sol
@@ -217,7 +217,7 @@ contract CompoundStrategy is BaseCompoundStrategy {
* @notice Approve the spending of all assets by their corresponding cToken,
* if for some reason is it necessary.
*/
- function safeApproveAllTokens() external override {
+ function safeApproveAllTokens() external override onlyGovernor {
uint256 assetCount = assetsMapped.length;
for (uint256 i = 0; i < assetCount; ++i) {
IERC20 asset = IERC20(assetsMapped[i]);
diff --git a/contracts/contracts/strategies/ConvexEthMetaStrategy.sol b/contracts/contracts/strategies/ConvexEthMetaStrategy.sol
deleted file mode 100644
index b62a7a4a98..0000000000
--- a/contracts/contracts/strategies/ConvexEthMetaStrategy.sol
+++ /dev/null
@@ -1,617 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.0;
-
-/**
- * @title Convex Automated Market Maker (AMO) Strategy
- * @notice AMO strategy for the Curve OETH/ETH pool
- * @author Origin Protocol Inc
- */
-import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
-import "@openzeppelin/contracts/utils/math/Math.sol";
-
-import { ICurveETHPoolV1 } from "./curve/ICurveETHPoolV1.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 { IConvexDeposits } from "./IConvexDeposits.sol";
-import { IRewardStaking } from "./IRewardStaking.sol";
-
-contract ConvexEthMetaStrategy is InitializableAbstractStrategy {
- using StableMath for uint256;
- using SafeERC20 for IERC20;
-
- uint256 public constant MAX_SLIPPAGE = 1e16; // 1%, same as the Curve UI
- address public constant ETH_ADDRESS =
- 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
-
- // The following slots have been deprecated with immutable variables
- // slither-disable-next-line constable-states
- address private _deprecated_cvxDepositorAddress;
- // slither-disable-next-line constable-states
- address private _deprecated_cvxRewardStaker;
- // slither-disable-next-line constable-states
- uint256 private _deprecated_cvxDepositorPTokenId;
- // slither-disable-next-line constable-states
- address private _deprecated_curvePool;
- // slither-disable-next-line constable-states
- address private _deprecated_lpToken;
- // slither-disable-next-line constable-states
- address private _deprecated_oeth;
- // slither-disable-next-line constable-states
- address private _deprecated_weth;
-
- // Ordered list of pool assets
- // slither-disable-next-line constable-states
- uint128 private _deprecated_oethCoinIndex;
- // slither-disable-next-line constable-states
- uint128 private _deprecated_ethCoinIndex;
-
- // New immutable variables that must be set in the constructor
- address public immutable cvxDepositorAddress;
- IRewardStaking public immutable cvxRewardStaker;
- uint256 public immutable cvxDepositorPTokenId;
- ICurveETHPoolV1 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[2] 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[2] 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");
- }
- }
-
- // Used to circumvent the stack too deep issue
- struct ConvexEthMetaConfig {
- address cvxDepositorAddress; //Address of the Convex depositor(AKA booster) for this pool
- address cvxRewardStakerAddress; //Address of the CVX rewards staker
- uint256 cvxDepositorPTokenId; //Pid of the pool referred to by Depositor and staker
- address oethAddress; //Address of OETH token
- address wethAddress; //Address of WETH
- }
-
- constructor(
- BaseStrategyConfig memory _baseConfig,
- ConvexEthMetaConfig memory _convexConfig
- ) InitializableAbstractStrategy(_baseConfig) {
- lpToken = IERC20(_baseConfig.platformAddress);
- curvePool = ICurveETHPoolV1(_baseConfig.platformAddress);
-
- cvxDepositorAddress = _convexConfig.cvxDepositorAddress;
- cvxRewardStaker = IRewardStaking(_convexConfig.cvxRewardStakerAddress);
- cvxDepositorPTokenId = _convexConfig.cvxDepositorPTokenId;
- oeth = IERC20(_convexConfig.oethAddress);
- weth = IWETH9(_convexConfig.wethAddress);
- }
-
- /**
- * Initializer for setting up strategy internal state. This overrides the
- * InitializableAbstractStrategy initializer as Curve strategies don't fit
- * well within that abstraction.
- * @param _rewardTokenAddresses Address of CRV & CVX
- * @param _assets Addresses of supported assets. eg WETH
- */
- function initialize(
- address[] calldata _rewardTokenAddresses, // CRV + CVX
- 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");
- weth.withdraw(_wethAmount);
-
- emit Deposit(_weth, address(lpToken), _wethAmount);
-
- // Get the asset and OToken balances in the Curve pool
- uint256[2] 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[2] memory _amounts;
- _amounts[ethCoinIndex] = _wethAmount;
- _amounts[oethCoinIndex] = oethToAdd;
-
- uint256 valueInLpTokens = (_wethAmount + oethToAdd).divPrecisely(
- curvePool.get_virtual_price()
- );
- uint256 minMintAmount = valueInLpTokens.mulTruncate(
- uint256(1e18) - MAX_SLIPPAGE
- );
-
- // Do the deposit to the Curve pool
- // slither-disable-next-line arbitrary-send
- uint256 lpDeposited = curvePool.add_liquidity{ value: _wethAmount }(
- _amounts,
- minMintAmount
- );
-
- // Deposit the Curve pool's LP tokens into the Convex rewards pool
- require(
- IConvexDeposits(cvxDepositorAddress).deposit(
- cvxDepositorPTokenId,
- lpDeposited,
- true // Deposit with staking
- ),
- "Depositing LP to Convex not successful"
- );
- }
-
- /**
- * @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, "Invalid amount");
- require(_weth == address(weth), "Can only withdraw WETH");
-
- emit Withdrawal(_weth, address(lpToken), _amount);
-
- uint256 requiredLpTokens = calcTokenToBurn(_amount);
-
- _lpWithdraw(requiredLpTokens);
-
- /* math in requiredLpTokens should correctly calculate the amount of LP to remove
- * in that the strategy receives enough WETH on balanced removal
- */
- uint256[2] memory _minWithdrawalAmounts = [uint256(0), uint256(0)];
- _minWithdrawalAmounts[ethCoinIndex] = _amount;
- // slither-disable-next-line unused-return
- curvePool.remove_liquidity(requiredLpTokens, _minWithdrawalAmounts);
-
- // Burn 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
- weth.deposit{ value: _amount }();
- require(
- weth.transfer(_recipient, _amount),
- "Transfer of WETH not successful"
- );
- }
-
- function calcTokenToBurn(uint256 _wethAmount)
- internal
- view
- returns (uint256 lpToBurn)
- {
- /* The rate between coins in the pool determines the rate at which pool returns
- * tokens when doing balanced removal (remove_liquidity call). And by knowing how much WETH
- * we want we can determine how much of OETH we receive by removing liquidity.
- *
- * Because we are doing balanced removal we should be making profit when removing liquidity in a
- * pool tilted to either side.
- *
- * Important: A downside is that the Strategist / Governor needs to be
- * cognisant of not removing too much liquidity. And while the proposal to remove liquidity
- * is being voted on the pool tilt might change so much that the proposal that has been valid while
- * created is no longer valid.
- */
-
- uint256 poolWETHBalance = curvePool.balances(ethCoinIndex);
- /* K is multiplied by 1e36 which is used for higher precision calculation of required
- * pool LP tokens. Without it the end value can have rounding errors up to precision of
- * 10 digits. This way we move the decimal point by 36 places when doing the calculation
- * and again by 36 places when we are done with it.
- */
- uint256 k = (1e36 * lpToken.totalSupply()) / poolWETHBalance;
- // prettier-ignore
- // slither-disable-next-line divide-before-multiply
- uint256 diff = (_wethAmount + 1) * k;
- lpToBurn = diff / 1e36;
- }
-
- /**
- * @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 = cvxRewardStaker.balanceOf(address(this));
- _lpWithdraw(gaugeTokens);
-
- // Withdraws are proportional to assets held by 3Pool
- uint256[2] memory minWithdrawAmounts = [uint256(0), uint256(0)];
-
- // Remove liquidity
- // slither-disable-next-line unused-return
- curvePool.remove_liquidity(
- lpToken.balanceOf(address(this)),
- minWithdrawAmounts
- );
-
- // Burn all OETH
- uint256 oethToBurn = oeth.balanceOf(address(this));
- IVault(vaultAddress).burnForStrategy(oethToBurn);
-
- // Get the strategy contract's ether 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 = address(this).balance;
- // Convert all the strategy contract's ether to WETH and transfer to the vault.
- weth.deposit{ value: ethBalance }();
- 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[2] memory amounts = [uint256(0), uint256(0)];
- 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 Convex rewards pool
- require(
- IConvexDeposits(cvxDepositorAddress).deposit(
- cvxDepositorPTokenId,
- lpDeposited,
- true // Deposit with staking
- ),
- "Failed to Deposit LP to Convex"
- );
-
- 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);
-
- 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 Convex and remove ETH from the Curve pool
- uint256 ethAmount = _withdrawAndRemoveFromPool(_lpTokens, ethCoinIndex);
-
- // Convert ETH to WETH and transfer to the vault
- weth.deposit{ value: ethAmount }();
- require(
- weth.transfer(vaultAddress, ethAmount),
- "Transfer of WETH not successful"
- );
-
- 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 Convex pool
- _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)
- );
- }
-
- /***************************************
- Assets and Rewards
- ****************************************/
-
- /**
- * @notice Collect accumulated CRV and CVX rewards and send to the Harvester.
- */
- function collectRewardTokens()
- external
- override
- onlyHarvester
- nonReentrant
- {
- // Collect CRV and CVX
- cvxRewardStaker.getReward();
- _collectRewardTokens();
- }
-
- function _lpWithdraw(uint256 _wethAmount) internal {
- // withdraw and unwrap with claim takes back the lpTokens
- // and also collects the rewards for deposit
- cvxRewardStaker.withdrawAndUnwrap(_wethAmount, true);
- }
-
- /**
- * @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");
-
- // Eth balance needed here for the balance check that happens from vault during depositing.
- balance = address(this).balance;
- uint256 lpTokens = cvxRewardStaker.balanceOf(address(this));
- if (lpTokens > 0) {
- balance += (lpTokens * curvePool.get_virtual_price()) / 1e18;
- }
- }
-
- /**
- * @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 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)
- // No approval is needed for ETH
- // slither-disable-next-line unused-return
- oeth.approve(platformAddress, type(uint256).max);
-
- // Approve Convex deposit contract to transfer Curve pool LP tokens
- // This is needed for deposits if Curve pool LP tokens into the Convex rewards pool
- // slither-disable-next-line unused-return
- lpToken.approve(cvxDepositorAddress, 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/contracts/strategies/ConvexOUSDMetaStrategy.sol b/contracts/contracts/strategies/ConvexOUSDMetaStrategy.sol
deleted file mode 100644
index 3ab141ab0f..0000000000
--- a/contracts/contracts/strategies/ConvexOUSDMetaStrategy.sol
+++ /dev/null
@@ -1,207 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.0;
-
-/**
- * @title Curve Convex Strategy
- * @notice Investment strategy for investing stablecoins via Curve 3Pool
- * @author Origin Protocol Inc
- */
-import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
-import "@openzeppelin/contracts/utils/Strings.sol";
-import "@openzeppelin/contracts/utils/math/Math.sol";
-
-import { IRewardStaking } from "./IRewardStaking.sol";
-import { IConvexDeposits } from "./IConvexDeposits.sol";
-import { ICurvePool } from "./curve/ICurvePool.sol";
-import { CurveThreeCoinFunctions } from "./curve/CurveThreeCoinFunctions.sol";
-import { CurveFunctions, IERC20, InitializableAbstractStrategy } from "./BaseCurveStrategy.sol";
-import { BaseConvexMetaStrategy, BaseCurveStrategy } from "./BaseConvexMetaStrategy.sol";
-import { StableMath } from "../utils/StableMath.sol";
-import { IVault } from "../interfaces/IVault.sol";
-
-contract ConvexOUSDMetaStrategy is
- CurveThreeCoinFunctions,
- BaseConvexMetaStrategy
-{
- using StableMath for uint256;
- using SafeERC20 for IERC20;
-
- constructor(
- BaseStrategyConfig memory _stratConfig,
- CurveConfig memory _curveConfig
- )
- InitializableAbstractStrategy(_stratConfig)
- BaseCurveStrategy(_curveConfig)
- CurveThreeCoinFunctions(_curveConfig.curvePool)
- {}
-
- function getCurveFunctions()
- internal
- pure
- override(BaseCurveStrategy, CurveThreeCoinFunctions)
- returns (CurveFunctions memory)
- {
- return CurveThreeCoinFunctions.getCurveFunctions();
- }
-
- /* Take 3pool LP and mint the corresponding amount of ousd. Deposit and stake that to
- * ousd Curve Metapool. Take the LP from metapool and deposit them to Convex.
- */
- function _lpDepositAll() internal override {
- uint256 threePoolLpBalance = IERC20(CURVE_LP_TOKEN).balanceOf(
- address(this)
- );
- uint256 curve3PoolVirtualPrice = ICurvePool(CURVE_POOL)
- .get_virtual_price();
- uint256 threePoolLpDollarValue = threePoolLpBalance.mulTruncate(
- curve3PoolVirtualPrice
- );
-
- // safe to cast since min value is at least 0
- uint256 ousdToAdd = uint256(
- _max(
- 0,
- int256(
- metapool.balances(crvCoinIndex).mulTruncate(
- curve3PoolVirtualPrice
- )
- ) -
- int256(metapool.balances(mainCoinIndex)) +
- int256(threePoolLpDollarValue)
- )
- );
-
- /* Add so much OUSD so that the pool ends up being balanced. And at minimum
- * add twice as much OUSD as 3poolLP and at maximum at twice as
- * much OUSD.
- */
- ousdToAdd = Math.max(ousdToAdd, threePoolLpDollarValue);
- ousdToAdd = Math.min(ousdToAdd, threePoolLpDollarValue * 2);
-
- /* Mint OUSD with a strategy that attempts to contribute to stability of OUSD metapool. Try
- * to mint so much OUSD that after deployment of liquidity pool ends up being balanced.
- *
- * To manage unpredictability minimal OUSD minted will always be at least equal or greater
- * to stablecoin(DAI, USDC, USDT) amount of 3CRVLP deployed. And never larger than twice the
- * stablecoin amount of 3CRVLP deployed even if it would have a further beneficial effect
- * on pool stability.
- */
- if (ousdToAdd > 0) {
- IVault(vaultAddress).mintForStrategy(ousdToAdd);
- }
-
- uint256[2] memory _amounts = [ousdToAdd, threePoolLpBalance];
-
- uint256 metapoolVirtualPrice = metapool.get_virtual_price();
- /**
- * First convert all the deposited tokens to dollar values,
- * then divide by virtual price to convert to metapool LP tokens
- * and apply the max slippage
- */
- uint256 minReceived = (ousdToAdd + threePoolLpDollarValue)
- .divPrecisely(metapoolVirtualPrice)
- .mulTruncate(uint256(1e18) - MAX_SLIPPAGE);
-
- uint256 metapoolLp = metapool.add_liquidity(_amounts, minReceived);
-
- bool success = IConvexDeposits(cvxDepositorAddress).deposit(
- cvxDepositorPTokenId,
- metapoolLp,
- true // Deposit with staking
- );
-
- require(success, "Failed to deposit to Convex");
- }
-
- /**
- * Withdraw the specified amount of tokens from the gauge. And use all the resulting tokens
- * to remove liquidity from metapool
- * @param num3CrvTokens Number of 3CRV tokens to withdraw from metapool
- */
- function _lpWithdraw(uint256 num3CrvTokens) internal override {
- ICurvePool curvePool = ICurvePool(platformAddress);
- /* The rate between coins in the metapool determines the rate at which metapool returns
- * tokens when doing balanced removal (remove_liquidity call). And by knowing how much 3crvLp
- * we want we can determine how much of OUSD 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 crvPoolBalance = metapool.balances(crvCoinIndex);
- /* K is multiplied by 1e36 which is used for higher precision calculation of required
- * metapool 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 * metapoolLPToken.totalSupply()) / crvPoolBalance;
- // simplifying below to: `uint256 diff = (num3CrvTokens - 1) * k` causes loss of precision
- // prettier-ignore
- // slither-disable-next-line divide-before-multiply
- uint256 diff = crvPoolBalance * k -
- (crvPoolBalance - num3CrvTokens - 1) * k;
- uint256 lpToBurn = diff / 1e36;
-
- uint256 gaugeTokens = IRewardStaking(cvxRewardStakerAddress).balanceOf(
- address(this)
- );
-
- require(
- lpToBurn <= gaugeTokens,
- string(
- bytes.concat(
- bytes("Attempting to withdraw "),
- bytes(Strings.toString(lpToBurn)),
- bytes(", metapoolLP but only "),
- bytes(Strings.toString(gaugeTokens)),
- bytes(" available.")
- )
- )
- );
-
- // withdraw and unwrap with claim takes back the lpTokens and also collects the rewards for deposit
- IRewardStaking(cvxRewardStakerAddress).withdrawAndUnwrap(
- lpToBurn,
- true
- );
-
- // calculate the min amount of OUSD expected for the specified amount of LP tokens
- uint256 minOUSDAmount = lpToBurn.mulTruncate(
- metapool.get_virtual_price()
- ) -
- num3CrvTokens.mulTruncate(curvePool.get_virtual_price()) -
- 1;
-
- // withdraw the liquidity from metapool
- uint256[2] memory _removedAmounts = metapool.remove_liquidity(
- lpToBurn,
- [minOUSDAmount, num3CrvTokens]
- );
-
- IVault(vaultAddress).burnForStrategy(_removedAmounts[mainCoinIndex]);
- }
-
- function _lpWithdrawAll() internal override {
- IERC20 metapoolErc20 = IERC20(address(metapool));
- uint256 gaugeTokens = IRewardStaking(cvxRewardStakerAddress).balanceOf(
- address(this)
- );
- IRewardStaking(cvxRewardStakerAddress).withdrawAndUnwrap(
- gaugeTokens,
- true
- );
-
- uint256[2] memory _minAmounts = [uint256(0), uint256(0)];
- uint256[2] memory _removedAmounts = metapool.remove_liquidity(
- metapoolErc20.balanceOf(address(this)),
- _minAmounts
- );
-
- IVault(vaultAddress).burnForStrategy(_removedAmounts[mainCoinIndex]);
- }
-}
diff --git a/contracts/contracts/strategies/README.md b/contracts/contracts/strategies/README.md
index e96fbd0992..9bc0f17714 100644
--- a/contracts/contracts/strategies/README.md
+++ b/contracts/contracts/strategies/README.md
@@ -88,9 +88,9 @@
![Frax ETH Strategy Squashed](../../docs/FraxETHStrategySquashed.svg)
-
+![Frax ETH Strategy Storage](../../docs/FraxETHStrategyStorage.svg)
## Generalized ERC-4626 Strategy
diff --git a/contracts/contracts/strategies/amo/BalancerEthAMOStrategy.sol b/contracts/contracts/strategies/amo/BalancerEthAMOStrategy.sol
new file mode 100644
index 0000000000..0353ac3c0f
--- /dev/null
+++ b/contracts/contracts/strategies/amo/BalancerEthAMOStrategy.sol
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+/**
+ * @title Convex Automated Market Maker (AMO) Strategy
+ * @notice AMO strategy for the Balancer OETH/WETH pool
+ * @author Origin Protocol Inc
+ */
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import { BaseBalancerAMOStrategy } from "./BaseBalancerAMOStrategy.sol";
+import { IRateProvider } from "../../interfaces/balancer/IRateProvider.sol";
+import { IERC4626 } from "../../../lib/openzeppelin/interfaces/IERC4626.sol";
+import { StableMath } from "../../utils/StableMath.sol";
+
+contract BalancerEthAMOStrategy is BaseBalancerAMOStrategy {
+ using StableMath for uint256;
+
+ constructor(
+ BaseStrategyConfig memory _baseConfig,
+ AMOConfig memory _amoConfig,
+ BalancerConfig memory _balancerConfig
+ ) BaseBalancerAMOStrategy(_baseConfig, _amoConfig, _balancerConfig) {}
+
+ /***************************************
+ Vault to Pool Asset Conversions
+ ****************************************/
+
+ /// @dev WETH is the Vault asset and the Balancer pool asset so
+ /// nothing to except return the vault asset amount
+ function _toPoolAsset(address, uint256 assets)
+ internal
+ pure
+ override
+ returns (uint256 poolAssets)
+ {
+ poolAssets = assets;
+ }
+
+ function _calcPoolAsset(address, uint256 vaultAssetAmount)
+ internal
+ pure
+ override
+ returns (uint256 poolAssetAmount)
+ {
+ poolAssetAmount = vaultAssetAmount;
+ }
+
+ /// @dev WETH is the Vault asset and the pool asset so return the WETH amount
+ /// @param poolAssetAmount Amount of WETH to convert to OETH
+ /// @param oTokenAmount Amount of OETH converted from WETH
+ function _toOTokens(uint256 poolAssetAmount)
+ internal
+ pure
+ override
+ returns (uint256 oTokenAmount)
+ {
+ oTokenAmount = poolAssetAmount;
+ }
+
+ /***************************************
+ Curve Pool Withdrawals
+ ****************************************/
+
+ /// @dev transfers the specified WETH amount to the recipient
+ function _withdrawAsset(
+ address,
+ uint256 vaultAssetAmount,
+ address _recipient
+ ) internal override {
+ // Transfer the WETH to the Vault
+ require(
+ vaultAsset.transfer(_recipient, vaultAssetAmount),
+ "WETH transfer failed"
+ );
+
+ emit Withdrawal(
+ address(vaultAsset),
+ address(lpToken),
+ vaultAssetAmount
+ );
+ }
+
+ /// @dev transfers the WETH balance of this strategy contract to the recipient
+ function _withdrawAllAsset(address _recipient) internal override {
+ uint256 vaultAssets = vaultAsset.balanceOf(address(this));
+
+ _withdrawAsset(address(vaultAsset), vaultAssets, _recipient);
+ }
+
+ /***************************************
+ Asset Balance
+ ****************************************/
+
+ /**
+ * @notice Get strategy's share of an assets in the Balancer pool.
+ * This is not denominated in OUSD/ETH value of the assets in the Balancer pool.
+ * @param _asset Address of the Vault asset. eg WETH
+ * @return balance the amount of vault assets
+ *
+ * IMPORTANT if this function is overridden it needs to have a whenNotInBalancerVaultContext
+ * modifier on it or it is susceptible to read-only re-entrancy attack
+ *
+ * @dev it is important that this function is not affected by reporting inflated
+ * values of assets in case of any pool manipulation. Such a manipulation could easily
+ * exploit the protocol by:
+ * - minting OETH
+ * - tilting Balancer pool to report higher balances of assets
+ * - rebasing() -> all that extra token balances get distributed to OETH holders
+ * - tilting pool back
+ * - redeeming OETH
+ */
+ function checkBalance(address _asset)
+ public
+ view
+ override
+ returns (uint256 balance)
+ {
+ require(_asset == address(vaultAsset), "Unsupported asset");
+
+ uint256 bptBalance = IERC4626(auraRewardPool).maxRedeem(address(this));
+
+ if (bptBalance > 0) {
+ balance = (bptBalance.mulTruncate(
+ IRateProvider(address(lpToken)).getRate()
+ ) / 2);
+ }
+ }
+
+ /***************************************
+ Approvals
+ ****************************************/
+
+ /// @dev Is not used
+ function _abstractSetPToken(address _asset, address _pToken)
+ internal
+ override
+ {}
+}
diff --git a/contracts/contracts/strategies/amo/BaseAMOStrategy.sol b/contracts/contracts/strategies/amo/BaseAMOStrategy.sol
new file mode 100644
index 0000000000..348837e537
--- /dev/null
+++ b/contracts/contracts/strategies/amo/BaseAMOStrategy.sol
@@ -0,0 +1,712 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+/**
+ * @title Abstract Convex Automated Market Maker (AMO) Strategy
+ * @notice Investment strategy for investing assets in Curve and Convex pools
+ * @author Origin Protocol Inc
+ */
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/utils/math/Math.sol";
+
+import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol";
+import { StableMath } from "../../utils/StableMath.sol";
+import { IVault } from "../../interfaces/IVault.sol";
+
+abstract contract BaseAMOStrategy is InitializableAbstractStrategy {
+ using StableMath for uint256;
+
+ uint256 public constant MAX_SLIPPAGE = 1e16; // 1%, same as the Curve UI
+
+ /// @notice The AMO pool LP token that the strategy invests in
+ IERC20 public immutable lpToken;
+ /// @notice The OToken that is used in the AMO pool. eg OETH or OUSD
+ IERC20 public immutable oToken;
+ /// @notice The vault asset of this strategy. eg WETH, frxETH or 3CRV
+ IERC20 public immutable vaultAsset;
+ /// @notice The token that is used in the AMO pool. eg ETH, frxETH or 3CRV
+ IERC20 public immutable poolAsset;
+
+ // Index position of oToken in AMO pool. For example
+ // for OETH/ETH, OETH = 1
+ // for OUSD/3CRV, OUSD = 0
+ // for frxETH/OUSD, OETH = 1
+ uint128 public immutable oTokenCoinIndex;
+ // Index position of asset in AMO pool. For example
+ // for OETH/ETH, ETH = 0
+ // for OUSD/3CRV, 3CRV = 1
+ // for frxETH/OUSD, frxETH = 0
+ uint128 public immutable assetCoinIndex;
+
+ /// @notice Validates the vault asset is supported by this strategy.
+ modifier onlyAsset(address _vaultAsset) {
+ require(_isVaultAsset(_vaultAsset), "Unsupported asset");
+ _;
+ }
+
+ /// @notice Validates all the vault assets are supported by this strategy.
+ modifier onlyAssets(address[] memory _vaultAssets) {
+ require(_isVaultAssets(_vaultAssets), "Unsupported assets");
+ _;
+ }
+
+ /// @dev Verifies that the caller is the Strategist
+ modifier onlyStrategist() {
+ require(
+ msg.sender == IVault(vaultAddress).strategistAddr(),
+ "Caller is not the Strategist"
+ );
+ _;
+ }
+
+ /**
+ * @dev Checks the AMO pool's balances have improved and the balances
+ * have not tipped to the other side.
+ */
+ modifier improvePoolBalance() {
+ // Get the asset and OToken balances in the AMO pool
+ uint256[2] memory balancesBefore = _getBalances();
+ // diff = asset balance - OToken balance
+ int256 diffBefore = int256(balancesBefore[assetCoinIndex]) -
+ int256(balancesBefore[oTokenCoinIndex]);
+
+ _;
+
+ // Get the asset and OToken balances in the AMO pool
+ uint256[2] memory balancesAfter = _getBalances();
+ // diff = asset balance - OToken balance
+ int256 diffAfter = int256(balancesAfter[assetCoinIndex]) -
+ int256(balancesAfter[oTokenCoinIndex]);
+
+ // started with balanced or more OTokens
+ if (diffBefore <= 0) {
+ // If the pool was originally imbalanced in favor of the OToken, then
+ // we want to check that the pool is now more balanced
+ require(diffAfter <= 0, "OTokens overshot peg");
+ require(diffBefore < diffAfter, "OTokens balance worse");
+ }
+ // Started with balanceed or more assets
+ if (diffBefore >= 0) {
+ // If the pool was originally imbalanced in favor of the asset, then
+ // we want to check that the pool is now more balanced
+ require(diffAfter >= 0, "Assets overshot peg");
+ require(diffAfter < diffBefore, "Assets balance worse");
+ }
+ }
+
+ // Used to circumvent the stack too deep issue
+ struct AMOConfig {
+ address oTokenAddress; // Address of the OToken. eg OETH or OUSD
+ address vaultAssetAddress; // Address of the asset token. eg WETH, frxETH or DAU/USDC/USDT
+ address poolAssetAddress; // Address of the asset token. eg ETH, frxETH or 3CRV
+ uint128 oTokenCoinIndex;
+ uint128 assetCoinIndex;
+ }
+
+ constructor(
+ BaseStrategyConfig memory _baseConfig,
+ AMOConfig memory _amoConfig
+ ) InitializableAbstractStrategy(_baseConfig) {
+ // This assumes the AMO pool and LP token have the same address
+ lpToken = IERC20(_baseConfig.platformAddress);
+ oToken = IERC20(_amoConfig.oTokenAddress);
+ vaultAsset = IERC20(_amoConfig.vaultAssetAddress);
+ poolAsset = IERC20(_amoConfig.poolAssetAddress);
+ oTokenCoinIndex = _amoConfig.oTokenCoinIndex;
+ assetCoinIndex = _amoConfig.assetCoinIndex;
+ }
+
+ /***************************************
+ Vault Asset Validation
+ ****************************************/
+
+ /// @dev Validates the vault asset is supported by this strategy.
+ /// The default implementation is the vault asset matches the pool asset.
+ /// This needs to be overriden for OUSD AMO as the vault assets are DAI, USDC and USDT
+ /// while the pool asset is 3CRV.
+ /// @param _vaultAsset Address of the vault asset
+ function _isVaultAsset(address _vaultAsset)
+ internal
+ view
+ virtual
+ returns (bool supported)
+ {
+ supported = _vaultAsset == address(vaultAsset);
+ }
+
+ /// @dev Returns bool indicating whether all the assets are supported by this strategy.
+ /// The default implementation is the vault asset matches the pool asset.
+ /// This needs to be overriden for OUSD AMO as the vault assets are DAI, USDC and USDT
+ /// while the pool asset is 3CRV.
+ /// @param _vaultAssets Addresses of the vault assets
+ function _isVaultAssets(address[] memory _vaultAssets)
+ internal
+ view
+ virtual
+ returns (bool)
+ {
+ require(_vaultAssets.length == 1, "Only one asset supported");
+ return _isVaultAsset(_vaultAssets[0]);
+ }
+
+ /***************************************
+ Vault to Pool Asset Conversions
+ ****************************************/
+
+ /// @dev Converts Vault assets to a pool assets.
+ /// @param vaultAsset The address of the Vault asset to convert. eg WETH, frxETH, DAI
+ /// @param vaultAssetAmount The amount of vault assets to convert.
+ /// @return poolAssets The amount of pool assets. eg ETH, frxETH or 3CRV
+ function _toPoolAsset(address vaultAsset, uint256 vaultAssetAmount)
+ internal
+ virtual
+ returns (uint256 poolAssets);
+
+ /// @dev Calculates the required amount of pool assets to be removed from
+ /// the pool in order to get the specified amount of vault assets.
+ /// For example, the amount of 3CRV to remove from the pool to withdraw USDT.
+ function _calcPoolAsset(address vaultAsset, uint256 vaultAssetAmount)
+ internal
+ virtual
+ returns (uint256 poolAssets);
+
+ /// @dev Convert pool asset amount to an oToken amount.
+ /// @param poolAssetAmount The amount of pool assets to convert. eg ETH, 3CRV or frxETH
+ function _toOTokens(uint256 poolAssetAmount)
+ internal
+ virtual
+ returns (uint256 oTokenAmount);
+
+ /// @dev Returns the index position of the coin in the AMO pool.
+ function _getCoinIndex(address _asset) internal view returns (uint128) {
+ if (_asset == address(oToken)) {
+ return oTokenCoinIndex;
+ } else if (_asset == address(poolAsset)) {
+ return assetCoinIndex;
+ } else {
+ revert("Unsupported asset");
+ }
+ }
+
+ /***************************************
+ AMO Pool Deposits
+ ****************************************/
+
+ /// @dev Adds pool assets and/or OTokens to the AMO pool
+ /// @param poolAmounts The amount of AMO pool assets and OTokens to add to the pool
+ /// @param minLpAmount The minimum amount of AMO pool LP tokens that is acceptable to receive
+ function _addLiquidityToPool(
+ uint256[2] memory poolAmounts,
+ uint256 minLpAmount
+ ) internal virtual returns (uint256 lpDeposited);
+
+ /// @dev Removes pool assets and/or OTokens from the AMO pool.
+ /// Curve will burn an exact amount of AMO LP tokens in exchange for a calculated amount of pool assets.
+ /// Balancer will withdraw and exact amount of pool assets in exchange for a calculated amount of AMO LP tokens.
+ /// @param lpTokens The maximum amount of AMO pool LP tokens to be burnt
+ /// @param poolAssetAmounts The minimum amount of AMO pool assets that are acceptable to receive
+ function _removeLiquidityFromPool(
+ uint256 lpTokens,
+ uint256[2] memory poolAssetAmounts
+ ) internal virtual;
+
+ /// @dev Removes either pool assets or OTokens from the AMO pool.
+ /// Curve will burn an exact amount of AMO LP tokens in exchange for a calculated amount of pool assets.
+ /// Balancer will withdraw and exact amount of pool assets in exchange for a calculated amount of AMO LP tokens.
+ /// @param poolAsset The address of the AMO pool asset to be removed. eg OETH, OUSD, ETH, 3CRV, frxETH
+ /// @param lpTokens The maximum amount of AMO pool LP tokens to be burnt
+ /// @param poolAssetAmount The minimum amount of AMO pool assets that are acceptable to receive. eg OETH or ETH
+ function _removeOneSidedLiquidityFromPool(
+ address poolAsset,
+ uint256 lpTokens,
+ uint256 poolAssetAmount
+ ) internal virtual returns (uint256 coinsRemoved);
+
+ /// @dev Returns the current balances of the AMO pool
+ function _getBalances()
+ internal
+ view
+ virtual
+ returns (uint256[2] memory balances);
+
+ /// @dev Returns the current balances of the AMO pool
+ function _getBalance(address poolAsset)
+ internal
+ view
+ virtual
+ returns (uint256 balance);
+
+ /// @dev Returns the price of one AMO pool LP token in base asset terms.
+ function _getVirtualPrice()
+ internal
+ view
+ virtual
+ returns (uint256 virtualPrice);
+
+ /***************************************
+ AMO Pool Withdrawals
+ ****************************************/
+
+ /// @dev Converts all the pool assets in this strategy to a single Vault asset
+ /// and transfers them to the Vault.
+ /// For OUSD, this converts 3CRV to either DAI, USDC or USDT.
+ function _withdrawAsset(
+ address vaultAsset,
+ uint256 vaultAssetAmount,
+ address recipient
+ ) internal virtual;
+
+ /// @dev Converts all the pool assets in this strategy to the Vault assets
+ /// and transfers them to the Vault.
+ /// For OUSD, this converts 3CRV to DAI, USDC and USDT.
+ function _withdrawAllAsset(address recipient) internal virtual;
+
+ /***************************************
+ Convex Reward Pool
+ ****************************************/
+
+ /// @dev Deposit the AMO pool LP tokens to the rewards pool.
+ /// eg Curve LP tokens into Convex or Balancer LP tokens into Aura
+ /// @param lpAmount the amount of AMO pool LP tokens to deposit
+ function _stakeCurveLp(uint256 lpAmount) internal virtual;
+
+ /// @dev Withdraw a specific amount of AMO pool LP tokens from the rewards pool
+ /// eg Curve LP tokens from Convex or Balancer LP tokens from Aura
+ /// @param lpAmount the amount of AMO pool LP tokens to withdraw
+ function _unStakeLpTokens(uint256 lpAmount) internal virtual;
+
+ /// @dev Withdraw all AMO pool LP tokens from the rewards pool
+ function _unStakeAllLpTokens() internal virtual;
+
+ /***************************************
+ Deposit
+ ****************************************/
+
+ /**
+ * @notice Deposit a vault asset into the AMO strategy.
+ * @param _vaultAsset Address of the vault asset token. eg WETH, frxETH, DAI, USDC or USDT
+ * @param _vaultAssetAmount Amount of vault asset tokens to deposit.
+ */
+ function deposit(address _vaultAsset, uint256 _vaultAssetAmount)
+ external
+ override
+ onlyVault
+ onlyAsset(_vaultAsset)
+ nonReentrant
+ {
+ emit Deposit(_vaultAsset, address(lpToken), _vaultAssetAmount);
+ uint256 poolAssetAmount = _toPoolAsset(_vaultAsset, _vaultAssetAmount);
+ _deposit(poolAssetAmount);
+ }
+
+ /**
+ * @notice Deposit multiple vault assets into the AMO strategy.
+ * @param _vaultAssets Addresses of the vault asset tokens. eg WETH, frxETH, DAI, USDC or USDT
+ * @param _vaultAssetAmounts Amounts of vault asset tokens to deposit.
+ * @dev Only the OUSD Curve AMO supports depositing multiple assets in DAI, USDC and USDT.
+ * The default implementation only supports a single asset and must be overriden for the OUSD AMO.
+ */
+ function deposit(
+ address[] memory _vaultAssets,
+ uint256[] memory _vaultAssetAmounts
+ ) external virtual onlyVault onlyAssets(_vaultAssets) nonReentrant {
+ // validate the number of assets matches the number of amounts
+ // The onlyAssets modified ensures the correct number of assets are supported.
+ // Most AMOs will be just one asset but for OUSD's 3CRV AMO it will be 3 assets.
+ require(
+ _vaultAssets.length == _vaultAssetAmounts.length,
+ "Assets and amounts mismatch"
+ );
+ uint256 poolAssetAmount = _toPoolAsset(
+ _vaultAssets[0],
+ _vaultAssetAmounts[0]
+ );
+ _deposit(poolAssetAmount);
+ }
+
+ function _deposit(uint256 _poolAssetAmount) internal {
+ require(_poolAssetAmount > 0, "Must deposit something");
+
+ // Get the asset and OToken balances in the AMO pool
+ uint256[2] memory balances = _getBalances();
+
+ // Add the old balance with the new deposit amount
+ // and then convert to the pool asset value.
+ // For example, convert 3CRV to USD value
+ uint256 newAssetsValueInOTokens = _toOTokens(
+ balances[assetCoinIndex] + _poolAssetAmount
+ );
+
+ // Calculate the amount of OTokens to add to the pool
+ // safe to cast since min value is at least 0
+ uint256 oTokensToAdd = uint256(
+ _max(
+ 0,
+ int256(newAssetsValueInOTokens) -
+ int256(balances[oTokenCoinIndex])
+ )
+ );
+
+ /* Add so much OTokens so that the pool ends up being balanced. And at minimum
+ * add as much OTokens as asset and at maximum twice as much OTokens.
+ */
+ oTokensToAdd = Math.max(oTokensToAdd, _poolAssetAmount);
+ oTokensToAdd = Math.min(oTokensToAdd, _poolAssetAmount * 2);
+
+ /* Mint OTokens with a strategy that attempts to contribute to stability of pool. Try
+ * to mint so much OTokens that after deployment of liquidity pool ends up being balanced.
+ *
+ * To manage unpredictability minimal OTokens minted will always be at least equal or greater
+ * to the asset amount deployed. And never larger than twice the asset amount deployed even if
+ * it would have a further beneficial effect on pool stability.
+ */
+ IVault(vaultAddress).mintForStrategy(oTokensToAdd);
+
+ emit Deposit(address(oToken), address(lpToken), oTokensToAdd);
+
+ uint256[2] memory _amounts;
+ _amounts[assetCoinIndex] = _poolAssetAmount;
+ _amounts[oTokenCoinIndex] = oTokensToAdd;
+
+ uint256 valueInLpTokens = (_poolAssetAmount + oTokensToAdd)
+ .divPrecisely(_getVirtualPrice());
+ uint256 minMintAmount = valueInLpTokens.mulTruncate(
+ uint256(1e18) - MAX_SLIPPAGE
+ );
+
+ // Deposit to the AMO pool and receive LP tokens. eg OUSD/3CRV-f or OETH/ETH-f
+ uint256 lpDeposited = _addLiquidityToPool(_amounts, minMintAmount);
+
+ // Stake the AMO pool LP tokens for rewards
+ _stakeCurveLp(lpDeposited);
+ }
+
+ /**
+ * @notice Deposit the strategy's entire balance of assets into the AMO pool
+ */
+ function depositAll() external virtual override onlyVault nonReentrant {
+ uint256 vaultAssetBalance = vaultAsset.balanceOf(address(this));
+
+ emit Deposit(address(vaultAsset), address(lpToken), vaultAssetBalance);
+
+ uint256 poolAssetAmount = _toPoolAsset(
+ address(vaultAsset),
+ vaultAssetBalance
+ );
+ if (vaultAssetBalance > 0) {
+ _deposit(poolAssetAmount);
+ }
+ }
+
+ /***************************************
+ Withdraw
+ ****************************************/
+
+ /**
+ * @notice Withdraw pool asset and OToken from the AMO pool, burn the OTokens,
+ * convert pool asset to vault asset and transfer the vault asset to the recipient.
+ * @param _recipient Address to receive withdrawn asset which is normally the vault or redeemer.
+ * @param _vaultAsset Address of the vault asset token. eg WETH, frxETH, DAI, USDC or USDT
+ * @param _vaultAssetAmount Amount of vault asset tokens to withdraw.
+ */
+ function withdraw(
+ address _recipient,
+ address _vaultAsset,
+ uint256 _vaultAssetAmount
+ ) external override onlyVault onlyAsset(_vaultAsset) nonReentrant {
+ _withdraw(_recipient, _vaultAsset, _vaultAssetAmount);
+ }
+
+ /**
+ * @notice Withdraw pool asset and OToken from the AMO pool, burn the OTokens,
+ * convert pool asset to vault assets and transfer the vault assets to the recipient.
+ * @dev Only the OUSD Curve AMO supports withdrawing multiple assets in DAI, USDC and USDT.
+ * The default implementation only supports a single asset and must be overriden for the OUSD AMO.
+ * @param _recipient Address to receive withdrawn asset which is normally the vault or redeemer.
+ * @param _vaultAssets Addresses of the vault asset tokens. eg WETH, frxETH, DAI, USDC or USDT
+ * @param _vaultAssetAmounts Amounts of vault asset tokens to withdraw.
+ */
+ function withdraw(
+ address _recipient,
+ address[] memory _vaultAssets,
+ uint256[] memory _vaultAssetAmounts
+ ) external virtual onlyVault onlyAssets(_vaultAssets) nonReentrant {
+ // validate the number of assets matches the number of amounts
+ // The onlyAssets modified ensures the correct number of assets are supported.
+ // Most AMOs will be just one asset but for OUSD's 3CRV AMO it will be 3 assets.
+ require(
+ _vaultAssets.length == _vaultAssetAmounts.length,
+ "Assets and amounts mismatch"
+ );
+ _withdraw(_recipient, _vaultAssets[0], _vaultAssetAmounts[0]);
+ }
+
+ function _withdraw(
+ address _recipient,
+ address _vaultAsset,
+ uint256 _vaultAssetAmount
+ ) internal {
+ require(_vaultAssetAmount > 0, "Must withdraw something");
+
+ // Calc required number of pool assets for specified number of vault assets
+ uint256 poolAssetAmount = _calcPoolAsset(
+ _vaultAsset,
+ _vaultAssetAmount
+ );
+
+ uint256 requiredLpTokens = calcLpTokensToBurn(poolAssetAmount);
+
+ // Withdraw AMO pool LP tokens from the rewards pool
+ _unStakeLpTokens(requiredLpTokens);
+
+ /* math in calcLpTokensToBurn should correctly calculate the amount of Curve LP tokens
+ * to burn so the strategy receives enough asset tokens on balanced removal
+ */
+ uint256[2] memory _minWithdrawalAmounts = [uint256(0), uint256(0)];
+ _minWithdrawalAmounts[assetCoinIndex] = poolAssetAmount;
+ _removeLiquidityFromPool(requiredLpTokens, _minWithdrawalAmounts);
+
+ // Burn all the removed OTokens and any that was left in the strategy
+ uint256 oTokenToBurn = oToken.balanceOf(address(this));
+ if (oTokenToBurn > 0) {
+ IVault(vaultAddress).burnForStrategy(oTokenToBurn);
+
+ emit Withdrawal(address(oToken), address(lpToken), oTokenToBurn);
+ }
+
+ // Convert the pool assets in this strategy to the Vault asset
+ // and transfer them to the vault.
+ // Also emits the Withdraw event.
+ _withdrawAsset(_vaultAsset, _vaultAssetAmount, _recipient);
+ }
+
+ function calcLpTokensToBurn(uint256 _vaultAssetAmount)
+ 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 assets
+ * we want we can determine how much of OToken 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 poolAssetBalance = _getBalance(address(poolAsset));
+ /* 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()) / poolAssetBalance;
+ // prettier-ignore
+ // slither-disable-next-line divide-before-multiply
+ uint256 diff = (_vaultAssetAmount + 1) * k;
+ lpToBurn = diff / 1e36;
+ }
+
+ /**
+ * @notice Remove all assets and OTokens from the pool, burn the OTokens,
+ * transfer the assets to the Vault contract.
+ */
+ function withdrawAll() external override onlyVaultOrGovernor nonReentrant {
+ _unStakeAllLpTokens();
+
+ // Withdraws are proportional to assets held by the Curve pool
+ uint256[2] memory minWithdrawAmounts = [uint256(0), uint256(0)];
+
+ // Only withdraw from pool if the strategy has assets in the pool
+ uint256 poolLpTokens = lpToken.balanceOf(address(this));
+ if (poolLpTokens > 0) {
+ // Remove liquidity from the pool
+ _removeLiquidityFromPool(poolLpTokens, minWithdrawAmounts);
+
+ // Burn all the OTokens. eg OETH or OUSD
+ uint256 oTokenToBurn = oToken.balanceOf(address(this));
+ IVault(vaultAddress).burnForStrategy(oTokenToBurn);
+
+ emit Withdrawal(address(oToken), address(lpToken), oTokenToBurn);
+ }
+
+ // Convert all the pool assets in this strategy to Vault assets
+ // and transfer them to the vault.
+ // Also emits the Withdraw events for each vault asset.
+ _withdrawAllAsset(vaultAddress);
+ }
+
+ /***************************************
+ AMO Pool Rebalancing
+ ****************************************/
+
+ /**
+ * @notice Mint OTokens and one-sided add to the AMO pool.
+ * This is used when the AMO 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[2] memory _amounts;
+ _amounts[assetCoinIndex] = 0;
+ _amounts[oTokenCoinIndex] = _oTokens;
+
+ // Convert OTokens, eg OETH, to AMO pool LP tokens
+ uint256 valueInLpTokens = (_oTokens).divPrecisely(_getVirtualPrice());
+ // Apply slippage to LP tokens
+ uint256 minMintAmount = valueInLpTokens.mulTruncate(
+ uint256(1e18) - MAX_SLIPPAGE
+ );
+
+ // Add the minted OTokens to the AMO pool
+ uint256 lpDeposited = _addLiquidityToPool(_amounts, minMintAmount);
+
+ // Deposit the AMO pool LP tokens to the rewards pool. eg Convex or Aura
+ _stakeCurveLp(lpDeposited);
+
+ emit Deposit(address(oToken), address(lpToken), _oTokens);
+ }
+
+ /**
+ * @notice One-sided remove of OTokens from the AMO pool which are then burned.
+ * This is used when the AMO 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 AMO pool LP tokens to be burned for OTokens.
+ */
+ function removeAndBurnOTokens(uint256 _lpTokens)
+ external
+ onlyStrategist
+ nonReentrant
+ improvePoolBalance
+ {
+ // Withdraw AMO pool LP tokens from the rewards pool and remove OTokens from the AMO pool
+ uint256 oTokenToBurn = _withdrawAndRemoveFromPool(
+ _lpTokens,
+ address(oToken)
+ );
+
+ // The vault burns the OTokens from this strategy
+ IVault(vaultAddress).burnForStrategy(oTokenToBurn);
+
+ emit Withdrawal(address(oToken), address(lpToken), oTokenToBurn);
+ }
+
+ /**
+ * @notice One-sided remove of assets, eg ETH, from the AMO pool,
+ * convert to the asset and transfer to the vault.
+ * This is used when the AMO pool does not have enough OTokens and too many assets.
+ * 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 AMO pool LP tokens to be burned for pool assets.
+ * @dev Curve pool LP tokens is used rather than assets as Curve does not
+ * have a way to accurately calculate the amount of LP tokens for a required
+ * amount of assets. 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 AMO pool LP tokens required off-chain.
+ */
+ function removeOnlyAssets(uint256 _lpTokens)
+ external
+ onlyStrategist
+ nonReentrant
+ improvePoolBalance
+ {
+ // Withdraw AMO pool LP tokens from rewards pool and remove asset from the AMO pool
+ _withdrawAndRemoveFromPool(_lpTokens, address(poolAsset));
+
+ // Convert all the pool assets in this strategy to Vault assets
+ // and transfer them to the vault
+ _withdrawAllAsset(vaultAddress);
+ }
+
+ /**
+ * @dev Remove AMO pool LP tokens from the rewards pool and
+ * do a one-sided remove of pool assets or OTokens from the AMO pool.
+ * @param _lpTokens The amount of AMO pool LP tokens to be removed from the Convex pool.
+ * @param removeAsset The address of the AMO pool asset or OTokens to be removed.
+ * @return coinsRemoved The amount of assets or OTokens removed from the AMO pool.
+ */
+ function _withdrawAndRemoveFromPool(uint256 _lpTokens, address removeAsset)
+ internal
+ returns (uint256 coinsRemoved)
+ {
+ // Withdraw AMO pool LP tokens from rewards pool
+ _unStakeLpTokens(_lpTokens);
+
+ // Convert AMO pool LP tokens to asset value
+ uint256 valueInEth = _lpTokens.mulTruncate(_getVirtualPrice());
+ // Apply slippage to asset value
+ uint256 minAmount = valueInEth.mulTruncate(
+ uint256(1e18) - MAX_SLIPPAGE
+ );
+
+ coinsRemoved = _removeOneSidedLiquidityFromPool(
+ removeAsset,
+ _lpTokens,
+ minAmount
+ );
+ }
+
+ /***************************************
+ Assets
+ ****************************************/
+
+ /**
+ * @notice Returns bool indicating whether asset is supported by this strategy
+ * @param _vaultAsset Address of the vault asset
+ */
+ function supportsAsset(address _vaultAsset)
+ public
+ view
+ override
+ returns (bool)
+ {
+ return _isVaultAsset(_vaultAsset);
+ }
+
+ /**
+ * @notice Approve the spending of all assets by their corresponding pool tokens,
+ * if for some reason is it necessary.
+ */
+ function safeApproveAllTokens()
+ external
+ virtual
+ override
+ onlyGovernor
+ nonReentrant
+ {
+ _approveBase();
+ }
+
+ function _approveBase() internal virtual {}
+
+ /***************************************
+ Utils
+ ****************************************/
+
+ /**
+ * @dev Returns the largest of two numbers int256 version
+ */
+ function _max(int256 a, int256 b) internal pure returns (int256) {
+ return a >= b ? a : b;
+ }
+}
diff --git a/contracts/contracts/strategies/amo/BaseBalancerAMOStrategy.sol b/contracts/contracts/strategies/amo/BaseBalancerAMOStrategy.sol
new file mode 100644
index 0000000000..2fe1cddd75
--- /dev/null
+++ b/contracts/contracts/strategies/amo/BaseBalancerAMOStrategy.sol
@@ -0,0 +1,335 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+/**
+ * @title Abstract Aura Automated Market Maker (AMO) Strategy
+ * @notice Investment strategy for investing assets in Balancer and Aura pools
+ * @author Origin Protocol Inc
+ */
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import { BaseAMOStrategy, InitializableAbstractStrategy } from "./BaseAMOStrategy.sol";
+import { IConvexDeposits } from "../IConvexDeposits.sol";
+import { IRewardStaking } from "../IRewardStaking.sol";
+import { IBalancerVault } from "../../interfaces/balancer/IBalancerVault.sol";
+import { IRateProvider } from "../../interfaces/balancer/IRateProvider.sol";
+import { IERC4626 } from "../../../lib/openzeppelin/interfaces/IERC4626.sol";
+
+abstract contract BaseBalancerAMOStrategy is BaseAMOStrategy {
+ /// @notice Address of the Balancer vault
+ IBalancerVault public immutable balancerVault;
+ /// @notice Balancer pool identifier
+ bytes32 public immutable balancerPoolId;
+ /// @notice Address of the Aura rewards pool
+ address public immutable auraRewardPool;
+
+ struct BalancerConfig {
+ address balancerVault; // Address of the Balancer vault
+ bytes32 balancerPoolId; // Balancer pool identifier
+ address auraRewardPool; // Address of the Aura rewards pool
+ }
+
+ constructor(
+ BaseStrategyConfig memory _baseConfig,
+ AMOConfig memory _amoConfig,
+ BalancerConfig memory _balancerConfig
+ ) BaseAMOStrategy(_baseConfig, _amoConfig) {
+ balancerVault = IBalancerVault(_balancerConfig.balancerVault);
+ balancerPoolId = _balancerConfig.balancerPoolId;
+ auraRewardPool = _balancerConfig.auraRewardPool;
+ }
+
+ /**
+ * Initializer for setting up strategy internal state. This overrides the
+ * InitializableAbstractStrategy initializer as Curve strategies don't fit
+ * well within that abstraction.
+ * @param _rewardTokenAddresses Addresses of BAL & AURA tokens
+ */
+ function initialize(address[] calldata _rewardTokenAddresses)
+ external
+ onlyGovernor
+ initializer
+ {
+ address[] memory assets = new address[](1);
+ assets[0] = address(vaultAsset);
+ // pTokens are not used by this strategy
+ // it is only included for backward compatibility with the
+ // parent InitializableAbstractStrategy contract
+ address[] memory pTokens = new address[](1);
+ pTokens[0] = address(balancerVault);
+
+ InitializableAbstractStrategy._initialize(
+ _rewardTokenAddresses,
+ assets,
+ pTokens
+ );
+
+ _approveBase();
+ }
+
+ /***************************************
+ Balancer Pool
+ ****************************************/
+
+ /// @dev Adds ETH and/or OETH to the Curve pool
+ /// @param poolAmounts The amount of Balancer pool assets and OTokens to add to the pool
+ function _addLiquidityToPool(
+ uint256[2] memory poolAmounts,
+ uint256 minMintAmount
+ ) internal override returns (uint256 lpDeposited) {
+ // TODO do we need to check if the tokens in the pool have changed?
+ // slither-disable-next-line unused-return
+ (IERC20[] memory tokens, , ) = balancerVault.getPoolTokens(
+ balancerPoolId
+ );
+ require(tokens[oTokenCoinIndex] == oToken, "Invalid Balancer oToken");
+ require(tokens[assetCoinIndex] == poolAsset, "Invalid Balancer asset");
+
+ uint256[] memory amountsIn = new uint256[](tokens.length);
+ amountsIn[oTokenCoinIndex] = poolAmounts[oTokenCoinIndex];
+ amountsIn[assetCoinIndex] = poolAmounts[assetCoinIndex];
+
+ address[] memory poolAssets = new address[](tokens.length);
+ poolAssets[oTokenCoinIndex] = address(oToken);
+ poolAssets[assetCoinIndex] = address(poolAsset);
+
+ /* EXACT_TOKENS_IN_FOR_BPT_OUT:
+ * User sends precise quantities of tokens, and receives an
+ * estimated but unknown (computed at run time) quantity of BPT.
+ *
+ * ['uint256', 'uint256[]', 'uint256']
+ * [EXACT_TOKENS_IN_FOR_BPT_OUT, amountsIn, minimumBPT]
+ */
+ bytes memory userData = abi.encode(
+ IBalancerVault.WeightedPoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT,
+ amountsIn,
+ minMintAmount
+ );
+
+ IBalancerVault.JoinPoolRequest memory request = IBalancerVault
+ .JoinPoolRequest(poolAssets, amountsIn, userData, false);
+
+ // Add the pool assets in this strategy to the balancer pool
+ balancerVault.joinPool(
+ balancerPoolId,
+ address(this),
+ address(this),
+ request
+ );
+
+ lpDeposited = lpToken.balanceOf(address(this));
+ }
+
+ /// @dev Removes pool assets and/or OTokens from the Balancer pool.
+ /// Balancer will withdraw and exact amount of pool assets in exchange for a calculated amount of AMO LP tokens.
+ /// @param lpTokens The maximum amount of AMO pool LP tokens to be burnt.
+ /// @param poolAssetAmounts The exact amount of AMO pool assets to be removed.
+ function _removeLiquidityFromPool(
+ uint256 lpTokens,
+ uint256[2] memory poolAssetAmounts
+ ) internal override {
+ uint256[] memory poolAssetsAmountsOut = new uint256[](2);
+ poolAssetsAmountsOut[oTokenCoinIndex] = poolAssetAmounts[
+ oTokenCoinIndex
+ ];
+ poolAssetsAmountsOut[assetCoinIndex] = poolAssetAmounts[assetCoinIndex];
+
+ _removeBalancerLiquidity(lpTokens, poolAssetsAmountsOut);
+ }
+
+ /// @dev Removes either pool assets or OTokens from the Balancer pool.
+ /// Balancer will withdraw and exact amount of pool assets in exchange for a calculated amount of AMO LP tokens.
+ /// @param poolAsset The address of the AMO pool asset to be removed. eg OETH or WETH
+ /// @param lpTokens The maximum amount of AMO pool LP tokens to be burnt.
+ /// @param poolAssetAmount The exact amount of AMO pool assets to be removed.
+ function _removeOneSidedLiquidityFromPool(
+ address poolAsset,
+ uint256 lpTokens,
+ uint256 poolAssetAmount
+ ) internal override returns (uint256 coinsRemoved) {
+ uint128 coinIndex = _getCoinIndex(poolAsset);
+ uint256[] memory poolAssetsAmountsOut = new uint256[](2);
+ poolAssetsAmountsOut[coinIndex] = poolAssetAmount;
+
+ _removeBalancerLiquidity(lpTokens, poolAssetsAmountsOut);
+
+ // As we are withdrawing and exact amount of pool assets, the amount received
+ // is the same as the amount requested.
+ coinsRemoved = poolAssetAmount;
+ }
+
+ function _removeBalancerLiquidity(
+ uint256 lpTokens,
+ uint256[] memory poolAssetsAmountsOut
+ ) internal {
+ // TODO do we need to check if the tokens in the pool have changed?
+ // slither-disable-next-line unused-return
+ (IERC20[] memory tokens, , ) = balancerVault.getPoolTokens(
+ balancerPoolId
+ );
+ require(tokens[oTokenCoinIndex] == oToken, "Invalid Balancer oToken");
+ require(tokens[assetCoinIndex] == poolAsset, "Invalid Balancer asset");
+
+ address[] memory poolAssets = new address[](tokens.length);
+ poolAssets[oTokenCoinIndex] = address(oToken);
+ poolAssets[assetCoinIndex] = address(poolAsset);
+
+ /* Custom asset exit: BPT_IN_FOR_EXACT_TOKENS_OUT:
+ * User sends an estimated but unknown (computed at run time) quantity of BPT,
+ * and receives precise quantities of specified tokens.
+ *
+ * ['uint256', 'uint256[]', 'uint256']
+ * [BPT_IN_FOR_EXACT_TOKENS_OUT, amountsOut, maxBPTAmountIn]
+ */
+ bytes memory userData = abi.encode(
+ IBalancerVault.WeightedPoolExitKind.BPT_IN_FOR_EXACT_TOKENS_OUT,
+ poolAssetsAmountsOut,
+ lpTokens
+ );
+ IBalancerVault.ExitPoolRequest memory request = IBalancerVault
+ .ExitPoolRequest(
+ poolAssets,
+ /* We specify the exact amount of a tokens we are expecting in the encoded
+ * userData, for that reason we don't need to specify the amountsOut here.
+ *
+ * Also Balancer has a rounding issue that can make a transaction fail:
+ * https://github.com/balancer/balancer-v2-monorepo/issues/2541
+ * which is an extra reason why this field is empty.
+ */
+ new uint256[](tokens.length),
+ userData,
+ false
+ );
+ balancerVault.exitPool(
+ balancerPoolId,
+ address(this),
+ /* Payable keyword is required because of the IBalancerVault interface even though
+ * this strategy shall never be receiving native ETH
+ */
+ payable(address(this)),
+ request
+ );
+ }
+
+ /// @dev Returns the current balances of the AMO pool
+ function _getBalances()
+ internal
+ view
+ override
+ returns (uint256[2] memory balances)
+ {
+ // Get all the supported Balancer pool assets and balances
+ // slither-disable-next-line unused-return
+ (IERC20[] memory tokens, uint256[] memory allBalances, ) = balancerVault
+ .getPoolTokens(balancerPoolId);
+ require(tokens[oTokenCoinIndex] == oToken, "Invalid Balancer oToken");
+ require(tokens[assetCoinIndex] == poolAsset, "Invalid Balancer asset");
+
+ balances[oTokenCoinIndex] = allBalances[oTokenCoinIndex];
+ balances[assetCoinIndex] = allBalances[assetCoinIndex];
+ }
+
+ /// @dev Returns the current balances of the AMO pool
+ function _getBalance(address poolAsset)
+ internal
+ view
+ override
+ returns (uint256 balance)
+ {
+ uint128 coinIndex = _getCoinIndex(poolAsset);
+
+ // Get all the supported Balancer pool assets and balances
+ // slither-disable-next-line unused-return
+ (IERC20[] memory tokens, uint256[] memory allBalances, ) = balancerVault
+ .getPoolTokens(balancerPoolId);
+ require(
+ address(tokens[coinIndex]) == address(poolAsset),
+ "Invalid Balancer index"
+ );
+
+ balance = allBalances[coinIndex];
+ }
+
+ /// @dev Returns the price of one AMO pool LP token in base asset terms.
+ function _getVirtualPrice()
+ internal
+ view
+ override
+ returns (uint256 virtualPrice)
+ {
+ virtualPrice = IRateProvider(address(lpToken)).getRate();
+ }
+
+ /***************************************
+ Aura Reward Pool
+ ****************************************/
+
+ /// @dev Deposit the AMO pool LP tokens to the rewards pool.
+ /// eg Curve LP tokens into Convex or Balancer LP tokens into Aura
+ function _stakeCurveLp(uint256 lpAmount) internal override {
+ uint256 lpDeposited = IERC4626(auraRewardPool).deposit(
+ lpAmount,
+ address(this)
+ );
+ require(lpAmount == lpDeposited, "Aura LP != BPT");
+ }
+
+ /**
+ * @dev Withdraw `_lpAmount` Balancer Pool Tokens (BPT) from
+ * the Aura rewards pool to this strategy contract.
+ * @param _lpAmount Number of Balancer Pool Tokens (BPT) to withdraw
+ */
+ function _unStakeLpTokens(uint256 _lpAmount) internal override {
+ IRewardStaking(auraRewardPool).withdrawAndUnwrap(
+ _lpAmount,
+ true // also claim reward tokens
+ );
+ }
+
+ /**
+ * @dev Withdraw all Balancer Pool Tokens (BPT) from
+ * the Aura rewards pool to this strategy contract.
+ */
+ function _unStakeAllLpTokens() internal override {
+ // Get all the strategy's BPTs in Aura
+ // maxRedeem is implemented as balanceOf(address) in Aura
+ uint256 bptBalance = IERC4626(auraRewardPool).maxRedeem(address(this));
+
+ IRewardStaking(auraRewardPool).withdrawAndUnwrap(
+ bptBalance,
+ true // also claim reward tokens
+ );
+ }
+
+ /**
+ * @notice Collect accumulated BAL and AURA rewards and send to the Harvester.
+ */
+ function collectRewardTokens()
+ external
+ override
+ onlyHarvester
+ nonReentrant
+ {
+ /* Similar to Convex, calling this function collects both of the
+ * accrued BAL and AURA tokens.
+ */
+ IRewardStaking(auraRewardPool).getReward();
+ _collectRewardTokens();
+ }
+
+ /***************************************
+ Approvals
+ ****************************************/
+
+ function _approveBase() internal override {
+ // Approve Balancer vault for WETH and OETH (required for adding liquidity)
+ // slither-disable-next-line unused-return
+ oToken.approve(address(balancerVault), type(uint256).max);
+ // slither-disable-next-line unused-return
+ poolAsset.approve(address(balancerVault), type(uint256).max);
+
+ // Approve Aura rewards pool to transfer Balancer pool tokens (BPT)
+ // slither-disable-next-line unused-return
+ lpToken.approve(auraRewardPool, type(uint256).max);
+ }
+}
diff --git a/contracts/contracts/strategies/amo/BaseConvexAMOStrategy.sol b/contracts/contracts/strategies/amo/BaseConvexAMOStrategy.sol
new file mode 100644
index 0000000000..5a4ebbbfb3
--- /dev/null
+++ b/contracts/contracts/strategies/amo/BaseConvexAMOStrategy.sol
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+/**
+ * @title Abstract Convex Automated Market Maker (AMO) Strategy
+ * @notice Investment strategy for investing assets in Curve and Convex pools
+ * @author Origin Protocol Inc
+ */
+import { BaseAMOStrategy, InitializableAbstractStrategy } from "./BaseAMOStrategy.sol";
+import { ICurveETHPoolV1 } from "../curve/ICurveETHPoolV1.sol";
+import { IConvexDeposits } from "../IConvexDeposits.sol";
+import { IRewardStaking } from "../IRewardStaking.sol";
+
+abstract contract BaseConvexAMOStrategy is BaseAMOStrategy {
+ // New immutable variables that must be set in the constructor
+ address public immutable cvxDepositorAddress;
+ IRewardStaking public immutable cvxRewardStaker;
+ uint256 public immutable cvxDepositorPTokenId;
+ /// @notice The Curve pool that the strategy invests in
+ ICurveETHPoolV1 public immutable curvePool;
+
+ // Used to circumvent the stack too deep issue
+ struct ConvexConfig {
+ address cvxDepositorAddress; // Address of the Convex depositor(AKA booster) for this pool
+ address cvxRewardStakerAddress; // Address of the CVX rewards staker
+ uint256 cvxDepositorPTokenId; // Pid of the pool referred to by Depositor and staker
+ }
+
+ constructor(
+ BaseStrategyConfig memory _baseConfig,
+ AMOConfig memory _amoConfig,
+ ConvexConfig memory _convexConfig
+ ) BaseAMOStrategy(_baseConfig, _amoConfig) {
+ // Is the AMO pool contract. eg OETH/ETH, OUSD/3CRV or OETH/frxETH
+ curvePool = ICurveETHPoolV1(_baseConfig.platformAddress);
+
+ cvxDepositorAddress = _convexConfig.cvxDepositorAddress;
+ cvxRewardStaker = IRewardStaking(_convexConfig.cvxRewardStakerAddress);
+ cvxDepositorPTokenId = _convexConfig.cvxDepositorPTokenId;
+ }
+
+ /**
+ * 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 reward tokens CRV & CVX
+ */
+ function initialize(
+ address[] calldata _rewardTokenAddresses,
+ address[] calldata _assets,
+ address[] calldata _pTokens
+ ) external onlyGovernor initializer {
+ InitializableAbstractStrategy._initialize(
+ _rewardTokenAddresses,
+ _assets,
+ _pTokens
+ );
+
+ _approveBase();
+ }
+
+ /***************************************
+ Curve Pool
+ ****************************************/
+
+ /// @dev Adds pool assets (3CRV, frxETH or ETH) and/or OTokens (OUSD or OETH) to the Curve pool
+ /// @param poolAmounts The amount of Curve pool assets to add to the pool
+ /// @param minLpAmount The minimum amount of Curve pool LP tokens that is acceptable to receive
+ function _addLiquidityToPool(
+ uint256[2] memory poolAmounts,
+ uint256 minLpAmount
+ ) internal virtual override returns (uint256 lpDeposited) {
+ lpDeposited = curvePool.add_liquidity(poolAmounts, minLpAmount);
+ }
+
+ /// @dev Removes pool assets and/or OTokens from the Curve pool
+ /// @param lpTokens The amount of Curve pool LP tokens to be burnt
+ /// @param minPoolAssetAmounts The minimum amount of AMO pool assets that are acceptable to receive
+ function _removeLiquidityFromPool(
+ uint256 lpTokens,
+ uint256[2] memory minPoolAssetAmounts
+ ) internal override {
+ // slither-disable-next-line unused-return
+ curvePool.remove_liquidity(lpTokens, minPoolAssetAmounts);
+ }
+
+ /// @dev Removes either pool assets or OTokens from the Curve pool.
+ /// @param poolAsset The address of the Curve pool asset to be removed. eg OETH, OUSD, ETH, 3CRV, frxETH
+ /// @param lpTokens The amount of Curve pool LP tokens to be burnt
+ /// @param minPoolAssetAmount The minimum amount of Curve pool assets that are acceptable to receive. eg OETH or ETH
+ function _removeOneSidedLiquidityFromPool(
+ address poolAsset,
+ uint256 lpTokens,
+ uint256 minPoolAssetAmount
+ ) internal override returns (uint256 coinsRemoved) {
+ uint128 coinIndex = _getCoinIndex(poolAsset);
+
+ // Remove only one asset from the Curve pool
+ coinsRemoved = curvePool.remove_liquidity_one_coin(
+ lpTokens,
+ int128(coinIndex),
+ minPoolAssetAmount,
+ address(this)
+ );
+ }
+
+ /// @dev Returns the current balances of the Curve pool
+ function _getBalances()
+ internal
+ view
+ override
+ returns (uint256[2] memory balances)
+ {
+ balances = curvePool.get_balances();
+ }
+
+ /// @dev Returns the current balances of the Curve pool
+ function _getBalance(address poolAsset)
+ internal
+ view
+ override
+ returns (uint256 balance)
+ {
+ uint128 coinIndex = _getCoinIndex(poolAsset);
+ balance = curvePool.balances(uint128(coinIndex));
+ }
+
+ /// @dev Returns the price of one Curve pool LP token in base asset terms.
+ function _getVirtualPrice()
+ internal
+ view
+ override
+ returns (uint256 virtualPrice)
+ {
+ virtualPrice = curvePool.get_virtual_price();
+ }
+
+ /***************************************
+ Convex Reward Pool
+ ****************************************/
+
+ /// @dev Deposit the AMO pool LP tokens to the rewards pool.
+ /// eg Curve LP tokens into Convex or Balancer LP tokens into Aura
+ function _stakeCurveLp(uint256 lpDeposited) internal override {
+ require(
+ IConvexDeposits(cvxDepositorAddress).deposit(
+ cvxDepositorPTokenId,
+ lpDeposited,
+ true // Deposit with staking
+ ),
+ "Failed to Deposit LP to Convex"
+ );
+ }
+
+ /// @dev Withdraw a specific amount of AMO pool LP tokens from the rewards pool
+ /// eg Curve LP tokens from Convex or Balancer LP tokens from Aura
+ function _unStakeLpTokens(uint256 _lpAmount) internal override {
+ // withdraw and unwrap with claim takes back the lpTokens.
+ // Do not collect any reward tokens as that will be done via the harvester
+ cvxRewardStaker.withdrawAndUnwrap(_lpAmount, false);
+ }
+
+ /// @dev Withdraw all AMO pool LP tokens from the rewards pool
+ function _unStakeAllLpTokens() internal override {
+ uint256 gaugeTokens = cvxRewardStaker.balanceOf(address(this));
+ // withdraw and unwrap with claim takes back the lpTokens.
+ // Do not collect any reward tokens as that will be done via the harvester
+ cvxRewardStaker.withdrawAndUnwrap(gaugeTokens, false);
+ }
+
+ /**
+ * @notice Collect accumulated CRV and CVX rewards and send to the Harvester.
+ */
+ function collectRewardTokens()
+ external
+ override
+ onlyHarvester
+ nonReentrant
+ {
+ // Collect CRV and CVX
+ cvxRewardStaker.getReward();
+ _collectRewardTokens();
+ }
+}
diff --git a/contracts/contracts/strategies/amo/ConvexEthMetaStrategy.sol b/contracts/contracts/strategies/amo/ConvexEthMetaStrategy.sol
new file mode 100644
index 0000000000..7cbb7824df
--- /dev/null
+++ b/contracts/contracts/strategies/amo/ConvexEthMetaStrategy.sol
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+/**
+ * @title Convex Automated Market Maker (AMO) Strategy
+ * @notice AMO strategy for the Curve OETH/ETH pool
+ * @author Origin Protocol Inc
+ */
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import { BaseConvexAMOStrategy } from "./BaseConvexAMOStrategy.sol";
+import { IWETH9 } from "../../interfaces/IWETH9.sol";
+
+contract ConvexEthMetaStrategy is BaseConvexAMOStrategy {
+ // The following slots have been deprecated with immutable variables
+ // slither-disable-next-line constable-states
+ address private _deprecated_cvxDepositorAddress;
+ // slither-disable-next-line constable-states
+ address private _deprecated_cvxRewardStaker;
+ // slither-disable-next-line constable-states
+ uint256 private _deprecated_cvxDepositorPTokenId;
+ // slither-disable-next-line constable-states
+ address private _deprecated_curvePool;
+ // slither-disable-next-line constable-states
+ address private _deprecated_lpToken;
+ // slither-disable-next-line constable-states
+ address private _deprecated_oeth;
+ // slither-disable-next-line constable-states
+ address private _deprecated_weth;
+
+ // Ordered list of pool assets
+ // slither-disable-next-line constable-states
+ uint128 private _deprecated_oethCoinIndex = 1;
+ // slither-disable-next-line constable-states
+ uint128 private _deprecated_ethCoinIndex = 0;
+
+ // Added for backward compatibility
+ address public immutable oeth;
+ address public immutable weth;
+
+ constructor(
+ BaseStrategyConfig memory _baseConfig,
+ AMOConfig memory _amoConfig,
+ ConvexConfig memory _convexConfig
+ ) BaseConvexAMOStrategy(_baseConfig, _amoConfig, _convexConfig) {
+ oeth = _amoConfig.oTokenAddress;
+ weth = _amoConfig.vaultAssetAddress;
+ }
+
+ /***************************************
+ Vault to Pool Asset Conversions
+ ****************************************/
+
+ /// @dev Unwraps the ETH from WETH using WETH withdraw
+ function _toPoolAsset(address, uint256 wethAmount)
+ internal
+ override
+ returns (uint256 ethAmount)
+ {
+ IWETH9(address(vaultAsset)).withdraw(wethAmount);
+ ethAmount = wethAmount;
+ }
+
+ function _calcPoolAsset(address, uint256 wethAmount)
+ internal
+ pure
+ override
+ returns (uint256 ethAmount)
+ {
+ ethAmount = wethAmount;
+ }
+
+ /// @dev The OETH OToken is 1:1 to ETH so return the ETH amount
+ function _toOTokens(uint256 ethAmount)
+ internal
+ pure
+ override
+ returns (uint256 oethAmount)
+ {
+ oethAmount = ethAmount;
+ }
+
+ /***************************************
+ Curve Pool Deposits
+ ****************************************/
+
+ /// @dev Adds ETH and/or OETH to the Curve pool
+ /// @param poolAmounts The amount of Curve pool assets (ETH/OETH) to add to the pool
+ /// @param minLpAmount The minimum amount of Curve pool LP tokens that is acceptable to receive
+ function _addLiquidityToPool(
+ uint256[2] memory poolAmounts,
+ uint256 minLpAmount
+ ) internal override returns (uint256 lpDeposited) {
+ // ETH is passed into the Pool as value in the add_liquidity call
+ lpDeposited = curvePool.add_liquidity{
+ value: poolAmounts[assetCoinIndex]
+ }(poolAmounts, minLpAmount);
+ }
+
+ /***************************************
+ Curve Pool Withdrawals
+ ****************************************/
+
+ function _withdrawAsset(
+ address,
+ uint256 vaultAssetAmount,
+ address recipient
+ ) internal override {
+ require(address(this).balance >= vaultAssetAmount, "Insufficient ETH");
+
+ // Convert ETH to WETH
+ IWETH9(address(vaultAsset)).deposit{ value: vaultAssetAmount }();
+
+ // Transfer the WETH to the Vault
+ require(
+ vaultAsset.transfer(recipient, vaultAssetAmount),
+ "WETH transfer failed"
+ );
+
+ emit Withdrawal(
+ address(vaultAsset),
+ address(lpToken),
+ vaultAssetAmount
+ );
+ }
+
+ /// @dev Gets the ETH balance of this strategy contract
+ /// and then converts all the ETH to WETH
+ function _withdrawAllAsset(address recipient) internal override {
+ // Get ETH balance of this strategy contract
+ uint256 ethBalance = address(this).balance;
+ if (ethBalance > 0) {
+ _withdrawAsset(address(vaultAsset), ethBalance, recipient);
+ }
+ }
+
+ /***************************************
+ Asset Balance
+ ****************************************/
+
+ /**
+ * @notice Get the total asset value held in the platform
+ * @param _asset Address of the asset
+ * @return balance ETH value of both OETH and ETH in the Curve pool
+ */
+ function checkBalance(address _asset)
+ public
+ view
+ override
+ onlyAsset(_asset)
+ returns (uint256 balance)
+ {
+ // Eth balance needed here for the balance check that happens from vault during depositing.
+ balance = address(this).balance;
+ uint256 lpTokens = cvxRewardStaker.balanceOf(address(this));
+ if (lpTokens > 0) {
+ balance += (lpTokens * curvePool.get_virtual_price()) / 1e18;
+ }
+ }
+
+ /***************************************
+ Approvals
+ ****************************************/
+
+ /**
+ * @notice Accept unwrapped WETH
+ */
+ receive() external payable {}
+
+ /**
+ * @dev Since we are unwrapping WETH before depositing it to Curve
+ * there is no need to 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 override {
+ // Approve Curve pool for OETH (required for adding liquidity)
+ // No approval is needed for ETH
+ // slither-disable-next-line unused-return
+ oToken.approve(platformAddress, type(uint256).max);
+
+ // Approve Convex deposit contract to transfer Curve pool LP tokens
+ // This is needed for deposits if Curve pool LP tokens into the Convex rewards pool
+ // slither-disable-next-line unused-return
+ lpToken.approve(cvxDepositorAddress, type(uint256).max);
+ }
+}
diff --git a/contracts/contracts/strategies/amo/ConvexFrxETHAMOStrategy.sol b/contracts/contracts/strategies/amo/ConvexFrxETHAMOStrategy.sol
new file mode 100644
index 0000000000..cc4386572c
--- /dev/null
+++ b/contracts/contracts/strategies/amo/ConvexFrxETHAMOStrategy.sol
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+/**
+ * @title Convex Automated Market Maker (AMO) Strategy
+ * @notice AMO strategy for the Curve OETH/frxETH pool
+ * @author Origin Protocol Inc
+ */
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import { BaseConvexAMOStrategy } from "./BaseConvexAMOStrategy.sol";
+
+contract ConvexFrxETHAMOStrategy is BaseConvexAMOStrategy {
+ constructor(
+ BaseStrategyConfig memory _baseConfig,
+ AMOConfig memory _amoConfig,
+ ConvexConfig memory _convexConfig
+ ) BaseConvexAMOStrategy(_baseConfig, _amoConfig, _convexConfig) {}
+
+ /***************************************
+ Vault to Pool Asset Conversions
+ ****************************************/
+
+ /// @dev frxETH is the Vault asset and the Curve pool asset so
+ /// nothing to except return the vault asset amount
+ function _toPoolAsset(address, uint256 assets)
+ internal
+ pure
+ override
+ returns (uint256 poolAssets)
+ {
+ poolAssets = assets;
+ }
+
+ function _calcPoolAsset(address, uint256 vaultAssetAmount)
+ internal
+ pure
+ override
+ returns (uint256 poolAssetAmount)
+ {
+ poolAssetAmount = vaultAssetAmount;
+ }
+
+ /// @dev frxETH is the Vault asset and the pool asset so return the frxETH amount
+ /// @param poolAssetAmount Amount of frxETH to convert to OETH
+ function _toOTokens(uint256 poolAssetAmount)
+ internal
+ pure
+ override
+ returns (uint256 oethAmount)
+ {
+ // TODO - does this need to be converted to OETH amount using the Oracle?
+ oethAmount = poolAssetAmount;
+ }
+
+ /***************************************
+ Curve Pool Withdrawals
+ ****************************************/
+
+ /// @dev transfers the specified frxETH amount to the recipient
+ function _withdrawAsset(
+ address,
+ uint256 vaultAssetAmount,
+ address _recipient
+ ) internal override {
+ // Transfer the frxETH to the Vault
+ require(
+ vaultAsset.transfer(_recipient, vaultAssetAmount),
+ "frxETH transfer failed"
+ );
+
+ emit Withdrawal(
+ address(vaultAsset),
+ address(lpToken),
+ vaultAssetAmount
+ );
+ }
+
+ /// @dev transfers the frxETH balance of this strategy contract to the recipient
+ function _withdrawAllAsset(address _recipient) internal override {
+ uint256 vaultAssets = vaultAsset.balanceOf(address(this));
+
+ if (vaultAssets > 0) {
+ _withdrawAsset(address(vaultAsset), vaultAssets, _recipient);
+ }
+ }
+
+ /***************************************
+ Asset Balance
+ ****************************************/
+
+ /**
+ * @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
+ onlyAsset(_asset)
+ returns (uint256 balance)
+ {
+ // Get the Curve LP tokens staked in the Convex pool
+ uint256 curveLpTokens = cvxRewardStaker.balanceOf(address(this));
+
+ /* We intentionally omit any Curve LP tokens held by this strategy that
+ * may have been left by a withdraw as this should be dust amounts.
+ * Under normal operation, there should not be any frxETH assets in this strategy so it is also ignore.
+ * We also ignore any assets accedentally sent to this contract by a 3rd party. eg ETH.
+ */
+
+ if (curveLpTokens > 0) {
+ balance = (curveLpTokens * curvePool.get_virtual_price()) / 1e18;
+
+ // Unlike the OUSD Vault assets, the OETH Vault assets do not need to be scaled down from 18 decimals
+ // as they are all 18 decimals
+ }
+ }
+
+ /***************************************
+ Approvals
+ ****************************************/
+
+ /**
+ * @dev Since we are unwrapping WETH before depositing it to Curve
+ * there is no need to 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 override {
+ // Approve Curve pool for frxETH and OETH (required for adding liquidity)
+ // slither-disable-next-line unused-return
+ oToken.approve(platformAddress, type(uint256).max);
+ // slither-disable-next-line unused-return
+ poolAsset.approve(platformAddress, type(uint256).max);
+
+ // Approve Convex deposit contract to transfer Curve pool LP tokens
+ // This is needed for deposits if Curve pool LP tokens into the Convex rewards pool
+ // slither-disable-next-line unused-return
+ lpToken.approve(cvxDepositorAddress, type(uint256).max);
+ }
+}
diff --git a/contracts/contracts/strategies/amo/ConvexOUSDMetaStrategy.sol b/contracts/contracts/strategies/amo/ConvexOUSDMetaStrategy.sol
new file mode 100644
index 0000000000..b8a321caa6
--- /dev/null
+++ b/contracts/contracts/strategies/amo/ConvexOUSDMetaStrategy.sol
@@ -0,0 +1,487 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+/**
+ * @title Convex Automated Market Maker (AMO) Strategy
+ * @notice AMO strategy for the Curve OUSD/3CRV pool
+ * @author Origin Protocol Inc
+ */
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+
+import { BaseConvexAMOStrategy } from "./BaseConvexAMOStrategy.sol";
+import { ICurvePool } from "../curve/ICurvePool.sol";
+import { StableMath } from "../../utils/StableMath.sol";
+
+contract ConvexOUSDMetaStrategy is BaseConvexAMOStrategy {
+ using StableMath for uint256;
+ using SafeERC20 for IERC20;
+
+ uint256 internal constant THREEPOOL_ASSET_COUNT = 3;
+
+ address public immutable DAI;
+ address public immutable USDC;
+ address public immutable USDT;
+ ICurvePool public immutable curve3Pool;
+
+ // The following slots have been deprecated with immutable variables
+ // slither-disable-next-line constable-states
+ address private _deprecated_pTokenAddresses;
+ // slither-disable-next-line constable-states
+ int256[49] private __reserved;
+ // slither-disable-next-line constable-states
+ address private _deprecated_cvxDepositorAddress;
+ // slither-disable-next-line constable-states
+ address private _deprecated_cvxRewardStakerAddress;
+ // slither-disable-next-line constable-states
+ uint256 private _deprecated_cvxDepositorPTokenId;
+ // slither-disable-next-line constable-states
+ address private _deprecated_metapool;
+ // slither-disable-next-line constable-states
+ address private _deprecated_metapoolMainToken;
+ // slither-disable-next-line constable-states
+ address private _deprecated_metapoolLPToken;
+ // slither-disable-next-line constable-states
+ address[] private _deprecated_metapoolAssets;
+ // slither-disable-next-line constable-states
+ uint256 private _deprecated_maxWithdrawalSlippage;
+ // slither-disable-next-line constable-states
+ uint128 private _deprecated_crvCoinIndex;
+ // slither-disable-next-line constable-states
+ uint128 private _deprecated_mainCoinIndex;
+
+ constructor(
+ BaseStrategyConfig memory _baseConfig,
+ AMOConfig memory _amoConfig,
+ ConvexConfig memory _convexConfig,
+ address _curve3Pool,
+ address[3] memory _curve3PoolAssets
+ ) BaseConvexAMOStrategy(_baseConfig, _amoConfig, _convexConfig) {
+ DAI = _curve3PoolAssets[0];
+ USDC = _curve3PoolAssets[1];
+ USDT = _curve3PoolAssets[2];
+ curve3Pool = ICurvePool(_curve3Pool);
+ }
+
+ /***************************************
+ Vault Asset Validation
+ ****************************************/
+
+ function _isVaultAsset(address _vaultAsset)
+ internal
+ view
+ override
+ returns (bool supported)
+ {
+ supported =
+ _vaultAsset == DAI ||
+ _vaultAsset == USDC ||
+ _vaultAsset == USDT;
+ }
+
+ /// @dev Returns bool indicating whether all the assets are supported by this strategy.
+ /// For the OUSD AMO, this is DAI, USDC or USDT.
+ /// @param _vaultAssets Addresses of the vault assets
+ function _isVaultAssets(address[] memory _vaultAssets)
+ internal
+ view
+ override
+ returns (bool)
+ {
+ if (_vaultAssets.length == 1) {
+ return _isVaultAsset(_vaultAssets[0]);
+ } else if (_vaultAssets.length == 2) {
+ return
+ _isVaultAsset(_vaultAssets[0]) &&
+ _isVaultAsset(_vaultAssets[1]);
+ } else if (_vaultAssets.length == 2) {
+ return
+ _isVaultAsset(_vaultAssets[0]) &&
+ _isVaultAsset(_vaultAssets[1]) &&
+ _isVaultAsset(_vaultAssets[2]);
+ }
+ require(_vaultAssets.length > 0, "No asset");
+ revert("Only three assets supported");
+ }
+
+ /***************************************
+ Curve Pool
+ ****************************************/
+
+ /**
+ * @dev Get the asset token's index position in the Curve 3Pool
+ * and the token's decimals.
+ */
+ function _coinIndexDecimals(address _asset)
+ internal
+ view
+ returns (uint256 index, uint256 decimals)
+ {
+ if (_asset == DAI) {
+ return (0, 18);
+ } else if (_asset == USDC) {
+ return (1, 6);
+ } else if (_asset == USDT) {
+ return (2, 6);
+ }
+ revert("Unsupported asset");
+ }
+
+ /***************************************
+ Vault to Pool Asset Conversions
+ ****************************************/
+
+ /// @dev DAI, USDC or USDC is the Vault asset and the Curve 3Pool lp token 3CRV is
+ /// is Curve's OUSD/3CRV Metapool asset
+ function _toPoolAsset(address _asset, uint256 _amount)
+ internal
+ override
+ returns (uint256 poolAssets)
+ {
+ (uint256 poolCoinIndex, uint256 decimals) = _coinIndexDecimals(_asset);
+
+ // 3Pool requires passing deposit amounts for all 3 assets, set to 0 for all
+ uint256[3] memory _amounts;
+ // Set the amount on the asset we want to deposit
+ _amounts[poolCoinIndex] = _amount;
+ uint256 minMintAmount = _amount
+ .scaleBy(18, decimals)
+ .divPrecisely(curve3Pool.get_virtual_price())
+ .mulTruncate(uint256(1e18) - MAX_SLIPPAGE);
+
+ // Do the deposit to 3pool
+ curve3Pool.add_liquidity(_amounts, minMintAmount);
+
+ poolAssets = poolAsset.balanceOf(address(this));
+ }
+
+ /**
+ * @dev Calculate amount of LP required when withdrawing specific amount of one
+ * of the underlying assets accounting for fees and slippage.
+ *
+ * Curve pools unfortunately do not contain a calculation function for
+ * amount of LP required when withdrawing a specific amount of one of the
+ * underlying tokens and also accounting for fees (Curve's calc_token_amount
+ * does account for slippage but not fees).
+ *
+ * Steps taken to calculate the metric:
+ * - get amount of LP required if fees wouldn't apply
+ * - increase the LP amount as if fees would apply to the entirety of the underlying
+ * asset withdrawal. (when withdrawing only one coin fees apply only to amounts
+ * of other assets pool would return in case of balanced removal - since those need
+ * to be swapped for the single underlying asset being withdrawn)
+ * - get amount of underlying asset withdrawn (this Curve function does consider slippage
+ * and fees) when using the increased LP amount. As LP amount is slightly over-increased
+ * so is amount of underlying assets returned.
+ * - since we know exactly how much asset we require take the rate of LP required for asset
+ * withdrawn to get the exact amount of LP.
+ */
+ function _calcPoolAsset(address _vaultAsset, uint256 _vaultAssetAmount)
+ internal
+ override
+ returns (uint256 required3Crv)
+ {
+ (uint256 coinIndex, ) = _coinIndexDecimals(_vaultAsset);
+ uint256[3] memory _amounts = [uint256(0), uint256(0), uint256(0)];
+ _amounts[coinIndex] = _vaultAssetAmount;
+
+ // 3Pool LP required when removing reuiqred vault assets ignoring fees
+ uint256 lpRequiredNoFees = curve3Pool.calc_token_amount(
+ _amounts,
+ false
+ );
+ /* 3Pool LP required if fees would apply to entirety of removed amount
+ *
+ * fee is 1e10 denominated number: https://curve.readthedocs.io/exchange-pools.html#StableSwap.fee
+ */
+ uint256 lpRequiredFullFees = lpRequiredNoFees.mulTruncateScale(
+ 1e10 + curve3Pool.fee(),
+ 1e10
+ );
+
+ /* asset received when withdrawing full fee applicable LP accounting for
+ * slippage and fees
+ */
+ uint256 assetReceivedForFullLPFees = curve3Pool.calc_withdraw_one_coin(
+ lpRequiredFullFees,
+ int128(uint128(coinIndex))
+ );
+
+ // exact amount of LP required
+ required3Crv =
+ (lpRequiredFullFees * _vaultAssetAmount) /
+ assetReceivedForFullLPFees;
+ }
+
+ /// @dev Converts 3CRV to OUSD by using the 3Pool virtual price
+ function _toOTokens(uint256 threeCrvAmount)
+ internal
+ view
+ override
+ returns (uint256 ousdAmount)
+ {
+ uint256 virtualPrice = curve3Pool.get_virtual_price();
+ ousdAmount = threeCrvAmount.mulTruncate(virtualPrice);
+ }
+
+ /***************************************
+ Curve Pool Deposits
+ ****************************************/
+
+ /**
+ * @notice Deposit multiple vault assets into the AMO strategy.
+ * @param _vaultAssets Addresses of the vault asset tokens. eg DAI, USDC or USDT
+ * @param _vaultAssetAmounts Amounts of vault asset tokens to deposit.
+ */
+ function deposit(
+ address[] memory _vaultAssets,
+ uint256[] memory _vaultAssetAmounts
+ ) external override onlyVault nonReentrant {
+ uint256[3] memory amounts = [uint256(0), uint256(0), uint256(0)];
+ uint256 depositValue = 0;
+ uint256 curve3PoolVirtualPrice = curve3Pool.get_virtual_price();
+
+ for (uint256 i = 0; i < _vaultAssets.length; ++i) {
+ require(_isVaultAsset(_vaultAssets[i]), "Unsupported asset");
+ if (_vaultAssetAmounts[i] > 0) {
+ depositValue += _addAmount(
+ _vaultAssetAmounts[i],
+ amounts,
+ _vaultAssets[i],
+ curve3PoolVirtualPrice
+ );
+ }
+ }
+ uint256 minMintAmount = depositValue.mulTruncate(
+ uint256(1e18) - MAX_SLIPPAGE
+ );
+
+ // deposit DAI, USDC and/or USDT to the Curve 3Pool
+ curve3Pool.add_liquidity(amounts, minMintAmount);
+
+ // Get the Curve OUSD/3CRV Metapool LP token balance of this strategy contract
+ uint256 threePoolLpBalance = poolAsset.balanceOf(address(this));
+
+ // AMO deposit to the Curve Metapool
+ _deposit(threePoolLpBalance);
+ }
+
+ function depositAll() external override onlyVault nonReentrant {
+ uint256[3] memory amounts = [uint256(0), uint256(0), uint256(0)];
+ uint256 depositValue = 0;
+ uint256 curve3PoolVirtualPrice = curve3Pool.get_virtual_price();
+
+ depositValue = _addBalanceToAmounts(
+ amounts,
+ DAI,
+ curve3PoolVirtualPrice
+ );
+ depositValue += _addBalanceToAmounts(
+ amounts,
+ USDC,
+ curve3PoolVirtualPrice
+ );
+ depositValue += _addBalanceToAmounts(
+ amounts,
+ USDT,
+ curve3PoolVirtualPrice
+ );
+
+ uint256 minMintAmount = depositValue.mulTruncate(
+ uint256(1e18) - MAX_SLIPPAGE
+ );
+
+ // deposit DAI, USDC and/or USDT to the Curve 3Pool
+ curve3Pool.add_liquidity(amounts, minMintAmount);
+
+ // Get the Curve OUSD/3CRV Metapool LP token balance of this strategy contract
+ uint256 threePoolLpBalance = poolAsset.balanceOf(address(this));
+
+ // AMO deposit to the Curve Metapool
+ _deposit(threePoolLpBalance);
+ }
+
+ function _addBalanceToAmounts(
+ uint256[3] memory amounts,
+ address usdAsset,
+ uint256 curve3PoolVirtualPrice
+ ) internal returns (uint256 depositValue) {
+ uint256 balance = IERC20(usdAsset).balanceOf(address(this));
+ if (balance > 0) {
+ depositValue = _addAmount(
+ balance,
+ amounts,
+ usdAsset,
+ curve3PoolVirtualPrice
+ );
+ }
+ }
+
+ function _addAmount(
+ uint256 amount,
+ uint256[3] memory amounts,
+ address usdAsset,
+ uint256 curve3PoolVirtualPrice
+ ) internal returns (uint256 depositValue) {
+ (uint256 poolCoinIndex, uint256 assetDecimals) = _coinIndexDecimals(
+ usdAsset
+ );
+ // Set the amount on the asset we want to deposit
+ amounts[poolCoinIndex] = amount;
+ // Get value of deposit in Curve LP token to later determine
+ // the minMintAmount argument for add_liquidity
+ depositValue = amount.scaleBy(18, assetDecimals).divPrecisely(
+ curve3PoolVirtualPrice
+ );
+ emit Deposit(usdAsset, address(lpToken), amount);
+ }
+
+ /***************************************
+ Curve Withdrawals
+ ****************************************/
+
+ function withdraw(
+ address,
+ address[] memory _vaultAssets,
+ uint256[] memory
+ ) external override onlyVault onlyAssets(_vaultAssets) nonReentrant {
+ // TODO add support for withdrawing multiple 3Pool assets
+ revert("Not supported");
+ }
+
+ /// @dev Converts all 3CRV in this strategy to the Vault asset DAI, USDC or USDT
+ /// by removing liquidity from the Curve 3Pool.
+ /// Then transfers each vault asset to the vault.
+ function _withdrawAsset(
+ address vaultAsset,
+ uint256 vaultAssetAmount,
+ address recipient
+ ) internal override {
+ uint256[3] memory _amounts = [uint256(0), uint256(0), uint256(0)];
+ (uint256 coinIndex, ) = _coinIndexDecimals(vaultAsset);
+ _amounts[coinIndex] = vaultAssetAmount;
+
+ // Remove just the specified vault asset from the 3Pool
+ // using all the previously removed 3CRV assets from the AMO pool
+ curve3Pool.remove_liquidity_imbalance(
+ _amounts,
+ IERC20(poolAsset).balanceOf(address(this))
+ );
+
+ // Transfer assets to the Vault
+ // Note that Curve will provide all 3 of the assets in 3pool even if
+ // we have not set PToken addresses for all of them in this strategy
+ _transferAssetBalance(recipient, IERC20(vaultAsset));
+ }
+
+ /// @dev Converts all 3CRV in this strategy to the Vault assets DAI, USDC and USDT
+ /// by removing liquidity from the Curve OUSD/3CRV Metapool.
+ /// Then transfers each vault asset to the vault.
+ function _withdrawAllAsset(address recipient) internal override {
+ // Withdraws are proportional to assets held by 3Pool
+ uint256[3] memory minWithdrawAmounts = [
+ uint256(0),
+ uint256(0),
+ uint256(0)
+ ];
+
+ // Remove all liquidity from the 3Pool
+ curve3Pool.remove_liquidity(
+ IERC20(poolAsset).balanceOf(address(this)),
+ minWithdrawAmounts
+ );
+
+ // Transfer assets to the Vault
+ // Note that Curve will provide all 3 of the assets in 3pool even if
+ // we have not set PToken addresses for all of them in this strategy
+ _transferAssetBalance(recipient, IERC20(DAI));
+ _transferAssetBalance(recipient, IERC20(USDC));
+ _transferAssetBalance(recipient, IERC20(USDT));
+ }
+
+ function _transferAssetBalance(address _recipient, IERC20 asset) internal {
+ uint256 assetBalance = asset.balanceOf(address(this));
+ if (assetBalance > 0) {
+ asset.safeTransfer(_recipient, assetBalance);
+ emit Withdrawal(address(asset), address(lpToken), assetBalance);
+ }
+ }
+
+ /***************************************
+ Asset Balance
+ ****************************************/
+
+ /**
+ * @notice Get the total asset value held in the platform
+ * @param _vaultAsset Address of the vault asset
+ * @return balance Total value of the asset in the platform
+ */
+ function checkBalance(address _vaultAsset)
+ public
+ view
+ override
+ onlyAsset(_vaultAsset)
+ returns (uint256 balance)
+ {
+ // 3Pool LP tokens (3Crv) in this strategy contract.
+ // This should generally be nothing as we should always stake
+ // the full balance in the Gauge, but include for safety
+ uint256 threePoolTokens = IERC20(poolAsset).balanceOf(address(this));
+ if (threePoolTokens > 0) {
+ // Getting the virtual price is gas expensive so only do if we have Cuve LP tokens
+ balance = threePoolTokens.mulTruncate(
+ curve3Pool.get_virtual_price()
+ );
+ }
+
+ /* We intentionally omit the Curve Metapool LP tokens held by this strategy
+ * since the contract should never (except in the middle of deposit/withdrawal
+ * transaction) hold any amount of those tokens in normal operation. There
+ * could be tokens sent to it by a 3rd party and we decide to actively ignore
+ * those.
+ */
+
+ // Curve LP tokens in the Convex rewards pool
+ uint256 curveLpTokens = cvxRewardStaker.balanceOf(address(this));
+
+ if (curveLpTokens > 0) {
+ // Getting the virtual price is gas expensive so only do if we have Cuve LP tokens
+ balance += curveLpTokens.mulTruncate(curvePool.get_virtual_price());
+ }
+
+ // Scale down from 18 decimals used by 3Crv to asset decimals. eg 6 for USDC or USDT
+ (, uint256 assetDecimals) = _coinIndexDecimals(_vaultAsset);
+ balance = balance.scaleBy(assetDecimals, 18) / THREEPOOL_ASSET_COUNT;
+ }
+
+ /***************************************
+ Approvals
+ ****************************************/
+
+ // solhint-disable-next-line no-unused-vars
+ function _abstractSetPToken(address _asset, address _pToken)
+ internal
+ override
+ {}
+
+ function _approveBase() internal override {
+ // Approve Curve 3Pool for DAI, USDC and USDT
+ // slither-disable-next-line unused-return
+ IERC20(DAI).approve(address(curve3Pool), type(uint256).max);
+ // slither-disable-next-line unused-return
+ IERC20(USDC).approve(address(curve3Pool), type(uint256).max);
+ // slither-disable-next-line unused-return
+ IERC20(USDT).approve(address(curve3Pool), type(uint256).max);
+
+ // Approve Curve OUSD/3CRV Metapool for 3CRV and OUSD (required for adding liquidity)
+ // slither-disable-next-line unused-return
+ oToken.approve(address(curvePool), type(uint256).max);
+ // slither-disable-next-line unused-return
+ poolAsset.approve(address(curvePool), type(uint256).max);
+
+ // Approve Convex deposit contract to transfer Curve pool LP tokens
+ // This is needed for deposits if Curve pool LP tokens into the Convex rewards pool
+ // slither-disable-next-line unused-return
+ lpToken.approve(cvxDepositorAddress, type(uint256).max);
+ }
+}
diff --git a/contracts/contracts/strategies/amo/README.md b/contracts/contracts/strategies/amo/README.md
new file mode 100644
index 0000000000..5399af05ff
--- /dev/null
+++ b/contracts/contracts/strategies/amo/README.md
@@ -0,0 +1,59 @@
+# Diagrams
+
+![AMO Contracts](../../../docs/AMOContractHierarchy.svg)
+
+## Convex AMO Strategy for OETH/ETH
+
+### Hierarchy
+
+![Convex OETH/ETH AMO Strategy Hierarchy](../../../docs/ConvexEthMetaStrategyHierarchy.svg)
+
+### Squashed
+
+![Convex OETH/ETH AMO Strategy Squashed](../../../docs/ConvexEthMetaStrategySquashed.svg)
+
+### Storage
+
+![Convex OETH/ETH AMO Strategy Storage](../../../docs/ConvexEthMetaStrategyStorage.svg)
+
+## Convex AMO Strategy for OETH/frxETH
+
+### Hierarchy
+
+![Convex frxETH AMO Strategy Hierarchy](../../../docs/ConvexFrxETHAMOStrategyHierarchy.svg)
+
+### Squashed
+
+![Convex frxETH AMO Strategy Squashed](../../../docs/ConvexFrxETHAMOStrategySquashed.svg)
+
+### Storage
+
+![Convex frxETH AMO Strategy Storage](../../../docs/ConvexFrxETHAMOStrategyStorage.svg)
+
+## Convex AMO Strategy for OUSD/3CRV
+
+### Hierarchy
+
+![Convex OUSD AMO Strategy Hierarchy](../../../docs/ConvexOUSDMetaStrategyHierarchy.svg)
+
+### Squashed
+
+![Convex OUSD AMO Strategy Squashed](../../../docs/ConvexOUSDMetaStrategySquashed.svg)
+
+### Storage
+
+![Convex OUSD AMO Strategy Storage](../../../docs/ConvexOUSDMetaStrategyStorage.svg)
+
+## Balancer AMO Strategy for OETH/WETH
+
+### Hierarchy
+
+![Balancer OETH AMO Strategy Hierarchy](../../../docs/BalancerEthAMOStrategyHierarchy.svg)
+
+### Squashed
+
+![Balancer OETH AMO Strategy Squashed](../../../docs/BalancerEthAMOStrategySquashed.svg)
+
+### Storage
+
+![Balancer OETH AMO Strategy Storage](../../../docs/BalancerEthAMOStrategyStorage.svg)
diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol
index 5d4adcf2e1..6b90906d8d 100644
--- a/contracts/contracts/vault/VaultAdmin.sol
+++ b/contracts/contracts/vault/VaultAdmin.sol
@@ -14,9 +14,9 @@ import { ISwapper } from "../interfaces/ISwapper.sol";
import { IVault } from "../interfaces/IVault.sol";
import { StableMath } from "../utils/StableMath.sol";
-import "./VaultStorage.sol";
+import "./VaultInitializer.sol";
-contract VaultAdmin is VaultStorage {
+contract VaultAdmin is VaultInitializer {
using SafeERC20 for IERC20;
using StableMath for uint256;
@@ -104,7 +104,7 @@ contract VaultAdmin is VaultStorage {
* @notice Set the default Strategy for an asset, i.e. the one which the asset
will be automatically allocated to and withdrawn from
* @param _asset Address of the asset
- * @param _strategy Address of the Strategy
+ * @param _strategy Address of the strategy contract or zero address if removing
*/
function setAssetDefaultStrategy(address _asset, address _strategy)
external
@@ -128,15 +128,16 @@ contract VaultAdmin is VaultStorage {
/**
* @notice Set maximum amount of OTokens that can at any point be minted and deployed
- * to strategy (used only by ConvexOUSDMetaStrategy for now).
+ * to a strategy. This is only used by the AMO strategies for now.
+ * @param _strategy Address of the strategy contract
* @param _threshold OToken amount with 18 fixed decimals.
*/
- function setNetOusdMintForStrategyThreshold(uint256 _threshold)
+ function setMintForStrategyThreshold(address _strategy, uint256 _threshold)
external
onlyGovernor
{
/**
- * Because `netOusdMintedForStrategy` check in vault core works both ways
+ * Because `mintedForStrategy` check in vault core works both ways
* (positive and negative) the actual impact of the amount of OToken minted
* could be double the threshold. E.g.:
* - contract has threshold set to 100
@@ -149,9 +150,14 @@ contract VaultAdmin is VaultStorage {
* amount and not have problems with current netOusdMinted being near
* limits on either side.
*/
- netOusdMintedForStrategy = 0;
- netOusdMintForStrategyThreshold = _threshold;
- emit NetOusdMintForStrategyThresholdChanged(_threshold);
+ // read from storage
+ Strategy memory strategyConfig = strategies[_strategy];
+ strategyConfig.mintForStrategy = int96(0);
+ strategyConfig.mintForStrategyThreshold = toInt96(_threshold);
+ // write back to storage
+ strategies[_strategy] = strategyConfig;
+
+ emit MintForStrategyThresholdChanged(_strategy, _threshold);
}
/***************************************
@@ -362,7 +368,12 @@ contract VaultAdmin is VaultStorage {
*/
function approveStrategy(address _addr) external onlyGovernor {
require(!strategies[_addr].isSupported, "Strategy already approved");
- strategies[_addr] = Strategy({ isSupported: true, _deprecated: 0 });
+ strategies[_addr] = Strategy({
+ isSupported: true,
+ isAMO: false,
+ mintForStrategy: 0,
+ mintForStrategyThreshold: 0
+ });
allStrategies.push(_addr);
emit StrategyApproved(_addr);
}
@@ -527,15 +538,16 @@ contract VaultAdmin is VaultStorage {
}
/**
- * @notice Set OToken Metapool strategy
- * @param _ousdMetaStrategy Address of OToken metapool strategy
+ * @notice Flag if a strategy is an AMO that is allowed to mint/burn OTokens.
+ * @param _strategyAddress Address of the strategy
+ * @param _isAMO true if AMO strategy
*/
- function setOusdMetaStrategy(address _ousdMetaStrategy)
+ function setAMOStrategy(address _strategyAddress, bool _isAMO)
external
onlyGovernor
{
- ousdMetaStrategy = _ousdMetaStrategy;
- emit OusdMetaStrategyUpdated(_ousdMetaStrategy);
+ strategies[_strategyAddress].isAMO = _isAMO;
+ emit AMOStrategyUpdated(_strategyAddress, _isAMO);
}
/***************************************
diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol
index 3eea4968fd..3a7f4fa3d3 100644
--- a/contracts/contracts/vault/VaultCore.sol
+++ b/contracts/contracts/vault/VaultCore.sol
@@ -22,11 +22,6 @@ import "./VaultInitializer.sol";
contract VaultCore is VaultInitializer {
using SafeERC20 for IERC20;
using StableMath for uint256;
- // max signed int
- uint256 internal constant MAX_INT = 2**255 - 1;
- // max un-signed int
- uint256 internal constant MAX_UINT =
- 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
/**
* @dev Verifies that the rebasing is not paused.
@@ -44,14 +39,6 @@ contract VaultCore is VaultInitializer {
_;
}
- modifier onlyOusdMetaStrategy() {
- require(
- msg.sender == ousdMetaStrategy,
- "Caller is not the OUSD meta strategy"
- );
- _;
- }
-
/**
* @notice Deposit a supported asset and mint OTokens.
* @param _asset Address of the asset being deposited
@@ -97,7 +84,7 @@ contract VaultCore is VaultInitializer {
}
/**
- * @notice Mint OTokens for a Metapool Strategy
+ * @notice Mint OTokens for an AMO strategy.
* @param _amount Amount of the asset being deposited
*
* Notice: can't use `nonReentrant` modifier since the `mint` function can
@@ -109,23 +96,27 @@ contract VaultCore is VaultInitializer {
* that are moving funds between the Vault and end user wallets can influence strategies
* utilizing this function.
*/
- function mintForStrategy(uint256 _amount)
- external
- whenNotCapitalPaused
- onlyOusdMetaStrategy
- {
- require(_amount < MAX_INT, "Amount too high");
-
- emit Mint(msg.sender, _amount);
+ function mintForStrategy(uint256 _amount) external whenNotCapitalPaused {
+ // read from storage
+ Strategy memory strategyConfig = strategies[msg.sender];
+ // is AMO check done in here rather than a modifier to save a storage read (SLOAD)
+ require(strategyConfig.isAMO, "Caller is not an AMO strategy");
- // safe to cast because of the require check at the beginning of the function
- netOusdMintedForStrategy += int256(_amount);
+ // toInt96 does a safe cast from uint256 to int96.
+ // the addition will revert if overflow of max int96
+ int96 newMintForStrategy = strategyConfig.mintForStrategy +
+ toInt96(_amount);
require(
- abs(netOusdMintedForStrategy) < netOusdMintForStrategyThreshold,
- "Minted ousd surpassed netOusdMintForStrategyThreshold."
+ abs(newMintForStrategy) < strategyConfig.mintForStrategyThreshold,
+ "OToken mint passes threshold"
);
+ // write back to storage
+ strategies[msg.sender].mintForStrategy = int96(newMintForStrategy);
+
+ emit Mint(msg.sender, _amount);
+
// Mint matching amount of OTokens
oUSD.mint(msg.sender, _amount);
}
@@ -216,7 +207,7 @@ contract VaultCore is VaultInitializer {
}
/**
- * @notice Burn OTokens for Metapool Strategy
+ * @notice Burn OTokens for an AMO strategy
* @param _amount Amount of OUSD to burn
*
* @dev Notice: can't use `nonReentrant` modifier since the `redeem` function could
@@ -228,23 +219,27 @@ contract VaultCore is VaultInitializer {
* that are moving funds between the Vault and end user wallets can influence strategies
* utilizing this function.
*/
- function burnForStrategy(uint256 _amount)
- external
- whenNotCapitalPaused
- onlyOusdMetaStrategy
- {
- require(_amount < MAX_INT, "Amount too high");
-
- emit Redeem(msg.sender, _amount);
+ function burnForStrategy(uint256 _amount) external whenNotCapitalPaused {
+ // read from storage
+ Strategy memory strategyConfig = strategies[msg.sender];
+ // is AMO check done in here rather than a modifier to save a storage read (SLOAD)
+ require(strategyConfig.isAMO, "Caller is not an AMO strategy");
- // safe to cast because of the require check at the beginning of the function
- netOusdMintedForStrategy -= int256(_amount);
+ // toInt96 does a safe cast from uint256 to int96
+ // the subtraction will revert if underflow of min int96
+ int96 newMintForStrategy = strategyConfig.mintForStrategy -
+ toInt96(_amount);
require(
- abs(netOusdMintedForStrategy) < netOusdMintForStrategyThreshold,
- "Attempting to burn too much OUSD."
+ abs(newMintForStrategy) < strategyConfig.mintForStrategyThreshold,
+ "OToken burn passes threshold"
);
+ // write back to storage
+ strategies[msg.sender].mintForStrategy = newMintForStrategy;
+
+ emit Redeem(msg.sender, _amount);
+
// Burn OTokens
oUSD.burn(msg.sender, _amount);
}
@@ -748,6 +743,17 @@ contract VaultCore is VaultInitializer {
return allStrategies;
}
+ /**
+ * @notice Gets the vault configuration of a supported strategy.
+ */
+ function getStrategyConfig(address _strategy)
+ external
+ view
+ returns (VaultStorage.Strategy memory config)
+ {
+ config = strategies[_strategy];
+ }
+
/**
* @notice Returns whether the vault supports the asset
* @param _asset address of the asset
@@ -796,8 +802,7 @@ contract VaultCore is VaultInitializer {
}
}
- function abs(int256 x) private pure returns (uint256) {
- require(x < int256(MAX_INT), "Amount too high");
- return x >= 0 ? uint256(x) : uint256(-x);
+ function abs(int96 x) private pure returns (int96) {
+ return x >= 0 ? x : -x;
}
}
diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol
index 066754e38f..54064b5fd6 100644
--- a/contracts/contracts/vault/VaultStorage.sol
+++ b/contracts/contracts/vault/VaultStorage.sol
@@ -32,7 +32,7 @@ contract VaultStorage is Initializable, Governable {
event RebasePaused();
event RebaseUnpaused();
event VaultBufferUpdated(uint256 _vaultBuffer);
- event OusdMetaStrategyUpdated(address _ousdMetaStrategy);
+ event AMOStrategyUpdated(address _addr, bool _isAMO);
event RedeemFeeUpdated(uint256 _redeemFeeBps);
event PriceProviderUpdated(address _priceProvider);
event AllocateThresholdUpdated(uint256 _threshold);
@@ -42,7 +42,10 @@ contract VaultStorage is Initializable, Governable {
event YieldDistribution(address _to, uint256 _yield, uint256 _fee);
event TrusteeFeeBpsChanged(uint256 _basis);
event TrusteeAddressChanged(address _address);
- event NetOusdMintForStrategyThresholdChanged(uint256 _threshold);
+ event MintForStrategyThresholdChanged(
+ address _strategy,
+ uint256 _threshold
+ );
event SwapperChanged(address _address);
event SwapAllowedUndervalueChanged(uint256 _basis);
event SwapSlippageChanged(address _asset, uint256 _basis);
@@ -74,12 +77,21 @@ contract VaultStorage is Initializable, Governable {
/// @dev list of all assets supported by the vault.
address[] internal allAssets;
- // Strategies approved for use by the Vault
+ /// @param isSupported flag if strategy is approved for use by the Vault
+ /// @param isAMO flag if AMO strategy that can mint and burn OTokens
+ /// @param mintForStrategy How much OTokens are currently minted by the strategy
+ /// @param mintForStrategyThreshold How much net total OTokens are allowed to be minted by the strategy
+ /// @dev The Strategy struct has been packed to fit in a single storage slot
+ /// 96-bits, signed integer is capped at 39b for 18 decimal numbers
struct Strategy {
bool isSupported;
- uint256 _deprecated; // Deprecated storage slot
+ bool isAMO;
+ int96 mintForStrategy;
+ int96 mintForStrategyThreshold;
+ // uint256 _deprecated; // Deprecated unused storage slot
}
- /// @dev mapping of strategy contracts to their configiration
+ /// @dev strategy configiration. ie is supported and is AMO
+ // slither-disable-next-line uninitialized-state
mapping(address => Strategy) internal strategies;
/// @dev list of all vault strategies
address[] internal allStrategies;
@@ -137,14 +149,17 @@ contract VaultStorage is Initializable, Governable {
uint256 constant MINT_MINIMUM_UNIT_PRICE = 0.998e18;
- /// @notice Metapool strategy that is allowed to mint/burn OTokens without changing collateral
- address public ousdMetaStrategy = address(0);
+ /// @dev Deprecated: AMO strategy that is allowed to mint/burn OTokens without changing collateral
+ // slither-disable-next-line constable-states
+ address private _deprecatedOusdMetaStrategy;
- /// @notice How much OTokens are currently minted by the strategy
- int256 public netOusdMintedForStrategy = 0;
+ /// @dev Deprecated: How much OTokens are currently minted by the strategy
+ // slither-disable-next-line constable-states
+ int256 private _deprecatedNetOusdMintedForStrategy;
- /// @notice How much net total OTokens are allowed to be minted by all strategies
- uint256 public netOusdMintForStrategyThreshold = 0;
+ /// @dev Deprecated: How much net total OTokens are allowed to be minted by all strategies
+ // slither-disable-next-line constable-states
+ uint256 private _deprecatedNetOusdMintForStrategyThreshold;
uint256 constant MIN_UNIT_PRICE_DRIFT = 0.7e18;
uint256 constant MAX_UNIT_PRICE_DRIFT = 1.3e18;
@@ -175,4 +190,19 @@ contract VaultStorage is Initializable, Governable {
sstore(position, newImpl)
}
}
+
+ /**
+ * @dev Returns the downcasted int96 from uint256, reverting on
+ * overflow (when the input is greater than largest int96).
+ *
+ * Based off OZ's SafeCast but implemented here as the OZ version
+ * used doesn't support casting to int96.
+ */
+ function toInt96(uint256 value) internal pure returns (int96) {
+ require(
+ value <= uint96(type(int96).max),
+ "value doesn't fit in 96 bits"
+ );
+ return int96(uint96(value));
+ }
}
diff --git a/contracts/deploy/000_mock.js b/contracts/deploy/000_mock.js
index 32ff97d3e3..963f95157a 100644
--- a/contracts/deploy/000_mock.js
+++ b/contracts/deploy/000_mock.js
@@ -1,7 +1,7 @@
const { parseUnits } = require("ethers").utils;
const { isMainnetOrFork } = require("../test/helpers");
const addresses = require("../utils/addresses");
-const { threeCRVPid } = require("../utils/constants");
+const { convex_3CRV_PID } = require("../utils/constants");
const { replaceContractAt } = require("../utils/hardhat");
const { hardhatSetBalance } = require("../test/_fund");
@@ -309,12 +309,12 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => {
args: [mockCVX.address, mockCRV.address, mockCVX.address],
});
const mockBooster = await ethers.getContract("MockBooster");
- await mockBooster.setPool(threeCRVPid, threePoolToken.address);
+ await mockBooster.setPool(convex_3CRV_PID, threePoolToken.address);
await deploy("MockRewardPool", {
from: deployerAddr,
args: [
- threeCRVPid,
+ convex_3CRV_PID,
threePoolToken.address,
mockCRV.address,
mockCVX.address,
diff --git a/contracts/deploy/001_core.js b/contracts/deploy/001_core.js
index d107360379..05da47e9f5 100644
--- a/contracts/deploy/001_core.js
+++ b/contracts/deploy/001_core.js
@@ -10,10 +10,12 @@ const {
} = require("../test/helpers.js");
const { deployWithConfirmation, withConfirmation } = require("../utils/deploy");
const {
- threeCRVPid,
- metapoolLPCRVPid,
- lusdMetapoolLPCRVPid,
- frxEthWethPoolLpPID,
+ convex_3CRV_PID,
+ convex_OUSD_3CRV_PID,
+ convex_LUSD_3CRV_PID,
+ convex_frxETH_WETH_PID,
+ convex_OETH_ETH_PID,
+ convex_frxETH_OETH_PID,
} = require("../utils/constants");
const log = require("../utils/logger")("deploy:001_core");
@@ -173,7 +175,7 @@ const deployConvexStrategy = async () => {
const cVaultProxy = await ethers.getContract("VaultProxy");
const mockBooster = await ethers.getContract("MockBooster");
- await mockBooster.setPool(threeCRVPid, assetAddresses.ThreePoolToken);
+ await mockBooster.setPool(convex_3CRV_PID, assetAddresses.ThreePoolToken);
await deployWithConfirmation("ConvexStrategyProxy", [], null, true);
const cConvexStrategyProxy = await ethers.getContract("ConvexStrategyProxy");
@@ -189,7 +191,7 @@ const deployConvexStrategy = async () => {
],
[
mockBooster.address, // _cvxDepositorAddress,
- threeCRVPid, // _cvxDepositorPTokenId
+ convex_3CRV_PID, // _cvxDepositorPTokenId
],
]
);
@@ -235,7 +237,7 @@ const deployConvexFrxEthWethStrategy = async () => {
const cVaultProxy = await ethers.getContract("OETHVaultProxy");
const mockBooster = await ethers.getContract("MockBooster");
await mockBooster.setPool(
- frxEthWethPoolLpPID,
+ convex_frxETH_WETH_PID,
assetAddresses.CurveFrxEthWethPool
);
@@ -255,7 +257,7 @@ const deployConvexFrxEthWethStrategy = async () => {
],
[
mockBooster.address, // _cvxDepositorAddress,
- frxEthWethPoolLpPID, // _cvxDepositorPTokenId
+ convex_frxETH_WETH_PID, // _cvxDepositorPTokenId
],
]
);
@@ -335,7 +337,7 @@ const deployConvexLUSDMetaStrategy = async () => {
LUSD.address, // LUSD
mockRewardPool.address, // _cvxRewardStakerAddress,
assetAddresses.LUSDMetapoolToken, // metapoolLpToken
- lusdMetapoolLPCRVPid, // _cvxDepositorPTokenId
+ convex_LUSD_3CRV_PID, // _cvxDepositorPTokenId
],
]
);
@@ -366,40 +368,51 @@ const deployConvexOUSDMetaStrategy = async () => {
"ConvexOUSDMetaStrategyProxy"
);
+ const mockBooster = await ethers.getContract("MockBooster");
+ // Get the Convex rewards pool contract
+ const poolInfo = await mockBooster.poolInfo(convex_OUSD_3CRV_PID);
+ const mockRewardPool = await ethers.getContractAt(
+ "MockRewardPool",
+ poolInfo.crvRewards
+ );
+ const ousd = await ethers.getContract("OUSDProxy");
+
const dConvexOUSDMetaStrategy = await deployWithConfirmation(
"ConvexOUSDMetaStrategy",
[
- [assetAddresses.ThreePool, cVaultProxy.address],
- [3, assetAddresses.ThreePool, assetAddresses.ThreePoolToken],
+ [assetAddresses.ThreePoolOUSDMetapool, cVaultProxy.address],
+ [
+ ousd.address, // oTokenAddress,
+ assetAddresses.USDT, // vaultAssetAddress (USDT)
+ assetAddresses.ThreePoolToken, // poolAssetAddress (3CRV)
+ 0, // Curve pool index for OUSD
+ 1, // Curve pool index for 3CRV
+ ],
+ [
+ mockBooster.address, // cvxDepositorAddress,
+ mockRewardPool.address, // cvxRewardStakerAddress,
+ convex_OUSD_3CRV_PID, // cvxDepositorPTokenId
+ ],
+ assetAddresses.ThreePool, // _curve3Pool
+ [assetAddresses.DAI, assetAddresses.USDC, assetAddresses.USDT], // _curve3PoolAssets
]
);
+
const cConvexOUSDMetaStrategy = await ethers.getContractAt(
"ConvexOUSDMetaStrategy",
cConvexOUSDMetaStrategyProxy.address
);
// Initialize Strategies
- const mockBooster = await ethers.getContract("MockBooster");
- const mockRewardPool = await ethers.getContract("MockRewardPool");
- const ousd = await ethers.getContract("OUSDProxy");
-
const initData = cConvexOUSDMetaStrategy.interface.encodeFunctionData(
- "initialize(address[],address[],address[],(address,address,address,address,address,uint256))",
+ "initialize(address[],address[],address[])",
[
[assetAddresses.CVX, assetAddresses.CRV],
[assetAddresses.DAI, assetAddresses.USDC, assetAddresses.USDT],
[
- assetAddresses.ThreePoolToken,
- assetAddresses.ThreePoolToken,
- assetAddresses.ThreePoolToken,
- ],
- [
- mockBooster.address, // _cvxDepositorAddress,
- assetAddresses.ThreePoolOUSDMetapool, // metapool address,
- ousd.address, // _ousdAddress,
- mockRewardPool.address, // _cvxRewardStakerAddress,
- assetAddresses.ThreePoolOUSDMetapool, // metapoolLpToken (metapool address),
- metapoolLPCRVPid, // _cvxDepositorPTokenId
+ assetAddresses.ThreePoolOUSDMetapool,
+ assetAddresses.ThreePoolOUSDMetapool,
+ assetAddresses.ThreePoolOUSDMetapool,
],
]
);
@@ -416,6 +429,136 @@ const deployConvexOUSDMetaStrategy = async () => {
return cConvexOUSDMetaStrategy;
};
+/**
+ * Deploys a Convex AMO Strategy for the Curve OETH/ETH pool
+ */
+const deployConvexOethEthAMOStrategy = async () => {
+ const assetAddresses = await getAssetAddresses(deployments);
+ const { governorAddr } = await getNamedAccounts();
+
+ const cVaultProxy = await ethers.getContract("OETHVaultProxy");
+
+ await deployWithConfirmation("ConvexEthMetaStrategyProxy", [], null, true);
+ const cConvexEthMetaStrategyProxy = await ethers.getContract(
+ "ConvexEthMetaStrategyProxy"
+ );
+
+ const mockBooster = await ethers.getContract("MockBooster");
+ // Get the Convex rewards pool contract
+ const poolInfo = await mockBooster.poolInfo(convex_OETH_ETH_PID);
+
+ const oeth = await ethers.getContract("OETHProxy");
+
+ const dConvexEthMetaStrategy = await deployWithConfirmation(
+ "ConvexEthMetaStrategy",
+ [
+ [assetAddresses.CurveOethEthPool, cVaultProxy.address],
+ [
+ oeth.address, // oTokenAddress,
+ assetAddresses.WETH, // vaultAssetAddress (WETH)
+ addresses.ETH, // poolAssetAddress (ETH)
+ 1, // Curve pool index for OToken OETH
+ 0, // Curve pool index for asset ETH
+ ],
+ [
+ mockBooster.address, // cvxDepositorAddress,
+ poolInfo.crvRewards, // cvxRewardStakerAddress,
+ convex_OETH_ETH_PID, // cvxDepositorPTokenId
+ ],
+ ]
+ );
+ const cConvexEthMetaStrategy = await ethers.getContractAt(
+ "ConvexEthMetaStrategy",
+ cConvexEthMetaStrategyProxy.address
+ );
+
+ // Initialize Strategies
+ const initData = cConvexEthMetaStrategy.interface.encodeFunctionData(
+ "initialize(address[],address[],address[])",
+ [
+ [assetAddresses.CVX, assetAddresses.CRV],
+ [assetAddresses.WETH],
+ [assetAddresses.CurveOethEthPool],
+ ]
+ );
+
+ await withConfirmation(
+ cConvexEthMetaStrategyProxy["initialize(address,address,bytes)"](
+ dConvexEthMetaStrategy.address,
+ governorAddr,
+ initData
+ )
+ );
+ log("Initialized ConvexEthMetaStrategyProxy");
+
+ return cConvexEthMetaStrategy;
+};
+
+/**
+ * Deploys a Convex AMO Strategy for the Curve frxETH/OETH pool
+ */
+const deployConvexFrxETHAMOStrategy = async () => {
+ const assetAddresses = await getAssetAddresses(deployments);
+ const { governorAddr } = await getNamedAccounts();
+
+ const cVaultProxy = await ethers.getContract("OETHVaultProxy");
+
+ await deployWithConfirmation("ConvexFrxETHAMOStrategyProxy", [], null, true);
+ const cConvexFrxETHAMOStrategyProxy = await ethers.getContract(
+ "ConvexFrxETHAMOStrategyProxy"
+ );
+
+ const mockBooster = await ethers.getContract("MockBooster");
+ // Get the Convex rewards pool contract
+ const poolInfo = await mockBooster.poolInfo(convex_frxETH_OETH_PID);
+
+ const oeth = await ethers.getContract("OETHProxy");
+
+ const dConvexFrxETHAMOStrategy = await deployWithConfirmation(
+ "ConvexFrxETHAMOStrategy",
+ [
+ [assetAddresses.CurveFrxEthOethPool, cVaultProxy.address],
+ [
+ oeth.address, // oTokenAddress,
+ assetAddresses.frxETH, // vaultAssetAddress (frxETH)
+ assetAddresses.frxETH, // poolAssetAddress (frxETH)
+ 1, // Curve pool index for OToken OETH
+ 0, // Curve pool index for asset frxETH
+ ],
+ [
+ mockBooster.address, // cvxDepositorAddress,
+ poolInfo.crvRewards, // cvxRewardStakerAddress,
+ convex_frxETH_OETH_PID, // cvxDepositorPTokenId
+ ],
+ ]
+ );
+ const cConvexFrxETHAMOStrategy = await ethers.getContractAt(
+ "ConvexFrxETHAMOStrategy",
+ cConvexFrxETHAMOStrategyProxy.address
+ );
+
+ // Initialize Strategies
+ const initData = cConvexFrxETHAMOStrategy.interface.encodeFunctionData(
+ "initialize(address[],address[],address[])",
+ [
+ [assetAddresses.CVX, assetAddresses.CRV],
+ [assetAddresses.frxETH],
+ [assetAddresses.CurveFrxEthOethPool],
+ ]
+ );
+
+ await withConfirmation(
+ cConvexFrxETHAMOStrategyProxy["initialize(address,address,bytes)"](
+ dConvexFrxETHAMOStrategy.address,
+ governorAddr,
+ initData
+ )
+ );
+ log("Initialized cConvexFrxETHAMOStrategyProxy");
+
+ return cConvexFrxETHAMOStrategy;
+};
+
/**
* Configure Vault by adding supported assets and Strategies.
*/
@@ -460,7 +603,7 @@ const configureVault = async () => {
};
/**
- * Configure OETH Vault by adding supported assets and Strategies.
+ * Configure OETH Vault by adding supported assets, unpause capital and set strategist.
*/
const configureOETHVault = async () => {
const assetAddresses = await getAssetAddresses(deployments);
@@ -919,37 +1062,69 @@ const deployCore = async () => {
log("Initialized OETH");
};
-// deploy curve metapool mocks
+// deploy Curve OUSD/3Crv Metapool mocks
const deployCurveMetapoolMocks = async () => {
- const ousd = await ethers.getContract("OUSDProxy");
const { deployerAddr } = await hre.getNamedAccounts();
const assetAddresses = await getAssetAddresses(deployments);
+ const ousd = await ethers.getContract("OUSDProxy");
+
await hre.deployments.deploy("MockCurveMetapool", {
from: deployerAddr,
args: [[ousd.address, assetAddresses.ThreePoolToken]],
});
- const metapoolToken = await ethers.getContract("MockCurveMetapool");
+ const curveLpToken = await ethers.getContract("MockCurveMetapool");
const mockBooster = await ethers.getContract("MockBooster");
- await mockBooster.setPool(metapoolLPCRVPid, metapoolToken.address);
+ await mockBooster.setPool(convex_OUSD_3CRV_PID, curveLpToken.address);
};
-// deploy curve metapool mocks
+// deploy Curve LUSD/3Crv Metapool mocks
const deployCurveLUSDMetapoolMocks = async () => {
const { deployerAddr } = await hre.getNamedAccounts();
const assetAddresses = await getAssetAddresses(deployments);
- const LUSD = await ethers.getContract("MockLUSD");
-
await hre.deployments.deploy("MockCurveLUSDMetapool", {
from: deployerAddr,
- args: [[LUSD.address, assetAddresses.ThreePoolToken]],
+ args: [[assetAddresses.LUSD, assetAddresses.ThreePoolToken]],
+ });
+
+ const curveLpToken = await ethers.getContract("MockCurveLUSDMetapool");
+ const mockBooster = await ethers.getContract("MockBooster");
+ await mockBooster.setPool(convex_LUSD_3CRV_PID, curveLpToken.address);
+};
+
+// deploy Curve OETH/ETH Pool mocks
+const deployCurveOethEthPoolMocks = async () => {
+ const { deployerAddr } = await hre.getNamedAccounts();
+
+ const oeth = await ethers.getContract("OETHProxy");
+
+ await hre.deployments.deploy("MockCurveOethEthPool", {
+ from: deployerAddr,
+ args: [[addresses.ETH, oeth.address]],
+ });
+
+ const curveLpToken = await ethers.getContract("MockCurveOethEthPool");
+ const mockBooster = await ethers.getContract("MockBooster");
+ await mockBooster.setPool(convex_OETH_ETH_PID, curveLpToken.address);
+};
+
+// deploy Curve frxETH/OETH Pool mocks
+const deployCurvefrxEthOethPoolMocks = async () => {
+ const { deployerAddr } = await hre.getNamedAccounts();
+ const assetAddresses = await getAssetAddresses(deployments);
+
+ const oeth = await ethers.getContract("OETHProxy");
+
+ await hre.deployments.deploy("MockCurveFrxEthOethPool", {
+ from: deployerAddr,
+ args: [[assetAddresses.frxETH, oeth.address]],
});
- const LUSDMetapoolToken = await ethers.getContract("MockCurveLUSDMetapool");
+ const curveLpToken = await ethers.getContract("MockCurveFrxEthOethPool");
const mockBooster = await ethers.getContract("MockBooster");
- await mockBooster.setPool(lusdMetapoolLPCRVPid, LUSDMetapoolToken.address);
+ await mockBooster.setPool(convex_frxETH_OETH_PID, curveLpToken.address);
};
// Deploy the Flipper trading contract
@@ -1217,12 +1392,16 @@ const main = async () => {
await deployCore();
await deployCurveMetapoolMocks();
await deployCurveLUSDMetapoolMocks();
+ await deployCurveOethEthPoolMocks();
+ await deployCurvefrxEthOethPoolMocks();
await deployCompoundStrategy();
await deployAaveStrategy();
await deployThreePoolStrategy();
await deployConvexStrategy();
await deployConvexOUSDMetaStrategy();
await deployConvexLUSDMetaStrategy();
+ await deployConvexOethEthAMOStrategy();
+ await deployConvexFrxETHAMOStrategy();
await deployConvexFrxEthWethStrategy();
await deployFraxEthStrategy();
const [harvesterProxy, oethHarvesterProxy] = await deployHarvesters();
diff --git a/contracts/deploy/045_convex_lusd_meta_strategy.js b/contracts/deploy/045_convex_lusd_meta_strategy.js
index bf59b333e7..13a34086f1 100644
--- a/contracts/deploy/045_convex_lusd_meta_strategy.js
+++ b/contracts/deploy/045_convex_lusd_meta_strategy.js
@@ -1,5 +1,5 @@
const generalizedConvexStratDeployment = require("../utils/generalizedConvexStratDeployment");
-const { lusdMetapoolLPCRVPid } = require("../utils/constants");
+const { convex_LUSD_3CRV_PID } = require("../utils/constants");
module.exports = generalizedConvexStratDeployment({
deployName: "045_convex_lusd_meta_strategy",
@@ -14,7 +14,7 @@ module.exports = generalizedConvexStratDeployment({
metapoolLPToken: "0xEd279fDD11cA84bEef15AF5D39BB4d4bEE23F0cA",
mainTokenAddress: "0x5f98805A4E8be255a32880FDeC7F6728C6568bA0", // LUSD
cvxRewardStakerAddress: "0x2ad92A7aE036a038ff02B96c88de868ddf3f8190",
- cvxDepositorPTokenId: lusdMetapoolLPCRVPid, // 33
+ cvxDepositorPTokenId: convex_LUSD_3CRV_PID, // 33
redeployVault: false,
deployStrategyImplementation: true,
skipMainnetDeploy: false,
diff --git a/contracts/deploy/055_curve_amo.js b/contracts/deploy/055_curve_amo.js
index 0da9ebef45..b8827ea92e 100644
--- a/contracts/deploy/055_curve_amo.js
+++ b/contracts/deploy/055_curve_amo.js
@@ -1,10 +1,12 @@
-const { deploymentWithGuardianGovernor } = require("../utils/deploy");
const addresses = require("../utils/addresses");
const hre = require("hardhat");
const { utils, Contract } = require("ethers");
+
const { getAssetAddresses, isMainnet } = require("../test/helpers.js");
-const { MAX_UINT256, oethPoolLpPID } = require("../utils/constants");
+const { MAX_UINT256, convex_OETH_ETH_PID } = require("../utils/constants");
+const { deploymentWithGuardianGovernor } = require("../utils/deploy");
const { impersonateAndFund } = require("../utils/signers");
+
const crvRewards = "0x24b65DC1cf053A8D96872c323d29e86ec43eB33A";
const poolAddress = "0x94b17476a93b3262d87b9a326965d1e91f9c13e7";
const tokenAddress = "0x94b17476a93b3262d87b9a326965d1e91f9c13e7";
@@ -147,7 +149,7 @@ const deployConvexETHMetaStrategy = async ({
addresses.mainnet.OETHProxy,
crvRewards,
tokenAddress,
- oethPoolLpPID,
+ convex_OETH_ETH_PID,
]
)
);
diff --git a/contracts/deploy/075_oeth_amo_upgrade.js b/contracts/deploy/075_oeth_amo_upgrade.js
index da5c690045..b2a35a0609 100644
--- a/contracts/deploy/075_oeth_amo_upgrade.js
+++ b/contracts/deploy/075_oeth_amo_upgrade.js
@@ -1,5 +1,5 @@
const addresses = require("../utils/addresses");
-const { oethPoolLpPID } = require("../utils/constants");
+const { convex_OETH_ETH_PID } = require("../utils/constants");
const { deploymentWithGovernanceProposal } = require("../utils/deploy");
module.exports = deploymentWithGovernanceProposal(
@@ -25,7 +25,7 @@ module.exports = deploymentWithGovernanceProposal(
[
addresses.mainnet.CVXBooster,
addresses.mainnet.CVXETHRewardsPool,
- oethPoolLpPID,
+ convex_OETH_ETH_PID,
addresses.mainnet.OETHProxy,
addresses.mainnet.WETH,
],
diff --git a/contracts/deploy/078_convex_frax_strategy.js b/contracts/deploy/078_convex_frax_strategy.js
index 9aca937d42..e59b84780c 100644
--- a/contracts/deploy/078_convex_frax_strategy.js
+++ b/contracts/deploy/078_convex_frax_strategy.js
@@ -1,12 +1,12 @@
const addresses = require("../utils/addresses");
-const { frxEthWethPoolLpPID } = require("../utils/constants");
+const { convex_frxETH_WETH_PID } = require("../utils/constants");
const { deploymentWithGovernanceProposal } = require("../utils/deploy");
module.exports = deploymentWithGovernanceProposal(
{
deployName: "078_convex_frax_strategy",
forceDeploy: false,
- // forceSkip: true,
+ forceSkip: true,
reduceQueueTime: true,
deployerIsProposer: true,
// proposalId: "",
@@ -46,7 +46,7 @@ module.exports = deploymentWithGovernanceProposal(
addresses.mainnet.CurveFrxEthWethPool, // Curve pool
addresses.mainnet.CurveFrxEthWethPool, // Curve LP token
],
- [addresses.mainnet.CVXBooster, frxEthWethPoolLpPID],
+ [addresses.mainnet.CVXBooster, convex_frxETH_WETH_PID],
]
);
const cConvexFrxEthWethStrategy = await ethers.getContractAt(
diff --git a/contracts/deploy/079_ousd_amo_upgrade.js b/contracts/deploy/079_ousd_amo_upgrade.js
new file mode 100644
index 0000000000..10cc61566c
--- /dev/null
+++ b/contracts/deploy/079_ousd_amo_upgrade.js
@@ -0,0 +1,102 @@
+const { parseUnits } = require("ethers/lib/utils");
+
+const addresses = require("../utils/addresses");
+const { convex_OUSD_3CRV_PID } = require("../utils/constants");
+const { deploymentWithGovernanceProposal } = require("../utils/deploy");
+
+module.exports = deploymentWithGovernanceProposal(
+ {
+ deployName: "079_ousd_amo_upgrade",
+ forceDeploy: false,
+ // forceSkip: true,
+ reduceQueueTime: false,
+ deployerIsProposer: true,
+ // proposalId: "",
+ },
+ async ({ ethers, deployWithConfirmation }) => {
+ // 1. Deploy new OUSD Vault Core and Admin implementations
+ // Need to override the storage safety check as we are changing the Strategy struct
+ const dVaultCore = await deployWithConfirmation(
+ "VaultCore",
+ [],
+ null,
+ true
+ );
+ const dVaultAdmin = await deployWithConfirmation(
+ "VaultAdmin",
+ [],
+ null,
+ true
+ );
+
+ // Connect to the OUSD Vault as its governor via the proxy
+ const cVaultProxy = await ethers.getContract("VaultProxy");
+ const cVault = await ethers.getContractAt("Vault", cVaultProxy.address);
+
+ const cConvexOUSDMetaStrategyProxy = await ethers.getContract(
+ "ConvexOUSDMetaStrategyProxy"
+ );
+
+ // Deploy and set the immutable variables
+ const dConvexOUSDMetaStrategy = await deployWithConfirmation(
+ "ConvexOUSDMetaStrategy",
+ [
+ [addresses.mainnet.CurveOUSDMetaPool, addresses.mainnet.VaultProxy],
+ [
+ addresses.mainnet.OUSDProxy, // oTokenAddress (OUSD),
+ addresses.mainnet.USDT, // vaultAssetAddress (USDT)
+ addresses.mainnet.ThreePoolToken, // poolAssetAddress (3CRV)
+ 0, // Curve pool index for OUSD
+ 1, // Curve pool index for 3CRV
+ ],
+ [
+ addresses.mainnet.CVXBooster, // cvxDepositorAddress,
+ addresses.mainnet.CVXRewardsPool, // cvxRewardStakerAddress,
+ convex_OUSD_3CRV_PID, // cvxDepositorPTokenId
+ ],
+ addresses.mainnet.ThreePool, // _curve3Pool
+ [addresses.mainnet.DAI, addresses.mainnet.USDC, addresses.mainnet.USDT], // _curve3PoolAssets
+ ],
+ null,
+ true // force deploy as storage slots have changed
+ );
+
+ // Governance Actions
+ // ----------------
+ return {
+ name: "Upgrade the OUSD AMO strategy.",
+ actions: [
+ // 1. Upgrade the OUSD Vault proxy to the new core vault implementation
+ {
+ contract: cVaultProxy,
+ signature: "upgradeTo(address)",
+ args: [dVaultCore.address],
+ },
+ // 2. set OUSD Vault proxy to the new admin vault implementation
+ {
+ contract: cVault,
+ signature: "setAdminImpl(address)",
+ args: [dVaultAdmin.address],
+ },
+ // 3. Flag the existing AMO strategy for Curve OUSD/3CRV pool to be an AMO in the OUSD Vault
+ {
+ contract: cVault,
+ signature: "setAMOStrategy(address,bool)",
+ args: [cConvexOUSDMetaStrategyProxy.address, true],
+ },
+ // 4. Reset the mint threshold for the old AMO strategy as its storage has changed to 50m
+ {
+ contract: cVault,
+ signature: "setMintForStrategyThreshold(address,uint256)",
+ args: [cConvexOUSDMetaStrategyProxy.address, parseUnits("50", 24)],
+ },
+ // 5. Upgrade the OUSD AMO strategy proxy to the new strategy implementation
+ {
+ contract: cConvexOUSDMetaStrategyProxy,
+ signature: "upgradeTo(address)",
+ args: [dConvexOUSDMetaStrategy.address],
+ },
+ ],
+ };
+ }
+);
diff --git a/contracts/deploy/083_frax_amo.js b/contracts/deploy/083_frax_amo.js
new file mode 100644
index 0000000000..1e4e5f525d
--- /dev/null
+++ b/contracts/deploy/083_frax_amo.js
@@ -0,0 +1,172 @@
+const { parseUnits } = require("ethers/lib/utils");
+
+const addresses = require("../utils/addresses");
+const { convex_frxETH_OETH_PID } = require("../utils/constants");
+const { deploymentWithGovernanceProposal } = require("../utils/deploy");
+const { getTxOpts } = require("../utils/tx");
+
+module.exports = deploymentWithGovernanceProposal(
+ {
+ deployName: "083_frax_amo",
+ forceDeploy: false,
+ // forceSkip: true,
+ reduceQueueTime: false,
+ deployerIsProposer: true,
+ // proposalId: "",
+ },
+ async ({ ethers, deployWithConfirmation, withConfirmation }) => {
+ const { deployerAddr, timelockAddr } = await getNamedAccounts();
+ const sDeployer = await ethers.provider.getSigner(deployerAddr);
+
+ // 1. Deploy new OETH Vault Core and Admin implementations
+ // Need to override the storage safety check as we are changing the Strategy struct
+ const dVaultCore = await deployWithConfirmation(
+ "OETHVaultCore",
+ [],
+ null,
+ true
+ );
+ const dVaultAdmin = await deployWithConfirmation(
+ "OETHVaultAdmin",
+ [],
+ null,
+ true
+ );
+
+ // Connect to the OETH Vault as its governor via the proxy
+ const cVaultProxy = await ethers.getContract("OETHVaultProxy");
+ const cVault = await ethers.getContractAt("OETHVault", cVaultProxy.address);
+
+ // 2. Deploy new frxETH/OETH AMO strategy
+ // Deploy proxy
+ const dConvexFrxETHAMOStrategyProxy = await deployWithConfirmation(
+ "ConvexFrxETHAMOStrategyProxy"
+ );
+ const cConvexFrxETHAMOStrategyProxy = await ethers.getContract(
+ "ConvexFrxETHAMOStrategyProxy"
+ );
+
+ // Deploy and set the immutable variables of implementation
+ const dConvexFrxETHAMOStrategy = await deployWithConfirmation(
+ "ConvexFrxETHAMOStrategy",
+ [
+ [
+ addresses.mainnet.CurveFrxETHOETHPool,
+ addresses.mainnet.OETHVaultProxy,
+ ],
+ [
+ addresses.mainnet.OETHProxy, // oTokenAddress (OETH),
+ addresses.mainnet.frxETH, // vaultAssetAddress (frxETH)
+ addresses.mainnet.frxETH, // poolAssetAddress (frxETH)
+ 1, // Curve pool index for OToken OETH
+ 0, // Curve pool index for asset frxETH
+ ],
+ [
+ addresses.mainnet.CVXBooster, // cvxDepositorAddress,
+ addresses.mainnet.CVXFrxETHRewardsPool, // cvxRewardStakerAddress,
+ convex_frxETH_OETH_PID, // cvxDepositorPTokenId
+ ],
+ ]
+ );
+
+ const cConvexFrxETHAMOStrategy = await ethers.getContractAt(
+ "ConvexFrxETHAMOStrategy",
+ dConvexFrxETHAMOStrategyProxy.address
+ );
+
+ // 3. Initialize the new frxETH/OETH AMO strategy
+ // Construct initialize call data to init and configure the new strategy
+ const initData = cConvexFrxETHAMOStrategy.interface.encodeFunctionData(
+ "initialize(address[],address[],address[])",
+ [
+ [addresses.mainnet.CRV, addresses.mainnet.CVX],
+ [addresses.mainnet.frxETH],
+ [addresses.mainnet.CurveFrxETHOETHPool],
+ ]
+ );
+
+ // prettier-ignore
+ await withConfirmation(
+ cConvexFrxETHAMOStrategyProxy
+ .connect(sDeployer)["initialize(address,address,bytes)"](
+ dConvexFrxETHAMOStrategy.address,
+ timelockAddr,
+ initData,
+ await getTxOpts()
+ )
+ );
+ console.log("Initialized Curve frxETH/ETH AMO Strategy");
+
+ const cHarvester = await ethers.getContractAt(
+ "OETHHarvester",
+ addresses.mainnet.OETHHarvesterProxy
+ );
+
+ const cConvexEthMetaStrategy = await ethers.getContractAt(
+ "ConvexEthMetaStrategy",
+ addresses.mainnet.ConvexOETHAMOStrategy
+ );
+
+ // Governance Actions
+ // ----------------
+ return {
+ name: "Upgrade OETH Vault and deploy new AMO strategy for Curve frxETH/OETH pool.",
+ actions: [
+ // 1. Upgrade the OETH Vault proxy to the new core vault implementation
+ {
+ contract: cVaultProxy,
+ signature: "upgradeTo(address)",
+ args: [dVaultCore.address],
+ },
+ // 2. set OETH Vault proxy to the new admin vault implementation
+ {
+ contract: cVault,
+ signature: "setAdminImpl(address)",
+ args: [dVaultAdmin.address],
+ },
+ // 3. Flag the existing AMO strategy for Curve OETH/ETH pool to be an AMO in the OETH Vault
+ {
+ contract: cVault,
+ signature: "setAMOStrategy(address,bool)",
+ args: [cConvexEthMetaStrategy.address, true],
+ },
+ // 4. Reset the mint threshold for the old AMO strategy as its storage has changed
+ {
+ contract: cVault,
+ signature: "setMintForStrategyThreshold(address,uint256)",
+ args: [cConvexEthMetaStrategy.address, parseUnits("25000")],
+ },
+ // 5. Approve the new frxETH AMO strategy in the OETH Vault
+ {
+ contract: cVault,
+ signature: "approveStrategy(address)",
+ args: [cConvexFrxETHAMOStrategy.address],
+ },
+ // 6. Flag the new AMO strategy for Curve frxETH/OETH pool to be an AMO in the OETH Vault
+ {
+ contract: cVault,
+ signature: "setAMOStrategy(address,bool)",
+ args: [cConvexFrxETHAMOStrategy.address, true],
+ },
+ // 7. Set the mint threshold for the new frxETH AMO strategy
+ {
+ contract: cVault,
+ signature: "setMintForStrategyThreshold(address,uint256)",
+ args: [cConvexFrxETHAMOStrategy.address, parseUnits("25000")],
+ },
+ // 8. Add the new frxETH AMO strategy to the OETH Harvester
+ {
+ contract: cHarvester,
+ signature: "setSupportedStrategy(address,bool)",
+ args: [cConvexFrxETHAMOStrategy.address, true],
+ },
+ // 9. Set the harvester address on the new frxETH AMO strategy
+ {
+ contract: cConvexFrxETHAMOStrategy,
+ signature: "setHarvesterAddress(address)",
+ args: [cHarvester.address],
+ },
+ ],
+ };
+ }
+);
diff --git a/contracts/docs/AMOContractHierarchy.svg b/contracts/docs/AMOContractHierarchy.svg
new file mode 100644
index 0000000000..e6542d8d9d
--- /dev/null
+++ b/contracts/docs/AMOContractHierarchy.svg
@@ -0,0 +1,142 @@
+
+
+
+
+
diff --git a/contracts/docs/AaveStrategyHierarchy.svg b/contracts/docs/AaveStrategyHierarchy.svg
index 74f116b8ab..17ad4614b9 100644
--- a/contracts/docs/AaveStrategyHierarchy.svg
+++ b/contracts/docs/AaveStrategyHierarchy.svg
@@ -16,44 +16,44 @@
Governable../contracts/governance/Governable.sol
-
+
-150
+141AaveStrategy../contracts/strategies/AaveStrategy.sol
-
+
-195
+183<<Abstract>>InitializableAbstractStrategy../contracts/utils/InitializableAbstractStrategy.sol
-
+
-150->195
+141->183
-
+
-194
+182<<Abstract>>Initializable../contracts/utils/Initializable.sol
-
+
-195->20
+183->18
-
+
-195->194
+183->182
diff --git a/contracts/docs/AaveStrategySquashed.svg b/contracts/docs/AaveStrategySquashed.svg
index 21d2e01374..c5e45fe801 100644
--- a/contracts/docs/AaveStrategySquashed.svg
+++ b/contracts/docs/AaveStrategySquashed.svg
@@ -9,9 +9,9 @@
UmlClassDiagram
-
+
-150
+141AaveStrategy../contracts/strategies/AaveStrategy.sol
diff --git a/contracts/docs/BalancerEthAMOStrategyHierarchy.svg b/contracts/docs/BalancerEthAMOStrategyHierarchy.svg
new file mode 100644
index 0000000000..086692b329
--- /dev/null
+++ b/contracts/docs/BalancerEthAMOStrategyHierarchy.svg
@@ -0,0 +1,89 @@
+
+
+
+
+
diff --git a/contracts/docs/BalancerEthAMOStrategySquashed.svg b/contracts/docs/BalancerEthAMOStrategySquashed.svg
new file mode 100644
index 0000000000..65279f7ab8
--- /dev/null
+++ b/contracts/docs/BalancerEthAMOStrategySquashed.svg
@@ -0,0 +1,137 @@
+
+
+
+
+
diff --git a/contracts/docs/BalancerEthAMOStrategyStorage.svg b/contracts/docs/BalancerEthAMOStrategyStorage.svg
new file mode 100644
index 0000000000..1217cdacbe
--- /dev/null
+++ b/contracts/docs/BalancerEthAMOStrategyStorage.svg
@@ -0,0 +1,149 @@
+
+
+
+
+
diff --git a/contracts/docs/CompStrategyHierarchy.svg b/contracts/docs/CompStrategyHierarchy.svg
new file mode 100644
index 0000000000..3854581834
--- /dev/null
+++ b/contracts/docs/CompStrategyHierarchy.svg
@@ -0,0 +1,75 @@
+
+
+
+
+
diff --git a/contracts/docs/CompStrategySquashed.svg b/contracts/docs/CompStrategySquashed.svg
new file mode 100644
index 0000000000..f74bc1fce8
--- /dev/null
+++ b/contracts/docs/CompStrategySquashed.svg
@@ -0,0 +1,99 @@
+
+
+
+
+
diff --git a/contracts/docs/CompStrategyStorage.svg b/contracts/docs/CompStrategyStorage.svg
new file mode 100644
index 0000000000..22929beac6
--- /dev/null
+++ b/contracts/docs/CompStrategyStorage.svg
@@ -0,0 +1,129 @@
+
+
+
+
+
diff --git a/contracts/docs/ConvexEthMetaStrategyHierarchy.svg b/contracts/docs/ConvexEthMetaStrategyHierarchy.svg
index cce9738332..b179ecf06a 100644
--- a/contracts/docs/ConvexEthMetaStrategyHierarchy.svg
+++ b/contracts/docs/ConvexEthMetaStrategyHierarchy.svg
@@ -4,17 +4,17 @@
-