From c7b299f8d8d6b8053ce73d8acef336459e38f17d Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 3 Dec 2024 08:37:13 +0100 Subject: [PATCH] feat: discussion feedback (#1) fix: move date --- .env.example | 1 + LICENSE | 117 ++++++++++++++++++ README.md | 52 +++++++- foundry.toml | 1 - ...HODirectMinter.sol => GhoDirectMinter.sol} | 37 +++--- src/RiskCouncilControlled.sol | 31 ----- ...ODirectMinter.sol => IGhoDirectMinter.sol} | 16 ++- src/proposals/LidoGHOListing.sol | 18 ++- ...inter.t.sol => Lido_GhoDirectMinter.t.sol} | 25 +++- 9 files changed, 234 insertions(+), 64 deletions(-) create mode 100644 .env.example create mode 100644 LICENSE rename src/{GHODirectMinter.sol => GhoDirectMinter.sol} (55%) delete mode 100644 src/RiskCouncilControlled.sol rename src/interfaces/{IGHODirectMinter.sol => IGhoDirectMinter.sol} (78%) rename test/{Lido_GHODirectMinter.t.sol => Lido_GhoDirectMinter.t.sol} (78%) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1f1ba76 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +RPC_MAINNET= diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fd7d20d --- /dev/null +++ b/LICENSE @@ -0,0 +1,117 @@ +Business Source License 1.1 + +License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. + +--- + +Parameters + +Licensor: Aave DAO, represented by its governance smart contracts + +Licensed Work: GhoDirectMinter +The Licensed Work is (c) 2024 Aave DAO, represented by its governance smart contracts + +Additional Use Grant: You are permitted to use, copy, and modify the Licensed Work, subject to +the following conditions: + +- Your use of the Licensed Work shall not, directly or indirectly, enable, facilitate, + or assist in any way with the migration of users and/or funds from the Aave ecosystem. + The "Aave ecosystem" is defined in the context of this License as the collection of + software protocols and applications approved by the Aave governance, including all + those produced within compensated service provider engagements with the Aave DAO. + The Aave DAO is able to waive this requirement for one or more third-parties, if and + only if explicitly indicating it on a record 'authorizations' on staketoken.aavelicense.eth. +- You are neither an individual nor a direct or indirect participant in any incorporated + organization, DAO, or identifiable group, that has deployed in production any original + or derived software ("fork") of the Aave ecosystem for purposes competitive to Aave, + within the preceding two years. + The Aave DAO is able to waive this requirement for one or more third-parties, if and + only if explicitly indicating it on a record 'authorizations' on staketoken.aavelicense.eth. +- You must ensure that the usage of the Licensed Work does not result in any direct or + indirect harm to the Aave ecosystem or the Aave brand. This encompasses, but is not limited to, + reputational damage, omission of proper credit/attribution, or utilization for any malicious + intent. + +Change Date: The earlier of: +- 2028-12-03 +- The date specified in the 'change-date' record on gho-direct-minter.aavelicense.eth + +Change License: MIT + +--- + +Notice + +The Business Source License (this document, or the “License”) is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. + +--- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. + +Covenants of Licensor + +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. + +3. To specify a Change Date. + +4. Not to modify this License in any other way. diff --git a/README.md b/README.md index 14a94f9..e680489 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,51 @@ -## GHODirectMinter +## GhoDirectMinter -The GHO direct minter is a facilitator that can **mint and supply** and **withdraw and burn** GHO tokens from a configured aave pool. +The GHO direct minter is a generic facilitator that can inject GHO into an aave pool. + +### Summary + +The `GhoDirectMinter` is a smart contract that can be used to mint & burn GHO directly into/from an Aave pool. +In order to mint GHO the `GhoDirectMinter` will need to be registered as a `Facilitator` in the GHO contract. + +This repository contains two contracts: + +- [`GhoDirectMinter`](./src/GhoDirectMinter.sol) which contains the actual Facilitator +- [`LidoGHOListing`](./src/proposals/LidoGHOListing.sol) which is a reference implementation of a proposal to 1) list GHO on Aave Lido instance and 2) deploy and active a `GhoDirectMinter` facilitator. + +### Specification + +**Prerequisites:** + +- the pool targeted by the `GhoDirectMinter` must have GHO listed as a reserve. +- the GHO AToken and VariableDebtToken implementations must not deviate from the Aave standard implementation. +- the `GhoDirectMinter` must be registered as a `Facilitator` with a non zero bucket capacity. +- the `GhoDirectMinter` must obtain the `RISK_ADMIN_ROLE` in order to supply GHO to the pool. + +The `GhoDirectMinter` offers the following functions: + +- `mintAndSupply` which allows a permissioned entity to mint GHO and supply it to the pool. +- `withdrawAndBurn` which allows a permissioned entity to withdraw GHO from the pool and burn it. +- `transferExcessToTreasury` which allows the permissionless transfer of the accrued fee to the collector. + +While default permissioned entity is the owner(likely the governance short executor), but the contract inherits from [UpgradeableOwnableWithGuardian](https://github.com/bgd-labs/solidity-utils/blob/main/src/contracts/access-control/UpgradeableOwnableWithGuardian.sol) which allows to share permissions with another party (e.g. the GHO stewards). + +### Risk considerations + +The `GhoDirectMinter` can only inject and remove available GHO from the pool. +The actual maximum exposure of the reserve is managed via the `BucketSize` and the chosen `borrow cap`. + +## Development + +This project uses [Foundry](https://getfoundry.sh). See the [book](https://book.getfoundry.sh/getting-started/installation.html) for detailed instructions on how to install and use Foundry. + +## Setup + +```sh +forge install +``` + +## Test + +```sh +forge test +``` diff --git a/foundry.toml b/foundry.toml index 1c3f28f..3f6e2f1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -32,6 +32,5 @@ zksync = "${RPC_ZKSYNC}" [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY_MAINNET}", chain = 1 } - [fmt] tab_width = 2 diff --git a/src/GHODirectMinter.sol b/src/GhoDirectMinter.sol similarity index 55% rename from src/GHODirectMinter.sol rename to src/GhoDirectMinter.sol index a1e3340..7e9ea7f 100644 --- a/src/GHODirectMinter.sol +++ b/src/GhoDirectMinter.sol @@ -2,38 +2,41 @@ pragma solidity ^0.8.0; import {IPool, DataTypes} from "aave-v3-origin/contracts/interfaces/IPool.sol"; -import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {IPoolAddressesProvider} from "aave-v3-origin/contracts/interfaces/IPoolAddressesProvider.sol"; +import {IPoolConfigurator} from "aave-v3-origin/contracts/interfaces/IPoolConfigurator.sol"; +import {ReserveConfiguration} from "aave-v3-origin/contracts/protocol/libraries/configuration/ReserveConfiguration.sol"; import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import {IERC20} from "solidity-utils/contracts/oz-common/interfaces/IERC20.sol"; import {SafeERC20} from "solidity-utils/contracts/oz-common/SafeERC20.sol"; +import {UpgradeableOwnableWithGuardian} from + "solidity-utils/contracts/access-control/UpgradeableOwnableWithGuardian.sol"; import {IGhoToken} from "./interfaces/IGhoToken.sol"; -import {IGHODirectMinter} from "./interfaces/IGHODirectMinter.sol"; -import {RiskCouncilControlled} from "./RiskCouncilControlled.sol"; +import {IGhoDirectMinter} from "./interfaces/IGhoDirectMinter.sol"; /** - * @title GHODirectMinter + * @title GhoDirectMinter * @notice The GHODirectMinter is a GHO facilitator, that can inject(mint) and remove(burn) GHO from an AAVE pool that has GHO listed as a non-custom AToken. * @author BGD Labs @bgdlabs */ -contract GHODirectMinter is Initializable, OwnableUpgradeable, IGHODirectMinter, RiskCouncilControlled { +contract GhoDirectMinter is Initializable, UpgradeableOwnableWithGuardian, IGhoDirectMinter { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using SafeERC20 for IERC20; // @inheritdoc IGHODirectMinter IPool public immutable POOL; // @inheritdoc IGHODirectMinter + IPoolConfigurator public immutable POOL_CONFIGURATOR; + // @inheritdoc IGHODirectMinter address public immutable COLLECTOR; // @inheritdoc IGHODirectMinter address public immutable GHO; // @inheritdoc IGHODirectMinter address public immutable GHO_A_TOKEN; - modifier onlyRiskCouncilOrOwner() { - require(RISK_COUNCIL == msg.sender || owner() == msg.sender, InvalidCaller()); - _; - } - - constructor(IPool pool, address collector, address gho, address council) RiskCouncilControlled(council) { + constructor(IPoolAddressesProvider poolAddressesProvider, address collector, address gho) { + IPool pool = IPool(poolAddressesProvider.getPool()); POOL = pool; + POOL_CONFIGURATOR = IPoolConfigurator(poolAddressesProvider.getPoolConfigurator()); COLLECTOR = collector; GHO = gho; DataTypes.ReserveDataLegacy memory reserveData = pool.getReserveData(gho); @@ -42,19 +45,25 @@ contract GHODirectMinter is Initializable, OwnableUpgradeable, IGHODirectMinter, _disableInitializers(); } - function initialize(address owner) external virtual initializer { + function initialize(address owner, address council) external virtual initializer { __Ownable_init(owner); + __Ownable_With_Guardian_init(council); } // @inheritdoc IGHODirectMinter - function mintAndSupply(uint256 amount) external onlyRiskCouncilOrOwner { + function mintAndSupply(uint256 amount) external onlyOwnerOrGuardian { IGhoToken(GHO).mint(address(this), amount); IERC20(GHO).forceApprove(address(POOL), amount); + DataTypes.ReserveConfigurationMap memory configuration = POOL.getConfiguration(GHO); + // setting supplycap to zero to disable it + POOL_CONFIGURATOR.setSupplyCap(GHO, 0); POOL.supply(GHO, amount, address(this), 0); + // setting supplycap back the original value + POOL_CONFIGURATOR.setSupplyCap(GHO, configuration.getSupplyCap()); } // @inheritdoc IGHODirectMinter - function withdrawAndBurn(uint256 amount) external onlyRiskCouncilOrOwner { + function withdrawAndBurn(uint256 amount) external onlyOwnerOrGuardian { uint256 amountWithdrawn = POOL.withdraw(GHO, amount, address(this)); IGhoToken(GHO).burn(amountWithdrawn); } diff --git a/src/RiskCouncilControlled.sol b/src/RiskCouncilControlled.sol deleted file mode 100644 index 86d0f80..0000000 --- a/src/RiskCouncilControlled.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.10; - -/** - * @title RiskCouncilControlled - * @author Aave Labs - * @notice Helper contract for controlling access to Steward and other functions restricted to Risk Council - */ -abstract contract RiskCouncilControlled { - error InvalidZeroAddress(); - error InvalidCaller(); - - address public immutable RISK_COUNCIL; - - /** - * @dev Constructor - * @param riskCouncil The address of the risk council - */ - constructor(address riskCouncil) { - require(riskCouncil != address(0), InvalidZeroAddress()); - RISK_COUNCIL = riskCouncil; - } - - /** - * @dev Only Risk Council can call functions marked by this modifier. - */ - modifier onlyRiskCouncil() { - require(RISK_COUNCIL == msg.sender, InvalidCaller()); - _; - } -} diff --git a/src/interfaces/IGHODirectMinter.sol b/src/interfaces/IGhoDirectMinter.sol similarity index 78% rename from src/interfaces/IGHODirectMinter.sol rename to src/interfaces/IGhoDirectMinter.sol index 9bf4604..9efaec3 100644 --- a/src/interfaces/IGHODirectMinter.sol +++ b/src/interfaces/IGhoDirectMinter.sol @@ -2,8 +2,9 @@ pragma solidity ^0.8.0; import {IPool} from "aave-v3-origin/contracts/interfaces/IPool.sol"; +import {IPoolConfigurator} from "aave-v3-origin/contracts/interfaces/IPoolConfigurator.sol"; -interface IGHODirectMinter { +interface IGhoDirectMinter { error InvalidAToken(); /** @@ -11,6 +12,11 @@ interface IGHODirectMinter { */ function POOL() external view returns (IPool); + /** + * @return Returns the pool address managed by the facilitator + */ + function POOL_CONFIGURATOR() external view returns (IPoolConfigurator); + /** * @return Returns the collector address that receives the GHO interest */ @@ -31,7 +37,9 @@ interface IGHODirectMinter { * @param amount Amount of GHO to mint and supply to the pool * @notice Due to aave rounding based on the index there might be a small rounding error, which can result in: * - receiving slightly more aTokens - * This error is neglectable and should not have any impact on the system + * This error is neglectable and should not have any impact on the system. + * This method avoid supply cap limitations on the pool, by: + * memoizing the current supply cap -> setting it to zero(disable) -> supplying -> setting it back to the memoized value. */ function mintAndSupply(uint256 amount) external; @@ -40,7 +48,7 @@ interface IGHODirectMinter { * @param amount Amount of GHO to withdraw and burn from the pool * @notice Due to aave rounding based on the index there might be a small rounding error, which can result in: * - withdrawing slightly less and thus burning slightly less - * This error is neglectable and should not have any impact on the system + * This error is neglectable and should not have any impact on the system. */ function withdrawAndBurn(uint256 amount) external; @@ -49,7 +57,7 @@ interface IGHODirectMinter { * @notice Due to aave rounding based on the index there might be a small rounding error, which can result in: * - transfering slightly more * - transfering slightly less - * This error is neglectable and should not have any impact on the system + * This error is neglectable and should not have any impact on the system. */ function transferExcessToTreasury() external; } diff --git a/src/proposals/LidoGHOListing.sol b/src/proposals/LidoGHOListing.sol index 28af3e0..dadbb24 100644 --- a/src/proposals/LidoGHOListing.sol +++ b/src/proposals/LidoGHOListing.sol @@ -13,10 +13,11 @@ import { ITransparentProxyFactory, ProxyAdmin } from "solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol"; +import {IAccessControl} from "openzeppelin-contracts/contracts/access/IAccessControl.sol"; import {IGhoToken} from "../interfaces/IGhoToken.sol"; import {IGhoBucketSteward} from "../interfaces/IGhoBucketSteward.sol"; -import {GHODirectMinter} from "../GHODirectMinter.sol"; +import {GhoDirectMinter} from "../GhoDirectMinter.sol"; /** * @title GHO listing on Lido pool @@ -36,17 +37,22 @@ contract LidoGHOListing is AaveV3PayloadEthereumLido { function _postExecute() internal override { address vaultImpl = address( - new GHODirectMinter( - AaveV3EthereumLido.POOL, address(AaveV3EthereumLido.COLLECTOR), AaveV3EthereumAssets.GHO_UNDERLYING, COUNCIL + new GhoDirectMinter( + AaveV3EthereumLido.POOL_ADDRESSES_PROVIDER, + address(AaveV3EthereumLido.COLLECTOR), + AaveV3EthereumAssets.GHO_UNDERLYING ) ); address vault = ITransparentProxyFactory(MiscEthereum.TRANSPARENT_PROXY_FACTORY).create( vaultImpl, ProxyAdmin(MiscEthereum.PROXY_ADMIN), - abi.encodeWithSelector(GHODirectMinter.initialize.selector, address(this)) + abi.encodeWithSelector(GhoDirectMinter.initialize.selector, address(this), COUNCIL) ); - IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING).addFacilitator(vault, "LidoGHODirectMinter", GHO_MINT_AMOUNT); - GHODirectMinter(vault).mintAndSupply(GHO_MINT_AMOUNT); + IAccessControl(address(AaveV3EthereumLido.ACL_MANAGER)).grantRole( + AaveV3EthereumLido.ACL_MANAGER.RISK_ADMIN_ROLE(), address(vault) + ); + IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING).addFacilitator(vault, "LidoGhoDirectMinter", GHO_MINT_AMOUNT); + GhoDirectMinter(vault).mintAndSupply(GHO_MINT_AMOUNT); // allow risk council to control the bucket capacity address[] memory vaults = new address[](1); diff --git a/test/Lido_GHODirectMinter.t.sol b/test/Lido_GhoDirectMinter.t.sol similarity index 78% rename from test/Lido_GHODirectMinter.t.sol rename to test/Lido_GhoDirectMinter.t.sol index b1739e2..872dc1c 100644 --- a/test/Lido_GHODirectMinter.t.sol +++ b/test/Lido_GhoDirectMinter.t.sol @@ -11,15 +11,19 @@ import { ITransparentProxyFactory, ProxyAdmin } from "solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol"; +import {UpgradeableOwnableWithGuardian} 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 {GHODirectMinter} from "../src/GHODirectMinter.sol"; -import {RiskCouncilControlled} from "../src/RiskCouncilControlled.sol"; +import {ReserveConfiguration} from "aave-v3-origin/contracts/protocol/libraries/configuration/ReserveConfiguration.sol"; +import {GhoDirectMinter} from "../src/GhoDirectMinter.sol"; import {LidoGHOListing} from "../src/proposals/LidoGHOListing.sol"; import {IGhoToken} from "../src/interfaces/IGhoToken.sol"; contract Lido_GHODirectMinter_Test is Test { - GHODirectMinter internal minter; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + + GhoDirectMinter internal minter; IERC20 internal ghoAToken; LidoGHOListing internal proposal; @@ -34,7 +38,7 @@ contract Lido_GHODirectMinter_Test is Test { GovV3Helpers.executePayload(vm, address(proposal)); address[] memory facilitators = IGhoToken(AaveV3EthereumAssets.GHO_UNDERLYING).getFacilitatorsList(); - minter = GHODirectMinter(facilitators[facilitators.length - 1]); + minter = GhoDirectMinter(facilitators[facilitators.length - 1]); ghoAToken = IERC20(minter.GHO_A_TOKEN()); // burn all supply to start with a clean state on the tests @@ -54,7 +58,9 @@ contract Lido_GHODirectMinter_Test is Test { } function test_mintAndSupply_rando() external { - vm.expectRevert(RiskCouncilControlled.InvalidCaller.selector); + vm.expectRevert( + abi.encodeWithSelector(UpgradeableOwnableWithGuardian.OnlyGuardianOrOwnerInvalidCaller.selector, address(this)) + ); minter.mintAndSupply(100); } @@ -67,7 +73,9 @@ contract Lido_GHODirectMinter_Test is Test { } function test_withdrawAndBurn_rando() external { - vm.expectRevert(RiskCouncilControlled.InvalidCaller.selector); + vm.expectRevert( + abi.encodeWithSelector(UpgradeableOwnableWithGuardian.OnlyGuardianOrOwnerInvalidCaller.selector, address(this)) + ); minter.withdrawAndBurn(100); } @@ -91,10 +99,15 @@ contract Lido_GHODirectMinter_Test is Test { function _mintAndSupply(uint256 amount, address caller) internal returns (uint256) { amount = bound(amount, 1, proposal.GHO_MINT_AMOUNT()); + DataTypes.ReserveConfigurationMap memory configurationBefore = + AaveV3EthereumLido.POOL.getConfiguration(AaveV3EthereumAssets.GHO_UNDERLYING); vm.prank(caller); minter.mintAndSupply(amount); + DataTypes.ReserveConfigurationMap memory configurationAfter = + AaveV3EthereumLido.POOL.getConfiguration(AaveV3EthereumAssets.GHO_UNDERLYING); assertEq(IERC20(ghoAToken).balanceOf(address(minter)), amount); assertEq(ghoAToken.totalSupply(), amount); + assertEq(configurationBefore.getSupplyCap(), configurationAfter.getSupplyCap()); return amount; }