Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/helpers/AddressBook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ address constant RETH_MAINNET = 0xae78736Cd615f374D3085123A210448E74Fc6393;
address constant ETHX_MAINNET = 0xA35b1B31Ce002FBF2058D22F30f95D405200A15b;
address constant GHO_MAINNET = 0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f;
address constant COMP_MAINNET = 0xc00e94Cb662C3520282E6f5717214004A7f26888;
address constant CRV3POOL_MAINNET = 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490;

// Main protocols contracts
address constant CONVEX_BOOSTER_MAINNET = 0xF403C135812408BFbE8713b5A23a04b3D48AAE31;
Expand Down Expand Up @@ -65,6 +66,8 @@ address constant COMPOUND_USDT_V3_COMMET_MAINNET = 0x3Afdc9BCA9213A35503b077a607
address constant COMPOUND_USDT_V3_REWARDS_MAINNET = 0x1B0e765F6224C21223AeA2af16c1C46E38885a40;
address constant CURVE_ALTETH_FRXETH_MAINNET = 0xB657B895B265C38c53FFF00166cF7F6A3C70587d;
address constant BEEFY_ALTETH_FRXETH_MAINNET = 0x26F44884D9744C0EDaB6283930DF325c200020A3;
address constant CURVE_THUSD_DAI_USDC_USDT_MAINNET = 0x91553BAD9Fbc8bD69Ff5d5678Cbf7D514d00De0b;
address constant BEEFY_THUSD_DAI_USDC_USDT_MAINNET = 0x3f5e39bf80798cB94846B48f1c635001a2E43066;

//////////////////////////////// POLYGON ////////////////////////////////
// Tokens
Expand Down
10 changes: 10 additions & 0 deletions src/interfaces/ICurve.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ interface ICurveTriPool {
function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256);

function exchange(int128 i, int128 j, uint256 _dx, uint256 _min_dy) external;

function get_virtual_price() external view returns (uint256);

function add_liquidity(uint256[3] calldata amounts, uint256 min_mint_amount) external;

function remove_liquidity_one_coin(uint256 token_amount, int128 i, uint256 min_amount) external;

function calc_token_amount(uint256[3] memory amounts, bool deposit) external view returns (uint256);

function calc_withdraw_one_coin(uint256 token_amount, int128 i) external view returns (uint256);
}

