Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions script/DeployHorizon.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
106 changes: 106 additions & 0 deletions src/proposals/HorizonGHOListing.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
221 changes: 221 additions & 0 deletions test/Horizon_GhoDirectMinter.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}