Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
path = lib/aave-helpers
url = https://github.com/aave-dao/aave-helpers
branch = main
[submodule "lib/aave-v4"]
path = lib/aave-v4
url = https://github.com/aave/aave-v4
branch = v0.5.9
45 changes: 38 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@

![header](./header.png)

The GHO direct minter is a generic facilitator that can inject GHO into an 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:
This repository contains the following 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.
- [`GhoDirectMinter`](./src/GhoDirectMinter.sol) — Facilitator for Aave v3 pools.
- [`GhoDirectMinterV4`](./src/GhoDirectMinterV4.sol) — Facilitator for the Aave v4 Hub.

### Specification
---

### GhoDirectMinter (v3)

**Prerequisites:**

Expand All @@ -29,13 +31,42 @@ The `GhoDirectMinter` offers the following functions:
- `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).
While default permissioned entity is the owner (likely the governance short executor), 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
**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`.

---

### GhoDirectMinterV4

The `GhoDirectMinterV4` is the Aave v4 equivalent. Instead of interacting with an Aave v3 Pool, it injects GHO as liquidity into an Aave v4 Hub.

**Prerequisites:**

- GHO must be registered as an asset on the Hub.
- the `GhoDirectMinterV4` must be registered as a **spoke** on the Hub for the GHO asset with `addCap` set to `MAX_ALLOWED_SPOKE_CAP` and `drawCap` set to `0`.
- the `GhoDirectMinterV4` must be registered as a GHO `Facilitator` with a non-zero bucket capacity.
- the Hub's `AccessManager` must grant `HUB_ADMIN_ROLE` to the entity that will call `addSpoke` (e.g. the governance executor).

The `GhoDirectMinterV4` offers the following functions:

- `mintAndSupply` — mints GHO directly to the Hub and calls `hub.add()` to register the added liquidity.
- `withdrawAndBurn` — calls `hub.remove()` to withdraw GHO and then burns it.
- `transferExcessToTreasury` — computes the excess shares (spoke balance above the facilitator bucket level) and transfers them to the Hub's fee receiver via `hub.payFeeShares()`.

**Caveats:**

- The Hub uses a share-based accounting model. Share-to-asset conversions may introduce small rounding differences (typically ±1 wei). The `transferExcessToTreasury` function underestimates excess shares to ensure the facilitator always remains at or above its bucket level.
- The constructor derives `ASSET_ID` from `hub.getAssetId(gho)`, so GHO must already be registered on the Hub at deployment time.

**Risk considerations:**

The `GhoDirectMinterV4` can only inject and remove available GHO from the Hub.
The actual maximum exposure is managed via the GHO `BucketSize`, the spoke's `addCap`, and Hub-level parameters.

## 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.
Expand Down
6 changes: 6 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
"name": "main",
"rev": "0459d4ef721c6306a194a36a559a8462f9b19633"
}
},
"lib/aave-v4": {
"tag": {
"name": "v0.5.9",
"rev": "c3b92877ec49c791e7eaaf77afe73eabd9bb7b1e"
}
}
}
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ gnosis = "${RPC_GNOSIS}"
zkEVM = "${RPC_ZKEVM}"
celo = "${RPC_CELO}"
zksync = "${RPC_ZKSYNC}"
devnet = "https://virtual.mainnet-aave.us-east.rpc.tenderly.co/38393fd3-0a79-4e60-b8cc-c6bb5903454a"
Comment thread
DhairyaSethi marked this conversation as resolved.

[fmt]
tab_width = 2
1 change: 1 addition & 0 deletions lib/aave-v4
Submodule aave-v4 added at c3b928
2 changes: 2 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ forge-std/=lib/aave-helpers/lib/forge-std/src/
openzeppelin-contracts-upgradeable/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/
openzeppelin-contracts/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/
solidity-utils/=lib/aave-helpers/lib/aave-address-book/lib/aave-v3-origin/lib/solidity-utils/src/
aave-v4/=lib/aave-v4/src/
lib/aave-v4/:src=lib/aave-v4/src/
20 changes: 18 additions & 2 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ 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 {GhoDirectMinter} from "src/GhoDirectMinter.sol";
import {GhoDirectMinterV4} from "src/GhoDirectMinterV4.sol";
import {IGhoToken} from "src/interfaces/IGhoToken.sol";

import {AaveV3Ethereum, AaveV3EthereumAssets} from "aave-address-book/AaveV3Ethereum.sol";
import {AaveV3EthereumLido} from "aave-address-book/AaveV3EthereumLido.sol";
Expand All @@ -32,6 +33,21 @@ library DeploymentLibrary {
);
}

function _deployV4Facilitator(
ITransparentProxyFactory proxyFactory,
address upgradeAdmin,
address hub,
address gho,
address council
) internal returns (address) {
address impl = address(new GhoDirectMinterV4(hub, gho));
return proxyFactory.create(
impl,
upgradeAdmin,
abi.encodeCall(GhoDirectMinterV4.initialize, (address(GovernanceV3Ethereum.EXECUTOR_LVL_1), council))
);
}