interface ICurveAtriCryptoZapper {
Expand Down
254 changes: 254 additions & 0 deletions src/strategies/base/BaseBeefyCurveMetaPoolStrategy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.19;

import { ERC20 } from "solady/tokens/ERC20.sol";
import { FixedPointMathLib as Math } from "solady/utils/FixedPointMathLib.sol";
import { CRV3POOL_MAINNET } from "src/helpers/AddressBook.sol";
import { IBeefyVault } from "src/interfaces/IBeefyVault.sol";
import { ICurveLpPool, ICurveTriPool } from "src/interfaces/ICurve.sol";
import { BaseBeefyStrategy, IMaxApyVault, SafeTransferLib } from "src/strategies/base/BaseBeefyStrategy.sol";

/// @title BaseBeefyCurveMetaPoolStrategy
/// @author Adapted from https://github.com/Grandthrax/yearn-steth-acc/blob/master/contracts/strategies.sol
/// @notice `BaseBeefyCurveMetaPoolStrategy` supplies an underlying token into a generic Beefy Vault,
/// earning the Beefy Vault's yield
contract BaseBeefyCurveMetaPoolStrategy is BaseBeefyStrategy {
using SafeTransferLib for address;

////////////////////////////////////////////////////////////////
/// STRATEGY GLOBAL STATE VARIABLES ///
////////////////////////////////////////////////////////////////

/*==================CURVE-RELATED STORAGE VARIABLES==================*/
/// @notice Curve Meta pool for this Strategy
ICurveLpPool public curveLpPool;
/// @notice Curve 3pool for this Strategy - DAI/USDC/USDT Pool
ICurveTriPool public curveTriPool;

address public crvTriPoolToken;

////////////////////////////////////////////////////////////////
/// INITIALIZATION ///
////////////////////////////////////////////////////////////////
constructor() initializer { }

/// @notice Initialize the Strategy
/// @param _vault The address of the MaxApy Vault associated to the strategy
/// @param _keepers The addresses of the keepers to be added as valid keepers to the strategy
/// @param _strategyName the name of the strategy
/// @param _curveLpPool The address of the strategy's main Curve pool
/// @param _beefyVault The address of the strategy's Beefy vault
/// @param _curveTriPool The address of the strategy's Curve Tripool
/// @param _crvTriPoolToken The address of the Curve Tripool's token
function initialize(
IMaxApyVault _vault,
address[] calldata _keepers,
bytes32 _strategyName,
address _strategist,
ICurveLpPool _curveLpPool,
IBeefyVault _beefyVault,
ICurveTriPool _curveTriPool,
address _crvTriPoolToken
)
public
virtual
initializer
{
super.initialize(_vault, _keepers, _strategyName, _strategist, _beefyVault);

// Curve init
curveLpPool = _curveLpPool;
curveTriPool = _curveTriPool;
crvTriPoolToken = _crvTriPoolToken;

underlyingAsset.safeApprove(address(curveTriPool), type(uint256).max);
crvTriPoolToken.safeApprove(address(curveLpPool), type(uint256).max);
address(curveLpPool).safeApprove(address(beefyVault), type(uint256).max);
/// min single trade by default
minSingleTrade = 10e6;
/// Unlimited max single trade by default
maxSingleTrade = 100_000e6;
}

////////////////////////////////////////////////////////////////
/// INTERNAL CORE FUNCTIONS ///
////////////////////////////////////////////////////////////////
/// @notice Invests `amount` of underlying into the Beefy vault
/// @dev
/// @param amount The amount of underlying to be deposited in the pool
/// @param minOutputAfterInvestment minimum expected output after `_invest()` (designated in Curve LP tokens)
/// @return The amount of tokens received, in terms of underlying
function _invest(uint256 amount, uint256 minOutputAfterInvestment) internal virtual override returns (uint256) {
// Don't do anything if amount to invest is 0
if (amount == 0) return 0;

uint256 underlyingBalance = _underlyingBalance();

assembly ("memory-safe") {
if gt(amount, underlyingBalance) {
// throw the `NotEnoughFundsToInvest` error
mstore(0x00, 0xb2ff68ae)
revert(0x1c, 0x04)
}
}

amount = Math.min(maxSingleTrade, amount);

uint256[3] memory amountsUsdc;
amountsUsdc[1] = amount;

uint256 _before = ERC20(crvTriPoolToken).balanceOf(address(this));
// uint256 _before = ERC20(_3crvToken).balanceOf(address(this));
// Add liquidity to the curveTriPool in underlying token [coin1 -> usdc]
curveTriPool.add_liquidity(amountsUsdc, 0);
uint256 _after = ERC20(crvTriPoolToken).balanceOf(address(this));
// uint256 _after = ERC20(_3crvToken).balanceOf(address(this));

uint256 _3crvTokenReceived;
assembly ("memory-safe") {
_3crvTokenReceived := sub(_after, _before)
}

uint256[2] memory amounts;
amounts[1] = _3crvTokenReceived;
// Add liquidity to the curve Metapool in 3crv token [coin1 -> 3crv]
uint256 lpReceived = curveLpPool.add_liquidity(amounts, 0, address(this));

_before = beefyVault.balanceOf(address(this));

// Deposit Curve LP tokens to Beefy vault
beefyVault.deposit(lpReceived);

_after = beefyVault.balanceOf(address(this));
uint256 shares;

assembly ("memory-safe") {
shares := sub(_after, _before)
if lt(shares, minOutputAfterInvestment) {
// throw the `MinOutputAmountNotReached` error
mstore(0x00, 0xf7c67a48)
revert(0x1c, 0x04)
}
}
emit Invested(address(this), amount);

return _shareValue(shares);
}

/// @dev care should be taken, as the `amount` parameter is not in terms of underlying,
/// but in terms of Beefy's moo tokens
/// Note that if minimum withdrawal amount is not reached, funds will not be divested, and this
/// will be accounted as a loss later.
/// @return amountDivested the total amount divested, in terms of underlying asset
function _divest(uint256 amount) internal virtual override returns (uint256 amountDivested) {
if (amount == 0) return 0;

uint256 _before = beefyVault.want().balanceOf(address(this));

// Withdraw from Beefy and unwrap directly to Curve LP tokens
beefyVault.withdraw(amount);

uint256 _after = beefyVault.want().balanceOf(address(this));

uint256 lptokens = _after - _before;

// Remove liquidity and obtain usdce
uint256 _3crvTokenReceived = curveLpPool.remove_liquidity_one_coin(
lptokens,
1,
//usdce
0,
address(this)
);

_before = underlyingAsset.balanceOf(address(this));

curveTriPool.remove_liquidity_one_coin(
_3crvTokenReceived,
1,
//usdce
0
);

amountDivested = underlyingAsset.balanceOf(address(this)) - _before;
}

/////////////////////////////////////////////////////////////////
/// VIEW FUNCTIONS ///
////////////////////////////////////////////////////////////////

/// @notice This function is meant to be called from the vault
/// @dev calculates the estimated real output of a withdrawal(including losses) for a @param requestedAmount
/// for the vault to be able to provide an accurate amount when calling `previewRedeem`
/// @return liquidatedAmount output in assets
function previewLiquidate(uint256 requestedAmount)
public
view
virtual
override
returns (uint256 liquidatedAmount)
{
uint256 loss;
uint256 underlyingBalance = _underlyingBalance();
// If underlying balance currently held by strategy is not enough to cover
// the requested amount, we divest from the beefy Vault
if (underlyingBalance < requestedAmount) {
uint256 amountToWithdraw;
unchecked {
amountToWithdraw = requestedAmount - underlyingBalance;
}
uint256 shares = _sharesForAmount(amountToWithdraw);
uint256 withdrawn = _shareValue(shares);
if (withdrawn < amountToWithdraw) loss = amountToWithdraw - withdrawn;
}
liquidatedAmount = (requestedAmount - loss);
}

////////////////////////////////////////////////////////////////
/// INTERNAL VIEW FUNCTIONS ///
////////////////////////////////////////////////////////////////

/// @notice Determines the current value of `shares`.
/// @return _assets the estimated amount of underlying computed from shares `shares`
function _shareValue(uint256 shares) internal view virtual override returns (uint256 _assets) {
uint256 expectedCurveLp = shares * beefyVault.balance() / beefyVault.totalSupply();
if (expectedCurveLp > 0) {
uint256 expected3Crv = curveLpPool.calc_withdraw_one_coin(expectedCurveLp, 1);
if (expected3Crv > 0) {
_assets = curveTriPool.calc_withdraw_one_coin(expected3Crv, 1);
}
}
}

/// @notice Determines how many shares depositor of `amount` of underlying would receive.
/// @return shares the estimated amount of shares computed in exchange for underlying `amount`
function _sharesForAmount(uint256 amount) internal view virtual override returns (uint256 shares) {
uint256[3] memory amounts;
amounts[1] = amount;

uint256 lpTokenAmount = curveTriPool.calc_token_amount(amounts, true);

uint256[2] memory _amounts;
_amounts[1] = lpTokenAmount;

lpTokenAmount = curveLpPool.calc_token_amount(_amounts, true);
shares = super._sharesForAmount(lpTokenAmount);
}

/// @notice Returns the estimated price for the strategy's curve's LP token
/// @return returns the estimated lp token price
function _lpPrice() internal view returns (uint256) {
return ((curveLpPool.get_virtual_price() * curveLpPool.get_dy(0, 1, 1 ether)) / 1 ether);
}

/// @notice Returns the estimated price for the strategy's curve's Tri pool LP token
/// @return returns the estimated lp token price
function _lpTriPoolPrice() internal view returns (uint256) {
return (
(
curveTriPool.get_virtual_price()
* Math.min(curveTriPool.get_dy(0, 1, 1 ether), curveTriPool.get_dy(1, 0, 1e6))
) / 1 ether
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.19;

import { FixedPointMathLib as Math } from "solady/utils/FixedPointMathLib.sol";

import { BaseBeefyCurveMetaPoolStrategy } from "src/strategies/base/BaseBeefyCurveMetaPoolStrategy.sol";
import { BaseBeefyStrategy, IMaxApyVault, SafeTransferLib } from "src/strategies/base/BaseBeefyStrategy.sol";


/// @title BeefythUSDDAIUSDCUSDTStrategy
/// @author Adapted from https://github.com/Grandthrax/yearn-steth-acc/blob/master/contracts/strategies.sol
/// @notice `BeefythUSDDAIUSDCUSDTStrategy` supplies an underlying token into a generic Beefy Vault,
/// earning the Beefy Vault's yield
contract BeefythUSDDAIUSDCUSDTStrategy is BaseBeefyCurveMetaPoolStrategy { }
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ contract BeefyaltETHfrxETHStrategy is BaseBeefyCurveStrategy {
// Add liquidity to the altETH<>frxETH pool in frxETH [coin1 -> frxETH]
lpReceived = curveLpPool.add_liquidity(amounts, 0, address(this));


uint256 _before = beefyVault.balanceOf(address(this));

// Deposit Curve LP tokens to Beefy vault
Expand Down
37 changes: 33 additions & 4 deletions test/fuzz/MaxApyTestMainnetUSDC.integration.fuzz.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { StrategyEvents } from "test/helpers/StrategyEvents.sol";
// Compound v3
import { CompoundV3USDTStrategyWrapper } from "../mock/CompoundV3USDTStrategyWrapper.sol";

// Beefy
import { BeefythUSDDAIUSDCUSDTStrategyWrapper } from "../mock/BeefythUSDDAIUSDCUSDTStrategyWrapper.sol";

// Vault fuzzer
import { MaxApyVaultFuzzer } from "./fuzzers/MaxApyVaultFuzzer.t.sol";
import { StrategyFuzzer } from "./fuzzers/StrategyFuzzer.t.sol";
Expand Down Expand Up @@ -52,6 +55,7 @@ contract MaxApyIntegrationTestMainnet is BaseTest, StrategyEvents {
////////////////////////////////////////////////////////////////

ICompoundV3StrategyWrapper public strategy1; // yearn weth
IStrategyWrapper public strategy2;

IMaxApyVault public vault;
ITransparentUpgradeableProxy public proxy;
Expand All @@ -77,6 +81,11 @@ contract MaxApyIntegrationTestMainnet is BaseTest, StrategyEvents {
address[] memory keepers = new address[](1);
keepers[0] = users.keeper;

/////////////////////////////////////////////////////////////////////////
/// STRATEGIES ///
/////////////////////////////////////////////////////////////////////////
/// Deploy transparent upgradeable proxy admin

// Deploy strategy1
CompoundV3USDTStrategyWrapper implementation1 = new CompoundV3USDTStrategyWrapper();
TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy(
Expand All @@ -94,18 +103,37 @@ contract MaxApyIntegrationTestMainnet is BaseTest, StrategyEvents {
UNISWAP_V3_ROUTER_MAINNET
)
);
proxy = ITransparentUpgradeableProxy(address(_proxy));
vm.label(COMPOUND_USDT_V3_COMMET_MAINNET, "CompoundV3USDT");
vm.label(address(proxy), "CompoundV3USDTStrategy");

strategy1 = ICompoundV3StrategyWrapper(address(_proxy));

address[] memory strategyList = new address[](1);
// Deploy strategy2
BeefythUSDDAIUSDCUSDTStrategyWrapper implementation2 = new BeefythUSDDAIUSDCUSDTStrategyWrapper();
_proxy = new TransparentUpgradeableProxy(
address(implementation2),
address(proxyAdmin),
abi.encodeWithSignature(
"initialize(address,address[],bytes32,address,address,address,address,address)",
address(vault),
keepers,
bytes32("MaxApy thUSDDAIUSDCUSDT Strategy"),
users.alice,
CURVE_THUSD_DAI_USDC_USDT_MAINNET,
BEEFY_THUSD_DAI_USDC_USDT_MAINNET,
CURVE_3POOL_POOL_MAINNET,
CRV3POOL_MAINNET
)
);
vm.label(BEEFY_THUSD_DAI_USDC_USDT_MAINNET, "BeefythUSDTDAIUSDCUSDT");
strategy2 = IStrategyWrapper(address(_proxy));

address[] memory strategyList = new address[](2);

strategyList[0] = address(strategy1);
strategyList[1] = address(strategy2);

// Add all the strategies
vault.addStrategy(address(strategy1), 700, type(uint72).max, 0, 0);
vault.addStrategy(address(strategy2), 700, type(uint72).max, 0, 0);

vm.label(address(USDC_MAINNET), "USDC");
/// Alice approves vault for deposits
Expand All @@ -123,6 +151,7 @@ contract MaxApyIntegrationTestMainnet is BaseTest, StrategyEvents {
uint256 _keeperRole = strategy1.KEEPER_ROLE();

strategy1.grantRoles(address(strategyFuzzer), _keeperRole);
strategy2.grantRoles(address(strategyFuzzer), _keeperRole);
}

function testFuzzMaxApyIntegrationMainnet__DepositAndRedeemWithoutHarvests(
Expand Down
Loading