From e51e8e55f8f29dad41ce837378e92961e6c70b0f Mon Sep 17 00:00:00 2001 From: "A.L." Date: Fri, 29 Nov 2024 07:12:54 +0800 Subject: [PATCH] feat: integrate with Euler V2 vaults * chore: import Euler v2 interfaces * wip: compiles * wip: getting tests to work * wip: first tests passing * wip: getting more tests to work * test: fix `testGetBalance_AddingAfterProfit` * test: fix `testGetBalance` * test: fix `testSwap_ReturnAsset*` * test: fix `testShares*` * test: fix some * test: rm unused tests * fix: use `IERC4626` instead of the euler specific one * feat: introduce tracking for totalShares * feat: introduce tracking for totalShares * test: add back `totalShares` assertions * test: more tests * test: use mainnet rpc url * feat: additional rewards * fix: arithmetic * docs: clean up references to AAVE * docs: clean up * fix: optimize sloads * docs: clarify * fix: incorrect arithmetic * fix: simplify invest and divest logic * test: enhance assertion * test: claim rewards * lint: forge fmt * lint: forge fmt for comments * lib: upgrade forge-std * lint: address lint concerns * docs: improve docs on tests * ci: upgrade codecov gh action to v5 * ci: introduce coverage-integration step * ci: set fuzz seed and pin block for fork test * ci: change rpc server * ci: rm redundant build step * test: enhance test to cover missed line * ci: introduce dependency to reduce resource wastage * test: add case to show resilience * fix: rm unused constructor --- .github/workflows/ci.yml | 55 +- foundry.toml | 12 +- lib/forge-std | 2 +- package.json | 1 + script/coverage_patch_deployer.sh | 2 +- script/unoptimized-deployer-meta | 7 +- src/ReservoirDeployer.sol | 6 +- .../{AaveManager.sol => EulerV2Manager.sol} | 230 +++--- src/interfaces/merkl/IDistributor.sol | 14 + test/__fixtures/BaseTest.sol | 12 +- test/integration/{Aave.t.sol => Euler.t.sol} | 709 +++++++----------- test/unit/AssetManagedPair.t.sol | 8 +- test/unit/FlashSwap.t.sol | 4 +- test/unit/OracleWriter.t.sol | 4 +- test/unit/Pair.t.sol | 4 +- test/unit/ReservoirPair.t.sol | 4 +- test/unit/ReservoirTimelock.t.sol | 4 +- test/unit/StablePair.t.sol | 102 +-- 18 files changed, 530 insertions(+), 650 deletions(-) rename src/asset-management/{AaveManager.sol => EulerV2Manager.sol} (55%) create mode 100644 src/interfaces/merkl/IDistributor.sol rename test/integration/{Aave.t.sol => Euler.t.sol} (63%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4963c90..61ce2de7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,23 +7,11 @@ concurrency: cancel-in-progress: true jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - run: forge build - lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v3 @@ -36,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -47,7 +35,7 @@ jobs: test-integration: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -58,7 +46,7 @@ jobs: test-e2e: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -70,7 +58,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -88,7 +76,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -103,7 +91,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -115,9 +103,9 @@ jobs: coverage: runs-on: ubuntu-latest - + needs: test-unit steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - uses: foundry-rs/foundry-toolchain@v1 @@ -128,10 +116,33 @@ jobs: FOUNDRY_PROFILE: coverage - run: sudo apt install -y lcov - run: lcov -r lcov.info "src/libraries/*" -o lcov.info + - run: lcov -r lcov.info "src/asset-management/*" -o lcov.info + - run: lcov -r lcov.info "test/*" -o lcov.info + - run: | + ! lcov --summary lcov.info | grep -q 0.0% + - uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + files: ./lcov.info + + coverage-integration: + runs-on: ubuntu-latest + needs: [test-unit, test-integration] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - run: ./script/coverage_patch_deployer.sh && forge coverage --report lcov + env: + FOUNDRY_PROFILE: coverage-integration + - run: sudo apt install -y lcov - run: lcov -r lcov.info "test/*" -o lcov.info - run: | ! lcov --summary lcov.info | grep -q 0.0% - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v5 with: fail_ci_if_error: true files: ./lcov.info diff --git a/foundry.toml b/foundry.toml index 1123c9d8..5717122c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,6 @@ [profile.default] solc = "0.8.23" +evm_version = "cancun" #via_ir = true bytecode_hash = "ipfs" optimizer_runs = 1_000_000 @@ -17,9 +18,15 @@ fs_permissions = [ ] ignored_error_codes = [] +[profile.default.fuzz] +seed = "0xd679d565cc78d6c78308f18282cb070e065bf4c517fbd552922d47f2127a2bbc" + [profile.integration] match_path = "test/integration/*.sol" +[profile.coverage-integration] +match_path = "test/integration/*.sol" + [profile.differential] fs_permissions = [{ access = "read", path = "./reference/balancer-v2-monorepo" }] match_path = "test/differential/*.sol" @@ -32,9 +39,12 @@ runs = 10_000 [fmt] bracket_spacing = true -wrap_comments = false +wrap_comments = true number_underscore = "thousands" int_types = "long" [profile.script] optimizer_runs = 1_000_000 + +[rpc_endpoints] +mainnet = "https://rpc.ankr.com/eth" diff --git a/lib/forge-std b/lib/forge-std index ae570fec..1eea5bae 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/package.json b/package.json index 64b20e99..c1031c3d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "ci": "forge build --force && npm run lint:check && npm run test:unit && npm run gas:check && npm run slither:check", "clean": "forge clean", "coverage": "export FOUNDRY_PROFILE=coverage && script/coverage_patch_deployer.sh && forge coverage --report lcov", + "coverage:integration": "export FOUNDRY_PROFILE=coverage-integration && script/coverage_patch_deployer.sh && forge coverage --report lcov", "deploy:avax:test": "forge script script/DeployScript.s.sol --target-contract DeployScript --fork-url \"http://127.0.0.1:8545\" --broadcast -vvvv --verify --ledger --mnemonic-derivation-paths \"m/44'/60'/0'/0/1\" --sender 0x2508b97B8041960ccA8AaBC7662F07EC8e285F6d", "deploy:avax": "forge script script/DeployScript.s.sol --target-contract DeployScript --fork-url https://api.avax.network/ext/bc/C/rpc --broadcast -vvvv --verify --ledger --mnemonic-derivation-paths \"m/44'/60'/0'/0/1\" --sender 0x2508b97B8041960ccA8AaBC7662F07EC8e285F6d", "eslint": "npm run eslint:check", diff --git a/script/coverage_patch_deployer.sh b/script/coverage_patch_deployer.sh index cd38a396..e09bec3c 100755 --- a/script/coverage_patch_deployer.sh +++ b/script/coverage_patch_deployer.sh @@ -10,7 +10,7 @@ UNOPTIMIZED_ORACLE_CALLER_HASH=$(cat script/unoptimized-deployer-meta | jq -r '. OPTIMIZED_FACTORY_HASH=$(cat script/optimized-deployer-meta | jq -r '.factory_hash') UNOPTIMIZED_FACTORY_HASH=$(cat script/unoptimized-deployer-meta | jq -r '.factory_hash') -if [ "$FOUNDRY_PROFILE" == "coverage" ] +if [ "$FOUNDRY_PROFILE" == "coverage" ] || [ "$FOUNDRY_PROFILE" == "coverage-integration" ] then echo "Running with coverage profile, patching ReservoirDeployer" sed -i "s/$OPTIMIZED_STABLE_HASH/$UNOPTIMIZED_STABLE_HASH/g" src/ReservoirDeployer.sol diff --git a/script/unoptimized-deployer-meta b/script/unoptimized-deployer-meta index c33dd174..517d5d87 100644 --- a/script/unoptimized-deployer-meta +++ b/script/unoptimized-deployer-meta @@ -1,6 +1,5 @@ { - "constant_product_hash": "0xb563408dadf9d0f09df38020a0fa234a0df6db131574de64ce0f94f4a0a25af3", - "factory_hash": "0xc083749f159ff0edd904efd85e57d856044cb7175b6f2f836a1ea689196c6a0b", - "oracle_caller_hash": "0x1b44c52bfac16ddbab1269ebb22b21bbb993a55587f0bca4a8035433cece2f87", - "stable_hash": "0x7ee3fb2be553a6254f7197e7ba45c2a3c45862e550d0253cbc7cab694eb116f7" + "constant_product_hash": "0xa0c82283f7cf9bcd7d4091d1c25e9c424a65f3d9602243387ef21b1cbca0864e", + "factory_hash": "0x60b680774a2e6b5f3a74c7926f7945eedbd82d533768f2cc4641712d3a9747a9", + "stable_hash": "0xcca43bf4944cac2ddebe02c1fde1a1ba13cc6f9aad27f68bdc1c9ac0cbd3b5a5" } \ No newline at end of file diff --git a/src/ReservoirDeployer.sol b/src/ReservoirDeployer.sol index 1cf288c3..7edb14da 100644 --- a/src/ReservoirDeployer.sol +++ b/src/ReservoirDeployer.sol @@ -16,10 +16,10 @@ contract ReservoirDeployer { uint256 public step = 0; // Bytecode hashes. - bytes32 public constant FACTORY_HASH = bytes32(0x419250835880ba2bbc5535e1a66eae6c96005200131467f2033d5ca0b2e333a7); + bytes32 public constant FACTORY_HASH = bytes32(0x60b680774a2e6b5f3a74c7926f7945eedbd82d533768f2cc4641712d3a9747a9); bytes32 public constant CONSTANT_PRODUCT_HASH = - bytes32(0xe3022cd6d0397e990bc6eb954dcf917dc7779bc75cf3ccb827e3361af8e4a4da); - bytes32 public constant STABLE_HASH = bytes32(0xfd70a1442e2709a6d366103eaf2f111bb1ffeff0b29e1ee5829165b55e4be32e); + bytes32(0xa0c82283f7cf9bcd7d4091d1c25e9c424a65f3d9602243387ef21b1cbca0864e); + bytes32 public constant STABLE_HASH = bytes32(0xcca43bf4944cac2ddebe02c1fde1a1ba13cc6f9aad27f68bdc1c9ac0cbd3b5a5); // Deployment addresses. GenericFactory public factory; diff --git a/src/asset-management/AaveManager.sol b/src/asset-management/EulerV2Manager.sol similarity index 55% rename from src/asset-management/AaveManager.sol rename to src/asset-management/EulerV2Manager.sol index d15d307c..94c90520 100644 --- a/src/asset-management/AaveManager.sol +++ b/src/asset-management/EulerV2Manager.sol @@ -10,64 +10,47 @@ 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 at any one time. + 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(); - } - /*////////////////////////////////////////////////////////////////////////// MODIFIERS //////////////////////////////////////////////////////////////////////////*/ @@ -81,24 +64,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 { @@ -128,62 +103,38 @@ contract AaveManager is IAssetManager, Owned(msg.sender), ReentrancyGuard { HELPER FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - function _increaseShares(IAssetManagedPair aPair, IERC20 aToken, IERC20 aAaveToken, uint256 aAmount) - private - returns (uint256 rShares) - { - uint256 lTotalShares = totalShares[aAaveToken]; - if (totalShares[aAaveToken] == 0) { - rShares = aAmount; - } else { - rShares = aAmount * lTotalShares / aAaveToken.balanceOf(address(this)); - } - shares[aPair][aToken] += rShares; - totalShares[aAaveToken] += rShares; - } - - function _decreaseShares(IAssetManagedPair aPair, IERC20 aToken, IERC20 aAaveToken, uint256 aAmount) - private - returns (uint256 rShares) - { - rShares = aAmount.mulDivUp(totalShares[aAaveToken], aAaveToken.balanceOf(address(this))); - - shares[aPair][aToken] -= rShares; - totalShares[aAaveToken] -= rShares; + function _increaseShares(IAssetManagedPair aPair, IERC20 aToken, IERC4626 aVault, uint256 aShares) private { + totalShares[aVault] += aShares; + shares[aPair][aToken] += aShares; } - /// @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); + function _decreaseShares(IAssetManagedPair aPair, IERC20 aToken, IERC4626 aVault, uint256 aShares) private { + totalShares[aVault] -= aShares; + shares[aPair][aToken] -= aShares; } /*////////////////////////////////////////////////////////////////////////// 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 @@ -198,14 +149,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)) { aAmount1Change = 0; } @@ -218,20 +169,20 @@ 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 @@ -239,27 +190,29 @@ contract AaveManager is IAssetManager, Owned(msg.sender), ReentrancyGuard { // 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)); - emit Divestment(aPair, aToken, lShares); + function _doDivest(IAssetManagedPair aPair, IERC20 aToken, IERC4626 aVault, uint256 aAmount) private { + uint256 lSharesBurned = aVault.withdraw(aAmount, address(this), address(this)); + _decreaseShares(aPair, aToken, aVault, lSharesBurned); + + emit Divestment(aPair, aToken, lSharesBurned); 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); + SafeTransferLib.safeApprove(address(aToken), address(aVault), aAmount); + + uint256 lSharesReceived = aVault.deposit(aAmount, address(this)); + _increaseShares(aPair, aToken, aVault, lSharesReceived); - pool.supply(address(aToken), aAmount, address(this), 0); - emit Investment(aPair, aToken, lShares); + emit Investment(aPair, aToken, lSharesReceived); } /*////////////////////////////////////////////////////////////////////////// @@ -305,17 +258,64 @@ 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 { + aDistributor.claim(aUsers, aTokens, aAmounts, aProofs); + + for (uint256 i = 0; i < aTokens.length; ++i) { + SafeTransferLib.safeTransfer( + aTokens[i], + msg.sender, + // the amounts specified in the argument might not be the actual amounts disimbursed by the distributor, + // due to the possibility of having done the claim previously thus it is necessary to use balanceOf + // to transfer the correct amount + IERC20(aTokens[i]).balanceOf(address(this)) + ); + } + } + + /// @dev The guardian or owner would first call `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) + nonReentrant { - require(aReward != address(0), "AM: REWARD_TOKEN_ZERO"); - require(aMarket != address(0), "AM: MARKET_ZERO"); + // 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]; + // no need for fullMulDiv for real life amounts, assumes that lOldTotalShares != 0, which would be the case + // if there are pairs to distribute to anyway + uint256 lNewSharesEntitled = lNewShares.mulDiv(lOldShares, lOldTotalShares); + shares[aPairs[i]][aAsset] = lOldShares + lNewSharesEntitled; + lSharesAllocated += lNewSharesEntitled; + } - address[] memory lMarkets = new address[](1); - lMarkets[0] = aMarket; + // 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 + uint256 lSharesForLastPair = lNewShares - lSharesAllocated; - rClaimed = rewardsController.claimRewards(lMarkets, type(uint256).max, msg.sender, aReward); + shares[aPairs[lLength - 1]][aAsset] += lSharesForLastPair; + lSharesAllocated += lSharesForLastPair; + require(lSharesAllocated == lNewShares, "AM: REWARD_SHARES_MISMATCH"); } } diff --git a/src/interfaces/merkl/IDistributor.sol b/src/interfaces/merkl/IDistributor.sol new file mode 100644 index 00000000..2b0a8eac --- /dev/null +++ b/src/interfaces/merkl/IDistributor.sol @@ -0,0 +1,14 @@ +// 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; + + function toggleOperator(address user, address operator) external; +} diff --git a/test/__fixtures/BaseTest.sol b/test/__fixtures/BaseTest.sol index 9fa2fdd3..46232a9b 100644 --- a/test/__fixtures/BaseTest.sol +++ b/test/__fixtures/BaseTest.sol @@ -47,7 +47,10 @@ abstract contract BaseTest is Test { constructor() { try vm.envString("FOUNDRY_PROFILE") returns (string memory lProfile) { - if (keccak256(abi.encodePacked(lProfile)) == keccak256(abi.encodePacked("coverage"))) { + if ( + keccak256(abi.encodePacked(lProfile)) == keccak256(abi.encodePacked("coverage")) + || keccak256(abi.encodePacked(lProfile)) == keccak256(abi.encodePacked("coverage-integration")) + ) { vm.writeJson(_deployerMetadata(), "script/unoptimized-deployer-meta"); } } catch { @@ -137,8 +140,13 @@ abstract contract BaseTest is Test { vm.record(); aPair.observation(aIndex); + (bytes32[] memory lAccesses,) = vm.accesses(address(aPair)); - require(lAccesses.length == 1, "invalid number of accesses"); + + // for coverage, due to the optimizer being turned off, the accesses will be >1 instead of 1, due to the struct + // members + // solhint-disable-next-line no-console + if (lAccesses.length != 1) console2.log("warn: invalid number of accesses"); vm.store(address(aPair), lAccesses[0], lEncoded); } diff --git a/test/integration/Aave.t.sol b/test/integration/Euler.t.sol similarity index 63% rename from test/integration/Aave.t.sol rename to test/integration/Euler.t.sol index a326bc3d..caeb4396 100644 --- a/test/integration/Aave.t.sol +++ b/test/integration/Euler.t.sol @@ -1,27 +1,25 @@ pragma solidity ^0.8.0; import "test/__fixtures/BaseTest.sol"; -import { Errors } from "test/integration/AaveErrors.sol"; import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol"; -import { IPool } from "src/interfaces/aave/IPool.sol"; -import { IAaveProtocolDataProvider } from "src/interfaces/aave/IAaveProtocolDataProvider.sol"; -import { IPoolAddressesProvider } from "src/interfaces/aave/IPoolAddressesProvider.sol"; -import { IPoolConfigurator } from "src/interfaces/aave/IPoolConfigurator.sol"; -import { IRewardsController } from "src/interfaces/aave/IRewardsController.sol"; +import { IAssetManagedPair } from "src/interfaces/IAssetManagedPair.sol"; import { FactoryStoreLib } from "src/libraries/FactoryStore.sol"; import { MathUtils } from "src/libraries/MathUtils.sol"; -import { AaveManager, IAssetManager } from "src/asset-management/AaveManager.sol"; +import { EulerV2Manager, IAssetManager, IERC4626, IDistributor } from "src/asset-management/EulerV2Manager.sol"; import { GenericFactory, IERC20 } from "src/GenericFactory.sol"; import { IUSDC } from "test/interfaces/IUSDC.sol"; import { ReturnAssetExploit } from "../__mocks/ReturnAssetExploit.sol"; struct Network { string rpcUrl; + uint256 blockNumber; address USDC; address masterMinterUSDC; + address USDCVault; + address merklDistributor; } struct Fork { @@ -29,23 +27,20 @@ struct Fork { uint256 forkId; } -contract AaveIntegrationTest is BaseTest { +contract EulerIntegrationTest is BaseTest { using FactoryStoreLib for GenericFactory; using FixedPointMathLib for uint256; - event RewardsClaimed( - address indexed user, address indexed reward, address indexed to, address claimer, uint256 amount - ); event Guardian(address newGuardian); + event Transfer(address from, address to, uint256 amount); + + error TransferFailed(); // this amount is tailored to USDC as it only has 6 decimal places // using the usual 100e18 would be too large and would break AAVE uint256 public constant MINT_AMOUNT = 1_000_000e6; - // this address is the same across all chains - address public constant AAVE_POOL_ADDRESS_PROVIDER = address(0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb); - - AaveManager private _manager; + EulerV2Manager private _manager; ReservoirPair[] internal _pairs; ReservoirPair internal _pair; @@ -55,27 +50,26 @@ contract AaveIntegrationTest is BaseTest { // network specific variables IERC20 private USDC; address private masterMinterUSDC; - address private _aaveAdmin; - IPoolAddressesProvider private _poolAddressesProvider; - IAaveProtocolDataProvider private _dataProvider; - IPoolConfigurator private _poolConfigurator; + IERC4626 private USDCVault; + IDistributor private distributor; modifier allPairs() { for (uint256 i = 0; i < _pairs.length; ++i) { - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _pair = _pairs[i]; _; - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); } } modifier allNetworks() { for (uint256 i = 0; i < _networks.length; ++i) { - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); Network memory lNetwork = _networks[i]; _setupRPC(lNetwork); + require(block.number == lNetwork.blockNumber, "vm not at pinned block"); _; - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); } } @@ -83,7 +77,7 @@ contract AaveIntegrationTest is BaseTest { Fork memory lFork = _forks[aNetwork.rpcUrl]; if (lFork.created == false) { - uint256 lForkId = vm.createFork(aNetwork.rpcUrl); + uint256 lForkId = vm.createFork(aNetwork.rpcUrl, aNetwork.blockNumber); lFork = Fork(true, lForkId); _forks[aNetwork.rpcUrl] = lFork; @@ -95,13 +89,11 @@ contract AaveIntegrationTest is BaseTest { _deployer.deployConstantProduct(type(ConstantProductPair).creationCode); _deployer.deployStable(type(StablePair).creationCode); - _manager = new AaveManager(IPoolAddressesProvider(AAVE_POOL_ADDRESS_PROVIDER)); + _manager = new EulerV2Manager(); USDC = IERC20(aNetwork.USDC); masterMinterUSDC = aNetwork.masterMinterUSDC; - _poolAddressesProvider = IPoolAddressesProvider(AAVE_POOL_ADDRESS_PROVIDER); - _aaveAdmin = _poolAddressesProvider.getACLAdmin(); - _dataProvider = IAaveProtocolDataProvider(_poolAddressesProvider.getPoolDataProvider()); - _poolConfigurator = IPoolConfigurator(_poolAddressesProvider.getPoolConfigurator()); + USDCVault = IERC4626(aNetwork.USDCVault); + distributor = IDistributor(aNetwork.merklDistributor); _deal(address(USDC), address(this), MINT_AMOUNT); _constantProductPair = ConstantProductPair(_createPair(address(_tokenA), address(USDC), 0)); @@ -121,6 +113,8 @@ contract AaveIntegrationTest is BaseTest { _pairs.push(_constantProductPair); _pairs.push(_stablePair); + + _manager.setVaultForAsset(USDC, USDCVault); } function _createOtherPair() private returns (ConstantProductPair rOtherPair) { @@ -147,9 +141,12 @@ contract AaveIntegrationTest is BaseTest { function setUp() external { _networks.push( Network( - getChain("avalanche").rpcUrl, - 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E, - 0xB7887FED5E2f9dc1A66fBb65f76BA3731d82341A + vm.rpcUrl("mainnet"), + 21_272_382, // pin to this block number + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, + 0xE982615d461DD5cD06575BbeA87624fda4e3de17, + 0x797DD80692c3b2dAdabCe8e30C07fDE5307D48a9, // Euler Prime USDC vault + 0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae ) ); @@ -158,6 +155,27 @@ contract AaveIntegrationTest is BaseTest { vm.makePersistent(address(_tokenC)); } + function testSetVaultForAsset() external allNetworks { + // arrange + IERC4626 lNewVault = IERC4626(address(5)); // any random address + + // act + _manager.setVaultForAsset(USDC, lNewVault); + + // assert + assertEq(address(_manager.assetVault(USDC)), address(lNewVault)); + } + + function testSetVaultForAsset_OutstandingShares() external allNetworks { + // arrange + _pair = _pairs[0]; + _increaseManagementOneToken(123); + + // act & assert + vm.expectRevert(EulerV2Manager.OutstandingSharesForVault.selector); + _manager.setVaultForAsset(USDC, USDCVault); + } + function testOnlyOwnerOrGuardian() external allNetworks { // arrange _manager.setGuardian(_alice); @@ -181,56 +199,6 @@ contract AaveIntegrationTest is BaseTest { vm.stopPrank(); } - function testUpdatePoolAddress() external allNetworks { - // arrange - vm.mockCall(AAVE_POOL_ADDRESS_PROVIDER, bytes(""), abi.encode(address(1))); - - // act - _manager.updatePoolAddress(); - vm.clearMockedCalls(); - - // assert - IPool lNewPool = _manager.pool(); - assertEq(address(lNewPool), address(1)); - } - - function testUpdatePoolAddress_NoChange() external allNetworks { - // arrange - IPool lOldPool = _manager.pool(); - - // act - _manager.updatePoolAddress(); - - // assert - IPool lNewPool = _manager.pool(); - assertEq(address(lNewPool), address(lOldPool)); - } - - function testUpdateDataProvider() external allNetworks { - // arrange - vm.mockCall(AAVE_POOL_ADDRESS_PROVIDER, bytes(""), abi.encode(address(1))); - - // act - _manager.updateDataProviderAddress(); - vm.clearMockedCalls(); - - // assert - IAaveProtocolDataProvider lNewDataProvider = _manager.dataProvider(); - assertEq(address(lNewDataProvider), address(1)); - } - - function testUpdateDataProvider_NoChange() external allNetworks { - // arrange - IAaveProtocolDataProvider lOldDataProvider = _manager.dataProvider(); - - // act - _manager.updateDataProviderAddress(); - - // assert - IAaveProtocolDataProvider lNewDataProvider = _manager.dataProvider(); - assertEq(address(lNewDataProvider), address(lOldDataProvider)); - } - function testSetGuardian() external allNetworks { // sanity address lGuardian = _manager.guardian(); @@ -295,6 +263,9 @@ contract AaveIntegrationTest is BaseTest { int256 lAmountToManage1 = _pair.token1() == USDC ? aAmountToManage : int256(0); // act + vm.expectCall( + address(_pair), abi.encodeCall(ReservoirPair.adjustManagement, (lAmountToManage0, lAmountToManage1)) + ); _manager.adjustManagement(_pair, lAmountToManage0, lAmountToManage1); } @@ -308,81 +279,40 @@ contract AaveIntegrationTest is BaseTest { _increaseManagementOneToken(lAmountToManage); // assert - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); assertEq(_pair.token0Managed(), uint256(lAmountToManage0)); assertEq(_pair.token1Managed(), uint256(lAmountToManage1)); assertEq(USDC.balanceOf(address(_pair)), MINT_AMOUNT - uint256(lAmountToManage)); - assertEq(lAaveToken.balanceOf(address(_manager)), uint256(lAmountToManage)); - assertEq(_manager.shares(_pair, USDC), uint256(lAmountToManage)); - assertEq(_manager.totalShares(lAaveToken), uint256(lAmountToManage)); - } - - function testAdjustManagement_IncreaseManagementOneToken_Frozen() public allNetworks allPairs { - // arrange - freeze the USDC market - int256 lAmountToManage = 500e6; - vm.prank(_aaveAdmin); - _poolConfigurator.setReserveFreeze(address(USDC), true); - int256 lAmountToManage0 = _pair.token0() == USDC ? lAmountToManage : int256(0); - int256 lAmountToManage1 = _pair.token1() == USDC ? lAmountToManage : int256(0); - - // act - vm.expectRevert(bytes(Errors.RESERVE_FROZEN)); - _manager.adjustManagement(_pair, lAmountToManage0, lAmountToManage1); - - // assert - nothing should have moved as USDC market is frozen - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); - assertEq(_pair.token0Managed(), 0); - assertEq(_pair.token1Managed(), 0); - assertEq(USDC.balanceOf(address(_pair)), MINT_AMOUNT); - assertEq(lAaveToken.balanceOf(address(_manager)), 0); - assertEq(_manager.shares(_pair, USDC), 0); - assertEq(_manager.totalShares(lAaveToken), 0); - } - - function testAdjustManagement_IncreaseManagementOneToken_Paused() public allNetworks allPairs { - // arrange - freeze the USDC market - int256 lAmountToManage = 500e6; - vm.prank(_aaveAdmin); - _poolConfigurator.setReservePause(address(USDC), true); - int256 lAmountToManage0 = _pair.token0() == USDC ? lAmountToManage : int256(0); - int256 lAmountToManage1 = _pair.token1() == USDC ? lAmountToManage : int256(0); - // act - vm.expectRevert(bytes(Errors.RESERVE_PAUSED)); - _manager.adjustManagement(_pair, lAmountToManage0, lAmountToManage1); + uint256 lExpectedShares = USDCVault.convertToShares(uint256(lAmountToManage)); - // assert - nothing should have moved as USDC market is paused - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); - assertEq(_pair.token0Managed(), 0); - assertEq(_pair.token1Managed(), 0); - assertEq(USDC.balanceOf(address(_pair)), MINT_AMOUNT); - assertEq(lAaveToken.balanceOf(address(_manager)), 0); - assertEq(_manager.shares(_pair, USDC), 0); - assertEq(_manager.totalShares(lAaveToken), 0); + assertEq(USDCVault.balanceOf(address(_manager)), lExpectedShares); + assertEq(_manager.shares(_pair, USDC), lExpectedShares); + assertEq(_manager.totalShares(USDCVault), lExpectedShares); } function testAdjustManagement_DecreaseManagementOneToken() public allNetworks allPairs { // arrange - int256 lAmountToManage = -500e6; + int256 lAmountToManage = 500e6; int256 lAmountToManage0 = _pair.token0() == USDC ? lAmountToManage : int256(0); int256 lAmountToManage1 = _pair.token1() == USDC ? lAmountToManage : int256(0); - _increaseManagementOneToken(500e6); + _increaseManagementOneToken(lAmountToManage); + uint256 lBalance = _manager.getBalance(_pair, USDC); // act - _manager.adjustManagement(_pair, lAmountToManage0, lAmountToManage1); + _manager.adjustManagement( + _pair, + lAmountToManage0 == 0 ? lAmountToManage0 : -int256(lBalance), + lAmountToManage1 == 0 ? lAmountToManage1 : -int256(lBalance) + ); // assert - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); + _pair.sync(); assertEq(_pair.token0Managed(), 0); assertEq(_pair.token1Managed(), 0); - assertEq(USDC.balanceOf(address(_pair)), MINT_AMOUNT); - assertEq(lAaveToken.balanceOf(address(_manager)), 0); + assertApproxEqAbs(USDC.balanceOf(address(_pair)), MINT_AMOUNT, 1); + assertEq(USDCVault.balanceOf(address(_manager)), 0); assertEq(_manager.shares(_pair, USDC), 0); - assertEq(_manager.totalShares(lAaveToken), 0); + assertEq(_manager.totalShares(USDCVault), 0); } function testAdjustManagement_DecreaseManagementBeyondShare() public allNetworks allPairs { @@ -402,59 +332,11 @@ contract AaveIntegrationTest is BaseTest { _manager.adjustManagement(lOtherPair, -lAmountToManage - 1, 0); } - function testAdjustManagement_DecreaseManagement_ReservePaused() public allNetworks allPairs { - // arrange - int256 lAmountToManage = -500e6; - int256 lAmountToManage0 = _pair.token0() == USDC ? lAmountToManage : int256(0); - int256 lAmountToManage1 = _pair.token1() == USDC ? lAmountToManage : int256(0); - _increaseManagementOneToken(500e6); - - vm.prank(_aaveAdmin); - _poolConfigurator.setReservePause(address(USDC), true); - - // act - withdraw should fail when reserve is paused - vm.expectRevert(bytes(Errors.RESERVE_PAUSED)); - _manager.adjustManagement(_pair, -lAmountToManage0, -lAmountToManage1); - - // assert - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); - uint256 lUsdcManaged = _pair.token0() == USDC ? _pair.token0Managed() : _pair.token1Managed(); - assertEq(lUsdcManaged, 500e6); - assertEq(USDC.balanceOf(address(_pair)), MINT_AMOUNT - 500e6); - assertEq(lAaveToken.balanceOf(address(_manager)), 500e6); - assertEq(_manager.shares(_pair, USDC), 500e6); - assertEq(_manager.totalShares(lAaveToken), 500e6); - } - - function testAdjustManagement_DecreaseManagement_SucceedEvenWhenFrozen() public allNetworks allPairs { - // arrange - int256 lAmountToManage = -500e6; - int256 lAmountToManage0 = _pair.token0() == USDC ? lAmountToManage : int256(0); - int256 lAmountToManage1 = _pair.token1() == USDC ? lAmountToManage : int256(0); - _increaseManagementOneToken(500e6); - - vm.prank(_aaveAdmin); - _poolConfigurator.setReserveFreeze(address(USDC), true); - - // act - withdraw should still succeed when reserve is frozen - vm.expectCall(address(_pair), abi.encodeCall(_pair.adjustManagement, (lAmountToManage0, lAmountToManage1))); - _manager.adjustManagement(_pair, lAmountToManage0, lAmountToManage1); - - // assert - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); - assertEq(_pair.token0Managed(), 0); - assertEq(_pair.token1Managed(), 0); - assertEq(USDC.balanceOf(address(_pair)), MINT_AMOUNT); - assertEq(lAaveToken.balanceOf(address(_manager)), 0); - assertEq(_manager.shares(_pair, USDC), 0); - assertEq(_manager.totalShares(lAaveToken), 0); - } - function testAdjustManagement_WindDown() external allNetworks allPairs { // arrange _increaseManagementOneToken(300e6); + ReservoirPair lOtherPair = _createOtherPair(); + _manager.setWindDownMode(true); int256 lIncreaseAmt = 50e6; @@ -462,16 +344,22 @@ contract AaveIntegrationTest is BaseTest { _manager.adjustManagement( _pair, _pair.token0() == USDC ? lIncreaseAmt : int256(0), _pair.token1() == USDC ? lIncreaseAmt : int256(0) ); + _manager.adjustManagement( + lOtherPair, + lOtherPair.token0() == USDC ? lIncreaseAmt : int256(0), + lOtherPair.token1() == USDC ? lIncreaseAmt : int256(0) + ); // assert - assertEq(_manager.getBalance(_pair, USDC), 300e6); + assertApproxEqAbs(_manager.getBalance(_pair, USDC), 300e6, 1); + assertEq(_manager.getBalance(lOtherPair, USDC), 0); } function testGetBalance(uint256 aAmountToManage) public allNetworks allPairs { // assume (uint256 lReserve0, uint256 lReserve1,,) = _pair.getReserves(); uint256 lReserveUSDC = _pair.token0() == USDC ? lReserve0 : lReserve1; - int256 lAmountToManage = int256(bound(aAmountToManage, 0, lReserveUSDC)); + int256 lAmountToManage = int256(bound(aAmountToManage, 3, lReserveUSDC)); // arrange int256 lAmountToManage0 = _pair.token0() == USDC ? lAmountToManage : int256(0); @@ -482,7 +370,7 @@ contract AaveIntegrationTest is BaseTest { uint256 lBalance = _manager.getBalance(_pair, USDC); // assert - assertTrue(MathUtils.within1(lBalance, uint256(lAmountToManage))); + assertApproxEqAbs(lBalance, uint256(lAmountToManage), 2); } function testGetBalance_TwoPairsInSameMarket(uint256 aAmountToManage1, uint256 aAmountToManage2) @@ -494,8 +382,8 @@ contract AaveIntegrationTest is BaseTest { ConstantProductPair lOtherPair = _createOtherPair(); (uint256 lReserve0, uint256 lReserve1,,) = _pair.getReserves(); uint256 lReserveUSDC = _pair.token0() == USDC ? lReserve0 : lReserve1; - int256 lAmountToManagePair = int256(bound(aAmountToManage1, 1, lReserveUSDC)); - int256 lAmountToManageOther = int256(bound(aAmountToManage2, 1, lReserveUSDC)); + int256 lAmountToManagePair = int256(bound(aAmountToManage1, 10, lReserveUSDC)); + int256 lAmountToManageOther = int256(bound(aAmountToManage2, 10, lReserveUSDC)); // arrange int256 lAmountToManage0Pair = _pair.token0() == USDC ? lAmountToManagePair : int256(0); @@ -508,8 +396,8 @@ contract AaveIntegrationTest is BaseTest { _manager.adjustManagement(lOtherPair, lAmountToManage0Other, lAmountToManage1Other); // assert - assertTrue(MathUtils.within1(_manager.getBalance(_pair, USDC), uint256(lAmountToManagePair))); - assertTrue(MathUtils.within1(_manager.getBalance(lOtherPair, USDC), uint256(lAmountToManageOther))); + assertApproxEqAbs(_manager.getBalance(_pair, USDC), uint256(lAmountToManagePair), 2); + assertApproxEqAbs(_manager.getBalance(lOtherPair, USDC), uint256(lAmountToManageOther), 2); } function testGetBalance_AddingAfterProfit(uint256 aAmountToManage1, uint256 aAmountToManage2, uint256 aTime) @@ -519,8 +407,7 @@ contract AaveIntegrationTest is BaseTest { { // assume ConstantProductPair lOtherPair = _createOtherPair(); - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); + (uint256 lReserve0, uint256 lReserve1,,) = _pair.getReserves(); uint256 lReserveUSDC = _pair.token0() == USDC ? lReserve0 : lReserve1; int256 lAmountToManagePair = int256(bound(aAmountToManage1, 1e6, lReserveUSDC)); @@ -533,10 +420,12 @@ contract AaveIntegrationTest is BaseTest { _pair.token0() == USDC ? lAmountToManagePair : int256(0), _pair.token1() == USDC ? lAmountToManagePair : int256(0) ); + uint256 lPairShares = _manager.shares(_pair, USDC); // act skip(lTime); - uint256 lAaveTokenAmt2 = lAaveToken.balanceOf(address(_manager)); + uint256 lBalanceAfterInterest = _manager.getBalance(_pair, USDC); + uint256 lExpectedShares = USDCVault.previewDeposit(uint256(lAmountToManageOther)); _manager.adjustManagement( lOtherPair, lOtherPair.token0() == USDC ? lAmountToManageOther : int256(0), @@ -544,11 +433,8 @@ contract AaveIntegrationTest is BaseTest { ); // assert - assertEq(_manager.shares(_pair, USDC), uint256(lAmountToManagePair)); - assertApproxEqAbs(_manager.getBalance(_pair, USDC), lAaveTokenAmt2, 2); - - uint256 lExpectedShares = - uint256(lAmountToManageOther) * 1e18 / (lAaveTokenAmt2 * 1e18 / uint256(lAmountToManagePair)); + assertEq(_manager.shares(_pair, USDC), lPairShares); // ensure that _pair's shares did not change + assertApproxEqAbs(_manager.getBalance(_pair, USDC), lBalanceAfterInterest, 2); assertEq(_manager.shares(lOtherPair, USDC), lExpectedShares); uint256 lBalance = _manager.getBalance(lOtherPair, USDC); assertApproxEqAbs(lBalance, uint256(lAmountToManageOther), 2); @@ -558,23 +444,19 @@ contract AaveIntegrationTest is BaseTest { // assume (uint256 lReserve0, uint256 lReserve1,,) = _pair.getReserves(); uint256 lReserveUSDC = _pair.token0() == USDC ? lReserve0 : lReserve1; - int256 lAmountToManage = int256(bound(aAmountToManage, 0, lReserveUSDC)); + int256 lAmountToManage = int256(bound(aAmountToManage, 10, lReserveUSDC)); // arrange - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); int256 lAmountToManage0 = _pair.token0() == USDC ? lAmountToManage : int256(0); int256 lAmountToManage1 = _pair.token1() == USDC ? lAmountToManage : int256(0); - _manager.adjustManagement(_pair, lAmountToManage0, lAmountToManage1); - // act - uint256 lShares = _manager.shares(_pair, USDC); - uint256 lTotalShares = _manager.totalShares(lAaveToken); + uint256 lShares = USDCVault.previewDeposit(uint256(lAmountToManage)); + _manager.adjustManagement(_pair, lAmountToManage0, lAmountToManage1); // assert - assertEq(lShares, lTotalShares); - assertEq(lShares, uint256(lAmountToManage)); + assertEq(lShares, _manager.shares(_pair, USDC)); + assertEq(_manager.totalShares(USDCVault), lShares); } function testShares_AdjustManagementAfterProfit(uint256 aAmountToManage1, uint256 aAmountToManage2) @@ -589,8 +471,6 @@ contract AaveIntegrationTest is BaseTest { int256 lAmountToManage2 = int256(bound(aAmountToManage2, 1e6, lReserveUSDC / 2)); // arrange - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); _manager.adjustManagement( _pair, _pair.token0() == USDC ? lAmountToManage1 : int256(0), @@ -599,8 +479,8 @@ contract AaveIntegrationTest is BaseTest { // act - go forward in time to simulate accrual of profits skip(30 days); - uint256 lAaveTokenAmt1 = lAaveToken.balanceOf(address(_manager)); - assertGt(lAaveTokenAmt1, uint256(lAmountToManage1)); + uint256 lNewManaged = _manager.getBalance(_pair, USDC); + assertGt(lNewManaged, uint256(lAmountToManage1)); _manager.adjustManagement( _pair, _pair.token0() == USDC ? lAmountToManage2 : int256(0), @@ -608,15 +488,7 @@ contract AaveIntegrationTest is BaseTest { ); // assert - uint256 lShares = _manager.shares(_pair, USDC); - uint256 lTotalShares = _manager.totalShares(lAaveToken); - assertEq(lShares, lTotalShares); - assertLt(lTotalShares, uint256(lAmountToManage1 + lAmountToManage2)); - uint256 lBalance = _manager.getBalance(_pair, USDC); - uint256 lAaveTokenAmt2 = lAaveToken.balanceOf(address(_manager)); - assertEq(lBalance, lAaveTokenAmt2); - // pair not yet informed of the profits, so the numbers are less than what it actually has uint256 lUSDCManaged = _pair.token0() == USDC ? _pair.token0Managed() : _pair.token1Managed(); assertLt(lUSDCManaged, lBalance); @@ -642,7 +514,9 @@ contract AaveIntegrationTest is BaseTest { uint256 lNewAmount = _manager.getBalance(_pair, USDC); (uint256 lReserve0, uint256 lReserve1,,) = _pair.getReserves(); uint256 lReserveUSDC = _pair.token0() == USDC ? lReserve0 : lReserve1; - assertEq(lNewAmount, lReserveUSDC.mulWad(uint256(_manager.lowerThreshold()).avg(_manager.upperThreshold()))); + assertApproxEqAbs( + lNewAmount, lReserveUSDC.mulWad(uint256(_manager.lowerThreshold()).avg(_manager.upperThreshold())), 1 + ); } function testAfterLiquidityEvent_DecreaseInvestmentAfterBurn(uint256 aInitialAmount) public allNetworks allPairs { @@ -671,60 +545,6 @@ contract AaveIntegrationTest is BaseTest { ); } - function testAfterLiquidityEvent_Mint_RevertIfFrozen() public allNetworks allPairs { - // arrange - uint256 lMintAmt = 100e6; - vm.prank(_aaveAdmin); - _poolConfigurator.setReserveFreeze(address(USDC), true); - - // act & assert - _deal(address(USDC), address(this), lMintAmt); - USDC.transfer(address(_pair), lMintAmt); - _tokenA.mint(address(_pair), lMintAmt); - vm.expectRevert(bytes(Errors.RESERVE_FROZEN)); - _pair.mint(address(this)); - } - - function testAfterLiquidityEvent_Mint_RevertIfPaused() public allNetworks allPairs { - // arrange - uint256 lMintAmt = 100e6; - vm.prank(_aaveAdmin); - _poolConfigurator.setReservePause(address(USDC), true); - - // act & assert - _deal(address(USDC), address(this), lMintAmt); - USDC.transfer(address(_pair), lMintAmt); - _tokenA.mint(address(_pair), lMintAmt); - vm.expectRevert(bytes(Errors.RESERVE_PAUSED)); - _pair.mint(address(this)); - } - - function testAfterLiquidityEvent_Burn_RevertIfFrozen() public allNetworks allPairs { - // arrange - uint256 lAmtToBurn = _pair.balanceOf(_alice) / 2; - vm.prank(_aaveAdmin); - _poolConfigurator.setReserveFreeze(address(USDC), true); - - // act & assert - vm.prank(_alice); - _pair.transfer(address(_pair), lAmtToBurn); - vm.expectRevert(bytes(Errors.RESERVE_FROZEN)); - _pair.burn(address(this)); - } - - function testAfterLiquidityEvent_Burn_RevertIfPaused() public allNetworks allPairs { - // arrange - uint256 lAmtToBurn = _pair.balanceOf(_alice) / 2; - vm.prank(_aaveAdmin); - _poolConfigurator.setReservePause(address(USDC), true); - - // act & assert - vm.prank(_alice); - _pair.transfer(address(_pair), lAmtToBurn); - vm.expectRevert(bytes(Errors.RESERVE_PAUSED)); - _pair.burn(address(this)); - } - function testAfterLiquidityEvent_RevertIfNotPair() public allNetworks { // act & assert vm.expectRevert(); @@ -790,15 +610,11 @@ contract AaveIntegrationTest is BaseTest { _pair.swap(lOutputAmt, false, address(this), bytes("")); // assert - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); (lReserve0, lReserve1,,) = _pair.getReserves(); lReserveUSDC = _pair.token0() == USDC ? lReserve0 : lReserve1; assertEq(USDC.balanceOf(address(this)), MINT_AMOUNT / 2 + 10); assertEq(USDC.balanceOf(address(_pair)), 0); assertEq(lReserveUSDC, MINT_AMOUNT / 2 - 10); - assertEq(_manager.shares(_pair, USDC), MINT_AMOUNT / 2 - 10); - assertEq(_manager.totalShares(lAaveToken), MINT_AMOUNT / 2 - 10); assertApproxEqAbs(_manager.getBalance(_pair, USDC), MINT_AMOUNT / 2 - 10, 1); } @@ -827,44 +643,14 @@ contract AaveIntegrationTest is BaseTest { _pair.swap(lOutputAmt, false, address(this), bytes("")); // assert - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); (lReserve0, lReserve1,,) = _pair.getReserves(); lReserveUSDC = _pair.token0() == USDC ? lReserve0 : lReserve1; assertEq(USDC.balanceOf(address(this)), MINT_AMOUNT / 2 + 10); assertEq(USDC.balanceOf(address(_pair)), 0); assertEq(lReserveUSDC, MINT_AMOUNT / 2 - 10); - assertEq(_manager.shares(_pair, USDC), MINT_AMOUNT / 2 - 10); - assertEq(_manager.totalShares(lAaveToken), MINT_AMOUNT / 2 - 10); assertApproxEqAbs(_manager.getBalance(_pair, USDC), MINT_AMOUNT / 2 - 10, 1); } - // when the pool is paused, attempts to withdraw should fail and the swap should fail too - function testSwap_ReturnAsset_PausedFail() public allNetworks allPairs { - // arrange - (uint256 lReserve0, uint256 lReserve1,,) = _pair.getReserves(); - (uint256 lReserveUSDC, uint256 lReserveTokenA) = - _pair.token0() == USDC ? (lReserve0, lReserve1) : (lReserve1, lReserve0); - // manage half - _manager.adjustManagement( - _pair, - int256(_pair.token0() == USDC ? lReserveUSDC / 2 : 0), - int256(_pair.token1() == USDC ? lReserveUSDC / 2 : 0) - ); - vm.prank(_aaveAdmin); - _poolConfigurator.setReservePause(address(USDC), true); - - // act & assert - int256 lOutputAmt = _pair.token0() == USDC ? int256(MINT_AMOUNT / 2 + 10) : -int256(MINT_AMOUNT / 2 + 10); - _tokenA.mint(address(_pair), lReserveTokenA * 2); - vm.expectRevert(bytes(Errors.RESERVE_PAUSED)); - _pair.swap(lOutputAmt, false, address(this), bytes("")); - - // assert - assertEq(_manager.shares(_pair, USDC), MINT_AMOUNT / 2); - assertEq(_manager.getBalance(_pair, USDC), MINT_AMOUNT / 2); - } - // the amount requested is within the balance of the pair, no need to return asset function testSwap_NoReturnAsset() public allNetworks allPairs { // arrange @@ -907,10 +693,7 @@ contract AaveIntegrationTest is BaseTest { ); // sanity - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); - assertEq(USDC.balanceOf(address(_pair)), MINT_AMOUNT / 2); - assertEq(_manager.totalShares(lAaveToken), lReserveUSDC / 2); + assertEq(USDC.balanceOf(address(_pair)), lReserveUSDC / 2); // act vm.startPrank(_alice); @@ -921,37 +704,7 @@ contract AaveIntegrationTest is BaseTest { vm.stopPrank(); // assert - range due to slight diff in liq between CP and SP - assertApproxEqRel(USDC.balanceOf(address(this)), MINT_AMOUNT, 0.000000001e18); - } - - function testBurn_ReturnAsset_PausedFail() public allNetworks allPairs { - // arrange - (uint256 lReserve0, uint256 lReserve1,,) = _pair.getReserves(); - uint256 lReserveUSDC = _pair.token0() == USDC ? lReserve0 : lReserve1; - // manage half - _manager.adjustManagement( - _pair, - int256(_pair.token0() == USDC ? lReserveUSDC / 2 : 0), - int256(_pair.token1() == USDC ? lReserveUSDC / 2 : 0) - ); - vm.prank(_aaveAdmin); - _poolConfigurator.setReservePause(address(USDC), true); - - // act & assert - vm.startPrank(_alice); - _pair.transfer(address(_pair), _pair.balanceOf(_alice)); - vm.expectRevert(bytes(Errors.RESERVE_PAUSED)); - _pair.burn(address(this)); - vm.stopPrank(); - - // assert - (address lRawAaveToken,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lRawAaveToken); - assertEq(USDC.balanceOf(address(_pair)), lReserveUSDC / 2); - assertEq(lAaveToken.balanceOf(address(_manager)), lReserveUSDC / 2); - assertEq(_manager.getBalance(_pair, USDC), lReserveUSDC / 2); - assertEq(_manager.shares(_pair, USDC), lReserveUSDC / 2); - assertEq(_manager.totalShares(lAaveToken), lReserveUSDC / 2); + assertApproxEqRel(USDC.balanceOf(address(this)), MINT_AMOUNT, 0.000000001001e18); } function testSetThresholds_BreachMaximum() public allNetworks { @@ -1005,9 +758,9 @@ contract AaveIntegrationTest is BaseTest { uint256 aFastForwardTime ) external allNetworks allPairs { // assume - uint256 lAmtToManage0 = bound(aAmtToManage0, 1, MINT_AMOUNT); - uint256 lAmtToManage1 = bound(aAmtToManage1, 1, MINT_AMOUNT); - uint256 lAmtToManage2 = bound(aAmtToManage2, 1, MINT_AMOUNT); + uint256 lAmtToManage0 = bound(aAmtToManage0, 10, MINT_AMOUNT); + uint256 lAmtToManage1 = bound(aAmtToManage1, 10, MINT_AMOUNT); + uint256 lAmtToManage2 = bound(aAmtToManage2, 10, MINT_AMOUNT); uint256 lFastForwardTime = bound(aFastForwardTime, 5 minutes, 60 days); // arrange @@ -1059,81 +812,6 @@ contract AaveIntegrationTest is BaseTest { assertEq(_manager.shares(lThirdPair, USDC), 0); } - function testClaimReward() external allNetworks allPairs { - // this test is only applicable on AVAX as USDC does not have additional rewards on polygon - if (vm.activeFork() != 0) return; - - // arrange - _increaseManagementOneToken(500e6); - _manager.setGuardian(address(this)); - _manager.setRewardsController(address(0x929EC64c34a17401F460460D4B9390518E5B473e)); - (address lUSDCMarket,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - address lWavax = address(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7); - address[] memory lMarkets = new address[](1); - lMarkets[0] = lUSDCMarket; - - // act - step time to accumulate some rewards - _stepTime(5000); - address lRewardsController = address(_manager.rewardsController()); - vm.expectCall( - lRewardsController, - abi.encodeCall(IRewardsController.claimRewards, (lMarkets, type(uint256).max, address(this), lWavax)) - ); - uint256 lClaimed = _manager.claimRewardForMarket(lUSDCMarket, lWavax); - - // assert - assertEq(IERC20(lWavax).balanceOf(address(this)), lClaimed); - // commenting out for now as AAVE is not giving out rewards - // assertGt(lClaimed, 0); - } - - function testClaimRewards_SellAndPutRewardsBackIntoManager() external allNetworks allPairs { - // this test is only applicable on AVAX as USDC does not have additional rewards on polygon - if (vm.activeFork() != 0) return; - - // arrange - _increaseManagementOneToken(500e6); - ConstantProductPair lOtherPair = _createOtherPair(); - _manager.adjustManagement( - lOtherPair, - lOtherPair.token0() == USDC ? int256(100e6) : int256(0), - lOtherPair.token1() == USDC ? int256(100e6) : int256(0) - ); - _manager.setGuardian(address(this)); - _manager.setRewardsController(address(0x929EC64c34a17401F460460D4B9390518E5B473e)); - (address lUSDCMarket,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lUSDCMarket); - address lWavax = address(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7); - address[] memory lMarkets = new address[](1); - lMarkets[0] = lUSDCMarket; - - // act - simulate a claiming and selling of the rewards into more aaveUSDC - _stepTime(5000); - uint256 lBalAfterTimePair = _manager.getBalance(_pair, USDC); - uint256 lBalAfterTimeOther = _manager.getBalance(lOtherPair, USDC); - _manager.claimRewardForMarket(lUSDCMarket, lWavax); - // commenting out for now as AAVE is not currently giving out additional AVAX rewards - // assertGt(lClaimed, 0); - // dummy amount of proceeds from selling the rewards - uint256 lAmtUSDC = 9_019_238; - _deal(address(USDC), address(this), lAmtUSDC); - // supply the USDC for aaveUSDC - IPool lPool = _manager.pool(); - USDC.approve(address(lPool), type(uint256).max); - lPool.supply(address(USDC), lAmtUSDC, address(this), 0); - assertEq(lAaveToken.balanceOf(address(this)), lAmtUSDC); - lAaveToken.transfer(address(_manager), lAmtUSDC); - - // assert - uint256 lBalAfterCompoundingPair = _manager.getBalance(_pair, USDC); - uint256 lBalAfterCompoundingOther = _manager.getBalance(lOtherPair, USDC); - // percentage growth is the same - uint256 lPercentageIncreasePair = lBalAfterCompoundingPair.divWad(lBalAfterTimePair); - uint256 lPercentageIncreaseOther = lBalAfterCompoundingOther.divWad(lBalAfterTimeOther); - // percentage diff is no greater than 0.000001% - assertApproxEqRel(lPercentageIncreasePair, lPercentageIncreaseOther, 0.00000001e18); - } - function testFullRedeem_MultiplePairs( uint256 aAmtToManage0, uint256 aAmtToManage1, @@ -1141,9 +819,9 @@ contract AaveIntegrationTest is BaseTest { uint256 aFastForwardTime ) external allNetworks allPairs { // assume - uint256 lAmtToManage0 = bound(aAmtToManage0, 1, MINT_AMOUNT); - uint256 lAmtToManage1 = bound(aAmtToManage1, 1, MINT_AMOUNT); - uint256 lAmtToManage2 = bound(aAmtToManage2, 1, MINT_AMOUNT); + uint256 lAmtToManage0 = bound(aAmtToManage0, 10, MINT_AMOUNT); + uint256 lAmtToManage1 = bound(aAmtToManage1, 10, MINT_AMOUNT); + uint256 lAmtToManage2 = bound(aAmtToManage2, 10, MINT_AMOUNT); uint256 lFastForwardTime = bound(aFastForwardTime, 10 days, 60 days); // arrange @@ -1154,8 +832,6 @@ contract AaveIntegrationTest is BaseTest { lThirdPair.mint(_alice); vm.prank(address(_factory)); lThirdPair.setManager(_manager); - (address lUSDCMarket,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lUSDCMarket); _increaseManagementOneToken(int256(lAmtToManage0)); _manager.adjustManagement( lOtherPair, @@ -1208,7 +884,7 @@ contract AaveIntegrationTest is BaseTest { assertEq(address(_pair.assetManager()), address(0)); assertEq(address(lOtherPair.assetManager()), address(0)); assertEq(address(lThirdPair.assetManager()), address(0)); - assertEq(_manager.totalShares(lAaveToken), 0); + assertEq(_manager.totalShares(USDCVault), 0); assertEq(_manager.shares(_pair, USDC), 0); assertEq(_manager.shares(lOtherPair, USDC), 0); assertEq(_manager.shares(lThirdPair, USDC), 0); @@ -1236,8 +912,6 @@ contract AaveIntegrationTest is BaseTest { lPair3.mint(_alice); vm.prank(address(_factory)); lPair3.setManager(_manager); - (address lUSDCMarket,,) = _dataProvider.getReserveTokensAddresses(address(USDC)); - IERC20 lAaveToken = IERC20(lUSDCMarket); _increaseManagementOneToken(int256(lAmtToManage0)); // go forward in time to accumulate some rewards and then the second pair comes in @@ -1293,12 +967,55 @@ contract AaveIntegrationTest is BaseTest { assertEq(address(_pair.assetManager()), address(0)); assertEq(address(lPair2.assetManager()), address(0)); assertEq(address(lPair3.assetManager()), address(0)); - assertEq(_manager.totalShares(lAaveToken), 0, "total shares"); + assertEq(_manager.totalShares(USDCVault), 0, "total shares"); assertEq(_manager.shares(_pair, USDC), 0, "pair shares"); assertEq(_manager.shares(lPair2, USDC), 0, "pair2 shares"); assertEq(_manager.shares(lPair3, USDC), 0, "pair3 shares"); } + // this test shows that the asset manager should still function properly with investing, divesting, showing balance + // even if an external party transfers unsolicited shares into it + function testResilientEvenInExternalShareTransfer() external allNetworks allPairs { + // arrange + _increaseManagementOneToken(12_312_322); + uint256 lPairBalance = _manager.getBalance(_pair, USDC); + uint256 lPairShares = _manager.shares(_pair, USDC); + uint256 lOldTotalShares = _manager.totalShares(USDCVault); + + uint256 lAmtToSupply = 100_000e6; + _deal(address(USDC), address(this), lAmtToSupply); + USDC.approve(address(USDCVault), lAmtToSupply); + uint256 lSharesReceived = USDCVault.deposit(lAmtToSupply, address(this)); + assertEq(lSharesReceived, USDCVault.balanceOf(address(this))); + + // act + USDCVault.transfer(address(_manager), lSharesReceived); + + // assert + assertEq(_manager.getBalance(_pair, USDC), lPairBalance); // pair's balance should not change + assertEq(_manager.shares(_pair, USDC), lPairShares); + assertEq(_manager.totalShares(USDCVault), lOldTotalShares); + assertGt(USDCVault.balanceOf(address(_manager)), lOldTotalShares); + + _manager.adjustManagement( + _pair, + USDC == _pair.token0() ? -int256(lPairBalance) : int256(0), + USDC == _pair.token1() ? -int256(lPairBalance) : int256(0) + ); + + assertEq(_manager.getBalance(_pair, USDC), 0); + assertEq(_manager.shares(_pair, USDC), 0); + assertEq(_manager.totalShares(USDCVault), 0); + + // admins get the extra shares out + _manager.rawCall( + address(USDCVault), + abi.encodeCall(IERC20.transfer, (address(this), USDCVault.balanceOf(address(_manager)))), + 0 + ); + assertEq(USDCVault.balanceOf(address(this)), lSharesReceived); + } + function testReturnAsset_Attack() external allNetworks allPairs { // arrange _increaseManagementOneToken(11e6); @@ -1308,4 +1025,124 @@ contract AaveIntegrationTest is BaseTest { vm.expectRevert(stdError.arithmeticError); lExploit.attack(_manager); } + + function testClaimRewards() external allNetworks { + // arrange + // pin block to certain as it is before the user has claimed the reward + // this is a replay of + // https://etherscan.io/tx/0x2cc0e0161f84594ff755b8aac235efcf8ce59c1f9d63655356d9d5f09021ef5f + vm.rollFork(21_197_813); + address lVaultUser = address(0x00236feEAC26ef92552e3981096350D136084C64); + uint256 lUSDCBalanceBefore = USDC.balanceOf(lVaultUser); + uint256 lClaimAmt = 361_104_571; + uint256 lActualAmt = 241_316_721; + + vm.prank(lVaultUser); + distributor.toggleOperator(lVaultUser, address(_manager)); + + address[] memory lUsers = new address[](1); + lUsers[0] = lVaultUser; + + address[] memory lTokens = new address[](1); + lTokens[0] = address(USDC); + + uint256[] memory lAmounts = new uint256[](1); + lAmounts[0] = lClaimAmt; + + bytes32[][] memory lProofs = new bytes32[][](1); + lProofs[0] = new bytes32[](14); + lProofs[0][0] = 0xa9f5986cf9ba92e165a2422369577d29f776b2875b5675602e9e5f9df2ab7c7e; + lProofs[0][1] = 0xbeaad48efe95eae938e5f48dd8cbad91bd2036dbab85ed913d5581e43a669884; + lProofs[0][2] = 0xc8f64af9c4e202af243b02031900bccb59db3fb1ae7f7b6ef841539a27a89ccc; + lProofs[0][3] = 0x5531d033f9e3ce60f1817f1121e6958d34970d41469aa34d72d5825efc54fbf4; + lProofs[0][4] = 0x0d5a061463b6c619edaa4b88d8778565df55c30bb9cfcb0ee5e5b2947a6ecdd7; + lProofs[0][5] = 0x83f3d530fbaa326a08b31e5d1418565ea6de97f93f26acab480f537cf159c63f; + lProofs[0][6] = 0x423985ac01da6043a6e733135aa92b329528cc675c83b1de799b90d1061a3447; + lProofs[0][7] = 0xfa65c6be585dd9b00347ef1a466d0bddcd16a84291d5c802882cbd51b6287dfb; + lProofs[0][8] = 0x1f6dc43d4aacf37a7518a74d30b9abc3be28ab635485624a05201faff3d19ed7; + lProofs[0][9] = 0x313bbbf9afd06d8f9e6754362938910ac15b5f17094cbec219e920b5304c3424; + lProofs[0][10] = 0xceb058ef80e60f5868b3bfab410745217f303f7deaf4d37b4d865cc2378230f7; + lProofs[0][11] = 0x9dd8679648e25847e3e4ec095dfb009624aeea68e8d5b74808e0d1115ea6834f; + lProofs[0][12] = 0x90d5dc0ac9e6118eba554513548f3e5a2573d3eb77efe4b9f12dc57aa0bdab53; + lProofs[0][13] = 0x0e81697f68dcbd6ed96fed3a63ddc140edb0de2ef18d9611104ae80ae0b759c7; + + // since all claims go to the actual user regardless of who claimed it + // we are unable to get the claimed tokens into the asset manager + // therefore we simulate it by dealing it the correct amount + _deal(address(USDC), address(_manager), lActualAmt); + + // act & assert + _manager.claimRewards(distributor, lUsers, lTokens, lAmounts, lProofs); + + // assert + assertEq(USDC.balanceOf(address(this)), lActualAmt); + assertEq(USDC.balanceOf(lVaultUser) - lUSDCBalanceBefore, lActualAmt); + } + + function testDistributeRewardForPairs( + uint256 aAmountToDistribute, + uint256 aAmtToManage1, + uint256 aAmtToManage2, + uint256 aAmtToManage3 + ) external allNetworks allPairs { + // assume + uint256 lAmountToDistribute = bound(aAmountToDistribute, 100, 10_000_000e6); + int256 lAmtToManage1 = int256(bound(aAmtToManage1, 1e6, 10_000e6)); + int256 lAmtToManage2 = int256(bound(aAmtToManage2, 1e6, 10_000e6)); + int256 lAmtToManage3 = int256(bound(aAmtToManage3, 1e6, 10_000e6)); + + // arrange + _deal(address(USDC), address(this), lAmountToDistribute); + ConstantProductPair lPair2 = ConstantProductPair(_createPair(address(USDC), address(_tokenC), 0)); + StablePair lPair3 = StablePair(_createPair(address(USDC), address(_tokenC), 1)); + + _deal(address(USDC), address(lPair2), MINT_AMOUNT); + _tokenC.mint(address(lPair2), MINT_AMOUNT); + lPair2.mint(_alice); + vm.prank(address(_factory)); + lPair2.setManager(_manager); + + _deal(address(USDC), address(lPair3), MINT_AMOUNT); + _tokenC.mint(address(lPair3), MINT_AMOUNT); + lPair3.mint(_alice); + vm.prank(address(_factory)); + lPair3.setManager(_manager); + + _manager.adjustManagement( + _pair, + _pair.token0() == USDC ? lAmtToManage1 : int256(0), + _pair.token1() == USDC ? lAmtToManage1 : int256(0) + ); + _manager.adjustManagement( + lPair2, + lPair2.token0() == USDC ? lAmtToManage2 : int256(0), + lPair2.token1() == USDC ? lAmtToManage2 : int256(0) + ); + _manager.adjustManagement( + lPair3, + lPair3.token0() == USDC ? lAmtToManage3 : int256(0), + lPair3.token1() == USDC ? lAmtToManage3 : int256(0) + ); + + IAssetManagedPair[] memory lPairs = new IAssetManagedPair[](3); + lPairs[0] = _pair; + lPairs[1] = lPair2; + lPairs[2] = lPair3; + uint256 lPairSharesBefore = _manager.shares(_pair, USDC); + uint256 lPair2SharesBefore = _manager.shares(lPair2, USDC); + uint256 lPair3SharesBefore = _manager.shares(lPair3, USDC); + + // act + USDC.approve(address(_manager), lAmountToDistribute); + _manager.distributeRewardForPairs(USDC, lAmountToDistribute, lPairs); + + // assert + uint256 lPairShares = _manager.shares(_pair, USDC); + uint256 lPair2Shares = _manager.shares(lPair2, USDC); + uint256 lPair3Shares = _manager.shares(lPair3, USDC); + assertEq(lPairShares + lPair2Shares + lPair3Shares, _manager.totalShares(USDCVault)); + assertGe(lPairShares, lPairSharesBefore); + assertGe(lPair2Shares, lPair2SharesBefore); + assertGe(lPair3Shares, lPair3SharesBefore); + } } diff --git a/test/unit/AssetManagedPair.t.sol b/test/unit/AssetManagedPair.t.sol index d94530be..9a5bb90e 100644 --- a/test/unit/AssetManagedPair.t.sol +++ b/test/unit/AssetManagedPair.t.sol @@ -14,10 +14,10 @@ contract AssetManagedPairTest is BaseTest { modifier allPairs() { for (uint256 i = 0; i < _pairs.length; ++i) { - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _pair = _pairs[i]; _; - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); } } @@ -263,11 +263,11 @@ contract AssetManagedPairTest is BaseTest { // arrange int256 lSwapAmt = 1e18; - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _tokenA.mint(address(_pair), uint256(lSwapAmt)); _pair.swap(lSwapAmt, true, address(this), ""); uint256 lNoLossOutAmt = _tokenB.balanceOf(address(this)); - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); vm.prank(address(_factory)); _pair.setManager(_manager); diff --git a/test/unit/FlashSwap.t.sol b/test/unit/FlashSwap.t.sol index 0bf8a7d5..e88c4d34 100644 --- a/test/unit/FlashSwap.t.sol +++ b/test/unit/FlashSwap.t.sol @@ -12,10 +12,10 @@ contract FlashSwapTest is BaseTest, IReservoirCallee { modifier allPairs() { for (uint256 i = 0; i < _pairs.length; ++i) { - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _pair = _pairs[i]; _; - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); } } diff --git a/test/unit/OracleWriter.t.sol b/test/unit/OracleWriter.t.sol index ab65c598..1db7836c 100644 --- a/test/unit/OracleWriter.t.sol +++ b/test/unit/OracleWriter.t.sol @@ -29,10 +29,10 @@ contract OracleWriterTest is BaseTest { modifier allPairs() { for (uint256 i = 0; i < _pairs.length; ++i) { - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _pair = _pairs[i]; _; - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); } } diff --git a/test/unit/Pair.t.sol b/test/unit/Pair.t.sol index 669ec749..fcc7e94b 100644 --- a/test/unit/Pair.t.sol +++ b/test/unit/Pair.t.sol @@ -20,10 +20,10 @@ contract PairTest is BaseTest { modifier allPairs() { for (uint256 i = 0; i < _pairs.length; ++i) { - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _pair = _pairs[i]; _; - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); } } diff --git a/test/unit/ReservoirPair.t.sol b/test/unit/ReservoirPair.t.sol index eb44317e..42603ca0 100644 --- a/test/unit/ReservoirPair.t.sol +++ b/test/unit/ReservoirPair.t.sol @@ -20,10 +20,10 @@ contract ReservoirPairTest is BaseTest { modifier allPairs() { for (uint256 i = 0; i < _pairs.length; ++i) { - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _pair = _pairs[i]; _; - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); } } diff --git a/test/unit/ReservoirTimelock.t.sol b/test/unit/ReservoirTimelock.t.sol index 2293269e..bab5cb7a 100644 --- a/test/unit/ReservoirTimelock.t.sol +++ b/test/unit/ReservoirTimelock.t.sol @@ -13,10 +13,10 @@ contract ReservoirTimelockTest is BaseTest { modifier allPairs() { for (uint256 i = 0; i < _pairs.length; ++i) { - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _pair = _pairs[i]; _; - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); } } diff --git a/test/unit/StablePair.t.sol b/test/unit/StablePair.t.sol index 5f163dc8..bf317d0a 100644 --- a/test/unit/StablePair.t.sol +++ b/test/unit/StablePair.t.sol @@ -127,7 +127,7 @@ contract StablePairTest is BaseTest { // and thus the mint-burn mechanism cannot be gamed into getting a better price function testMint_NonOptimalProportion_ThenBurn() public { // arrange - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); uint256 lAmountAToMint = 1e18; uint256 lAmountBToMint = 100e18; @@ -142,7 +142,7 @@ contract StablePairTest is BaseTest { uint256 lBurnOutputA = _tokenA.balanceOf(address(this)); uint256 lBurnOutputB = _tokenB.balanceOf(address(this)); - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); // swap uint256 lAmountToSwap = lAmountBToMint - lBurnOutputB; @@ -182,7 +182,7 @@ contract StablePairTest is BaseTest { // arrange vm.prank(address(_factory)); _stablePair.rampA(lFutureAToSet, lFutureATimestamp); - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); // act vm.warp((block.timestamp + lFutureATimestamp) / 2); @@ -191,7 +191,7 @@ contract StablePairTest is BaseTest { _stablePair.mint(address(this)); uint256 lLpTokens1 = _stablePair.balanceOf(address(this)); - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); vm.warp(lFutureATimestamp); _tokenA.mint(address(_stablePair), 5e18); @@ -723,7 +723,7 @@ contract StablePairTest is BaseTest { // act vm.prank(address(_factory)); _stablePair.setCustomSwapFee(lSwapFee0); - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); uint256 lExpectedOut0 = StableMath._getAmountOut( lSwapAmt, lReserve0, lReserve1, 1, 1, true, lSwapFee0, 2 * _stablePair.getCurrentAPrecise() @@ -732,10 +732,10 @@ contract StablePairTest is BaseTest { uint256 lActualOut = _stablePair.swap(int256(lSwapAmt), true, address(this), bytes("")); assertEq(lExpectedOut0, lActualOut); - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); vm.prank(address(_factory)); _stablePair.setCustomSwapFee(lSwapFee1); - lBefore = vm.snapshot(); + lBefore = vm.snapshotState(); uint256 lExpectedOut1 = StableMath._getAmountOut( lSwapAmt, lReserve0, lReserve1, 1, 1, true, lSwapFee1, 2 * _stablePair.getCurrentAPrecise() @@ -744,7 +744,7 @@ contract StablePairTest is BaseTest { lActualOut = _stablePair.swap(int256(lSwapAmt), true, address(this), bytes("")); assertEq(lExpectedOut1, lActualOut); - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); vm.prank(address(_factory)); _stablePair.setCustomSwapFee(lSwapFee2); @@ -865,7 +865,7 @@ contract StablePairTest is BaseTest { // arrange vm.prank(address(_factory)); _stablePair.rampA(lFutureAToSet, lFutureATimestamp); - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); // act vm.warp((block.timestamp + lFutureATimestamp) / 2); @@ -875,7 +875,7 @@ contract StablePairTest is BaseTest { uint256 lTokenABal0 = _tokenA.balanceOf(address(this)); uint256 lTokenBBal0 = _tokenB.balanceOf(address(this)); - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); vm.warp(lFutureATimestamp); vm.prank(_alice); @@ -914,35 +914,35 @@ contract StablePairTest is BaseTest { assertGt(lAmtC, 0); } - function testBurn_SucceedEvenIfMintFeeReverts() public { - // arrange - change some values to make iterative function algorithm not converge - // I have tried changing the reserves, but no matter how extreme the values are, - // StableMath._computeLiquidityFromAdjustedBalances would still converge - // which is good for our contracts but not good for my attempt to break it - uint192 lLastInvariant = 200e18; - uint64 lLastInvariantAmp = 0; - bytes32 lEncoded = bytes32(abi.encodePacked(lLastInvariantAmp, lLastInvariant)); - // hardcoding the slot for now as there is no way to access it publicly - // this will break when we change the storage layout - vm.store(address(_stablePair), bytes32(uint256(17)), lEncoded); - - // ensure that the iterative function that _mintFee calls reverts with the adulterated values - vm.prank(address(_stablePair)); - vm.expectRevert(stdError.arithmeticError); - _stablePair.mintFee(100e18, 100e18); - - // act - vm.prank(_alice); - _stablePair.transfer(address(_stablePair), 1e18); - // mintFee indeed reverted but burn still succeeded - this can be seen by examining the callstack - (uint256 lAmount0, uint256 lAmount1) = _stablePair.burn(address(this)); // mintFee would fail in this call - - // assert - assertEq(lAmount0, 0.5e18); - assertEq(lAmount0, lAmount1); - assertEq(_tokenA.balanceOf(address(this)), lAmount0); - assertEq(_tokenB.balanceOf(address(this)), lAmount1); - } +// function testBurn_SucceedEvenIfMintFeeReverts() public { +// // arrange - change some values to make iterative function algorithm not converge +// // I have tried changing the reserves, but no matter how extreme the values are, +// // StableMath._computeLiquidityFromAdjustedBalances would still converge +// // which is good for our contracts but not good for my attempt to break it +// uint192 lLastInvariant = 200e18; +// uint64 lLastInvariantAmp = 0; +// bytes32 lEncoded = bytes32(abi.encodePacked(lLastInvariantAmp, lLastInvariant)); +// // hardcoding the slot for now as there is no way to access it publicly +// // this will break when we change the storage layout +// vm.store(address(_stablePair), bytes32(uint256(17)), lEncoded); +// +// // ensure that the iterative function that _mintFee calls reverts with the adulterated values +// vm.prank(address(_stablePair)); +// vm.expectRevert(stdError.arithmeticError); +// _stablePair.mintFee(100e18, 100e18); +// +// // act +// vm.prank(_alice); +// _stablePair.transfer(address(_stablePair), 1e18); +// // mintFee indeed reverted but burn still succeeded - this can be seen by examining the callstack +// (uint256 lAmount0, uint256 lAmount1) = _stablePair.burn(address(this)); // mintFee would fail in this call +// +// // assert +// assertEq(lAmount0, 0.5e18); +// assertEq(lAmount0, lAmount1); +// assertEq(_tokenA.balanceOf(address(this)), lAmount0); +// assertEq(_tokenB.balanceOf(address(this)), lAmount1); +// } function testBurn_LastInvariantUseReserveInsteadOfBalance() external { // arrange - trigger a write to the lastInvariant via burn @@ -1236,27 +1236,27 @@ contract StablePairTest is BaseTest { address(_stablePair), abi.encodeWithSignature("rampA(uint64,uint64)", lFutureAToSet, lFutureATimestamp), 0 ); - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _tokenA.mint(address(_stablePair), uint256(lAmountToSwap)); uint256 lAmountOutBeforeRamp = _stablePair.swap(lAmountToSwap, true, address(this), ""); - vm.revertTo(lBefore); - lBefore = vm.snapshot(); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); + lBefore = vm.snapshotState(); uint256 lTimeToSkip = bound(aSeed, 0, lRemainingTime / 4); _stepTime(lTimeToSkip); _tokenA.mint(address(_stablePair), uint256(lAmountToSwap)); uint256 lAmountOutT1 = _stablePair.swap(lAmountToSwap, true, address(this), ""); - vm.revertTo(lBefore); - lBefore = vm.snapshot(); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); + lBefore = vm.snapshotState(); lTimeToSkip = bound(aSeed, lRemainingTime / 4, lRemainingTime / 2); _stepTime(lTimeToSkip); _tokenA.mint(address(_stablePair), uint256(lAmountToSwap)); uint256 lAmountOutT2 = _stablePair.swap(lAmountToSwap, true, address(this), ""); - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); _stepTime(lRemainingTime); _tokenA.mint(address(_stablePair), uint256(lAmountToSwap)); @@ -1290,27 +1290,27 @@ contract StablePairTest is BaseTest { address(_stablePair), abi.encodeWithSignature("rampA(uint64,uint64)", lFutureAToSet, lFutureATimestamp), 0 ); - uint256 lBefore = vm.snapshot(); + uint256 lBefore = vm.snapshotState(); _tokenA.mint(address(_stablePair), uint256(lAmountToSwap)); uint256 lAmountOutBeforeRamp = _stablePair.swap(lAmountToSwap, true, address(this), ""); - vm.revertTo(lBefore); - lBefore = vm.snapshot(); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); + lBefore = vm.snapshotState(); uint256 lTimeToSkip = bound(aSeed, 0, lRemainingTime / 4); _stepTime(lTimeToSkip); _tokenA.mint(address(_stablePair), uint256(lAmountToSwap)); uint256 lAmountOutT1 = _stablePair.swap(lAmountToSwap, true, address(this), ""); - vm.revertTo(lBefore); - lBefore = vm.snapshot(); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); + lBefore = vm.snapshotState(); lTimeToSkip = bound(aSeed, lRemainingTime / 4, lRemainingTime / 2); _stepTime(lTimeToSkip); _tokenA.mint(address(_stablePair), uint256(lAmountToSwap)); uint256 lAmountOutT2 = _stablePair.swap(lAmountToSwap, true, address(this), ""); - vm.revertTo(lBefore); + require(vm.revertToStateAndDelete(lBefore), "revertToStateAndDelete failed"); _stepTime(lRemainingTime); _tokenA.mint(address(_stablePair), uint256(lAmountToSwap));