diff --git a/src/20240224_Multi_FundingUpdate/AaveV3Ethereum_FundingUpdate_20240224.sol b/src/20240224_Multi_FundingUpdate/AaveV3Ethereum_FundingUpdate_20240224.sol new file mode 100644 index 000000000..b3937a115 --- /dev/null +++ b/src/20240224_Multi_FundingUpdate/AaveV3Ethereum_FundingUpdate_20240224.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {AaveV2Ethereum, AaveV2EthereumAssets} from 'aave-address-book/AaveV2Ethereum.sol'; +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveSwapper} from 'aave-helpers/swaps/AaveSwapper.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/interfaces/IProposalGenericExecutor.sol'; + +/** + * @title Funding Update + * @author karpatkey_TokenLogic + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0x4dd4dff7096bf7ab8c4c071975d40f4cf709c41b4b6b7c60777a6dd50d2ecd09 + * - Discussion: https://governance.aave.com/t/arfc-funding-update/16675 + */ +contract AaveV3Ethereum_FundingUpdate_20240224 is IProposalGenericExecutor { + using SafeERC20 for IERC20; + + struct TokenToMigrate { + address underlying; + address aToken; + uint256 qty; + } + + struct TokenToSwap { + address underlying; + address aToken; + uint256 balance; + } + + AaveSwapper public constant SWAPPER = AaveSwapper(MiscEthereum.AAVE_SWAPPER); + address public constant ALC_SAFE = 0x205e795336610f5131Be52F09218AF19f0f3eC60; + + address public constant MILKMAN = 0x11C76AD590ABDFFCD980afEC9ad951B160F02797; + address public constant PRICE_CHECKER = 0xe80a1C615F75AFF7Ed8F08c9F21f9d00982D666c; + + address public constant GHO_USD_FEED = 0x3f12643D3f6f874d39C2a4c9f2Cd6f2DbAC877FC; + address public constant DPI_USD_FEED = 0xD2A593BF7594aCE1faD597adb697b5645d5edDB2; + + uint256 public constant USDC_V2_TO_MIGRATE = 300_000e6; + + uint256 public constant USDC_V3_TO_SWAP = 1_250_000e6; + uint256 public constant USDT_V3_TO_SWAP = 1_500_000e6; + uint256 public constant USDT_V2_TO_SWAP = 200_000e6; + + function execute() external { + // Transer USDT to Swapper + AaveV3Ethereum.COLLECTOR.transfer( + AaveV3EthereumAssets.USDT_UNDERLYING, + address(SWAPPER), + IERC20(AaveV3EthereumAssets.USDT_UNDERLYING).balanceOf(address(AaveV3Ethereum.COLLECTOR)) + ); + + _migrateV2ToV3(); + _transferToALC(); + _swap(); + } + + /** + * @notice Swaps withdrawn tokens from V2 and V3 for GHO + * - withdraws aDAIv2, aUSDTv3, aUSDTv2, aUSDCv3, aUSDCv2, aFRAXv2, aDPIv2 + * - transfers raw LUSD + */ + function _swap() internal { + _withdrawV3Tokens(); + _withdrawV2Tokens(); + + // LUSD + AaveV3Ethereum.COLLECTOR.transfer( + AaveV3EthereumAssets.LUSD_UNDERLYING, + address(SWAPPER), + IERC20(AaveV3EthereumAssets.LUSD_UNDERLYING).balanceOf(address(AaveV3Ethereum.COLLECTOR)) + ); + + SWAPPER.swap( + MILKMAN, + PRICE_CHECKER, + AaveV3EthereumAssets.LUSD_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.LUSD_ORACLE, + GHO_USD_FEED, + address(AaveV3Ethereum.COLLECTOR), + IERC20(AaveV3EthereumAssets.LUSD_UNDERLYING).balanceOf(address(SWAPPER)), + 500 + ); + + SWAPPER.swap( + MILKMAN, + PRICE_CHECKER, + AaveV3EthereumAssets.DAI_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.DAI_ORACLE, + GHO_USD_FEED, + address(AaveV3Ethereum.COLLECTOR), + IERC20(AaveV3EthereumAssets.DAI_UNDERLYING).balanceOf(address(SWAPPER)), + 100 + ); + + SWAPPER.swap( + MILKMAN, + PRICE_CHECKER, + AaveV2EthereumAssets.DPI_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + DPI_USD_FEED, + GHO_USD_FEED, + address(AaveV3Ethereum.COLLECTOR), + IERC20(AaveV2EthereumAssets.DPI_UNDERLYING).balanceOf(address(SWAPPER)), + 300 + ); + + SWAPPER.swap( + MILKMAN, + PRICE_CHECKER, + AaveV3EthereumAssets.FRAX_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.FRAX_ORACLE, + GHO_USD_FEED, + address(AaveV3Ethereum.COLLECTOR), + IERC20(AaveV3EthereumAssets.FRAX_UNDERLYING).balanceOf(address(SWAPPER)), + 300 + ); + + SWAPPER.swap( + MILKMAN, + PRICE_CHECKER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.USDC_ORACLE, + GHO_USD_FEED, + address(AaveV3Ethereum.COLLECTOR), + IERC20(AaveV3EthereumAssets.USDC_UNDERLYING).balanceOf(address(SWAPPER)), + 50 + ); + + SWAPPER.swap( + MILKMAN, + PRICE_CHECKER, + AaveV3EthereumAssets.USDT_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.USDT_ORACLE, + GHO_USD_FEED, + address(AaveV3Ethereum.COLLECTOR), + IERC20(AaveV3EthereumAssets.USDT_UNDERLYING).balanceOf(address(SWAPPER)), + 50 + ); + } + + /** + * @notice migrates all but one unit of the specified assets from AaveV2 to AaveV3 + * assets: WETH, WBTC and USDC + */ + function _migrateV2ToV3() internal { + TokenToMigrate[] memory tokens = new TokenToMigrate[](3); + tokens[0] = TokenToMigrate( + AaveV2EthereumAssets.WBTC_UNDERLYING, + AaveV2EthereumAssets.WBTC_A_TOKEN, + IERC20(AaveV2EthereumAssets.WBTC_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - 1e8 + ); + tokens[1] = TokenToMigrate( + AaveV2EthereumAssets.WETH_UNDERLYING, + AaveV2EthereumAssets.WETH_A_TOKEN, + IERC20(AaveV2EthereumAssets.WETH_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[2] = TokenToMigrate( + AaveV2EthereumAssets.USDC_UNDERLYING, + AaveV2EthereumAssets.USDC_A_TOKEN, + USDC_V2_TO_MIGRATE + ); + + uint256 tokensLength = tokens.length; + for (uint256 i = 0; i < tokensLength; i++) { + AaveV3Ethereum.COLLECTOR.transfer(tokens[i].aToken, address(this), tokens[i].qty); + + AaveV2Ethereum.POOL.withdraw(tokens[i].underlying, type(uint256).max, address(this)); + + uint256 amount = IERC20(tokens[i].underlying).balanceOf(address(this)); + IERC20(tokens[i].underlying).forceApprove(address(AaveV3Ethereum.POOL), amount); + + AaveV3Ethereum.POOL.deposit( + tokens[i].underlying, + amount, + address(AaveV3Ethereum.COLLECTOR), + 0 + ); + } + } + + /** + * @notice transfers strategic assets to the ALC_SAFE + * - withdraws aBALv2, aBALv3, aCRVv2, aCRVv3 to ALC_SAFE + * - transfers raw BAL / CRV to ALC_SAFE + */ + function _transferToALC() internal { + // Aave V2 BAL + AaveV3Ethereum.COLLECTOR.transfer( + AaveV2EthereumAssets.BAL_A_TOKEN, + address(this), + IERC20(AaveV2EthereumAssets.BAL_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + + AaveV2Ethereum.POOL.withdraw(AaveV2EthereumAssets.BAL_UNDERLYING, type(uint256).max, ALC_SAFE); + + // Aave V3 BAL + AaveV3Ethereum.COLLECTOR.transfer( + AaveV3EthereumAssets.BAL_A_TOKEN, + address(this), + IERC20(AaveV3EthereumAssets.BAL_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + + AaveV3Ethereum.POOL.withdraw(AaveV3EthereumAssets.BAL_UNDERLYING, type(uint256).max, ALC_SAFE); + + // Aave V2 CRV + AaveV3Ethereum.COLLECTOR.transfer( + AaveV2EthereumAssets.CRV_A_TOKEN, + address(this), + IERC20(AaveV2EthereumAssets.CRV_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + + AaveV2Ethereum.POOL.withdraw(AaveV2EthereumAssets.CRV_UNDERLYING, type(uint256).max, ALC_SAFE); + + // Aave V3 CRV + AaveV3Ethereum.COLLECTOR.transfer( + AaveV3EthereumAssets.CRV_A_TOKEN, + address(this), + IERC20(AaveV3EthereumAssets.CRV_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + + AaveV3Ethereum.POOL.withdraw(AaveV3EthereumAssets.CRV_UNDERLYING, type(uint256).max, ALC_SAFE); + + // BAL + AaveV3Ethereum.COLLECTOR.transfer( + AaveV3EthereumAssets.BAL_UNDERLYING, + ALC_SAFE, + IERC20(AaveV3EthereumAssets.BAL_UNDERLYING).balanceOf(address(AaveV3Ethereum.COLLECTOR)) + ); + + // CRV + AaveV3Ethereum.COLLECTOR.transfer( + AaveV3EthereumAssets.CRV_UNDERLYING, + ALC_SAFE, + IERC20(AaveV3EthereumAssets.CRV_UNDERLYING).balanceOf(address(AaveV3Ethereum.COLLECTOR)) + ); + } + + /** + * @notice Withdraws v2 tokens + */ + function _withdrawV2Tokens() internal { + TokenToSwap[] memory tokens = new TokenToSwap[](5); + tokens[0] = TokenToSwap( + AaveV2EthereumAssets.LUSD_UNDERLYING, + AaveV2EthereumAssets.LUSD_A_TOKEN, + IERC20(AaveV2EthereumAssets.LUSD_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[1] = TokenToSwap( + AaveV2EthereumAssets.DAI_UNDERLYING, + AaveV2EthereumAssets.DAI_A_TOKEN, + IERC20(AaveV2EthereumAssets.DAI_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[2] = TokenToSwap( + AaveV2EthereumAssets.DPI_UNDERLYING, + AaveV2EthereumAssets.DPI_A_TOKEN, + IERC20(AaveV2EthereumAssets.DPI_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[3] = TokenToSwap( + AaveV2EthereumAssets.FRAX_UNDERLYING, + AaveV2EthereumAssets.FRAX_A_TOKEN, + IERC20(AaveV2EthereumAssets.FRAX_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[4] = TokenToSwap( + AaveV2EthereumAssets.USDT_UNDERLYING, + AaveV2EthereumAssets.USDT_A_TOKEN, + USDT_V2_TO_SWAP + ); + + uint256 tokensLength = tokens.length; + for (uint256 i = 0; i < tokensLength; i++) { + AaveV3Ethereum.COLLECTOR.transfer(tokens[i].aToken, address(this), tokens[i].balance); + + AaveV2Ethereum.POOL.withdraw(tokens[i].underlying, type(uint256).max, address(SWAPPER)); + } + } + + /** + * @notice Withdraws v3 tokens + */ + function _withdrawV3Tokens() internal { + TokenToSwap[] memory tokens = new TokenToSwap[](3); + tokens[0] = TokenToSwap( + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.USDC_A_TOKEN, + USDC_V3_TO_SWAP + ); + tokens[1] = TokenToSwap( + AaveV3EthereumAssets.USDT_UNDERLYING, + AaveV3EthereumAssets.USDT_A_TOKEN, + USDT_V3_TO_SWAP - IERC20(AaveV3EthereumAssets.USDT_UNDERLYING).balanceOf(address(SWAPPER)) + ); + tokens[2] = TokenToSwap( + AaveV3EthereumAssets.LUSD_UNDERLYING, + AaveV3EthereumAssets.LUSD_A_TOKEN, + IERC20(AaveV3EthereumAssets.LUSD_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + + uint256 tokensLength = tokens.length; + for (uint256 i = 0; i < tokensLength; i++) { + AaveV3Ethereum.COLLECTOR.transfer(tokens[i].aToken, address(this), tokens[i].balance); + + AaveV3Ethereum.POOL.withdraw(tokens[i].underlying, type(uint256).max, address(SWAPPER)); + } + } +} diff --git a/src/20240224_Multi_FundingUpdate/AaveV3Ethereum_FundingUpdate_20240224.t.sol b/src/20240224_Multi_FundingUpdate/AaveV3Ethereum_FundingUpdate_20240224.t.sol new file mode 100644 index 000000000..7c9ce2c87 --- /dev/null +++ b/src/20240224_Multi_FundingUpdate/AaveV3Ethereum_FundingUpdate_20240224.t.sol @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {AaveV2Ethereum, AaveV2EthereumAssets} from 'aave-address-book/AaveV2Ethereum.sol'; +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/ProtocolV3TestBase.sol'; + +import {AaveV3Ethereum_FundingUpdate_20240224} from './AaveV3Ethereum_FundingUpdate_20240224.sol'; + +/** + * @dev Test for AaveV3Ethereum_FundingUpdate_20240224 + * command: make test-contract filter=AaveV3Ethereum_FundingUpdate_20240224 + */ +contract AaveV3Ethereum_FundingUpdate_20240224_Test is ProtocolV3TestBase { + event SwapRequested( + address milkman, + address indexed fromToken, + address indexed toToken, + address fromOracle, + address toOracle, + uint256 amount, + address indexed recipient, + uint256 slippage + ); + + struct TokenToMigrate { + address underlying; + address aToken; + uint256 qty; + } + + struct TokenToSwap { + address underlying; + address aToken; + uint256 balance; + } + + AaveV3Ethereum_FundingUpdate_20240224 internal proposal; + + uint256 balanceUsdtBefore; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 19363481); + proposal = new AaveV3Ethereum_FundingUpdate_20240224(); + } + + function test_alcTransfers() public { + uint256 balanceCRVBeforeALC = IERC20(AaveV3EthereumAssets.CRV_UNDERLYING).balanceOf( + proposal.ALC_SAFE() + ); + uint256 balanceBALBeforeALC = IERC20(AaveV3EthereumAssets.CRV_UNDERLYING).balanceOf( + proposal.ALC_SAFE() + ); + uint256 balanceCRVBeforeCollector = IERC20(AaveV3EthereumAssets.CRV_UNDERLYING).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceBALBeforeCollector = IERC20(AaveV3EthereumAssets.BAL_UNDERLYING).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceAEthCRVBeforeCollector = IERC20(AaveV2EthereumAssets.CRV_A_TOKEN).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceAEthBALBeforeCollector = IERC20(AaveV2EthereumAssets.BAL_A_TOKEN).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + + assertGt(balanceCRVBeforeCollector, 0); + assertGt(balanceBALBeforeCollector, 0); + + executePayload(vm, address(proposal)); + + uint256 balanceCRVAfterALC = IERC20(AaveV3EthereumAssets.CRV_UNDERLYING).balanceOf( + proposal.ALC_SAFE() + ); + uint256 balanceBALAfterALC = IERC20(AaveV3EthereumAssets.CRV_UNDERLYING).balanceOf( + proposal.ALC_SAFE() + ); + uint256 balanceCRVAfterCollector = IERC20(AaveV3EthereumAssets.CRV_UNDERLYING).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceBALAfterCollector = IERC20(AaveV3EthereumAssets.BAL_UNDERLYING).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + + assertEq(balanceCRVAfterCollector, 0, 'collector CRV balance after not 0'); + assertEq(balanceBALAfterCollector, 0, 'collector BAL balance after not 0'); + assertGt( + balanceCRVAfterALC, + balanceCRVBeforeALC + balanceCRVBeforeCollector + balanceAEthCRVBeforeCollector + ); + assertGt( + balanceBALAfterALC, + balanceBALBeforeALC + balanceBALBeforeCollector + balanceAEthBALBeforeCollector + ); + } + + function test_migrateV2toV3() internal { + uint256 balanceAUSDCBefore = IERC20(AaveV2EthereumAssets.USDC_A_TOKEN).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceEthAWBTCBefore = IERC20(AaveV3EthereumAssets.WBTC_A_TOKEN).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceEthAWETHBefore = IERC20(AaveV3EthereumAssets.WETH_A_TOKEN).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceEthAUSDCBefore = IERC20(AaveV3EthereumAssets.USDC_A_TOKEN).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + + executePayload(vm, address(proposal)); + + assertLt( + IERC20(AaveV2EthereumAssets.USDC_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)), + balanceAUSDCBefore + ); + + assertGt( + IERC20(AaveV3EthereumAssets.WBTC_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)), + balanceEthAWBTCBefore + ); + assertGt( + IERC20(AaveV3EthereumAssets.WETH_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)), + balanceEthAWETHBefore + ); + assertGt( + IERC20(AaveV3EthereumAssets.USDC_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)), + balanceEthAUSDCBefore - proposal.USDC_V3_TO_SWAP() + ); + } + + function test_withdrawV2() public { + TokenToSwap[] memory tokens = new TokenToSwap[](5); + tokens[0] = TokenToSwap( + AaveV2EthereumAssets.LUSD_UNDERLYING, + AaveV2EthereumAssets.LUSD_A_TOKEN, + IERC20(AaveV2EthereumAssets.LUSD_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[1] = TokenToSwap( + AaveV2EthereumAssets.DAI_UNDERLYING, + AaveV2EthereumAssets.DAI_A_TOKEN, + IERC20(AaveV2EthereumAssets.DAI_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[2] = TokenToSwap( + AaveV2EthereumAssets.DPI_UNDERLYING, + AaveV2EthereumAssets.DPI_A_TOKEN, + IERC20(AaveV2EthereumAssets.DPI_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[3] = TokenToSwap( + AaveV2EthereumAssets.FRAX_UNDERLYING, + AaveV2EthereumAssets.FRAX_A_TOKEN, + IERC20(AaveV2EthereumAssets.FRAX_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + tokens[4] = TokenToSwap( + AaveV2EthereumAssets.USDT_UNDERLYING, + AaveV2EthereumAssets.USDT_A_TOKEN, + proposal.USDT_V2_TO_SWAP() + ); + + for (uint256 i = 0; i < tokens.length; i++) { + assertGt(IERC20(tokens[i].aToken).balanceOf(address(AaveV3Ethereum.COLLECTOR)), 0); + } + + _expectEmits(); + + executePayload(vm, address(proposal)); + + for (uint256 i = 0; i < tokens.length; i++) { + assertApproxEqAbs( + IERC20(tokens[i].aToken).balanceOf(address(AaveV3Ethereum.COLLECTOR)), + 1 ether, + 1000 ether + ); + } // V2 Withdrawals leave a lot behind + } + + function test_withdrawV3() public { + TokenToSwap[] memory tokens = new TokenToSwap[](3); + tokens[0] = TokenToSwap( + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.USDC_A_TOKEN, + proposal.USDC_V3_TO_SWAP() + ); + tokens[1] = TokenToSwap( + AaveV3EthereumAssets.USDT_UNDERLYING, + AaveV3EthereumAssets.USDT_A_TOKEN, + proposal.USDT_V3_TO_SWAP() - + IERC20(AaveV3EthereumAssets.USDT_UNDERLYING).balanceOf(address(AaveV3Ethereum.COLLECTOR)) + ); + tokens[2] = TokenToSwap( + AaveV3EthereumAssets.LUSD_UNDERLYING, + AaveV3EthereumAssets.LUSD_A_TOKEN, + IERC20(AaveV3EthereumAssets.LUSD_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)) - + 1 ether + ); + + for (uint256 i = 0; i < tokens.length; i++) { + assertGt(IERC20(tokens[i].aToken).balanceOf(address(AaveV3Ethereum.COLLECTOR)), 0); + } + + uint256 balanceAEthUSDCBefore = IERC20(AaveV3EthereumAssets.USDC_A_TOKEN).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceAEthUSDTBefore = IERC20(AaveV3EthereumAssets.USDT_A_TOKEN).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + uint256 balanceUSDTBefore = IERC20(AaveV3EthereumAssets.USDT_UNDERLYING).balanceOf( + address(AaveV3Ethereum.COLLECTOR) + ); + + _expectEmits(); + + executePayload(vm, address(proposal)); + + assertApproxEqAbs( + IERC20(AaveV3EthereumAssets.LUSD_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)), + 1 ether, + 1, + 'aEthLUSD not within 1 ether' + ); + assertApproxEqAbs( + IERC20(AaveV3EthereumAssets.USDC_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)), + balanceAEthUSDCBefore - proposal.USDC_V3_TO_SWAP() + proposal.USDC_V2_TO_MIGRATE(), + 1, + 'aEthUSDC not within 1 ether' + ); + assertApproxEqAbs( + IERC20(AaveV3EthereumAssets.USDT_A_TOKEN).balanceOf(address(AaveV3Ethereum.COLLECTOR)), + balanceAEthUSDTBefore - proposal.USDT_V3_TO_SWAP() + balanceUSDTBefore, + 1, + 'aEthUSDT not within 1 ether' + ); + } + + function _expectEmits() internal { + vm.expectEmit(true, true, true, true, MiscEthereum.AAVE_SWAPPER); + emit SwapRequested( + proposal.MILKMAN(), + AaveV3EthereumAssets.LUSD_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.LUSD_ORACLE, + proposal.GHO_USD_FEED(), + 32761384430524382295524, // Hardcoded because of V2 withdrawal + address(AaveV3Ethereum.COLLECTOR), + 500 + ); + + vm.expectEmit(true, true, true, true, MiscEthereum.AAVE_SWAPPER); + emit SwapRequested( + proposal.MILKMAN(), + AaveV3EthereumAssets.DAI_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.DAI_ORACLE, + proposal.GHO_USD_FEED(), + 400349503743721455670834, // Hardcoded because of V2 withdrawal + address(AaveV3Ethereum.COLLECTOR), + 100 + ); + + vm.expectEmit(true, true, true, true, MiscEthereum.AAVE_SWAPPER); + emit SwapRequested( + proposal.MILKMAN(), + AaveV2EthereumAssets.DPI_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + proposal.DPI_USD_FEED(), + proposal.GHO_USD_FEED(), + 486852204700306697462, // Hardcoded because of V2 withdrawal + address(AaveV3Ethereum.COLLECTOR), + 300 + ); + + vm.expectEmit(true, true, true, true, MiscEthereum.AAVE_SWAPPER); + emit SwapRequested( + proposal.MILKMAN(), + AaveV3EthereumAssets.FRAX_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.FRAX_ORACLE, + proposal.GHO_USD_FEED(), + 29115641783196347431241, // Hardcoded because of V2 withdrawal + address(AaveV3Ethereum.COLLECTOR), + 300 + ); + + vm.expectEmit(true, true, true, true, MiscEthereum.AAVE_SWAPPER); + emit SwapRequested( + proposal.MILKMAN(), + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.USDC_ORACLE, + proposal.GHO_USD_FEED(), + proposal.USDC_V3_TO_SWAP(), + address(AaveV3Ethereum.COLLECTOR), + 50 + ); + + vm.expectEmit(true, true, true, true, MiscEthereum.AAVE_SWAPPER); + emit SwapRequested( + proposal.MILKMAN(), + AaveV3EthereumAssets.USDT_UNDERLYING, + AaveV3EthereumAssets.GHO_UNDERLYING, + AaveV3EthereumAssets.USDT_ORACLE, + proposal.GHO_USD_FEED(), + proposal.USDT_V3_TO_SWAP() + proposal.USDT_V2_TO_SWAP(), + address(AaveV3Ethereum.COLLECTOR), + 50 + ); + } +} diff --git a/src/20240224_Multi_FundingUpdate/AaveV3Polygon_FundingUpdate_20240224.sol b/src/20240224_Multi_FundingUpdate/AaveV3Polygon_FundingUpdate_20240224.sol new file mode 100644 index 000000000..26d63151c --- /dev/null +++ b/src/20240224_Multi_FundingUpdate/AaveV3Polygon_FundingUpdate_20240224.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; +import {MiscPolygon} from 'aave-address-book/MiscPolygon.sol'; +import {AaveV3Polygon, AaveV3PolygonAssets} from 'aave-address-book/AaveV3Polygon.sol'; +import {IProposalGenericExecutor} from 'aave-helpers/interfaces/IProposalGenericExecutor.sol'; + +interface IAavePolEthERC20Bridge { + function bridge(address token, uint256 amount) external; +} + +/** + * @title Funding Update + * @author karpatkey_TokenLogic + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0x4dd4dff7096bf7ab8c4c071975d40f4cf709c41b4b6b7c60777a6dd50d2ecd09 + * - Discussion: https://governance.aave.com/t/arfc-funding-update/16675 + */ +contract AaveV3Polygon_FundingUpdate_20240224 is IProposalGenericExecutor { + using SafeERC20 for IERC20; + + IAavePolEthERC20Bridge public constant bridge = + IAavePolEthERC20Bridge(MiscPolygon.AAVE_POL_ETH_BRIDGE); + + function execute() external { + /// Transfer + AaveV3Polygon.COLLECTOR.transfer( + AaveV3PolygonAssets.WETH_A_TOKEN, + address(this), + IERC20(AaveV3PolygonAssets.WETH_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)) - 1 ether + ); + + AaveV3Polygon.COLLECTOR.transfer( + AaveV3PolygonAssets.DAI_A_TOKEN, + address(this), + IERC20(AaveV3PolygonAssets.DAI_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)) - 1 ether + ); + + AaveV3Polygon.COLLECTOR.transfer( + AaveV3PolygonAssets.BAL_A_TOKEN, + address(this), + IERC20(AaveV3PolygonAssets.BAL_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)) - 1 ether + ); + + AaveV3Polygon.COLLECTOR.transfer( + AaveV3PolygonAssets.CRV_A_TOKEN, + address(this), + IERC20(AaveV3PolygonAssets.CRV_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)) - 1 ether + ); + + /// Withdraw + AaveV3Polygon.POOL.withdraw( + AaveV3PolygonAssets.WETH_UNDERLYING, + type(uint256).max, + address(bridge) + ); + + AaveV3Polygon.POOL.withdraw( + AaveV3PolygonAssets.DAI_UNDERLYING, + type(uint256).max, + address(bridge) + ); + + AaveV3Polygon.POOL.withdraw( + AaveV3PolygonAssets.BAL_UNDERLYING, + type(uint256).max, + address(bridge) + ); + + AaveV3Polygon.POOL.withdraw( + AaveV3PolygonAssets.CRV_UNDERLYING, + type(uint256).max, + address(bridge) + ); + + /// Bridge + + bridge.bridge( + AaveV3PolygonAssets.WETH_UNDERLYING, + IERC20(AaveV3PolygonAssets.WETH_UNDERLYING).balanceOf(address(bridge)) + ); + + bridge.bridge( + AaveV3PolygonAssets.DAI_UNDERLYING, + IERC20(AaveV3PolygonAssets.DAI_UNDERLYING).balanceOf(address(bridge)) + ); + + bridge.bridge( + AaveV3PolygonAssets.BAL_UNDERLYING, + IERC20(AaveV3PolygonAssets.BAL_UNDERLYING).balanceOf(address(bridge)) + ); + + bridge.bridge( + AaveV3PolygonAssets.CRV_UNDERLYING, + IERC20(AaveV3PolygonAssets.CRV_UNDERLYING).balanceOf(address(bridge)) + ); + } +} diff --git a/src/20240224_Multi_FundingUpdate/AaveV3Polygon_FundingUpdate_20240224.t.sol b/src/20240224_Multi_FundingUpdate/AaveV3Polygon_FundingUpdate_20240224.t.sol new file mode 100644 index 000000000..2d50709cd --- /dev/null +++ b/src/20240224_Multi_FundingUpdate/AaveV3Polygon_FundingUpdate_20240224.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {MiscPolygon} from 'aave-address-book/MiscPolygon.sol'; +import {AaveV3Polygon, AaveV3PolygonAssets} from 'aave-address-book/AaveV3Polygon.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/ProtocolV3TestBase.sol'; + +import {AaveV3Polygon_FundingUpdate_20240224} from './AaveV3Polygon_FundingUpdate_20240224.sol'; + +/** + * @dev Test for AaveV3Polygon_FundingUpdate_20240224 + * command: make test-contract filter=AaveV3Polygon_FundingUpdate_20240224 + */ +contract AaveV3Polygon_FundingUpdate_20240224_Test is ProtocolV3TestBase { + event Bridge(address token, uint256 amount); + + AaveV3Polygon_FundingUpdate_20240224 internal proposal; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('polygon'), 53919052); + proposal = new AaveV3Polygon_FundingUpdate_20240224(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + assertGt( + IERC20(AaveV3PolygonAssets.WETH_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)), + 0, + 'aPolWETH not greater than 0' + ); + + assertGt( + IERC20(AaveV3PolygonAssets.DAI_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)), + 0, + 'aPolDAI not greater than 0' + ); + + assertGt( + IERC20(AaveV3PolygonAssets.BAL_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)), + 0, + 'aPolBAL not greater than 0' + ); + + assertGt( + IERC20(AaveV3PolygonAssets.CRV_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)), + 0, + 'aPolCRV not greater than 0' + ); + + vm.expectEmit(true, true, true, true, MiscPolygon.AAVE_POL_ETH_BRIDGE); + emit Bridge(AaveV3PolygonAssets.WETH_UNDERLYING, 268586927209511561696); // ~268 units + + vm.expectEmit(true, true, true, true, MiscPolygon.AAVE_POL_ETH_BRIDGE); + emit Bridge(AaveV3PolygonAssets.DAI_UNDERLYING, 559109670176764998505378); // ~559,109 units + + vm.expectEmit(true, true, true, true, MiscPolygon.AAVE_POL_ETH_BRIDGE); + emit Bridge(AaveV3PolygonAssets.BAL_UNDERLYING, 6029363813545159141241); // ~6,029 units + + vm.expectEmit(true, true, true, true, MiscPolygon.AAVE_POL_ETH_BRIDGE); + emit Bridge(AaveV3PolygonAssets.CRV_UNDERLYING, 12017495831846027272036); // ~12,017 units + + executePayload(vm, address(proposal)); + + assertApproxEqAbs( + IERC20(AaveV3PolygonAssets.USDC_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)), + 1 ether, + 1 ether, + 'aPolWETH token remainder greater than 1 unit' + ); + + assertApproxEqAbs( + IERC20(AaveV3PolygonAssets.DAI_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)), + 1 ether, + 1 ether, + 'aPolDAI token remainder greater than 1 unit' + ); + + assertApproxEqAbs( + IERC20(AaveV3PolygonAssets.BAL_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)), + 1 ether, + 1 ether, + 'aPolBAL token remainder greater than 1 unit' + ); + + assertApproxEqAbs( + IERC20(AaveV3PolygonAssets.CRV_A_TOKEN).balanceOf(address(AaveV3Polygon.COLLECTOR)), + 1 ether, + 1 ether, + 'aPolCRV token remainder greater than 1 unit' + ); + } +} diff --git a/src/20240224_Multi_FundingUpdate/FundingUpdate.md b/src/20240224_Multi_FundingUpdate/FundingUpdate.md new file mode 100644 index 000000000..8dd91fd44 --- /dev/null +++ b/src/20240224_Multi_FundingUpdate/FundingUpdate.md @@ -0,0 +1,55 @@ +--- +title: "Funding Update" +author: "karpatkey_TokenLogic" +discussions: "https://governance.aave.com/t/arfc-funding-update/16675" +--- + +## Simple Summary + +This publication mainly proposes consolidating DAO assets in preparation of foreseeable DAO expenses. There are additional small treasury management actions included in this proposal for house-keeping. + +## Motivation + +In preparation for the upcoming launch of the Merit program and the expected renewal of DAO service providers, this proposal intends to: + +- Consolidate the DAOs smaller holdings to GHO +- Swap larger stablecoins holdings for GHO +- Replenish the stablecoins Reserves in Aave v3 +- Migrate funds from Aave v2 to v3 +- Bridge funds from Polygon to Mainnet + +To reduce governance burden, this proposal also includes withdrawing BAL and CRV from mainnet Aave v3 and v2, as well as withdrawing and bridging them from Polygon Aave v3 to Ethereum. All the BAL and CRV included in this proposal will then be transferred to the Aave Liquidity Committee (ALC) for its future allocation on behalf of the DAO. + +## Specification + +This is Part 1 of the specified proposal and it will do as follows: + +| Withdraw & Swap to GHO | Migrate ETH v2 to v3 | Polygon to ETH | +| ---------------------- | -------------------- | ---------------- | +| aEthLUSD (All-1) | awBTC (All-1) | aPolwETH (All-1) | +| aEthUSDC (1.25M) | awETH (All-1) | aPolDAI (All-1) | +| aEthUSDT (1.5M) | aUSDC (300k) | aPolBAL (All-1) | +| LUSD (All) | | aPolCRV (All-1) | +| aLUSD (All-1) | | | +| aUSDT (200k) | | | +| aDAI (All-1) | | | +| aDPI (All-1) | | | +| aFRAX (All-1) | | | + +_Note_ + +The forum post specified withdrawing TUSD and BUSD but these will be kept in order to help the offboarding of the two tokens in case of bad debt. +The forum post also asks to withdraw all from Aave V2 AMPL, but the pool has no liquidity available. + +Withdraw aCRV, aEthCRV, aBAL and aEthBAL and transfer BAL and CRV to ALC SAFE. + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240224_Multi_FundingUpdate/AaveV3Ethereum_FundingUpdate_20240224.sol), [AaveV3Polygon](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240224_Multi_FundingUpdate/AaveV3Polygon_FundingUpdate_20240224.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240224_Multi_FundingUpdate/AaveV3Ethereum_FundingUpdate_20240224.t.sol), [AaveV3Polygon](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240224_Multi_FundingUpdate/AaveV3Polygon_FundingUpdate_20240224.t.sol) +- [Snapshot](https://snapshot.org/#/aave.eth/proposal/0x4dd4dff7096bf7ab8c4c071975d40f4cf709c41b4b6b7c60777a6dd50d2ecd09) +- [Discussion](https://governance.aave.com/t/arfc-funding-update/16675) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20240224_Multi_FundingUpdate/FundingUpdate_20240224.s.sol b/src/20240224_Multi_FundingUpdate/FundingUpdate_20240224.s.sol new file mode 100644 index 000000000..b05d0bfea --- /dev/null +++ b/src/20240224_Multi_FundingUpdate/FundingUpdate_20240224.s.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/GovV3Helpers.sol'; +import {EthereumScript, PolygonScript} from 'aave-helpers/ScriptUtils.sol'; +import {AaveV3Ethereum_FundingUpdate_20240224} from './AaveV3Ethereum_FundingUpdate_20240224.sol'; +import {AaveV3Polygon_FundingUpdate_20240224} from './AaveV3Polygon_FundingUpdate_20240224.sol'; + +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20240224_Multi_FundingUpdate/FundingUpdate_20240224.s.sol:DeployEthereum chain=mainnet + * verify-command: npx catapulta-verify -b broadcast/FundingUpdate_20240224.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Ethereum_FundingUpdate_20240224).creationCode + ); + + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0] = GovV3Helpers.buildAction(payload0); + + // register action at payloadsController + GovV3Helpers.createPayload(actions); + } +} + +/** + * @dev Deploy Polygon + * deploy-command: make deploy-ledger contract=src/20240224_Multi_FundingUpdate/FundingUpdate_20240224.s.sol:DeployPolygon chain=polygon + * verify-command: npx catapulta-verify -b broadcast/FundingUpdate_20240224.s.sol/137/run-latest.json + */ +contract DeployPolygon is PolygonScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(AaveV3Polygon_FundingUpdate_20240224).creationCode + ); + + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0] = GovV3Helpers.buildAction(payload0); + + // register action at payloadsController + GovV3Helpers.createPayload(actions); + } +} + +/** + * @dev Create Proposal + * command: make deploy-ledger contract=src/20240224_Multi_FundingUpdate/FundingUpdate_20240224.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](2); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsEthereum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsEthereum[0] = GovV3Helpers.buildAction( + type(AaveV3Ethereum_FundingUpdate_20240224).creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + IPayloadsControllerCore.ExecutionAction[] + memory actionsPolygon = new IPayloadsControllerCore.ExecutionAction[](1); + actionsPolygon[0] = GovV3Helpers.buildAction( + type(AaveV3Polygon_FundingUpdate_20240224).creationCode + ); + payloads[1] = GovV3Helpers.buildPolygonPayload(vm, actionsPolygon); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovV3Helpers.ipfsHashFile(vm, 'src/20240224_Multi_FundingUpdate/FundingUpdate.md') + ); + } +} diff --git a/src/20240224_Multi_FundingUpdate/config.ts b/src/20240224_Multi_FundingUpdate/config.ts new file mode 100644 index 000000000..6383a62f1 --- /dev/null +++ b/src/20240224_Multi_FundingUpdate/config.ts @@ -0,0 +1,16 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + pools: ['AaveV3Ethereum', 'AaveV3Polygon'], + title: 'Funding Update', + shortName: 'FundingUpdate', + date: '20240224', + author: 'karpatkey_TokenLogic', + discussion: 'https://governance.aave.com/t/arfc-funding-update/16675', + snapshot: '', + }, + poolOptions: { + AaveV3Ethereum: {configs: {OTHERS: {}}, cache: {blockNumber: 19300831}}, + AaveV3Polygon: {configs: {OTHERS: {}}, cache: {blockNumber: 53919052}}, + }, +};