diff --git a/script/DeployHorizon.s.sol b/script/DeployHorizon.s.sol new file mode 100644 index 0000000..8046fc3 --- /dev/null +++ b/script/DeployHorizon.s.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {EthereumScript} from "solidity-utils/contracts/utils/ScriptUtils.sol"; +import {IPoolAddressesProvider} from "aave-v3-origin/contracts/interfaces/IPoolAddressesProvider.sol"; +import {ICollector} from "aave-v3-origin/contracts/treasury/ICollector.sol"; +import {ITransparentProxyFactory} from "solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol"; +import {GhoDirectMinter} from "../src/GhoDirectMinter.sol"; +import {IGhoToken} from "../src/interfaces/IGhoToken.sol"; + +import {AaveV3Ethereum} from "aave-address-book/AaveV3Ethereum.sol"; +import {GovernanceV3Ethereum} from "aave-address-book/GovernanceV3Ethereum.sol"; +import {MiscEthereum} from "aave-address-book/MiscEthereum.sol"; +import {GhoEthereum} from "aave-address-book/GhoEthereum.sol"; + +library DeploymentLibrary { + address public constant EXECUTOR_LVL_1 = + GovernanceV3Ethereum.EXECUTOR_LVL_1; + IPoolAddressesProvider public constant POOL_ADDRESSES_PROVIDER = + IPoolAddressesProvider(0x5D39E06b825C1F2B80bf2756a73e28eFAA128ba0); // horizon pool addresses provider + address public constant COLLECTOR = address(AaveV3Ethereum.COLLECTOR); + address public constant COUNCIL = + 0x8513e6F37dBc52De87b166980Fa3F50639694B60; // council used on other GHO stewards + + function _deployHorizon() internal returns (address) { + address impl = address( + new GhoDirectMinter( + POOL_ADDRESSES_PROVIDER, + COLLECTOR, + GhoEthereum.GHO_TOKEN + ) + ); + address proxy = ITransparentProxyFactory( + MiscEthereum.TRANSPARENT_PROXY_FACTORY + ).create( + impl, + EXECUTOR_LVL_1, + abi.encodeCall( + GhoDirectMinter.initialize, + (EXECUTOR_LVL_1, COUNCIL) + ) + ); + return proxy; + } +} + +contract DeployHorizon is EthereumScript { + function run() external broadcast { + DeploymentLibrary._deployHorizon(); + } +} diff --git a/src/proposals/HorizonGHOListing.sol b/src/proposals/HorizonGHOListing.sol new file mode 100644 index 0000000..0d4b8d1 --- /dev/null +++ b/src/proposals/HorizonGHOListing.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IProposalGenericExecutor} from "aave-helpers/src/interfaces/IProposalGenericExecutor.sol"; +import {IEmissionManager} from "aave-v3-origin/contracts/rewards/interfaces/IEmissionManager.sol"; +import {IPoolConfigurator} from "aave-v3-origin/contracts/interfaces/IPoolConfigurator.sol"; +import {IPoolDataProvider} from "aave-v3-origin/contracts/interfaces/IPoolDataProvider.sol"; +import {IACLManager} from "aave-v3-origin/contracts/interfaces/IACLManager.sol"; +import {IAccessControl} from "aave-v3-origin/contracts/dependencies/openzeppelin/contracts/IAccessControl.sol"; +import {GhoEthereum} from "aave-address-book/GhoEthereum.sol"; + +import {IGhoBucketSteward} from "../interfaces/IGhoBucketSteward.sol"; +import {IGhoDirectMinter} from "../interfaces/IGhoDirectMinter.sol"; +import {IGhoToken} from "../interfaces/IGhoToken.sol"; + +// copied from AIP draft +contract HorizonGHOListing is IProposalGenericExecutor { + // stablecoins + address public constant GHO_TOKEN = + 0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f; + address public constant RLUSD_TOKEN = + 0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD; + address public constant USDC_TOKEN = + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + // rwa tokens + address public constant USTB_TOKEN = + 0x43415eB6ff9DB7E26A15b704e7A3eDCe97d31C4e; + address public constant USCC_TOKEN = + 0x14d60E7FDC0D71d8611742720E4C50E7a974020c; + address public constant USYC_TOKEN = + 0x136471a34f6ef19fE571EFFC1CA711fdb8E49f2b; + address public constant JTRSY_TOKEN = + 0x8c213ee79581Ff4984583C6a801e5263418C4b86; + address public constant JAAA_TOKEN = + 0x5a0F93D040De44e78F251b03c43be9CF317Dcf64; + // Horizon addresses + address public constant EMISSION_ADMIN = + 0xac140648435d03f784879cd789130F22Ef588Fcd; + IEmissionManager public constant EMISSION_MANAGER = + IEmissionManager(0xC2201708289b2C6A1d461A227A7E5ee3e7fE9A2F); + IPoolDataProvider public constant PROTOCOL_DATA_PROVIDER = + IPoolDataProvider(0x53519c32f73fE1797d10210c4950fFeBa3b21504); + address public constant POOL_CONFIGURATOR = + 0x83Cb1B4af26EEf6463aC20AFbAC9c0e2E017202F; + address public constant ACL_MANAGER = + 0xEFD5df7b87d2dCe6DD454b4240b3e0A4db562321; + // Gho + IGhoBucketSteward public constant GHO_BUCKET_STEWARD = + IGhoBucketSteward(GhoEthereum.GHO_BUCKET_STEWARD); + address public immutable GHO_DIRECT_MINTER; + + uint128 public constant GHO_BUCKET_CAPACITY = 1_000_000e18; + + constructor(address ghoDirectMinter) { + GHO_DIRECT_MINTER = ghoDirectMinter; + } + + function execute() external { + // unpause pool + IPoolConfigurator(POOL_CONFIGURATOR).setPoolPause(false); + // set emission admins on all listed tokens + _setEmissionAdmins(); + // grant gho minter role and add facilitator + _setGhoMinterAndSteward(); + } + + function _setGhoMinterAndSteward() internal { + IGhoToken gho = IGhoToken(GHO_TOKEN); + + IAccessControl(ACL_MANAGER).grantRole( + IACLManager(ACL_MANAGER).RISK_ADMIN_ROLE(), + address(GHO_DIRECT_MINTER) + ); + gho.addFacilitator( + GHO_DIRECT_MINTER, + "HorizonGhoDirectMinter", + GHO_BUCKET_CAPACITY + ); + + // allow risk council to control the bucket capacity + address[] memory facilitators = new address[](1); + facilitators[0] = address(GHO_DIRECT_MINTER); + GHO_BUCKET_STEWARD.setControlledFacilitator(facilitators, true); + } + + function _setEmissionAdmins() internal { + // stable coins + _setEmissionAdminStablecoin(GHO_TOKEN); + _setEmissionAdminStablecoin(RLUSD_TOKEN); + _setEmissionAdminStablecoin(USDC_TOKEN); + // rwa tokens + EMISSION_MANAGER.setEmissionAdmin(USTB_TOKEN, EMISSION_ADMIN); + EMISSION_MANAGER.setEmissionAdmin(USCC_TOKEN, EMISSION_ADMIN); + EMISSION_MANAGER.setEmissionAdmin(USYC_TOKEN, EMISSION_ADMIN); + EMISSION_MANAGER.setEmissionAdmin(JAAA_TOKEN, EMISSION_ADMIN); + EMISSION_MANAGER.setEmissionAdmin(JTRSY_TOKEN, EMISSION_ADMIN); + } + + function _setEmissionAdminStablecoin(address token) internal { + (address aToken, , ) = PROTOCOL_DATA_PROVIDER.getReserveTokensAddresses( + token + ); + EMISSION_MANAGER.setEmissionAdmin(token, EMISSION_ADMIN); + EMISSION_MANAGER.setEmissionAdmin(aToken, EMISSION_ADMIN); + } +} diff --git a/test/Horizon_GhoDirectMinter.t.sol b/test/Horizon_GhoDirectMinter.t.sol new file mode 100644 index 0000000..7fba947 --- /dev/null +++ b/test/Horizon_GhoDirectMinter.t.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import {MiscEthereum} from "aave-address-book/MiscEthereum.sol"; +import {AaveV3EthereumAssets} from "aave-address-book/AaveV3Ethereum.sol"; +import {GovernanceV3Ethereum} from "aave-address-book/GovernanceV3Ethereum.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ITransparentProxyFactory} from "solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol"; +import {UpgradeableOwnableWithGuardian, IWithGuardian} from "solidity-utils/contracts/access-control/UpgradeableOwnableWithGuardian.sol"; +import {GovV3Helpers} from "aave-helpers/src/GovV3Helpers.sol"; +import {IPool, DataTypes} from "aave-v3-origin/contracts/interfaces/IPool.sol"; +import {ReserveConfiguration} from "aave-v3-origin/contracts/protocol/libraries/configuration/ReserveConfiguration.sol"; +import {GhoDirectMinter} from "../src/GhoDirectMinter.sol"; +import {HorizonGHOListing} from "../src/proposals/HorizonGHOListing.sol"; +import {IGhoToken} from "../src/interfaces/IGhoToken.sol"; +import {DeploymentLibrary} from "../script/DeployHorizon.s.sol"; + +contract Horizon_GHODirectMinter_Test is Test { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + IPool public constant POOL = + IPool(0xAe05Cd22df81871bc7cC2a04BeCfb516bFe332C8); // horizon pool + address public constant USTB_TOKEN = + 0x43415eB6ff9DB7E26A15b704e7A3eDCe97d31C4e; + + address internal council = DeploymentLibrary.COUNCIL; + + GhoDirectMinter internal minter; + IERC20 internal ghoAToken; + HorizonGHOListing internal proposal; + + address internal owner = DeploymentLibrary.EXECUTOR_LVL_1; + + function setUp() external { + vm.createSelectFork(vm.rpcUrl("mainnet"), 23130843); + + _listingPayload(); + + // execute payload + address facilitator = DeploymentLibrary._deployHorizon(); + proposal = new HorizonGHOListing(facilitator); + GovV3Helpers.executePayload(vm, address(proposal)); + + address[] memory facilitators = IGhoToken( + AaveV3EthereumAssets.GHO_UNDERLYING + ).getFacilitatorsList(); + minter = GhoDirectMinter(facilitators[facilitators.length - 1]); + assertEq(address(minter), facilitator); + ghoAToken = IERC20(minter.GHO_A_TOKEN()); + } + + function _listingPayload() internal { + address EMERGENCY_MULTISIG = 0x13B57382c36BAB566E75C72303622AF29E27e1d3; + address LISTING_EXECUTOR_ADDRESS = 0x09e8E1408a68778CEDdC1938729Ea126710E7Dda; + address horizonPhaseOneListing = 0x7547670c534823AcbBDB214AdD9D9D3395F57e0C; + vm.prank(EMERGENCY_MULTISIG); + (bool success, bytes memory data) = LISTING_EXECUTOR_ADDRESS.call( + abi.encodeWithSignature( + "executeTransaction(address,uint256,string,bytes,bool)", + address(horizonPhaseOneListing), // target + 0, // value + "execute()", // signature + "", // data + true // withDelegatecall + ) + ); + assembly { + if iszero(success) { + revert(add(data, 32), mload(data)) + } + } + } + + function test_mintAndSupply_owner(uint256 amount) public returns (uint256) { + return _mintAndSupply(amount, owner); + } + + function test_mintAndSupply_council( + uint256 amount + ) external returns (uint256) { + return _mintAndSupply(amount, council); + } + + function test_mintAndSupply_rando() external { + vm.expectRevert( + abi.encodeWithSelector( + IWithGuardian.OnlyGuardianOrOwnerInvalidCaller.selector, + address(this) + ) + ); + minter.mintAndSupply(100); + } + + function test_withdrawAndBurn_owner( + uint256 supplyAmount, + uint256 withdrawAmount + ) external { + _withdrawAndBurn(supplyAmount, withdrawAmount, owner); + } + + function test_withdrawAndBurn_council( + uint256 supplyAmount, + uint256 withdrawAmount + ) external { + _withdrawAndBurn(supplyAmount, withdrawAmount, council); + } + + function test_withdrawAndBurn_rando() external { + vm.expectRevert( + abi.encodeWithSelector( + IWithGuardian.OnlyGuardianOrOwnerInvalidCaller.selector, + address(this) + ) + ); + minter.withdrawAndBurn(vm.randomUint(1, 100e18)); + } + + function test_transferExcessToTreasury() external { + uint256 amount = test_mintAndSupply_owner(1000 ether); + // supply USTB and borrow gho + vm.startPrank(owner); + deal(USTB_TOKEN, owner, 10_000e6); + IERC20(USTB_TOKEN).approve(address(POOL), 10_000e6); + POOL.deposit(USTB_TOKEN, 10_000e6, owner, 0); + POOL.borrow(AaveV3EthereumAssets.GHO_UNDERLYING, amount, 2, 0, owner); + + // generate some yield + vm.warp(block.timestamp + 1000); + + uint256 collectorBalanceBeforeTransfer = ghoAToken.balanceOf( + address(minter.COLLECTOR()) + ); + uint256 balanceBeforeTransfer = ghoAToken.balanceOf(address(minter)); + assertGt(balanceBeforeTransfer, amount); + minter.transferExcessToTreasury(); + assertApproxEqAbs(ghoAToken.balanceOf(address(minter)), amount, 1); + assertApproxEqAbs( + ghoAToken.balanceOf(address(minter.COLLECTOR())) - + collectorBalanceBeforeTransfer, + balanceBeforeTransfer - amount, + 1 + ); + } + + /// @dev supplies a bounded value of [amount, 1, type(uint256).max] to the pool + function _mintAndSupply( + uint256 amount, + address caller + ) internal returns (uint256) { + // setup + amount = bound(amount, 1, 100e18); + DataTypes.ReserveConfigurationMap memory configurationBefore = POOL + .getConfiguration(AaveV3EthereumAssets.GHO_UNDERLYING); + uint256 totalATokenSupplyBefore = ghoAToken.totalSupply(); + uint256 minterATokenSupplyBefore = IERC20(ghoAToken).balanceOf( + address(minter) + ); + (, uint256 levelBefore) = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING) + .getFacilitatorBucket(proposal.GHO_DIRECT_MINTER()); + + // mint + vm.prank(caller); + minter.mintAndSupply(amount); + + // check + DataTypes.ReserveConfigurationMap memory configurationAfter = POOL + .getConfiguration(AaveV3EthereumAssets.GHO_UNDERLYING); + (, uint256 levelAfter) = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING) + .getFacilitatorBucket(proposal.GHO_DIRECT_MINTER()); + // after supplying the minters aToken balance should increase by the supplied amount + assertEq( + IERC20(ghoAToken).balanceOf(address(minter)), + minterATokenSupplyBefore + amount + ); + // the aToken total supply should be adjusted by the same amount + assertEq(ghoAToken.totalSupply(), totalATokenSupplyBefore + amount); + // the cap should not be touched + assertEq( + configurationBefore.getSupplyCap(), + configurationAfter.getSupplyCap() + ); + // level should be increased by the minted amount + assertEq(levelAfter, levelBefore + amount); + return amount; + } + + // burns a bounded value of [withdrawAmount, 1, boundedSupplyAmount] from the pool + function _withdrawAndBurn( + uint256 supplyAmount, + uint256 withdrawAmount, + address caller + ) internal { + // setup + uint256 amount = _mintAndSupply(supplyAmount, owner); + withdrawAmount = bound(withdrawAmount, 1, amount); + uint256 totalATokenSupplyBefore = ghoAToken.totalSupply(); + (, uint256 levelBefore) = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING) + .getFacilitatorBucket(proposal.GHO_DIRECT_MINTER()); + + // burn + vm.prank(caller); + minter.withdrawAndBurn(withdrawAmount); + + // check + (, uint256 levelAfter) = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING) + .getFacilitatorBucket(proposal.GHO_DIRECT_MINTER()); + // aToken total supply should be decreased by the burned amount + assertEq( + ghoAToken.totalSupply(), + totalATokenSupplyBefore - withdrawAmount + ); + // the minter supply should shrink by the same amount + assertEq( + IERC20(ghoAToken).balanceOf(address(minter)), + amount - withdrawAmount + ); + // the minter level should shrink by the same amount + assertEq(levelAfter, levelBefore - withdrawAmount); + } +}