Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 38 additions & 5 deletions src/contracts/facilitators/gsm/GhoReserve.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.10;

import {IERC20} from '@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol';
import {VersionedInitializable} from '@aave/core-v3/contracts/protocol/libraries/aave-upgradeability/VersionedInitializable.sol';
import {IGhoReserve} from './interfaces/IGhoReserve.sol';

Expand All @@ -13,22 +14,23 @@ import {IGhoReserve} from './interfaces/IGhoReserve.sol';
* @dev To be covered by a proxy contract.
*/
contract GhoReserve is Ownable, VersionedInitializable, IGhoReserve {
using EnumerableSet for EnumerableSet.AddressSet;

/// @inheritdoc IGhoReserve
address public immutable GHO_TOKEN;

/// Map of entities and their assigned capacity and amount of GHO used
mapping(address => GhoUsage) private _ghoUsage;

Copy link
Contributor

@miguelmtzinf miguelmtzinf May 16, 2025

Choose a reason for hiding this comment

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

Wondering if we should add global variables for total limit and total used. It's a bit hard to iterate over entities also, to calculate totals

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would total limit be every time we set a limit +/- to the global variable? For used it's easy to keep track

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, would be keeping global accounting every time an entity updates its usage (limit or used). Not sure it's completely needed

/// Set of entities with a GHO limit available
EnumerableSet.AddressSet private entities;

/**
* @dev Constructor
* @param initialOwner The address of the owner
* @param ghoAddress The address of the GHO token on the remote chain
*/
constructor(address initialOwner, address ghoAddress) Ownable() {
require(initialOwner != address(0), 'ZERO_ADDRESS_NOT_VALID');
constructor(address ghoAddress) {
require(ghoAddress != address(0), 'ZERO_ADDRESS_NOT_VALID');

_transferOwnership(initialOwner);
GHO_TOKEN = ghoAddress;
}

Expand Down Expand Up @@ -64,13 +66,34 @@ contract GhoReserve is Ownable, VersionedInitializable, IGhoReserve {
emit GhoTransferred(to, amount);
}

/// @inheritdoc IGhoReserve
function addEntity(address entity) external onlyOwner {
entities.add(entity);
emit EntityAdded(entity);
}

/// @inheritdoc IGhoReserve
function removeEntity(address entity) external onlyOwner {
GhoUsage memory usage = _ghoUsage[entity];
require(usage.used == 0, 'CANNOT_REMOVE_ENTITY_WITH_BALANCE');
entities.remove(entity);

emit EntityRemoved(entity);
}

/// @inheritdoc IGhoReserve
function setLimit(address entity, uint256 limit) external onlyOwner {
require(entities.contains(entity), 'ENTITY_NOT_ALLOWED');
_ghoUsage[entity].limit = uint128(limit);

emit GhoLimitUpdated(entity, limit);
}

/// @inheritdoc IGhoReserve
function getEntities() external view returns (address[] memory) {
return entities.values();
}

/// @inheritdoc IGhoReserve
function getUsed(address entity) external view returns (uint256) {
return _ghoUsage[entity].used;
Expand All @@ -87,6 +110,16 @@ contract GhoReserve is Ownable, VersionedInitializable, IGhoReserve {
return _ghoUsage[entity].limit;
}

/// @inheritdoc IGhoReserve
function isEntity(address entity) external view returns (bool) {
return entities.contains(entity);
}

/// @inheritdoc IGhoReserve
function totalEntities() external view returns (uint256) {
return entities.length();
}

/// @inheritdoc IGhoReserve
function GHO_REMOTE_RESERVE_REVISION() public pure virtual override returns (uint256) {
return 1;
Expand Down
43 changes: 43 additions & 0 deletions src/contracts/facilitators/gsm/interfaces/IGhoReserve.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ interface IGhoReserve {
uint128 used;
}

/**
* @dev Emitted when an entity is added to the GhoReserve entities set
* @param entity The address of the entity
*/
event EntityAdded(address indexed entity);

/**
* @dev Emitted when an entity is removed from the GhoReserve entities set
* @param entity The address of the entity
*/
event EntityRemoved(address indexed entity);

/**
* @dev Emitted when GHO is restored to the reserve by an entity
* @param entity The address restoring the GHO tokens
Expand Down Expand Up @@ -67,6 +79,18 @@ interface IGhoReserve {
*/
function transfer(address to, uint256 amount) external;

/**
* @notice Adds an entity to the reserve
* @param entity The address of the entity
*/
function addEntity(address entity) external;

/**
* @notice Removes an entity from the reserve
* @param entity The address of the entity
*/
function removeEntity(address entity) external;

/**
* @notice Sets a usage limit for a specified entity.
* @dev The new usage limit can be set below the amount of GHO currently used
Expand All @@ -81,6 +105,12 @@ interface IGhoReserve {
*/
function GHO_TOKEN() external view returns (address);

/**
* @notice Returns the list of all entities currently in the reserve
* @return The array of addresses
*/
function getEntities() external view returns (address[] memory);

/**
* @notice Returns the amount of GHO used by a specified entity
* @param entity The address of the entity
Expand All @@ -103,6 +133,19 @@ interface IGhoReserve {
*/
function getLimit(address entity) external view returns (uint256);

/**
* @notice Returns whether the entity is part of the reserve
* @param entity The address of the entity
* @return True if the entity is part of the set
*/
function isEntity(address entity) external view returns (bool);

/**
* @notice Returns the number of entities in the reserve
* @return The total number of entities
*/
function totalEntities() external view returns (uint256);

/**
* @notice Returns the GhoReserve revision number
* @return The revision number
Expand Down
5 changes: 1 addition & 4 deletions src/script/DeployGsmLaunch.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ contract DeployGsmLaunch is Script {
// ------------------------------------------------
// 0. GhoReserve
// ------------------------------------------------
GhoReserve ghoReserveImpl = new GhoReserve(
GovernanceV3Ethereum.EXECUTOR_LVL_1,
AaveV3EthereumAssets.GHO_UNDERLYING
);
GhoReserve ghoReserveImpl = new GhoReserve(AaveV3EthereumAssets.GHO_UNDERLYING);
ghoReserveImpl.initialize(GovernanceV3Ethereum.EXECUTOR_LVL_1);
console2.log('GhoReserve Implementation: ', address(ghoReserveImpl));

Expand Down
4 changes: 3 additions & 1 deletion src/test/TestGhoBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ contract TestGhoBase is Test, Constants, Events {
GHO_TOKEN.addFacilitator(address(GHO_ATOKEN), 'Aave V3 Pool', DEFAULT_CAPACITY);
POOL.setGhoTokens(GHO_DEBT_TOKEN, GHO_ATOKEN);

GHO_RESERVE = new GhoReserve(address(this), address(GHO_TOKEN));
GHO_RESERVE = new GhoReserve(address(GHO_TOKEN));
GHO_RESERVE.initialize(address(this));

OWNABLE_FACILITATOR = new OwnableFacilitator(address(this), address(GHO_TOKEN));
Expand Down Expand Up @@ -303,6 +303,8 @@ contract TestGhoBase is Test, Constants, Events {
address(GHO_RESERVE)
);

GHO_RESERVE.addEntity(address(GHO_GSM));
GHO_RESERVE.addEntity(address(GHO_GSM_4626));
GHO_RESERVE.setLimit(address(GHO_GSM), DEFAULT_CAPACITY);
GHO_RESERVE.setLimit(address(GHO_GSM_4626), DEFAULT_CAPACITY);

Expand Down
112 changes: 102 additions & 10 deletions src/test/TestGhoReserve.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,26 @@ import './TestGhoBase.t.sol';

contract TestGhoReserve is TestGhoBase {
function testConstructor() public {
GhoReserve reserve = new GhoReserve(address(this), address(GHO_TOKEN));
GhoReserve reserve = new GhoReserve(address(GHO_TOKEN));
assertEq(reserve.GHO_TOKEN(), address(GHO_TOKEN));
assertEq(reserve.owner(), address(this));
}

function testRevertConstructorInvalidOwner() public {
vm.expectRevert('ZERO_ADDRESS_NOT_VALID');
new GhoReserve(address(0), address(GHO_TOKEN));
}

function testRevertConstructorInvalidGhoToken() public {
vm.expectRevert('ZERO_ADDRESS_NOT_VALID');
new GhoReserve(address(this), address(0));
new GhoReserve(address(0));
}

function testInitialize() public {
GhoReserve reserve = new GhoReserve(address(this), address(GHO_TOKEN));
GhoReserve reserve = new GhoReserve(address(GHO_TOKEN));
vm.expectEmit(true, true, true, true, address(reserve));
emit OwnershipTransferred(address(this), address(this));
reserve.initialize(address(this));
assertEq(reserve.owner(), address(this));
}

function testRevertInitializeInvalidZeroOwner() public {
GhoReserve reserve = new GhoReserve(address(this), address(GHO_TOKEN));
GhoReserve reserve = new GhoReserve(address(GHO_TOKEN));
vm.expectRevert('ZERO_ADDRESS_NOT_VALID');
reserve.initialize(address(0));
}
Expand All @@ -47,6 +42,7 @@ contract TestGhoReserve is TestGhoBase {

function testUse() public {
uint256 capacity = 100_000 ether;
GHO_RESERVE.addEntity(address(this));
GHO_RESERVE.setLimit(address(this), capacity);
assertEq(GHO_RESERVE.getUsed(address(this)), 0);
assertEq(GHO_RESERVE.getLimit(address(this)), capacity);
Expand All @@ -62,6 +58,7 @@ contract TestGhoReserve is TestGhoBase {
}

function testRevertRestoreNoWithdrawnAmount() public {
GHO_RESERVE.addEntity(address(this));
GHO_RESERVE.setLimit(address(this), 10_000 ether);

vm.expectRevert();
Expand All @@ -70,6 +67,7 @@ contract TestGhoReserve is TestGhoBase {

function testRestore() public {
uint256 capacity = 100_000 ether;
GHO_RESERVE.addEntity(address(this));
GHO_RESERVE.setLimit(address(this), capacity);
assertEq(GHO_RESERVE.getUsed(address(this)), 0);
assertEq(GHO_RESERVE.getLimit(address(this)), capacity);
Expand All @@ -96,9 +94,72 @@ contract TestGhoReserve is TestGhoBase {
assertEq(limit - used, capacity - repayAmount);
}

function testAddEntity() public {
address alice = makeAddr('alice');
vm.expectEmit(true, true, true, true, address(GHO_RESERVE));
emit EntityAdded(alice);
GHO_RESERVE.addEntity(address(alice));

assertTrue(GHO_RESERVE.isEntity(alice));
}

function testAddEntityAlreadyInSet() public {
address alice = makeAddr('alice');
vm.expectEmit(true, true, true, true, address(GHO_RESERVE));
emit EntityAdded(alice);
GHO_RESERVE.addEntity(address(alice));

// Set already contains two entities from constructor
assertEq(GHO_RESERVE.totalEntities(), 3);

GHO_RESERVE.addEntity(address(alice));

assertEq(GHO_RESERVE.totalEntities(), 3);
}

function testRemoveEntity() public {
address alice = makeAddr('alice');
vm.expectEmit(true, true, true, true, address(GHO_RESERVE));
emit EntityAdded(alice);
GHO_RESERVE.addEntity(address(alice));

assertTrue(GHO_RESERVE.isEntity(alice));

vm.expectEmit(true, true, true, true, address(GHO_RESERVE));
emit EntityRemoved(alice);
GHO_RESERVE.removeEntity(address(alice));

assertFalse(GHO_RESERVE.isEntity(alice));
}

function testRemoveEntityNotInSet() public {
address alice = makeAddr('alice');
assertFalse(GHO_RESERVE.isEntity(alice));
assertEq(GHO_RESERVE.totalEntities(), 2);

GHO_RESERVE.removeEntity(address(alice));

assertFalse(GHO_RESERVE.isEntity(alice));
assertEq(GHO_RESERVE.totalEntities(), 2);
}

function testRevertRemoveEntityBalanceOutstanding() public {
address alice = makeAddr('alice');
uint256 capacity = 100_000 ether;
GHO_RESERVE.addEntity(address(alice));
GHO_RESERVE.setLimit(alice, capacity);

vm.prank(alice);
GHO_RESERVE.use(5_000 ether);

vm.expectRevert('CANNOT_REMOVE_ENTITY_WITH_BALANCE');
GHO_RESERVE.removeEntity(alice);
}

function testSetLimit() public {
address alice = makeAddr('alice');
uint256 capacity = 100_000 ether;
GHO_RESERVE.addEntity(address(alice));

vm.expectEmit(true, true, true, true, address(GHO_RESERVE));
emit GhoLimitUpdated(alice, capacity);
Expand Down Expand Up @@ -170,6 +231,7 @@ contract TestGhoReserve is TestGhoBase {
address facilitator = makeAddr('facilitator');
uint256 amount = 1_000 ether;

reserve.addEntity(address(this));
reserve.setLimit(address(this), amount);
deal(address(GHO_TOKEN), address(reserve), amount);

Expand Down Expand Up @@ -198,10 +260,40 @@ contract TestGhoReserve is TestGhoBase {
assertEq(GHO_TOKEN.balanceOf(address(reserve)), 0);
}

function testGetEntities() public {
address alice = makeAddr('alice');
address[] memory entities = GHO_RESERVE.getEntities();

assertEq(entities.length, 2);

GHO_RESERVE.addEntity(alice);

entities = GHO_RESERVE.getEntities();

assertEq(entities.length, 3);

assertEq(address(GHO_GSM), entities[0]);
assertEq(address(GHO_GSM_4626), entities[1]);
assertEq(alice, entities[2]);
}

function testIsEntity() public {
assertTrue(GHO_RESERVE.isEntity(address(GHO_GSM)));
assertFalse(GHO_RESERVE.isEntity(makeAddr('NOT_AN_ENTITY')));
}

function testTotalEntities() public {
assertEq(GHO_RESERVE.totalEntities(), 2);

GHO_RESERVE.addEntity(makeAddr('alice'));

assertEq(GHO_RESERVE.totalEntities(), 3);
}

function _deployReserve() public returns (GhoReserve) {
address proxyAdmin = makeAddr('PROXY_ADMIN');

GhoReserve reserve = new GhoReserve(address(this), address(GHO_TOKEN));
GhoReserve reserve = new GhoReserve(address(GHO_TOKEN));
reserve.initialize(address(this));

bytes memory ghoReserveInitParams = abi.encodeWithSignature(
Expand Down
2 changes: 2 additions & 0 deletions src/test/TestGsm4626Edge.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ contract TestGsm4626Edge is TestGhoBase {
address(GHO_GSM_4626_FIXED_PRICE_STRATEGY)
);
gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1, address(GHO_RESERVE));
GHO_RESERVE.addEntity(address(gsm));
GHO_RESERVE.setLimit(address(gsm), DEFAULT_CAPACITY);

uint128 depositAmount = DEFAULT_GSM_USDC_EXPOSURE / 2;
Expand Down Expand Up @@ -224,6 +225,7 @@ contract TestGsm4626Edge is TestGhoBase {
address(GHO_GSM_4626_FIXED_PRICE_STRATEGY)
);
gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1, address(GHO_RESERVE));
GHO_RESERVE.addEntity(address(gsm));
GHO_RESERVE.setLimit(address(gsm), DEFAULT_CAPACITY);

uint128 depositAmount = DEFAULT_GSM_USDC_EXPOSURE * 2;
Expand Down
Loading