function _deployCore() internal returns (address) {
address council = 0x8513e6F37dBc52De87b166980Fa3F50639694B60;

Expand Down
75 changes: 75 additions & 0 deletions src/GhoDirectMinterV4.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import {
UpgradeableOwnableWithGuardian
} from "solidity-utils/contracts/access-control/UpgradeableOwnableWithGuardian.sol";
import {Initializable} from "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import {SafeERC20, IERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IHub} from "aave-v4/hub/interfaces/IHub.sol";
import {IGhoDirectMinterV4} from "src/interfaces/IGhoDirectMinterV4.sol";
import {IGhoToken} from "src/interfaces/IGhoToken.sol";

/// @title GhoDirectMinterV4
/// @author Aave Labs
/// @notice A GHO facilitator that injects (mints) and removes (burns) GHO from an Aave V4 Hub.
/// @dev The GhoDirectMinterV4 is expected to be registered as a spoke on the Hub with infinite addCap.
contract GhoDirectMinterV4 is Initializable, UpgradeableOwnableWithGuardian, IGhoDirectMinterV4 {
IGhoToken internal immutable GHO;
IHub internal immutable HUB;
uint256 internal immutable ASSET_ID;

/// @dev Constructor.
/// @param hub_ The address of the Aave v4 Hub.
/// @param gho_ The address of the GHO token.
constructor(address hub_, address gho_) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we think about having this more flexible so it can mint GHO to multiple hubs if needed?

Is that unnecessary?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while that's possible, I think we'd need to do like AIP to grant access to each, rather than allowing the same owner/guardian to supply into multiple hubs, so would need to add more granular access controls on a per-hub basis perhaps

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no i dont think we should mix facilitator capacities also

_disableInitializers();
HUB = IHub(hub_);
ASSET_ID = HUB.getAssetId(gho_); // reverts on invalid `underlying`
GHO = IGhoToken(gho_);
}

/// @inheritdoc IGhoDirectMinterV4
function initialize(address owner, address council) external virtual initializer {
__Ownable_With_Guardian_init(owner, council);
}

/// @inheritdoc IGhoDirectMinterV4
function mintAndSupply(uint256 amount) external onlyOwnerOrGuardian {
GHO.mint(address(HUB), amount);
HUB.add(ASSET_ID, amount); // this spoke is given infinite cap
}

/// @inheritdoc IGhoDirectMinterV4
function withdrawAndBurn(uint256 amount) external onlyOwnerOrGuardian {
HUB.remove(ASSET_ID, amount, address(this));
GHO.burn(amount);
}

/// @inheritdoc IGhoDirectMinterV4
function transferExcessToTreasury() external {
(, uint256 level) = GHO.getFacilitatorBucket(address(this));
uint256 balance = HUB.getSpokeAddedAssets(ASSET_ID, address(this));
uint256 excess = balance - level;
// underestimate excess shares to ensure the facilitator remains at or above bucket level
uint256 excessShares = HUB.previewAddByAssets(ASSET_ID, excess);
if (excessShares > 0) {
HUB.payFeeShares(ASSET_ID, excessShares);
}
}

/// @inheritdoc IGhoDirectMinterV4
function hub() external view returns (address) {
return address(HUB);
}

/// @inheritdoc IGhoDirectMinterV4
function assetId() external view returns (uint256) {
return ASSET_ID;
}

/// @inheritdoc IGhoDirectMinterV4
function gho() external view returns (address) {
return address(GHO);
}
}
40 changes: 40 additions & 0 deletions src/interfaces/IGhoDirectMinterV4.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

/// @title IGhoDirectMinterV4
/// @author Aave Labs
/// @notice Interface for the GhoDirectMinter, a GHO facilitator that injects (mints) and removes (burns) GHO from an Aave V4 Hub.
/// @dev The GhoDirectMinter is expected to be registered as a spoke on the Hub with infinite addCap.
interface IGhoDirectMinterV4 {
/// @notice Initializes the contract.
/// @param owner The address of the owner.
/// @param council The address of the guardian council.
function initialize(address owner, address council) external;

/// @notice Mints GHO and adds it as liquidity to the Hub.
/// @dev Only callable by the owner or guardian.
/// @param amount The amount of GHO to mint and supply.
function mintAndSupply(uint256 amount) external;

/// @notice Removes GHO liquidity from the Hub and burns it.
/// @dev Only callable by the owner or guardian.
/// @param amount The amount of GHO to withdraw and burn.
function withdrawAndBurn(uint256 amount) external;

/// @notice Transfers excess GHO interest (added shares above facilitator bucket level) to the fee receiver.
/// @dev Callable by anyone.
/// @dev Due to rounding in the share conversion, the amount transferred may be slightly less than the true excess.
function transferExcessToTreasury() external;

/// @notice Returns the address of the Aave v4 Hub.
/// @return The Hub contract address.
function hub() external view returns (address);

/// @notice Returns the asset identifier for GHO in the Hub.
/// @return The asset identifier.
function assetId() external view returns (uint256);

/// @notice Returns the address of the GHO token.
/// @return The GHO token address.
function gho() external view returns (address);
}
Loading