Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: integrate with Euler V2 vaults #231

Merged
merged 42 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1be34c6
chore: import Euler v2 interfaces
xenide Nov 14, 2024
9963eff
wip: compiles
xenide Nov 14, 2024
dcd930c
wip: getting tests to work
xenide Nov 16, 2024
a667399
wip: first tests passing
xenide Nov 16, 2024
15f47e5
wip: getting more tests to work
xenide Nov 17, 2024
756c8c9
test: fix `testGetBalance_AddingAfterProfit`
xenide Nov 17, 2024
38dcc66
test: fix `testGetBalance`
xenide Nov 17, 2024
66e5652
test: fix `testSwap_ReturnAsset*`
xenide Nov 17, 2024
1f95ece
test: fix `testShares*`
xenide Nov 17, 2024
c301b38
test: fix some
xenide Nov 17, 2024
b289fbf
test: rm unused tests
xenide Nov 17, 2024
3bac0e4
fix: use `IERC4626` instead of the euler specific one
xenide Nov 17, 2024
07261a4
feat: introduce tracking for totalShares
xenide Nov 19, 2024
a461939
feat: introduce tracking for totalShares
xenide Nov 19, 2024
b4bd2cf
test: add back `totalShares` assertions
xenide Nov 19, 2024
a9baa6a
test: more tests
xenide Nov 19, 2024
a6575f7
test: use mainnet rpc url
xenide Nov 19, 2024
4803c67
feat: additional rewards
xenide Nov 20, 2024
842db70
fix: arithmetic
xenide Nov 20, 2024
825f93c
docs: clean up references to AAVE
xenide Nov 20, 2024
d9bd36f
docs: clean up
xenide Nov 20, 2024
35c0708
fix: optimize sloads
xenide Nov 20, 2024
39fb6eb
docs: clarify
xenide Nov 20, 2024
b231e38
fix: incorrect arithmetic
xenide Nov 20, 2024
d5c4201
fix: simplify invest and divest logic
xenide Nov 20, 2024
0720c03
test: enhance assertion
xenide Nov 21, 2024
a221f86
test: claim rewards
xenide Nov 22, 2024
9f011be
lint: forge fmt
xenide Nov 22, 2024
67ef29f
lint: forge fmt for comments
xenide Nov 24, 2024
f544367
merge: dev
xenide Nov 24, 2024
f36c826
lib: upgrade forge-std
xenide Nov 25, 2024
91ae3e6
lint: address lint concerns
xenide Nov 25, 2024
218dfe9
docs: improve docs on tests
xenide Nov 25, 2024
27a6944
ci: upgrade codecov gh action to v5
xenide Nov 26, 2024
37d3dff
ci: introduce coverage-integration step
xenide Nov 26, 2024
0a302de
ci: set fuzz seed and pin block for fork test
xenide Nov 26, 2024
f4aa6f3
ci: change rpc server
xenide Nov 26, 2024
27c4b2e
ci: rm redundant build step
xenide Nov 27, 2024
a52434a
test: enhance test to cover missed line
xenide Nov 27, 2024
42c0433
ci: introduce dependency to reduce resource wastage
xenide Nov 27, 2024
eca2cd7
test: add case to show resilience
xenide Nov 27, 2024
c1a6373
fix: rm unused constructor
xenide Nov 28, 2024
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
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[profile.default]
solc = "0.8.23"
evm_version = "cancun"
#via_ir = true
bytecode_hash = "ipfs"
optimizer_runs = 1_000_000
Expand Down
8 changes: 4 additions & 4 deletions script/optimized-deployer-meta
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"constant_product_hash": "0x0e8418aba85c38e3e25a9e655cb69242f1da220d6be7a7f20b7509a3ee853a14",
"factory_hash": "0xdf646e2ccb4c813b15e8e98f3e2b4697eb49a634c1d48552667c5039d19e54be",
"oracle_caller_hash": "0xcea9b38a517ef35b1b8a588828aa1f52c13e73326034aac3a6eed1181d411fb2",
"stable_hash": "0x9cec2442157c554854bd1bdecb0e4162e31f272d288f724178e0c0eb8bab1d01"
"constant_product_hash": "0xc71cecbc0e1d1f469238240af364b1e630c26ba1fc642f8a3fd21c0e52daf1e9",
"factory_hash": "0x17febc039446d6858c3c0977ce47bd73b0a088c27037bc9d57c2755dc2ec6632",
"oracle_caller_hash": "0x620d9bad49990a6cc26a8d0f8054c912c21868feaff000970fd42da4852c5cb1",
"stable_hash": "0xcb74036cb3dce8a651e27facc0e4bf8c4aa7f97a44773689ed075209699edc51"
}
8 changes: 4 additions & 4 deletions src/ReservoirDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ contract ReservoirDeployer {
uint256 public step = 0;

// Bytecode hashes.
bytes32 public constant FACTORY_HASH = bytes32(0xdf646e2ccb4c813b15e8e98f3e2b4697eb49a634c1d48552667c5039d19e54be);
bytes32 public constant FACTORY_HASH = bytes32(0x17febc039446d6858c3c0977ce47bd73b0a088c27037bc9d57c2755dc2ec6632);
bytes32 public constant CONSTANT_PRODUCT_HASH =
bytes32(0x0e8418aba85c38e3e25a9e655cb69242f1da220d6be7a7f20b7509a3ee853a14);
bytes32 public constant STABLE_HASH = bytes32(0x9cec2442157c554854bd1bdecb0e4162e31f272d288f724178e0c0eb8bab1d01);
bytes32(0xc71cecbc0e1d1f469238240af364b1e630c26ba1fc642f8a3fd21c0e52daf1e9);
bytes32 public constant STABLE_HASH = bytes32(0xcb74036cb3dce8a651e27facc0e4bf8c4aa7f97a44773689ed075209699edc51);
bytes32 public constant ORACLE_CALLER_HASH =
bytes32(0xcea9b38a517ef35b1b8a588828aa1f52c13e73326034aac3a6eed1181d411fb2);
bytes32(0x620d9bad49990a6cc26a8d0f8054c912c21868feaff000970fd42da4852c5cb1);

// Deployment addresses.
GenericFactory public factory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,63 +10,51 @@ import { Address } from "@openzeppelin/utils/Address.sol";

import { IAssetManagedPair } from "src/interfaces/IAssetManagedPair.sol";
import { IAssetManager, IERC20 } from "src/interfaces/IAssetManager.sol";
import { IPoolAddressesProvider } from "src/interfaces/aave/IPoolAddressesProvider.sol";
import { IPool } from "src/interfaces/aave/IPool.sol";
import { IAaveProtocolDataProvider } from "src/interfaces/aave/IAaveProtocolDataProvider.sol";
import { IRewardsController } from "src/interfaces/aave/IRewardsController.sol";
import { IDistributor } from "src/interfaces/merkl/IDistributor.sol";
import { IERC4626 } from "lib/forge-std/src/interfaces/IERC4626.sol";

contract AaveManager is IAssetManager, Owned(msg.sender), ReentrancyGuard {
contract EulerV2Manager is IAssetManager, Owned(msg.sender), ReentrancyGuard {
using FixedPointMathLib for uint256;
using SafeCast for uint256;

event Pool(IPool newPool);
event DataProvider(IAaveProtocolDataProvider newDataProvider);
event RewardsController(IRewardsController newRewardsController);
event Guardian(address newGuardian);
event WindDownMode(bool windDown);
event VaultForAsset(IERC20 asset, IERC4626 vault);
event Thresholds(uint128 newLowerThreshold, uint128 newUpperThreshold);
event Investment(IAssetManagedPair pair, IERC20 token, uint256 shares);
event Divestment(IAssetManagedPair pair, IERC20 token, uint256 shares);

/// @dev tracks how many aToken each pair+token owns
error OutstandingSharesForVault();

/// @dev Mapping from an ERC20 token to an Euler V2 vault.
/// This implies that for a given asset, there can only be one vault.
/// If the admin of the manager wishes to specify a different vault for an asset, they would have to manually ensure that all pairs have
/// divested, otherwise the pairs might not be able to retrieve their assets.
xenide marked this conversation as resolved.
Show resolved Hide resolved
mapping(IERC20 => IERC4626) public assetVault;

/// @dev Tracks how many shares each pair+token owns.
mapping(IAssetManagedPair => mapping(IERC20 => uint256)) public shares;

/// @dev for each aToken, tracks the total number of shares issued
mapping(IERC20 => uint256) public totalShares;
/// @dev Tracks the total number of shares for a given vault held by this contract.
mapping(IERC4626 => uint256) public totalShares;

/// @dev percentage of the pool's assets, above and below which
/// the manager will divest the shortfall and invest the excess
/// @dev Percentage of the pool's assets, above and below which
/// the manager will divest the shortfall and invest the excess.
/// 1e18 == 100%
uint128 public upperThreshold = 0.7e18; // 70%
uint128 public lowerThreshold = 0.3e18; // 30%

/// @dev this contract itself is immutable and is the source of truth for all relevant addresses for aave
IPoolAddressesProvider public immutable addressesProvider;

/// @dev we interact with this address for deposits and withdrawals
IPool public pool;

/// @dev this address is not permanent, aave can change this address to upgrade to a new impl
IAaveProtocolDataProvider public dataProvider;

/// @dev trusted party to adjust asset management parameters such as thresholds and windDownMode and
/// @dev Trusted party to adjust asset management parameters such as thresholds and windDownMode and
/// to claim and sell additional rewards (through a DEX/aggregator) into the corresponding
/// Aave Token on behalf of the asset manager and then transfers the Aave Tokens back into the manager
/// underlying tokens on behalf of the asset manager and then transfers them back into the manager.
address public guardian;

/// @dev contract that manages additional rewards on top of interest bearing aave tokens
/// also known as the incentives contract
IRewardsController public rewardsController;

/// @dev when set to true by the owner or guardian, it will only allow divesting but not investing by
/// the pairs in this mode to facilitate replacement of asset managers to newer versions
/// @dev When set to true by the owner or guardian, it will only allow divesting but not investing by
/// the pairs in this mode to facilitate replacement of asset managers to newer versions.
bool public windDownMode;

constructor(IPoolAddressesProvider aPoolAddressesProvider) {
addressesProvider = aPoolAddressesProvider;
updatePoolAddress();
updateDataProviderAddress();
}
// solhint-disable-next-line no-empty-blocks
constructor() { }

/*//////////////////////////////////////////////////////////////////////////
MODIFIERS
Expand All @@ -81,24 +69,16 @@ contract AaveManager is IAssetManager, Owned(msg.sender), ReentrancyGuard {
ADMIN ACTIONS
//////////////////////////////////////////////////////////////////////////*/

function updatePoolAddress() public onlyOwner {
address lNewPool = addressesProvider.getPool();
require(lNewPool != address(0), "AM: POOL_ADDRESS_ZERO");
pool = IPool(lNewPool);
emit Pool(IPool(lNewPool));
}

function updateDataProviderAddress() public onlyOwner {
address lNewDataProvider = addressesProvider.getPoolDataProvider();
require(lNewDataProvider != address(0), "AM: DATA_PROVIDER_ADDRESS_ZERO");
dataProvider = IAaveProtocolDataProvider(lNewDataProvider);
emit DataProvider(IAaveProtocolDataProvider(lNewDataProvider));
}
function setVaultForAsset(IERC20 aAsset, IERC4626 aVault) external onlyOwner {
IERC4626 lVault = assetVault[aAsset];
// this is to prevent accidental moving of vaults when there are still shares outstanding
// as it will prevent the AMM pairs from redeeming underlying tokens from the vault
if (address(lVault) != address(0) && totalShares[lVault] != 0) {
revert OutstandingSharesForVault();
}

function setRewardsController(address aRewardsController) external onlyOwner {
require(aRewardsController != address(0), "AM: REWARDS_CONTROLLER_ZERO");
rewardsController = IRewardsController(aRewardsController);
emit RewardsController(IRewardsController(aRewardsController));
assetVault[aAsset] = aVault;
emit VaultForAsset(aAsset, aVault);
}

function setGuardian(address aGuardian) external onlyOwner {
Expand Down Expand Up @@ -128,62 +108,46 @@ contract AaveManager is IAssetManager, Owned(msg.sender), ReentrancyGuard {
HELPER FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

function _increaseShares(IAssetManagedPair aPair, IERC20 aToken, IERC20 aAaveToken, uint256 aAmount)
function _increaseShares(IAssetManagedPair aPair, IERC20 aToken, IERC4626 aVault, uint256 aAmount)
private
returns (uint256 rShares)
{
uint256 lTotalShares = totalShares[aAaveToken];
if (totalShares[aAaveToken] == 0) {
rShares = aAmount;
} else {
rShares = aAmount * lTotalShares / aAaveToken.balanceOf(address(this));
}
rShares = aVault.previewDeposit(aAmount);
xenide marked this conversation as resolved.
Show resolved Hide resolved
totalShares[aVault] += rShares;
shares[aPair][aToken] += rShares;
totalShares[aAaveToken] += rShares;
}

function _decreaseShares(IAssetManagedPair aPair, IERC20 aToken, IERC20 aAaveToken, uint256 aAmount)
function _decreaseShares(IAssetManagedPair aPair, IERC20 aToken, IERC4626 aVault, uint256 aAmount)
private
returns (uint256 rShares)
{
rShares = aAmount.mulDivUp(totalShares[aAaveToken], aAaveToken.balanceOf(address(this)));

rShares = aVault.previewWithdraw(aAmount);
totalShares[aVault] -= rShares;
shares[aPair][aToken] -= rShares;
totalShares[aAaveToken] -= rShares;
}

/// @notice returns the address of the AAVE token.
/// If an AAVE token doesn't exist for the asset, returns address 0
function _getATokenAddress(IERC20 aToken) private view returns (IERC20) {
(address lATokenAddress,,) = dataProvider.getReserveTokensAddresses(address(aToken));

return IERC20(lATokenAddress);
}

/*//////////////////////////////////////////////////////////////////////////
GET BALANCE
//////////////////////////////////////////////////////////////////////////*/

/// @dev returns the balance of the token managed by various markets in the native precision
/// @dev Returns the balance of the underlying token managed the asset manager in the native precision.
function getBalance(IAssetManagedPair aOwner, IERC20 aToken) external view returns (uint256) {
return _getBalance(aOwner, aToken);
}

function _getBalance(IAssetManagedPair aOwner, IERC20 aToken) private view returns (uint256 rTokenBalance) {
IERC20 lAaveToken = _getATokenAddress(aToken);
uint256 lTotalShares = totalShares[lAaveToken];
if (lTotalShares == 0) {
return 0;
}
IERC4626 lVault = assetVault[aToken];

rTokenBalance = shares[aOwner][aToken] * lAaveToken.balanceOf(address(this)) / lTotalShares;
if (address(lVault) != address(0)) {
uint256 lShares = shares[aOwner][aToken];
rTokenBalance = lVault.convertToAssets(lShares);
}
}

/*//////////////////////////////////////////////////////////////////////////
ADJUST MANAGEMENT
//////////////////////////////////////////////////////////////////////////*/

/// @notice if token0 or token1 does not have a market in AAVE, the tokens will not be transferred
function adjustManagement(IAssetManagedPair aPair, int256 aAmount0Change, int256 aAmount1Change)
external
onlyOwner
Expand All @@ -198,14 +162,14 @@ contract AaveManager is IAssetManager, Owned(msg.sender), ReentrancyGuard {
IERC20 lToken0 = aPair.token0();
IERC20 lToken1 = aPair.token1();

IERC20 lToken0AToken = _getATokenAddress(lToken0);
IERC20 lToken1AToken = _getATokenAddress(lToken1);
IERC4626 lToken0Vault = assetVault[lToken0];
IERC4626 lToken1Vault = assetVault[lToken1];

// do not do anything if there isn't a market for the token
if (address(lToken0AToken) == address(0)) {
// do not do anything if there isn't a designated vault for the token
if (address(lToken0Vault) == address(0)) {
aAmount0Change = 0;
}
if (address(lToken1AToken) == address(0)) {
if (address(lToken1Vault) == address(0)) {
xenide marked this conversation as resolved.
Show resolved Hide resolved
aAmount1Change = 0;
}

Expand All @@ -218,48 +182,53 @@ contract AaveManager is IAssetManager, Owned(msg.sender), ReentrancyGuard {
}
}

// withdraw from the market
// withdraw from the vault
if (aAmount0Change < 0) {
uint256 lAmount0Change;
unchecked {
lAmount0Change = uint256(-aAmount0Change);
}
_doDivest(aPair, lToken0, lToken0AToken, lAmount0Change);
_doDivest(aPair, lToken0, lToken0Vault, lAmount0Change);
}
if (aAmount1Change < 0) {
uint256 lAmount1Change;
unchecked {
lAmount1Change = uint256(-aAmount1Change);
}
_doDivest(aPair, lToken1, lToken1AToken, lAmount1Change);
_doDivest(aPair, lToken1, lToken1Vault, lAmount1Change);
}

// transfer tokens to/from the pair
aPair.adjustManagement(aAmount0Change, aAmount1Change);

// transfer the managed tokens to the destination
if (aAmount0Change > 0) {
_doInvest(aPair, lToken0, lToken0AToken, uint256(aAmount0Change));
_doInvest(aPair, lToken0, lToken0Vault, uint256(aAmount0Change));
}
if (aAmount1Change > 0) {
_doInvest(aPair, lToken1, lToken1AToken, uint256(aAmount1Change));
_doInvest(aPair, lToken1, lToken1Vault, uint256(aAmount1Change));
}
}

function _doDivest(IAssetManagedPair aPair, IERC20 aToken, IERC20 aAaveToken, uint256 aAmount) private {
uint256 lShares = _decreaseShares(aPair, aToken, aAaveToken, aAmount);
pool.withdraw(address(aToken), aAmount, address(this));
function _doDivest(IAssetManagedPair aPair, IERC20 aToken, IERC4626 aVault, uint256 aAmount) private {
uint256 lShares = _decreaseShares(aPair, aToken, aVault, aAmount);
uint256 lSharesBurned = aVault.withdraw(aAmount, address(this), address(this));

require(lShares == lSharesBurned, "AM: DIVEST_SHARES_MISMATCH");

emit Divestment(aPair, aToken, lShares);
SafeTransferLib.safeApprove(address(aToken), address(aPair), aAmount);
}

function _doInvest(IAssetManagedPair aPair, IERC20 aToken, IERC20 aAaveToken, uint256 aAmount) private {
function _doInvest(IAssetManagedPair aPair, IERC20 aToken, IERC4626 aVault, uint256 aAmount) private {
require(aToken.balanceOf(address(this)) == aAmount, "AM: TOKEN_AMOUNT_MISMATCH");
uint256 lShares = _increaseShares(aPair, aToken, aAaveToken, aAmount);
SafeTransferLib.safeApprove(address(aToken), address(pool), aAmount);
uint256 lExpectedShares = _increaseShares(aPair, aToken, aVault, aAmount);
SafeTransferLib.safeApprove(address(aToken), address(aVault), aAmount);

pool.supply(address(aToken), aAmount, address(this), 0);
emit Investment(aPair, aToken, lShares);
uint256 lSharesReceived = aVault.deposit(aAmount, address(this));
require(lExpectedShares == lSharesReceived, "AM: INVEST_SHARES_MISMATCH");

emit Investment(aPair, aToken, lSharesReceived);
}

/*//////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -305,17 +274,50 @@ contract AaveManager is IAssetManager, Owned(msg.sender), ReentrancyGuard {
ADDITIONAL REWARDS
//////////////////////////////////////////////////////////////////////////*/

function claimRewardForMarket(address aMarket, address aReward)
function claimRewards(
IDistributor aDistributor,
address[] calldata aUsers,
address[] calldata aTokens,
uint256[] calldata aAmounts,
bytes32[][] calldata aProofs
) external onlyGuardianOrOwner {
xenide marked this conversation as resolved.
Show resolved Hide resolved
aDistributor.claim(aUsers, aTokens, aAmounts, aProofs);

for (uint256 i = 0; i < aTokens.length; ++i) {
SafeTransferLib.safeTransfer(aTokens[i], msg.sender, aAmounts[i]);
}
}

/// @dev The guardian or owner would first claim rewards on behalf of the asset manager by calling `claimRewards` and sell it for the underlying token.
/// The asset manager pulls the assets, deposits it to the vault, and distribute the proceeds in the form of ERC4626 shares to the pairs.
/// Due to integer arithmetic the last pair of the array will get one or two more shares, so as to maintain the invariant that
/// the sum of shares for all pair+token equals the totalShares.
function distributeRewardForPairs(IERC20 aAsset, uint256 aAmount, IAssetManagedPair[] calldata aPairs)
external
onlyGuardianOrOwner
returns (uint256 rClaimed)
{
require(aReward != address(0), "AM: REWARD_TOKEN_ZERO");
require(aMarket != address(0), "AM: MARKET_ZERO");

address[] memory lMarkets = new address[](1);
lMarkets[0] = aMarket;
// pull assets from guardian / owner
SafeTransferLib.safeTransferFrom(address(aAsset), msg.sender, address(this), aAmount);
IERC4626 lVault = assetVault[aAsset];
SafeTransferLib.safeApprove(address(aAsset), address(lVault), aAmount);
uint256 lNewShares = lVault.deposit(aAmount, address(this));

uint256 lOldTotalShares = totalShares[lVault];
totalShares[lVault] = lOldTotalShares + lNewShares;
uint256 lSharesAllocated;
uint256 lLength = aPairs.length;
for (uint256 i = 0; i < lLength - 1; ++i) {
uint256 lOldShares = shares[aPairs[i]][aAsset];
uint256 lNewSharesEntitled = lNewShares.mulDiv(lOldShares, lOldTotalShares);
shares[aPairs[i]][aAsset] = lOldShares + lNewSharesEntitled;
lSharesAllocated += lNewSharesEntitled;
}

rClaimed = rewardsController.claimRewards(lMarkets, type(uint256).max, msg.sender, aReward);
// the last in the list will take all the remaining shares, and sometimes will get 1 or 2 more than they're entitled to
// due to the rounding down in previous calculations for other pairs
// this is to prevent the sum of each pair+token's shares not summing up to totalShares
shares[aPairs[lLength - 1]][aAsset] = lNewShares - lSharesAllocated;
lSharesAllocated += shares[aPairs[lLength - 1]][aAsset];
require(lSharesAllocated == lNewShares, "AM: REWARD_SHARES_MISMATCH");
OliverNChalk marked this conversation as resolved.
Show resolved Hide resolved
}
}
12 changes: 12 additions & 0 deletions src/interfaces/merkl/IDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

// adapted from https://github.com/AngleProtocol/merkl-contracts/blob/main/contracts/Distributor.sol
interface IDistributor {
function claim(
address[] calldata users,
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs
) external;
}
Loading
Loading