diff --git a/lib/aave-helpers b/lib/aave-helpers index 85e73ccd0..fae3da081 160000 --- a/lib/aave-helpers +++ b/lib/aave-helpers @@ -1 +1 @@ -Subproject commit 85e73ccd0ea110f7c71dfe8b4f5e2a55bc9a9353 +Subproject commit fae3da0811f41ecb30da1129976c53d15350b895 diff --git a/src/20240119_Gho_GHOStabilityModule/GHOStabilityModule.md b/src/20240119_Gho_GHOStabilityModule/GHOStabilityModule.md new file mode 100644 index 000000000..00ca1ce4b --- /dev/null +++ b/src/20240119_Gho_GHOStabilityModule/GHOStabilityModule.md @@ -0,0 +1,73 @@ +--- +title: "GHO Stability Module" +author: "Aave Labs @aave" +discussions: "https://governance.aave.com/t/gho-stability-module-update/14442" +--- + +## Simple Summary + +This AIP proposes the deployment of the GHO Stability Module (GSM), a system facilitating the conversion between GHO and two governance-accepted stablecoins, USDC and USDT, at a predetermined ratio. + +The GSM has undergone thorough iterations of design, development, testing, and implementation with Aave Labs driving this process and actively seeking community feedback. Additionally, the security aspect was carefully addressed through collaboration with DAO service providers SigmaPrime and Certora for code reviews. Furthermore, an extra layer of security was added by commissioning an independent review from a security researcher (Emanuele Ricci [@stermi](https://governance.aave.com/u/stermi/summary)). + +Following extensive community discussion and multiple phases of Aave DAO governance, this AIP suggests deploying two GSM contracts for seamless conversions between GHO and USDC as well as GHO and USDT. + +## Motivation + +The GHO Stability Module (GSM) is a contract designed to facilitate conversions between two tokens with its primary purpose being to help further maintain GHO's peg. The module allows swaps between GHO and other governance-accepted stablecoins, offering a variety of functionalities that make it paramount in the fields of security and risk management. + +Summarizing the functionality offered by the GHO Stability Module (GSM), here is a list of these features and their planned implementation for this proposal: + +- **Exposure Cap**: Denominated in token units, it limits exposure to the exogenous asset. +- **Price Strategies**: Utilizing a fixed price strategy with a 1:1 ratio for stablecoins. +- **Fee Strategies**: Employing a flat basis point (bps) approach, differentiated by direction (sell/buy). +- **Last Resort Liquidation**: Aave DAO is the exclusive entity granted with the role of last resort liquidation, empowering it to take control of GSM funds in worst-case scenarios. +- **Swap Freeze**: Aave DAO and a chainlink-automated keeper contract have the authority to freeze the swap functionality. The chainlink-automated keeper contract bases its actions on the price of the exogenous asset, freezing if the price is outside the range and unfreezing if inside the range. +- **Capital Allocation**: Supporting this feature by allowing ERC4626 assets as underlying assets. This enables redirecting the yield generated by the ERC4626 asset, while residing in the GSM contract, to the GHO Treasury. + +## Specification + +The proposed payload entails the comprehensive activation of GSM USDC and GSM USDT, involving the following steps: + +1. Incorporate GSM USDC and GSM USDT as facilitators of the GHO Token on Ethereum. +2. Adjust the Fee Strategy for both GSMs to implement a 0.2% flat fee for both directions (buy/sell). +3. Add both GSMs to the GSM Registry. +4. Designate OracleSwapFreezer contracts and Aave DAO as SwapFreezer entities in each GSM contract, respectively. +5. Activate these OracleSwapFreezer contracts as keepers of the Aave DAO through AaveRobot with a funding of 80 LINK for each. + +The table below outlines the initially proposed risk parameters for each GSM contract, as approved through the snapshot: + +**GSM USDC** +| Parameter | Value | +|------------------------------------------ |----------------- | +| Underlying Price Range for Swap Freeze | [0.99 - 1.01] | +| Underlying Price Range for Swap Unfreeze | [0.995 - 1.005] | +| Buy Fee | 0.2% | +| Sell Fee | 0.2% | +| Exposure Cap | 500,000 USDC | +| Facilitator Bucket Capacity | 500,000 GHO | +| Swap Active | True | + +**GSM USDT** +| Parameter | Value | +|------------------------------------------ |----------------- | +| Underlying Price Range for Swap Freeze | [0.99 - 1.01] | +| Underlying Price Range for Swap Unfreeze | [0.995 - 1.005] | +| Buy Fee | 0.2% | +| Sell Fee | 0.2% | +| Exposure Cap | 500,000 USDT | +| Facilitator Bucket Capacity | 500,000 GHO | +| Swap Active | True | + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240119_Gho_GHOStabilityModule/AaveV3Ethereum_GHOStabilityModule_20240119.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20240119_Gho_GHOStabilityModule/AaveV3Ethereum_GHOStabilityModule_20240119.t.sol) +- [Snapshot](https://snapshot.org/#/aave.eth/proposal/0xe9b62e197a98832da7d1231442b5960588747f184415fba4699b6325d7618842) +- [Discussion](https://governance.aave.com/t/gho-stability-module-update/14442) +- [GSM Repository](https://github.com/aave/gho-core/tree/main/src/contracts/facilitators/gsm) +- [GSM Audit Reports](https://github.com/aave/gho-core/tree/main/audits) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/src/20240119_Gho_GHOStabilityModule/GHOStabilityModule_20240119.s.sol b/src/20240119_Gho_GHOStabilityModule/GHOStabilityModule_20240119.s.sol new file mode 100644 index 000000000..99fb0f632 --- /dev/null +++ b/src/20240119_Gho_GHOStabilityModule/GHOStabilityModule_20240119.s.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/GovV3Helpers.sol'; +import {EthereumScript} from 'aave-helpers/ScriptUtils.sol'; +import {Gho_GHOStabilityModule_20240119} from './Gho_GHOStabilityModule_20240119.sol'; + +/** + * @dev Deploy Ethereum + * deploy-command: make deploy-ledger contract=src/20240119_Gho_GHOStabilityModule/GHOStabilityModule_20240119.s.sol:DeployEthereum chain=mainnet + * verify-command: npx catapulta-verify -b broadcast/GHOStabilityModule_20240119.s.sol/1/run-latest.json + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + address payload0 = GovV3Helpers.deployDeterministic( + type(Gho_GHOStabilityModule_20240119).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/20240119_Gho_GHOStabilityModule/GHOStabilityModule_20240119.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](1); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsEthereum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsEthereum[0] = GovV3Helpers.buildAction( + type(Gho_GHOStabilityModule_20240119).creationCode + ); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal( + vm, + payloads, + GovV3Helpers.ipfsHashFile(vm, 'src/20240119_Gho_GHOStabilityModule/GHOStabilityModule.md') + ); + } +} diff --git a/src/20240119_Gho_GHOStabilityModule/Gho_GHOStabilityModule_20240119.sol b/src/20240119_Gho_GHOStabilityModule/Gho_GHOStabilityModule_20240119.sol new file mode 100644 index 000000000..c68c0e388 --- /dev/null +++ b/src/20240119_Gho_GHOStabilityModule/Gho_GHOStabilityModule_20240119.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IProposalGenericExecutor} from 'aave-helpers/interfaces/IProposalGenericExecutor.sol'; +import {AaveV3Ethereum, AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; + +interface IGhoToken { + function addFacilitator( + address facilitatorAddress, + string calldata facilitatorLabel, + uint128 bucketCapacity + ) external; +} + +interface IGsm { + function updateFeeStrategy(address feeStrategy) external; + + function SWAP_FREEZER_ROLE() external pure returns (bytes32); + + function grantRole(bytes32 role, address account) external; +} + +interface IGsmRegistry { + function addGsm(address gsmAddress) external; +} + +interface IAaveCLRobotOperator { + function register( + string memory name, + address upkeepContract, + uint32 gasLimit, + uint96 amountToFund + ) external returns (uint256); +} + +/** + * @title GHO Stability Module + * @author Aave labs (@aave) + * @dev This proposal enables 2 GHO Stability Modules (USDC, USDT): + * - Addition of USDC and USDT GSMs as GHO Facilitators + * - Give Swap Freezer permissions to OracleSwapFreezers, one per module + * - Give Swap Freezer permissions to the DAO Level 1 Executor + * - Install a 0.2% fee strategy into both modules + * - Register both GSMs in the GsmRegistry + * - Activate OracleSwapFreezer contracts as AaveRobot Keepers + * Relevant governance links: + * 1. GHO Stability Module + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0x98bdd30f645b2981320f82c671ae9fee31ee771766c13cd2627b66a22f0d438e + * - Discussion: https://governance.aave.com/t/temp-check-gho-stability-module/13927 + * 2. GHO Stability Module Update + * - Discussion: https://governance.aave.com/t/gho-stability-module-update/14442 + * 3. GHO Stability Module Launch + * - Snapshot: https://snapshot.org/#/aave.eth/proposal/0xe9b62e197a98832da7d1231442b5960588747f184415fba4699b6325d7618842 + */ +contract Gho_GHOStabilityModule_20240119 is IProposalGenericExecutor { + using SafeERC20 for IERC20; + + address public constant GSM_USDC = 0x0d8eFfC11dF3F229AA1EA0509BC9DFa632A13578; + address public constant GSM_USDC_ORACLE_SWAP_FREEZER = 0xef6beCa8D9543eC007bceA835aF768B58F730C1f; + address public constant GSM_USDT = 0x686F8D21520f4ecEc7ba577be08354F4d1EB8262; + address public constant GSM_USDT_ORACLE_SWAP_FREEZER = 0x71381e6718b37C12155CB961Ca3D374A8BfFa0e5; + address public constant GSM_REGISTRY = 0x167527DB01325408696326e3580cd8e55D99Dc1A; + address public constant GSM_FIXED_FEE_STRATEGY = 0xD4478A76aCeA81D3768A0ACB6e38f25eEB6Eb1B5; + + string public constant GSM_USDC_FACILITATOR_LABEL = 'GSM USDC'; + uint128 public constant GSM_USDC_BUCKET_CAPACITY = 500_000e18; + string public constant GSM_USDT_FACILITATOR_LABEL = 'GSM USDT'; + uint128 public constant GSM_USDT_BUCKET_CAPACITY = 500_000e18; + + address public constant ROBOT_OPERATOR = 0x020E452b463568f55BAc6Dc5aFC8F0B62Ea5f0f3; + uint96 public constant LINK_AMOUNT_ORACLE_FREEZER_KEEPER = 80 ether; + uint96 public constant TOTAL_LINK_AMOUNT_KEEPERS = LINK_AMOUNT_ORACLE_FREEZER_KEEPER * 2; // 2 GSMs + uint32 public constant KEEPER_GAS_LIMIT = 150_000; + + function execute() external { + // 1. Enroll GSMs as GHO Facilitators + IGhoToken(MiscEthereum.GHO_TOKEN).addFacilitator( + GSM_USDC, + GSM_USDC_FACILITATOR_LABEL, + GSM_USDC_BUCKET_CAPACITY + ); + IGhoToken(MiscEthereum.GHO_TOKEN).addFacilitator( + GSM_USDT, + GSM_USDT_FACILITATOR_LABEL, + GSM_USDT_BUCKET_CAPACITY + ); + + // 2. Add GSM Swap Freezer role to OracleSwapFreezers + IGsm(GSM_USDC).grantRole(IGsm(GSM_USDC).SWAP_FREEZER_ROLE(), GSM_USDC_ORACLE_SWAP_FREEZER); + IGsm(GSM_USDT).grantRole(IGsm(GSM_USDT).SWAP_FREEZER_ROLE(), GSM_USDT_ORACLE_SWAP_FREEZER); + IGsm(GSM_USDC).grantRole( + IGsm(GSM_USDC).SWAP_FREEZER_ROLE(), + GovernanceV3Ethereum.EXECUTOR_LVL_1 + ); + IGsm(GSM_USDT).grantRole( + IGsm(GSM_USDT).SWAP_FREEZER_ROLE(), + GovernanceV3Ethereum.EXECUTOR_LVL_1 + ); + + // 3. Update Fee Strategy + IGsm(GSM_USDC).updateFeeStrategy(GSM_FIXED_FEE_STRATEGY); + IGsm(GSM_USDT).updateFeeStrategy(GSM_FIXED_FEE_STRATEGY); + + // 4. Add GSMs to GSM Registry + IGsmRegistry(GSM_REGISTRY).addGsm(GSM_USDC); + IGsmRegistry(GSM_REGISTRY).addGsm(GSM_USDT); + + // 5. Register OracleSwapFreezer as keepers + AaveV3Ethereum.COLLECTOR.transfer( + AaveV3EthereumAssets.LINK_UNDERLYING, + address(this), + TOTAL_LINK_AMOUNT_KEEPERS + ); + IERC20(AaveV3EthereumAssets.LINK_UNDERLYING).forceApprove( + ROBOT_OPERATOR, + TOTAL_LINK_AMOUNT_KEEPERS + ); + + IAaveCLRobotOperator(ROBOT_OPERATOR).register( + 'GHO GSM USDC OracleSwapFreezer', + GSM_USDC_ORACLE_SWAP_FREEZER, + KEEPER_GAS_LIMIT, + LINK_AMOUNT_ORACLE_FREEZER_KEEPER + ); + IAaveCLRobotOperator(ROBOT_OPERATOR).register( + 'GHO GSM USDT OracleSwapFreezer', + GSM_USDT_ORACLE_SWAP_FREEZER, + KEEPER_GAS_LIMIT, + LINK_AMOUNT_ORACLE_FREEZER_KEEPER + ); + } +} diff --git a/src/20240119_Gho_GHOStabilityModule/Gho_GHOStabilityModule_20240119.t.sol b/src/20240119_Gho_GHOStabilityModule/Gho_GHOStabilityModule_20240119.t.sol new file mode 100644 index 000000000..aaaf5c2ba --- /dev/null +++ b/src/20240119_Gho_GHOStabilityModule/Gho_GHOStabilityModule_20240119.t.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {AaveV3EthereumAssets, AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; +import {GovernanceV3Ethereum} from 'aave-address-book/GovernanceV3Ethereum.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {ProtocolV3TestBase} from 'aave-helpers/ProtocolV3TestBase.sol'; +import {IPoolConfigurator} from 'aave-address-book/AaveV3.sol'; +import {Gho_GHOStabilityModule_20240119} from './Gho_GHOStabilityModule_20240119.sol'; + +interface IGhoToken { + struct Facilitator { + uint128 bucketCapacity; + uint128 bucketLevel; + string label; + } + + function getFacilitator(address facilitator) external view returns (Facilitator memory); + + function getFacilitatorsList() external view returns (address[] memory); +} + +interface IGsm { + function hasRole(bytes32 role, address account) external view returns (bool); + + function getFeeStrategy() external view returns (address); + + function getAvailableUnderlyingExposure() external view returns (uint256); + + function getIsFrozen() external view returns (bool); + + function getIsSeized() external view returns (bool); + + function UNDERLYING_ASSET() external view returns (address); + + function CONFIGURATOR_ROLE() external pure returns (bytes32); + + function TOKEN_RESCUER_ROLE() external pure returns (bytes32); + + function SWAP_FREEZER_ROLE() external view returns (bytes32); + + function LIQUIDATOR_ROLE() external pure returns (bytes32); +} + +interface IFeeStrategy { + function getBuyFee(uint256 grossAmount) external view returns (uint256); + + function getSellFee(uint256 grossAmount) external view returns (uint256); +} + +interface IOracleSwapFreezer { + function getCanUnfreeze() external view returns (bool); + + function getFreezeBound() external view returns (uint128, uint128); + + function getUnfreezeBound() external view returns (uint128, uint128); + + function checkUpkeep(bytes calldata) external view returns (bool, bytes memory); + + function performUpkeep(bytes calldata) external; +} + +interface IPriceOracle { + function getAssetPrice(address asset) external view returns (uint256); +} + +/** + * @dev Test for Gho_GHOStabilityModule_20240119 + * command: make test-contract filter=Gho_GHOStabilityModule_20240119 + */ +contract Gho_GHOStabilityModule_20240119_Test is ProtocolV3TestBase { + Gho_GHOStabilityModule_20240119 internal proposal; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 19075310); + proposal = new Gho_GHOStabilityModule_20240119(); + } + + function test_defaultProposalExecution() public { + defaultTest('Gho_GHOStabilityModule_20240119', AaveV3Ethereum.POOL, address(proposal)); + } + + function test_checkConfig() public { + uint256 facilitatorListLengthBefore = IGhoToken(MiscEthereum.GHO_TOKEN) + .getFacilitatorsList() + .length; + + executePayload(vm, address(proposal)); + + assertTrue( + IGhoToken(MiscEthereum.GHO_TOKEN).getFacilitatorsList().length == + facilitatorListLengthBefore + 2 + ); + IGhoToken.Facilitator memory gsmUsdc = IGhoToken(MiscEthereum.GHO_TOKEN).getFacilitator( + proposal.GSM_USDC() + ); + assertEq(gsmUsdc.label, proposal.GSM_USDC_FACILITATOR_LABEL()); + assertEq(gsmUsdc.bucketCapacity, proposal.GSM_USDC_BUCKET_CAPACITY()); + assertEq(gsmUsdc.bucketLevel, 0); + + IGhoToken.Facilitator memory gsmUsdt = IGhoToken(MiscEthereum.GHO_TOKEN).getFacilitator( + proposal.GSM_USDT() + ); + assertEq(gsmUsdt.label, proposal.GSM_USDT_FACILITATOR_LABEL()); + assertEq(gsmUsdt.bucketCapacity, proposal.GSM_USDT_BUCKET_CAPACITY()); + assertEq(gsmUsdt.bucketLevel, 0); + + // GSM USDC + GsmConfig memory gsmUsdcConfig = GsmConfig({ + sellFee: 0.0020e4, // 0.2% + buyFee: 0.0020e4, // 0.2% + exposureCap: 500_000e6, + isFrozen: false, + isSeized: false, + freezerCanUnfreeze: true, + freezeLowerBound: 0.99e8, + freezeUpperBound: 1.01e8, + unfreezeLowerBound: 0.995e8, + unfreezeUpperBound: 1.005e8 + }); + _checkGsmConfig( + IGsm(proposal.GSM_USDC()), + AaveV3EthereumAssets.USDC_UNDERLYING, + IOracleSwapFreezer(proposal.GSM_USDC_ORACLE_SWAP_FREEZER()), + gsmUsdcConfig + ); + + // GSM USDT + GsmConfig memory gsmUsdtConfig = GsmConfig({ + sellFee: 0.0020e4, // 0.2% + buyFee: 0.0020e4, // 0.2% + exposureCap: 500_000e6, + isFrozen: false, + isSeized: false, + freezerCanUnfreeze: true, + freezeLowerBound: 0.99e8, + freezeUpperBound: 1.01e8, + unfreezeLowerBound: 0.995e8, + unfreezeUpperBound: 1.005e8 + }); + _checkGsmConfig( + IGsm(proposal.GSM_USDT()), + AaveV3EthereumAssets.USDT_UNDERLYING, + IOracleSwapFreezer(proposal.GSM_USDT_ORACLE_SWAP_FREEZER()), + gsmUsdtConfig + ); + } + + function test_oracleSwapFreezers() public { + // OracleSwapFreezers are not authorized + assertEq( + IGsm(proposal.GSM_USDC()).hasRole( + IGsm(proposal.GSM_USDC()).SWAP_FREEZER_ROLE(), + proposal.GSM_USDC_ORACLE_SWAP_FREEZER() + ), + false + ); + assertEq( + IGsm(proposal.GSM_USDT()).hasRole( + IGsm(proposal.GSM_USDT()).SWAP_FREEZER_ROLE(), + proposal.GSM_USDT_ORACLE_SWAP_FREEZER() + ), + false + ); + + IOracleSwapFreezer usdcFreezer = IOracleSwapFreezer(proposal.GSM_USDC_ORACLE_SWAP_FREEZER()); + IOracleSwapFreezer usdtFreezer = IOracleSwapFreezer(proposal.GSM_USDT_ORACLE_SWAP_FREEZER()); + (uint128 usdcFreezeLowerBound, ) = usdcFreezer.getFreezeBound(); + (uint128 usdcUnfreezeLowerBound, ) = usdcFreezer.getUnfreezeBound(); + (uint128 usdtFreezeLowerBound, ) = usdtFreezer.getFreezeBound(); + (uint128 usdtUnfreezeLowerBound, ) = usdtFreezer.getUnfreezeBound(); + + // Price outside the price range + // Freezers cannot execute freeze without authorization + _mockAssetPrice( + address(AaveV3Ethereum.ORACLE), + AaveV3EthereumAssets.USDC_UNDERLYING, + usdcFreezeLowerBound - 1 + ); + _mockAssetPrice( + address(AaveV3Ethereum.ORACLE), + AaveV3EthereumAssets.USDT_UNDERLYING, + usdtFreezeLowerBound - 1 + ); + + (bool canPerformUpkeep, ) = usdcFreezer.checkUpkeep(bytes('')); + assertEq(canPerformUpkeep, false); + usdcFreezer.performUpkeep(bytes('')); + assertEq(IGsm(proposal.GSM_USDC()).getIsFrozen(), false); + + (canPerformUpkeep, ) = usdtFreezer.checkUpkeep(bytes('')); + assertEq(canPerformUpkeep, false); + usdtFreezer.performUpkeep(bytes('')); + assertEq(IGsm(proposal.GSM_USDT()).getIsFrozen(), false); + + // Payload execution + executePayload(vm, address(proposal)); + + // Freezers are authorized now + assertEq( + IGsm(proposal.GSM_USDC()).hasRole( + IGsm(proposal.GSM_USDC()).SWAP_FREEZER_ROLE(), + proposal.GSM_USDC_ORACLE_SWAP_FREEZER() + ), + true + ); + assertEq( + IGsm(proposal.GSM_USDT()).hasRole( + IGsm(proposal.GSM_USDT()).SWAP_FREEZER_ROLE(), + proposal.GSM_USDT_ORACLE_SWAP_FREEZER() + ), + true + ); + + // Freezers freeze GSM contracts + (canPerformUpkeep, ) = usdcFreezer.checkUpkeep(bytes('')); + assertEq(canPerformUpkeep, true); + usdcFreezer.performUpkeep(bytes('')); + assertEq(IGsm(proposal.GSM_USDC()).getIsFrozen(), true); + + (canPerformUpkeep, ) = usdtFreezer.checkUpkeep(bytes('')); + assertEq(canPerformUpkeep, true); + usdtFreezer.performUpkeep(bytes('')); + assertEq(IGsm(proposal.GSM_USDT()).getIsFrozen(), true); + + // Price back to normal + _mockAssetPrice( + address(AaveV3Ethereum.ORACLE), + AaveV3EthereumAssets.USDC_UNDERLYING, + usdcUnfreezeLowerBound + 1 + ); + _mockAssetPrice( + address(AaveV3Ethereum.ORACLE), + AaveV3EthereumAssets.USDT_UNDERLYING, + usdtUnfreezeLowerBound + 1 + ); + + (canPerformUpkeep, ) = usdcFreezer.checkUpkeep(bytes('')); + assertEq(canPerformUpkeep, true); + usdcFreezer.performUpkeep(bytes('')); + assertEq(IGsm(proposal.GSM_USDC()).getIsFrozen(), false); + + (canPerformUpkeep, ) = usdtFreezer.checkUpkeep(bytes('')); + assertEq(canPerformUpkeep, true); + usdtFreezer.performUpkeep(bytes('')); + assertEq(IGsm(proposal.GSM_USDT()).getIsFrozen(), false); + } + + function test_checkRoles() public { + executePayload(vm, address(proposal)); + + _checkRolesConfig(IGsm(proposal.GSM_USDC())); + _checkRolesConfig(IGsm(proposal.GSM_USDT())); + } + + function _checkRolesConfig(IGsm gsm) internal { + // DAO permissions + assertTrue( + gsm.hasRole(bytes32(0), GovernanceV3Ethereum.EXECUTOR_LVL_1), + 'Executor is not admin' + ); + assertTrue( + gsm.hasRole(gsm.SWAP_FREEZER_ROLE(), GovernanceV3Ethereum.EXECUTOR_LVL_1), + 'Executor is not swap freezer' + ); + assertTrue( + gsm.hasRole(gsm.CONFIGURATOR_ROLE(), GovernanceV3Ethereum.EXECUTOR_LVL_1), + 'Executor is not configurator' + ); + // No need to be liquidator or token rescuer at the beginning + assertFalse(gsm.hasRole(gsm.LIQUIDATOR_ROLE(), GovernanceV3Ethereum.EXECUTOR_LVL_1)); + assertFalse(gsm.hasRole(gsm.TOKEN_RESCUER_ROLE(), GovernanceV3Ethereum.EXECUTOR_LVL_1)); + + // Deployer does not have permissions + address deployer = 0x99C7A4A4Ab99882C422eF777b182eBda204D5B02; + assertFalse(gsm.hasRole(bytes32(0), deployer), 'Deployer cannot be admin'); + assertFalse(gsm.hasRole(gsm.SWAP_FREEZER_ROLE(), deployer), 'Deployer cannot be swap freezer'); + assertFalse(gsm.hasRole(gsm.CONFIGURATOR_ROLE(), deployer), 'Deployer cannot be configurator'); + assertFalse(gsm.hasRole(gsm.LIQUIDATOR_ROLE(), deployer), 'Deployer cannot be liquidator'); + assertFalse( + gsm.hasRole(gsm.TOKEN_RESCUER_ROLE(), deployer), + 'Deployer cannot be token rescuer' + ); + } + + function _mockAssetPrice(address priceOracle, address asset, uint256 price) internal { + vm.mockCall( + priceOracle, + abi.encodeWithSelector(IPriceOracle.getAssetPrice.selector, asset), + abi.encode(price) + ); + } + + struct GsmConfig { + uint256 sellFee; + uint256 buyFee; + uint256 exposureCap; + bool isFrozen; + bool isSeized; + bool freezerCanUnfreeze; + uint256 freezeLowerBound; + uint256 freezeUpperBound; + uint256 unfreezeLowerBound; + uint256 unfreezeUpperBound; + } + + function _checkGsmConfig( + IGsm gsm, + address underlying, + IOracleSwapFreezer freezer, + GsmConfig memory config + ) internal { + assertEq(gsm.UNDERLYING_ASSET(), underlying, 'wrong underlying asset'); + assertEq(gsm.getAvailableUnderlyingExposure(), config.exposureCap, 'wrong exposure cap'); + assertEq(gsm.getIsFrozen(), config.isFrozen, 'wrong freeze state'); + assertEq(gsm.getIsSeized(), config.isSeized, 'wrong seized state'); + + IFeeStrategy feeStrategy = IFeeStrategy(gsm.getFeeStrategy()); + assertEq(feeStrategy.getSellFee(10000), config.sellFee, 'wrong sell fee'); + assertEq(feeStrategy.getBuyFee(10000), config.buyFee, 'wrong buy fee'); + + // Oracle freezer + assertEq(freezer.getCanUnfreeze(), config.freezerCanUnfreeze, 'wrong freezer config'); + (uint256 lowerBound, uint256 upperBound) = freezer.getFreezeBound(); + assertEq(lowerBound, config.freezeLowerBound, 'wrong freeze lower bound'); + assertEq(upperBound, config.freezeUpperBound, 'wrong freeze upper bound'); + (lowerBound, upperBound) = freezer.getUnfreezeBound(); + assertEq(lowerBound, config.unfreezeLowerBound, 'wrong unfreeze lower bound'); + assertEq(upperBound, config.unfreezeUpperBound, 'wrong unfreeze upper bound'); + } +} diff --git a/src/20240119_Gho_GHOStabilityModule/config.ts b/src/20240119_Gho_GHOStabilityModule/config.ts new file mode 100644 index 000000000..6144c9e3a --- /dev/null +++ b/src/20240119_Gho_GHOStabilityModule/config.ts @@ -0,0 +1,14 @@ +import {ConfigFile} from '../../generator/types'; +export const config: ConfigFile = { + rootOptions: { + pools: ['AaveV3Ethereum'], + title: 'GHO Stability Module', + shortName: 'GHOStabilityModule', + date: '20240119', + author: 'Aave Labs @aave', + discussion: 'https://governance.aave.com/t/gho-stability-module-update/14442', + snapshot: + 'https://snapshot.org/#/aave.eth/proposal/0xe9b62e197a98832da7d1231442b5960588747f184415fba4699b6325d7618842', + }, + poolOptions: {AaveV3Ethereum: {configs: {OTHERS: {}}, cache: {blockNumber: 19037596}}}, +};