diff --git a/src/contracts/facilitators/gsm/GhoReserve.sol b/src/contracts/facilitators/gsm/GhoReserve.sol new file mode 100644 index 00000000..147dd810 --- /dev/null +++ b/src/contracts/facilitators/gsm/GhoReserve.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +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/periphery-v3/contracts/treasury/libs/VersionedInitializable.sol'; +import {IGhoReserve} from './interfaces/IGhoReserve.sol'; + +/** + * @title GhoReserve + * @author Aave/TokenLogic + * @notice It allows approved entities to withdraw and return GHO funds, with a defined maximum withdrawal capacity per entity. + * @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; + + /// Set of entities with a GHO limit available + EnumerableSet.AddressSet private entities; + + /** + * @dev Constructor + * @param ghoAddress The address of the GHO token on the remote chain + */ + constructor(address ghoAddress) { + require(ghoAddress != address(0), 'ZERO_ADDRESS_NOT_VALID'); + GHO_TOKEN = ghoAddress; + } + + /** + * @dev Initializer + * @param newOwner The address of the new owner + */ + function initialize(address newOwner) external initializer { + require(newOwner != address(0), 'ZERO_ADDRESS_NOT_VALID'); + _transferOwnership(newOwner); + } + + /// @inheritdoc IGhoReserve + function use(uint256 amount) external { + GhoUsage storage entity = _ghoUsage[msg.sender]; + require(entity.limit >= entity.used + amount, 'LIMIT_EXCEEDED'); + + entity.used += uint128(amount); + IERC20(GHO_TOKEN).transfer(msg.sender, amount); + emit GhoUsed(msg.sender, amount); + } + + /// @inheritdoc IGhoReserve + function restore(uint256 amount) external { + _ghoUsage[msg.sender].used -= uint128(amount); + IERC20(GHO_TOKEN).transferFrom(msg.sender, address(this), amount); + emit GhoRestored(msg.sender, amount); + } + + /// @inheritdoc IGhoReserve + function transfer(address to, uint256 amount) external onlyOwner { + IERC20(GHO_TOKEN).transfer(to, amount); + 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; + } + + /// @inheritdoc IGhoReserve + function getUsage(address entity) external view returns (uint256, uint256) { + GhoUsage memory usage = _ghoUsage[entity]; + return (usage.limit, usage.used); + } + + /// @inheritdoc IGhoReserve + function getLimit(address entity) external view returns (uint256) { + 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; + } + + /// @inheritdoc VersionedInitializable + function getRevision() internal pure virtual override returns (uint256) { + return GHO_REMOTE_RESERVE_REVISION(); + } +} diff --git a/src/contracts/facilitators/gsm/Gsm.sol b/src/contracts/facilitators/gsm/Gsm.sol index acc0729a..889456ae 100644 --- a/src/contracts/facilitators/gsm/Gsm.sol +++ b/src/contracts/facilitators/gsm/Gsm.sol @@ -12,6 +12,7 @@ import {IGhoFacilitator} from '../../gho/interfaces/IGhoFacilitator.sol'; import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; import {IGsmPriceStrategy} from './priceStrategy/interfaces/IGsmPriceStrategy.sol'; import {IGsmFeeStrategy} from './feeStrategy/interfaces/IGsmFeeStrategy.sol'; +import {IGhoReserve} from './interfaces/IGhoReserve.sol'; import {IGsm} from './interfaces/IGsm.sol'; /** @@ -67,6 +68,7 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { uint128 internal _exposureCap; uint128 internal _currentExposure; uint128 internal _accruedFees; + address internal _ghoReserve; /** * @dev Require GSM to not be frozen for functions marked by this modifier @@ -107,17 +109,20 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { * @param admin The address of the default admin role * @param ghoTreasury The address of the GHO treasury * @param exposureCap Maximum amount of user-supplied underlying asset in GSM + * @param ghoReserve The address of the GHO reserve to use tokens from */ function initialize( address admin, address ghoTreasury, - uint128 exposureCap + uint128 exposureCap, + address ghoReserve ) external initializer { require(admin != address(0), 'ZERO_ADDRESS_NOT_VALID'); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(CONFIGURATOR_ROLE, admin); _updateGhoTreasury(ghoTreasury); _updateExposureCap(exposureCap); + _updateGhoReserve(ghoReserve); } /// @inheritdoc IGsm @@ -222,13 +227,12 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { _currentExposure = 0; _updateExposureCap(0); - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); uint256 underlyingBalance = IERC20(UNDERLYING_ASSET).balanceOf(address(this)); if (underlyingBalance > 0) { IERC20(UNDERLYING_ASSET).safeTransfer(_ghoTreasury, underlyingBalance); } - emit Seized(msg.sender, _ghoTreasury, underlyingBalance, ghoMinted); + emit Seized(msg.sender, _ghoTreasury, underlyingBalance, _getUsedGho()); return underlyingBalance; } @@ -237,14 +241,15 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { require(_isSeized, 'GSM_NOT_SEIZED'); require(amount > 0, 'INVALID_AMOUNT'); - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); - if (amount > ghoMinted) { - amount = ghoMinted; + uint256 usedGho = _getUsedGho(); + if (amount > usedGho) { + amount = usedGho; } + IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), amount); - IGhoToken(GHO_TOKEN).burn(amount); + IGhoReserve(_ghoReserve).restore(amount); - emit BurnAfterSeize(msg.sender, amount, (ghoMinted - amount)); + emit BurnAfterSeize(msg.sender, amount, (usedGho - amount)); return amount; } @@ -258,6 +263,11 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { _updateExposureCap(exposureCap); } + /// @inheritdoc IGsm + function updateGhoReserve(address newGhoReserve) external onlyRole(CONFIGURATOR_ROLE) { + _updateGhoReserve(newGhoReserve); + } + /// @inheritdoc IGhoFacilitator function distributeFeesToTreasury() public virtual override { uint256 accruedFees = _accruedFees; @@ -363,6 +373,21 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { return _isSeized; } + /// @inheritdoc IGsm + function getGhoReserve() external view returns (address) { + return _ghoReserve; + } + + /// @inheritdoc IGsm + function getUsedGho() external view returns (uint256) { + return _getUsedGho(); + } + + /// @inheritdoc IGsm + function getLimit() external view returns (uint256) { + return _getLimit(); + } + /// @inheritdoc IGsm function canSwap() external view returns (bool) { return !_isFrozen && !_isSeized; @@ -405,8 +430,9 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { _currentExposure -= uint128(assetAmount); _accruedFees += fee.toUint128(); + IGhoToken(GHO_TOKEN).transferFrom(originator, address(this), ghoSold); - IGhoToken(GHO_TOKEN).burn(grossAmount); + IGhoReserve(_ghoReserve).restore(grossAmount); IERC20(UNDERLYING_ASSET).safeTransfer(receiver, assetAmount); emit BuyAsset(originator, receiver, assetAmount, ghoSold, fee); @@ -451,7 +477,7 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { _accruedFees += fee.toUint128(); IERC20(UNDERLYING_ASSET).safeTransferFrom(originator, address(this), assetAmount); - IGhoToken(GHO_TOKEN).mint(address(this), grossAmount); + IGhoReserve(_ghoReserve).use(grossAmount); IGhoToken(GHO_TOKEN).transfer(receiver, ghoBought); emit SellAsset(originator, receiver, assetAmount, grossAmount, fee); @@ -527,6 +553,31 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { return (finalAssetAmount, finalGrossAmount - finalFee, finalGrossAmount, finalFee); } + /** + * @dev Returns the maximum amount of GHO that can be used. + * @return The usage limit of GHO + */ + function _getLimit() internal view returns (uint256) { + return IGhoReserve(_ghoReserve).getLimit(address(this)); + } + + /** + * @dev Returns the usage data of a specified entity. + * @return The usage limit of GHO + * @return The amount of GHO used + */ + function _getUsage() internal view returns (uint256, uint256) { + return IGhoReserve(_ghoReserve).getUsage(address(this)); + } + + /** + * @dev Returns the amount of GHO currently used. + * @return The amount of GHO used + */ + function _getUsedGho() internal view returns (uint256) { + return IGhoReserve(_ghoReserve).getUsed(address(this)); + } + /** * @dev Updates Fee Strategy * @param feeStrategy The address of the new Fee Strategy @@ -558,6 +609,21 @@ contract Gsm is AccessControl, VersionedInitializable, EIP712, IGsm { emit GhoTreasuryUpdated(oldGhoTreasury, newGhoTreasury); } + /** + * @dev Updates the address of GHO reserve + * @param newGhoReserve The address of the GHO reserve for the GSM + */ + function _updateGhoReserve(address newGhoReserve) internal { + require(newGhoReserve != address(0), 'ZERO_ADDRESS_NOT_VALID'); + address oldReserve = _ghoReserve; + _ghoReserve = newGhoReserve; + + IGhoToken(GHO_TOKEN).approve(oldReserve, 0); + IGhoToken(GHO_TOKEN).approve(newGhoReserve, type(uint256).max); + + emit GhoReserveUpdated(oldReserve, newGhoReserve); + } + /// @inheritdoc VersionedInitializable function getRevision() internal pure virtual override returns (uint256) { return GSM_REVISION(); diff --git a/src/contracts/facilitators/gsm/Gsm4626.sol b/src/contracts/facilitators/gsm/Gsm4626.sol index 733e3d42..e890b861 100644 --- a/src/contracts/facilitators/gsm/Gsm4626.sol +++ b/src/contracts/facilitators/gsm/Gsm4626.sol @@ -8,6 +8,7 @@ import {IGhoFacilitator} from '../../gho/interfaces/IGhoFacilitator.sol'; import {IGhoToken} from '../../gho/interfaces/IGhoToken.sol'; import {IGsmPriceStrategy} from './priceStrategy/interfaces/IGsmPriceStrategy.sol'; import {IGsm4626} from './interfaces/IGsm4626.sol'; +import {IGhoReserve} from './interfaces/IGhoReserve.sol'; import {Gsm} from './Gsm.sol'; /** @@ -43,14 +44,14 @@ contract Gsm4626 is Gsm, IGsm4626 { ) external notSeized onlyRole(CONFIGURATOR_ROLE) returns (uint256) { require(amount > 0, 'INVALID_AMOUNT'); - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); - (, uint256 deficit) = _getCurrentBacking(ghoMinted); + uint256 usedGho = _getUsedGho(); + (, uint256 deficit) = _getCurrentBacking(usedGho); require(deficit > 0, 'NO_CURRENT_DEFICIT_BACKING'); uint256 ghoToBack = amount > deficit ? deficit : amount; IGhoToken(GHO_TOKEN).transferFrom(msg.sender, address(this), ghoToBack); - IGhoToken(GHO_TOKEN).burn(ghoToBack); + IGhoReserve(_ghoReserve).restore(ghoToBack); emit BackingProvided(msg.sender, GHO_TOKEN, ghoToBack, ghoToBack, deficit - ghoToBack); return ghoToBack; @@ -62,8 +63,7 @@ contract Gsm4626 is Gsm, IGsm4626 { ) external notSeized onlyRole(CONFIGURATOR_ROLE) returns (uint256) { require(amount > 0, 'INVALID_AMOUNT'); - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); - (, uint256 deficit) = _getCurrentBacking(ghoMinted); + (, uint256 deficit) = _getCurrentBacking(_getUsedGho()); require(deficit > 0, 'NO_CURRENT_DEFICIT_BACKING'); uint128 deficitInUnderlying = IGsmPriceStrategy(PRICE_STRATEGY) @@ -95,8 +95,7 @@ contract Gsm4626 is Gsm, IGsm4626 { /// @inheritdoc IGsm4626 function getCurrentBacking() external view returns (uint256, uint256) { - (, uint256 ghoMinted) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(this)); - return _getCurrentBacking(ghoMinted); + return _getCurrentBacking(_getUsedGho()); } /// @inheritdoc IGhoFacilitator @@ -116,36 +115,34 @@ contract Gsm4626 is Gsm, IGsm4626 { /** * @dev Cumulates yield in form of GHO, aimed to be redirected to the treasury * @dev It mints GHO backed by the excess of underlying produced by the ERC4626 yield - * @dev If the GHO amount exceeds the amount available, it will mint up to the remaining capacity + * @dev If the GHO amount exceeds the amount available, it will mint up to the remaining limit */ function _cumulateYieldInGho() internal { - (uint256 ghoCapacity, uint256 ghoLevel) = IGhoToken(GHO_TOKEN).getFacilitatorBucket( - address(this) - ); - uint256 ghoAvailableToMint = ghoCapacity > ghoLevel ? ghoCapacity - ghoLevel : 0; - (uint256 ghoExcess, ) = _getCurrentBacking(ghoLevel); + (uint256 ghoLimit, uint256 ghoUsed) = _getUsage(); + uint256 ghoAvailableToMint = ghoLimit > ghoUsed ? ghoLimit - ghoUsed : 0; + (uint256 ghoExcess, ) = _getCurrentBacking(ghoUsed); if (ghoExcess > 0 && ghoAvailableToMint > 0) { ghoExcess = ghoExcess > ghoAvailableToMint ? ghoAvailableToMint : ghoExcess; _accruedFees += uint128(ghoExcess); - IGhoToken(GHO_TOKEN).mint(address(this), ghoExcess); + IGhoReserve(_ghoReserve).use(ghoExcess); } } /** * @dev Calculates the excess or deficit of GHO minted, reflective of GSM backing - * @param ghoMinted The amount of GHO currently minted by the GSM - * @return The excess amount of GHO minted, relative to the value of the underlying - * @return The deficit of GHO minted, relative to the value of the underlying + * @param usedGho The amount of GHO currently used by the GSM + * @return The excess amount of GHO used, relative to the value of the underlying + * @return The deficit of GHO used, relative to the value of the underlying */ - function _getCurrentBacking(uint256 ghoMinted) internal view returns (uint256, uint256) { + function _getCurrentBacking(uint256 usedGho) internal view returns (uint256, uint256) { uint256 ghoToBack = IGsmPriceStrategy(PRICE_STRATEGY).getAssetPriceInGho( _currentExposure, false ); - if (ghoToBack >= ghoMinted) { - return (ghoToBack - ghoMinted, 0); + if (ghoToBack >= usedGho) { + return (ghoToBack - usedGho, 0); } else { - return (0, ghoMinted - ghoToBack); + return (0, usedGho - ghoToBack); } } } diff --git a/src/contracts/facilitators/gsm/OwnableFacilitator.sol b/src/contracts/facilitators/gsm/OwnableFacilitator.sol new file mode 100644 index 00000000..2efdbcb5 --- /dev/null +++ b/src/contracts/facilitators/gsm/OwnableFacilitator.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; +import {IOwnableFacilitator} from 'src/contracts/facilitators/gsm/interfaces/IOwnableFacilitator.sol'; +import {IGhoToken} from 'src/contracts/gho/interfaces/IGhoToken.sol'; + +/** + * @title OwnableFacilitator + * @author Aave/TokenLogic + * @notice GHO Facilitator used to directly mint GHO to a given address. + */ +contract OwnableFacilitator is Ownable, IOwnableFacilitator { + /// @inheritdoc IOwnableFacilitator + address public immutable GHO_TOKEN; + + /** + * @dev Constructor + * @param initialOwner The address of the initial owner + * @param ghoAddress The address of GHO token + */ + constructor(address initialOwner, address ghoAddress) { + require(initialOwner != address(0), 'ZERO_ADDRESS_NOT_VALID'); + require(ghoAddress != address(0), 'ZERO_ADDRESS_NOT_VALID'); + + _transferOwnership(initialOwner); + GHO_TOKEN = ghoAddress; + } + + /// @inheritdoc IOwnableFacilitator + function mint(address account, uint256 amount) external onlyOwner { + IGhoToken(GHO_TOKEN).mint(account, amount); + } + + /// @inheritdoc IOwnableFacilitator + function burn(uint256 amount) external onlyOwner { + IGhoToken(GHO_TOKEN).burn(amount); + } +} diff --git a/src/contracts/facilitators/gsm/interfaces/IGhoReserve.sol b/src/contracts/facilitators/gsm/interfaces/IGhoReserve.sol new file mode 100644 index 00000000..9c950801 --- /dev/null +++ b/src/contracts/facilitators/gsm/interfaces/IGhoReserve.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IGhoReserve + * @author Aave/TokenLogic + * @notice Defines the behaviour of a GhoReserve + */ +interface IGhoReserve { + /** + * @dev Struct data representing GHO usage. + * @param limit The maximum amount of GHO that can be used + * @param used The current amount of GHO used + */ + struct GhoUsage { + uint128 limit; + 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 + * @param amount The amount of token restored + */ + event GhoUsed(address indexed entity, uint256 amount); + + /** + * @dev Emitted when GHO is transferred from the reserve to an entity + * @param entity The address receiving the GHO tokens + * @param amount The amount of token to transfer + */ + event GhoRestored(address indexed entity, uint256 amount); + + /** + * @dev Emitted when GHO is transferred from the reserve + * @param to The address receiving the GHO tokens + * @param amount The amount of token to transfer + */ + event GhoTransferred(address indexed to, uint256 amount); + + /** + * @dev Emitted when the GHO limit for a given entity is updated + * @param entity The address of the entity + * @param limit The new usage limit + */ + event GhoLimitUpdated(address indexed entity, uint256 limit); + + /** + * @notice Restores a specified amount of GHO to the reserve. + * @dev The entity must grant allowance in advance to enable the reserve to pull the funds. + * @dev Only callable by approved reserve entities. + * @param amount The amount of GHO to restore. + */ + function restore(uint256 amount) external; + + /** + * @notice Uses a specified amount of GHO from the reserve. + * @dev Callable only by entities with sufficient usage limit. + * @param amount The amount of GHO to use. + */ + function use(uint256 amount) external; + + /** + * @notice Transfers a specified amount of GHO from the reserve + * @param to The address receiving the GHO tokens + * @param amount The amount of GHO to transfer + */ + 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 + * @param entity The address of the entity + * @param limit The maximum amount of GHO that can be used + */ + function setLimit(address entity, uint256 limit) external; + + /** + * @notice Returns the address of the GHO token + * @return The address of GHO token contract + */ + 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 + * @return The amount of GHO used + */ + function getUsed(address entity) external view returns (uint256); + + /** + * @notice Returns the usage data of a specified entity + * @param entity The address of the entity + * @return The usage limit + * @return The amount of GHO used + */ + function getUsage(address entity) external view returns (uint256, uint256); + + /** + * @notice Returns the usage limit of a specified entity + * @param entity The address of the entity + * @return The usage limit + */ + 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 + */ + function GHO_REMOTE_RESERVE_REVISION() external pure returns (uint256); +} diff --git a/src/contracts/facilitators/gsm/interfaces/IGsm.sol b/src/contracts/facilitators/gsm/interfaces/IGsm.sol index f1728052..fe26954d 100644 --- a/src/contracts/facilitators/gsm/interfaces/IGsm.sol +++ b/src/contracts/facilitators/gsm/interfaces/IGsm.sol @@ -26,6 +26,13 @@ interface IGsm is IAccessControl, IGhoFacilitator { uint256 fee ); + /** + * @dev Emitted when the GHO reserve is updated + * @param oldReserve The address of the old reserve + * @param newReserve The address of the new reserve + */ + event GhoReserveUpdated(address oldReserve, address newReserve); + /** * @dev Emitted when a user sells an asset (buying GHO) in the GSM * @param originator The address of the seller originating the request @@ -199,6 +206,13 @@ interface IGsm is IAccessControl, IGhoFacilitator { */ function updateExposureCap(uint128 exposureCap) external; + /** + * @notice Updates the GHO reserve address + * @dev It revokes the allowance to the old reserve and grants maximum allowance to the new one. + * @param newGhoReserve The new address of the GHO reserve + */ + function updateGhoReserve(address newGhoReserve) external; + /** * @notice Returns the EIP712 domain separator * @return The EIP712 domain separator @@ -297,6 +311,24 @@ interface IGsm is IAccessControl, IGhoFacilitator { */ function getIsSeized() external view returns (bool); + /** + * @notice Returns the address of the GHO reserve + * @return The address of the GHO reserve + */ + function getGhoReserve() external view returns (address); + + /** + * @notice Returns the amount of GHO used by the GSM + * @return The amount of GHO used + */ + function getUsedGho() external view returns (uint256); + + /** + * @notice Returns the maximum amount of GHO that can be used + * @return The maximum amount of GHO that can be used + */ + function getLimit() external view returns (uint256); + /** * @notice Returns whether or not swaps via buyAsset/sellAsset are currently possible * @return True if the GSM has swapping enabled, false otherwise diff --git a/src/contracts/facilitators/gsm/interfaces/IOwnableFacilitator.sol b/src/contracts/facilitators/gsm/interfaces/IOwnableFacilitator.sol new file mode 100644 index 00000000..9ec88c98 --- /dev/null +++ b/src/contracts/facilitators/gsm/interfaces/IOwnableFacilitator.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IOwnableFacilitator + * @author Aave/TokenLogic + * @notice Defines the behaviour of an OwnableFacilitator + */ +interface IOwnableFacilitator { + /** + * @notice Mint an amount of GHO to an address + * @dev Only callable by the owner of the Facilitator. + * @param account The address receiving GHO + * @param amount The amount of GHO to be minted + */ + function mint(address account, uint256 amount) external; + + /** + * @notice Burns an amount of GHO + * @dev Only callable by the owner of the Facilitator. + * @param amount The amount of GHO to be burned + */ + function burn(uint256 amount) external; + + /** + * @notice Returns the address of the GHO token + * @return The address of GHO token contract + */ + function GHO_TOKEN() external view returns (address); +} diff --git a/src/contracts/facilitators/gsm/misc/SampleLiquidator.sol b/src/contracts/facilitators/gsm/misc/SampleLiquidator.sol index 70fc3261..4326fc76 100644 --- a/src/contracts/facilitators/gsm/misc/SampleLiquidator.sol +++ b/src/contracts/facilitators/gsm/misc/SampleLiquidator.sol @@ -29,9 +29,9 @@ contract SampleLiquidator is Ownable { */ function triggerBurnAfterSeize(address gsm, uint256 amount) external onlyOwner returns (uint256) { IERC20 ghoToken = IERC20(IGsm(gsm).GHO_TOKEN()); - (, uint256 ghoMinted) = IGhoToken(address(ghoToken)).getFacilitatorBucket(gsm); - if (amount > ghoMinted) { - amount = ghoMinted; + uint256 usedGho = IGsm(gsm).getUsedGho(); + if (amount > usedGho) { + amount = usedGho; } ghoToken.transferFrom(msg.sender, address(this), amount); ghoToken.approve(gsm, amount); diff --git a/src/script/DeployGsmLaunch.s.sol b/src/script/DeployGsmLaunch.s.sol index 9983c5b1..952a5e21 100644 --- a/src/script/DeployGsmLaunch.s.sol +++ b/src/script/DeployGsmLaunch.s.sol @@ -13,6 +13,7 @@ import {FixedPriceStrategy} from '../contracts/facilitators/gsm/priceStrategy/Fi import {FixedFeeStrategy} from '../contracts/facilitators/gsm/feeStrategy/FixedFeeStrategy.sol'; import {GsmRegistry} from '../contracts/facilitators/gsm/misc/GsmRegistry.sol'; import {OracleSwapFreezer} from '../contracts/facilitators/gsm/swapFreezer/OracleSwapFreezer.sol'; +import {GhoReserve} from '../contracts/facilitators/gsm/GhoReserve.sol'; // GSM USDC uint8 constant USDC_DECIMALS = 6; @@ -49,6 +50,27 @@ contract DeployGsmLaunch is Script { } function _deploy() internal { + // ------------------------------------------------ + // 0. GhoReserve + // ------------------------------------------------ + GhoReserve ghoReserveImpl = new GhoReserve(AaveV3EthereumAssets.GHO_UNDERLYING); + ghoReserveImpl.initialize(GovernanceV3Ethereum.EXECUTOR_LVL_1); + console2.log('GhoReserve Implementation: ', address(ghoReserveImpl)); + + bytes memory ghoReserveInitParams = abi.encodeWithSignature( + 'initialize(address)', + GovernanceV3Ethereum.EXECUTOR_LVL_1 + ); + + TransparentUpgradeableProxy ghoReserveProxy = new TransparentUpgradeableProxy( + address(ghoReserveImpl), + MiscEthereum.PROXY_ADMIN, + ghoReserveInitParams + ); + + GhoReserve ghoReserve = GhoReserve(address(ghoReserveProxy)); + console2.log('GhoReserve Proxy: ', address(ghoReserveProxy)); + // ------------------------------------------------ // 1. FixedPriceStrategy // ------------------------------------------------ @@ -86,12 +108,14 @@ contract DeployGsmLaunch is Script { gsmUsdcImpl.initialize( GovernanceV3Ethereum.EXECUTOR_LVL_1, address(AaveV3Ethereum.COLLECTOR), - USDC_EXPOSURE_CAP + USDC_EXPOSURE_CAP, + address(ghoReserve) ); gsmUsdtImpl.initialize( GovernanceV3Ethereum.EXECUTOR_LVL_1, address(AaveV3Ethereum.COLLECTOR), - USDT_EXPOSURE_CAP + USDT_EXPOSURE_CAP, + address(ghoReserve) ); // ------------------------------------------------ @@ -101,7 +125,8 @@ contract DeployGsmLaunch is Script { 'initialize(address,address,uint128)', GovernanceV3Ethereum.EXECUTOR_LVL_1, address(AaveV3Ethereum.COLLECTOR), - USDC_EXPOSURE_CAP + USDC_EXPOSURE_CAP, + address(ghoReserve) ); TransparentUpgradeableProxy gsmUsdcProxy = new TransparentUpgradeableProxy( address(gsmUsdcImpl), @@ -115,7 +140,8 @@ contract DeployGsmLaunch is Script { 'initialize(address,address,uint128)', GovernanceV3Ethereum.EXECUTOR_LVL_1, address(AaveV3Ethereum.COLLECTOR), - USDT_EXPOSURE_CAP + USDT_EXPOSURE_CAP, + address(ghoReserve) ); TransparentUpgradeableProxy gsmUsdtProxy = new TransparentUpgradeableProxy( address(gsmUsdtImpl), diff --git a/src/test/TestGhoBase.t.sol b/src/test/TestGhoBase.t.sol index b382234c..53b6cdee 100644 --- a/src/test/TestGhoBase.t.sol +++ b/src/test/TestGhoBase.t.sol @@ -87,6 +87,9 @@ import {IGhoCcipSteward} from '../contracts/misc/interfaces/IGhoCcipSteward.sol' import {GhoCcipSteward} from '../contracts/misc/GhoCcipSteward.sol'; import {GhoBucketSteward} from '../contracts/misc/GhoBucketSteward.sol'; +import {GhoReserve} from '../contracts/facilitators/gsm/GhoReserve.sol'; +import {OwnableFacilitator} from '../contracts/facilitators/gsm/OwnableFacilitator.sol'; + contract TestGhoBase is Test, Constants, Events { using WadRayMath for uint256; using SafeCast for uint256; @@ -142,6 +145,9 @@ contract TestGhoBase is Test, Constants, Events { FixedFeeStrategyFactory FIXED_FEE_STRATEGY_FACTORY; MockUpgradeableLockReleaseTokenPool GHO_TOKEN_POOL; + GhoReserve GHO_RESERVE; + OwnableFacilitator OWNABLE_FACILITATOR; + constructor() { setupGho(); } @@ -234,6 +240,17 @@ 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(GHO_TOKEN)); + GHO_RESERVE.initialize(address(this)); + + OWNABLE_FACILITATOR = new OwnableFacilitator(address(this), address(GHO_TOKEN)); + // Give OwnableFacilitator twice the default capacity to fully fund two GSMs + GHO_TOKEN.addFacilitator( + address(OWNABLE_FACILITATOR), + 'OwnableFacilitator', + DEFAULT_CAPACITY * 2 + ); + GHO_FLASH_MINTER = new GhoFlashMinter( address(GHO_TOKEN), TREASURY, @@ -273,13 +290,26 @@ contract TestGhoBase is Test, Constants, Events { ); GHO_GSM = Gsm(address(gsmProxy)); - GHO_GSM.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + GHO_GSM.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); GHO_GSM_4626 = new Gsm4626( address(GHO_TOKEN), address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); - GHO_GSM_4626.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + GHO_GSM_4626.initialize( + address(this), + TREASURY, + DEFAULT_GSM_USDC_EXPOSURE, + 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); + + // Mint twice default capacity for both GSMs to be fully funded + OWNABLE_FACILITATOR.mint(address(GHO_RESERVE), DEFAULT_CAPACITY * 2); GHO_GSM_FIXED_FEE_STRATEGY = new FixedFeeStrategy(DEFAULT_GSM_BUY_FEE, DEFAULT_GSM_SELL_FEE); GHO_GSM.updateFeeStrategy(address(GHO_GSM_FIXED_FEE_STRATEGY)); @@ -290,9 +320,6 @@ contract TestGhoBase is Test, Constants, Events { GHO_GSM_4626.grantRole(GSM_LIQUIDATOR_ROLE, address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_GSM_4626.grantRole(GSM_SWAP_FREEZER_ROLE, address(GHO_GSM_SWAP_FREEZER)); - GHO_TOKEN.addFacilitator(address(GHO_GSM), 'GSM Facilitator', DEFAULT_CAPACITY); - GHO_TOKEN.addFacilitator(address(GHO_GSM_4626), 'GSM 4626 Facilitator', DEFAULT_CAPACITY); - GHO_TOKEN.addFacilitator(FAUCET, 'Faucet Facilitator', type(uint128).max); GHO_GSM_REGISTRY = new GsmRegistry(address(this)); diff --git a/src/test/TestGhoReserve.t.sol b/src/test/TestGhoReserve.t.sol new file mode 100644 index 00000000..7530f5a8 --- /dev/null +++ b/src/test/TestGhoReserve.t.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; + +contract TestGhoReserve is TestGhoBase { + function testConstructor() public { + GhoReserve reserve = new GhoReserve(address(GHO_TOKEN)); + assertEq(reserve.GHO_TOKEN(), address(GHO_TOKEN)); + assertEq(reserve.owner(), address(this)); + } + + function testRevertConstructorInvalidGhoToken() public { + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new GhoReserve(address(0)); + } + + function testInitialize() public { + 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(GHO_TOKEN)); + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + reserve.initialize(address(0)); + } + + function testRevertInitializeTwice() public { + GhoReserve reserve = _deployReserve(); + vm.expectRevert('Contract instance has already been initialized'); + reserve.initialize(address(0)); + } + + function testRevertUseNoCapacity() public { + vm.expectRevert('LIMIT_EXCEEDED'); + GHO_RESERVE.use(100 ether); + } + + 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); + + vm.expectEmit(true, true, true, true, address(GHO_RESERVE)); + emit GhoUsed(address(this), capacity / 2); + GHO_RESERVE.use(capacity / 2); + + (uint256 limit, uint256 used) = GHO_RESERVE.getUsage(address(this)); + + assertEq(GHO_RESERVE.getUsed(address(this)), capacity / 2); + assertEq(limit - used, capacity / 2); + } + + function testRevertRestoreNoWithdrawnAmount() public { + GHO_RESERVE.addEntity(address(this)); + GHO_RESERVE.setLimit(address(this), 10_000 ether); + + vm.expectRevert(); + GHO_RESERVE.restore(10_000 ether); + } + + 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); + + vm.expectEmit(true, true, true, true, address(GHO_RESERVE)); + emit GhoUsed(address(this), capacity / 2); + GHO_RESERVE.use(capacity / 2); + + (uint256 limit, uint256 used) = GHO_RESERVE.getUsage(address(this)); + + assertEq(GHO_RESERVE.getUsed(address(this)), capacity / 2); + assertEq(limit - used, capacity / 2); + + uint256 repayAmount = 25_000 ether; + GHO_TOKEN.approve(address(GHO_RESERVE), repayAmount); + + vm.expectEmit(true, true, true, true, address(GHO_RESERVE)); + emit GhoRestored(address(this), repayAmount); + GHO_RESERVE.restore(repayAmount); + + (limit, used) = GHO_RESERVE.getUsage(address(this)); + + assertEq(GHO_RESERVE.getUsed(address(this)), capacity / 4); + 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); + GHO_RESERVE.setLimit(alice, capacity); + } + + function testTransfer() public { + GhoReserve reserve = _deployReserve(); + address facilitator = makeAddr('facilitator'); + uint256 amount = 1_000 ether; + + deal(address(GHO_TOKEN), address(reserve), 5_000 ether); + + vm.expectEmit(true, true, true, true, address(reserve)); + emit GhoTransferred(facilitator, amount); + reserve.transfer(facilitator, amount); + + assertEq(GHO_TOKEN.balanceOf(address(reserve)), 5_000 ether - amount); + } + + function testRevertTransferInvalidCaller() public { + GhoReserve reserve = _deployReserve(); + address facilitator = makeAddr('facilitator'); + uint256 amount = 1_000 ether; + + vm.expectRevert('Ownable: caller is not the owner'); + vm.prank(address(GHO_TOKEN)); + reserve.transfer(facilitator, amount); + } + + function testRevertTransferNoFunds() public { + GhoReserve reserve = _deployReserve(); + address facilitator = makeAddr('facilitator'); + uint256 amount = 1_000 ether; + + assertEq(GHO_TOKEN.balanceOf(address(reserve)), 0); + + vm.expectRevert(); + reserve.transfer(facilitator, amount); + } + + function testTransferFull() public { + GhoReserve reserve = _deployReserve(); + address facilitator = makeAddr('facilitator'); + uint256 amount = 1_000 ether; + + deal(address(GHO_TOKEN), address(reserve), amount); + + vm.expectEmit(true, true, true, true, address(reserve)); + emit GhoTransferred(facilitator, amount); + reserve.transfer(facilitator, amount); + + assertEq(GHO_TOKEN.balanceOf(address(reserve)), 0); + } + + function testRevertTransferAmountGreaterThanBalance() public { + GhoReserve reserve = _deployReserve(); + address facilitator = makeAddr('facilitator'); + uint256 amount = 1_000 ether; + + deal(address(GHO_TOKEN), address(reserve), amount); + + vm.expectRevert(); + reserve.transfer(facilitator, amount + 1); + } + + function testTransferAfterGhoUsedAndReturned() public { + GhoReserve reserve = _deployReserve(); + 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); + + assertEq(GHO_TOKEN.balanceOf(address(reserve)), amount); + + vm.expectEmit(true, true, true, true, address(reserve)); + emit GhoUsed(address(this), amount); + reserve.use(amount); + + assertEq(GHO_TOKEN.balanceOf(address(reserve)), 0); + + // No GHO to transfer + vm.expectRevert(); + reserve.transfer(facilitator, amount); + + GHO_TOKEN.approve(address(reserve), amount / 2); + + vm.expectEmit(true, true, true, true, address(reserve)); + emit GhoRestored(address(this), amount / 2); + reserve.restore(amount / 2); + + assertEq(GHO_TOKEN.balanceOf(address(reserve)), amount / 2); + + reserve.transfer(facilitator, amount / 2); + + 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(GHO_TOKEN)); + reserve.initialize(address(this)); + + bytes memory ghoReserveInitParams = abi.encodeWithSignature( + 'initialize(address)', + address(this) + ); + + TransparentUpgradeableProxy reserveProxy = new TransparentUpgradeableProxy( + address(reserve), + proxyAdmin, + ghoReserveInitParams + ); + + return GhoReserve(address(reserveProxy)); + } +} diff --git a/src/test/TestGsm.t.sol b/src/test/TestGsm.t.sol index 3cf1e4b0..c1b45594 100644 --- a/src/test/TestGsm.t.sol +++ b/src/test/TestGsm.t.sol @@ -56,7 +56,7 @@ contract TestGsm is TestGhoBase { emit GhoTreasuryUpdated(address(0), address(TREASURY)); vm.expectEmit(true, true, false, true); emit ExposureCapUpdated(0, DEFAULT_GSM_USDC_EXPOSURE); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); assertEq(gsm.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } @@ -67,7 +67,17 @@ contract TestGsm is TestGhoBase { address(GHO_GSM_FIXED_PRICE_STRATEGY) ); vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); - gsm.initialize(address(0), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(0), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); + } + + function testRevertInitializeZeroGhoReserve() public { + Gsm gsm = new Gsm( + address(GHO_TOKEN), + address(USDC_TOKEN), + address(GHO_GSM_FIXED_PRICE_STRATEGY) + ); + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(0)); } function testRevertInitializeTwice() public { @@ -76,9 +86,9 @@ contract TestGsm is TestGhoBase { address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); vm.expectRevert('Contract instance has already been initialized'); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); } function testSellAssetZeroFee() public { @@ -363,7 +373,7 @@ contract TestGsm is TestGhoBase { address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Bucket Cap', DEFAULT_CAPACITY - 1); uint256 defaultCapInUsdc = DEFAULT_CAPACITY / (10 ** (18 - USDC_TOKEN.decimals())); @@ -372,7 +382,7 @@ contract TestGsm is TestGhoBase { vm.startPrank(ALICE); USDC_TOKEN.approve(address(gsm), defaultCapInUsdc); - vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); + vm.expectRevert('LIMIT_EXCEEDED'); gsm.sellAsset(defaultCapInUsdc, ALICE); vm.stopPrank(); } @@ -383,7 +393,7 @@ contract TestGsm is TestGhoBase { address(USDC_TOKEN), address(GHO_GSM_FIXED_PRICE_STRATEGY) ); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1, address(GHO_RESERVE)); GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Exposure Cap', DEFAULT_CAPACITY); vm.prank(FAUCET); @@ -727,22 +737,14 @@ contract TestGsm is TestGhoBase { emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_EXPOSURE, DEFAULT_CAPACITY, 0); GHO_GSM.sellAsset(DEFAULT_GSM_USDC_EXPOSURE, ALICE); - (uint256 ghoCapacity, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); - assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after initial sell'); - assertEq( - GHO_TOKEN.balanceOf(ALICE), - DEFAULT_CAPACITY, - 'Unexpected Alice GHO balance after sell' - ); - // Buy 1 of the underlying GHO_TOKEN.approve(address(GHO_GSM), 1e18); vm.expectEmit(true, true, true, true, address(GHO_GSM)); emit BuyAsset(ALICE, ALICE, 1e6, 1e18, 0); GHO_GSM.buyAsset(1e6, ALICE); - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); - assertEq(ghoLevel, DEFAULT_CAPACITY - 1e18, 'Unexpected GHO bucket level after buy'); + uint256 usedGho = GHO_GSM.getUsedGho(); + assertEq(usedGho, DEFAULT_CAPACITY - 1e18, 'Unexpected GHO bucket level after buy'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY - 1e18, @@ -757,8 +759,8 @@ contract TestGsm is TestGhoBase { GHO_GSM.sellAsset(1e6, ALICE); vm.stopPrank(); - (ghoCapacity, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); - assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after second sell'); + usedGho = GHO_GSM.getUsedGho(); + assertEq(usedGho, DEFAULT_CAPACITY, 'Unexpected GHO bucket level after second sell'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY, @@ -1097,7 +1099,7 @@ contract TestGsm is TestGhoBase { address(GHO_GSM_FIXED_PRICE_STRATEGY) ); vm.expectRevert(bytes('ZERO_ADDRESS_NOT_VALID')); - gsm.initialize(address(this), address(0), DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(this), address(0), DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); } function testUpdateGhoTreasuryRevertIfZero() public { @@ -1330,8 +1332,11 @@ contract TestGsm is TestGhoBase { uint256 seizedAmount = GHO_GSM.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); + uint256 usedGho = GHO_GSM.getUsedGho(); + assertTrue(usedGho > 0, 'Unexpected usedGho amount'); + vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); - GHO_TOKEN.removeFacilitator(address(GHO_GSM)); + GHO_TOKEN.removeFacilitator(address(OWNABLE_FACILITATOR)); ghoFaucet(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT); vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); @@ -1341,10 +1346,7 @@ contract TestGsm is TestGhoBase { uint256 burnedAmount = GHO_GSM.burnAfterSeize(DEFAULT_GSM_GHO_AMOUNT); vm.stopPrank(); assertEq(burnedAmount, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected burned amount of GHO'); - - vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); - emit FacilitatorRemoved(address(GHO_GSM)); - GHO_TOKEN.removeFacilitator(address(GHO_GSM)); + assertEq(GHO_GSM.getUsedGho(), 0, 'Unexpected amount of used GHO'); } function testBurnAfterSeizeGreaterAmount() public { diff --git a/src/test/TestGsm4626.t.sol b/src/test/TestGsm4626.t.sol index 14401c18..fa694cfa 100644 --- a/src/test/TestGsm4626.t.sol +++ b/src/test/TestGsm4626.t.sol @@ -50,7 +50,7 @@ contract TestGsm4626 is TestGhoBase { emit RoleGranted(DEFAULT_ADMIN_ROLE, address(this), address(this)); vm.expectEmit(true, true, false, true); emit ExposureCapUpdated(0, DEFAULT_GSM_USDC_EXPOSURE); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); assertEq(gsm.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); } @@ -60,9 +60,9 @@ contract TestGsm4626 is TestGhoBase { address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); vm.expectRevert('Contract instance has already been initialized'); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(GHO_RESERVE)); } function testSellAssetZeroFee() public { @@ -167,7 +167,7 @@ contract TestGsm4626 is TestGhoBase { address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1, address(GHO_RESERVE)); GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Exposure Cap', DEFAULT_CAPACITY); _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, DEFAULT_GSM_USDC_EXPOSURE); @@ -392,8 +392,9 @@ contract TestGsm4626 is TestGhoBase { emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_EXPOSURE, DEFAULT_CAPACITY, 0); GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_EXPOSURE, ALICE); - (uint256 ghoCapacity, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); - assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after initial sell'); + uint256 usedGho = GHO_GSM_4626.getUsedGho(); + uint256 ghoLimit = GHO_RESERVE.getLimit(address(GHO_GSM_4626)); + assertEq(usedGho, ghoLimit, 'Unexpected GHO bucket level after initial sell'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY, @@ -406,8 +407,8 @@ contract TestGsm4626 is TestGhoBase { emit BuyAsset(ALICE, ALICE, 1e6, 1e18, 0); GHO_GSM_4626.buyAsset(1e6, ALICE); - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); - assertEq(ghoLevel, DEFAULT_CAPACITY - 1e18, 'Unexpected GHO bucket level after buy'); + usedGho = GHO_GSM_4626.getUsedGho(); + assertEq(usedGho, DEFAULT_CAPACITY - 1e18, 'Unexpected GHO bucket level after buy'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY - 1e18, @@ -422,8 +423,9 @@ contract TestGsm4626 is TestGhoBase { GHO_GSM_4626.sellAsset(1e6, ALICE); vm.stopPrank(); - (ghoCapacity, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); - assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after second sell'); + usedGho = GHO_GSM_4626.getUsedGho(); + ghoLimit = GHO_RESERVE.getLimit(address(GHO_GSM_4626)); + assertEq(usedGho, ghoLimit, 'Unexpected GHO bucket level after second sell'); assertEq( GHO_TOKEN.balanceOf(ALICE), DEFAULT_CAPACITY, @@ -924,9 +926,6 @@ contract TestGsm4626 is TestGhoBase { uint256 seizedAmount = GHO_GSM_4626.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); - vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); - GHO_TOKEN.removeFacilitator(address(GHO_GSM_4626)); - ghoFaucet(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT); vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT); @@ -935,10 +934,7 @@ contract TestGsm4626 is TestGhoBase { uint256 burnedAmount = GHO_GSM_4626.burnAfterSeize(DEFAULT_GSM_GHO_AMOUNT); vm.stopPrank(); assertEq(burnedAmount, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected burned amount of GHO'); - - vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); - emit FacilitatorRemoved(address(GHO_GSM_4626)); - GHO_TOKEN.removeFacilitator(address(GHO_GSM_4626)); + assertEq(GHO_GSM_4626.getUsedGho(), 0, 'Unexpected leftover amount of GHO'); } function testBurnAfterSeizeGreaterAmount() public { @@ -952,6 +948,12 @@ contract TestGsm4626 is TestGhoBase { uint256 seizedAmount = GHO_GSM_4626.seize(); assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seized amount'); + uint256 usedGho = GHO_GSM_4626.getUsedGho(); + assertTrue(usedGho > 0, 'Unexpected usedGho amount'); + + vm.expectRevert('FACILITATOR_BUCKET_LEVEL_NOT_ZERO'); + GHO_TOKEN.removeFacilitator(address(OWNABLE_FACILITATOR)); + ghoFaucet(address(GHO_GSM_LAST_RESORT_LIQUIDATOR), DEFAULT_GSM_GHO_AMOUNT + 1); vm.startPrank(address(GHO_GSM_LAST_RESORT_LIQUIDATOR)); GHO_TOKEN.approve(address(GHO_GSM_4626), DEFAULT_GSM_GHO_AMOUNT + 1); @@ -1041,16 +1043,16 @@ contract TestGsm4626 is TestGhoBase { GHO_TOKEN.approve(address(GHO_GSM_4626), type(uint256).max); uint256 balanceBefore = GHO_TOKEN.balanceOf(address(this)); - (, uint256 ghoLevelBefore) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 usedGhoBefore = GHO_GSM_4626.getUsedGho(); uint256 ghoUsedForBacking = GHO_GSM_4626.backWithGho((DEFAULT_GSM_GHO_AMOUNT / 2) + 1); uint256 balanceAfter = GHO_TOKEN.balanceOf(address(this)); - (, uint256 ghoLevelAfter) = IGhoToken(GHO_TOKEN).getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 usedGhoAfter = GHO_GSM_4626.getUsedGho(); assertEq(DEFAULT_GSM_GHO_AMOUNT / 2, ghoUsedForBacking); assertEq(balanceBefore - balanceAfter, ghoUsedForBacking); - assertEq(ghoLevelBefore - ghoLevelAfter, ghoUsedForBacking); + assertEq(usedGhoBefore - usedGhoAfter, ghoUsedForBacking); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, 0, 'Unexpected excess value of GHO'); diff --git a/src/test/TestGsm4626Edge.t.sol b/src/test/TestGsm4626Edge.t.sol index f1f6725c..bfd71294 100644 --- a/src/test/TestGsm4626Edge.t.sol +++ b/src/test/TestGsm4626Edge.t.sol @@ -8,7 +8,7 @@ contract TestGsm4626Edge is TestGhoBase { using PercentageMath for uint128; function testOngoingExposureSellAsset() public { - (, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, 0); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), 0); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), 0); @@ -28,7 +28,7 @@ contract TestGsm4626Edge is TestGhoBase { ); assertEq(ghoBought, calcGhoMinted - sellFee, 'Unexpected GHO amount bought'); - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), sellAssetAmount); assertEq( @@ -70,7 +70,7 @@ contract TestGsm4626Edge is TestGhoBase { uint256 ghoAmountAfter = GHO_TOKEN.balanceOf(ALICE) - ghoAmountBefore; assertEq(ghoAmountAfter, ghoReceived * 2); - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( @@ -196,8 +196,9 @@ contract TestGsm4626Edge is TestGhoBase { address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Exposure Cap', DEFAULT_CAPACITY); + 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; _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, depositAmount); @@ -223,8 +224,9 @@ contract TestGsm4626Edge is TestGhoBase { address(USDC_4626_TOKEN), address(GHO_GSM_4626_FIXED_PRICE_STRATEGY) ); - gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE - 1); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM Modified Exposure Cap', DEFAULT_CAPACITY); + 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; _mintVaultAssets(USDC_4626_TOKEN, USDC_TOKEN, ALICE, depositAmount); @@ -301,7 +303,7 @@ contract TestGsm4626Edge is TestGhoBase { GHO_GSM_4626.getAvailableLiquidity(), true ); - (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 totalMintedGho = GHO_GSM_4626.getUsedGho(); assertEq(totalBackedGho, totalMintedGho + DEFAULT_GSM_GHO_AMOUNT); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); @@ -328,7 +330,7 @@ contract TestGsm4626Edge is TestGhoBase { 'Unexpected GHO balance in treasury' ); - (, totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + totalMintedGho = GHO_GSM_4626.getUsedGho(); assertEq( totalBackedGho, GHO_GSM_4626_FIXED_PRICE_STRATEGY.getAssetPriceInGho( @@ -397,7 +399,7 @@ contract TestGsm4626Edge is TestGhoBase { GHO_GSM_4626.getAvailableLiquidity(), false ); - (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 totalMintedGho = GHO_GSM_4626.getUsedGho(); assertEq(totalBackedGho, totalMintedGho); GHO_GSM_4626.distributeFeesToTreasury(); @@ -410,7 +412,7 @@ contract TestGsm4626Edge is TestGhoBase { ); assertEq(GHO_TOKEN.balanceOf(TREASURY), fee, 'Unexpected GHO balance in treasury'); - (, totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + totalMintedGho = GHO_GSM_4626.getUsedGho(); assertEq(totalBackedGho, totalMintedGho); } @@ -479,7 +481,7 @@ contract TestGsm4626Edge is TestGhoBase { GHO_GSM_4626.getAvailableLiquidity(), false ); - (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 totalMintedGho = GHO_GSM_4626.getUsedGho(); assertEq(totalBackedGho, totalMintedGho / 2); vm.expectEmit(true, true, true, true, address(GHO_GSM_4626)); @@ -536,7 +538,7 @@ contract TestGsm4626Edge is TestGhoBase { uint128 feePercentToMint = 0.3e4; // 30% uint128 margin = uint128(fee.percentMul(feePercentToMint)); uint128 capacity = DEFAULT_GSM_GHO_AMOUNT + margin; - GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_GSM_4626), capacity); + GHO_RESERVE.setLimit(address(GHO_GSM_4626), capacity); // Inflate exchange rate _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_AMOUNT, true); @@ -545,8 +547,9 @@ contract TestGsm4626Edge is TestGhoBase { assertEq(excessBeforeDistribution, (DEFAULT_GSM_USDC_AMOUNT) * 1e12, 'Unexpected excess'); assertEq(deficitBeforeDistribution, 0, 'Unexpected non-zero deficit'); - (uint256 ghoCapacity, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); - uint256 ghoAvailableToMint = ghoCapacity - ghoLevel; + uint256 ghoLevel = GHO_GSM_4626.getUsedGho(); + uint256 ghoLimit = GHO_RESERVE.getLimit(address(GHO_GSM_4626)); + uint256 ghoAvailableToMint = ghoLimit - ghoLevel; assertEq(ghoAvailableToMint, margin, 'Unexpected GHO amount available to mint'); @@ -555,8 +558,9 @@ contract TestGsm4626Edge is TestGhoBase { emit FeesDistributedToTreasury(TREASURY, address(GHO_TOKEN), ongoingAccruedFees + margin); GHO_GSM_4626.distributeFeesToTreasury(); - (ghoCapacity, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); - ghoAvailableToMint = ghoCapacity - ghoLevel; + ghoLevel = GHO_GSM_4626.getUsedGho(); + ghoLimit = GHO_RESERVE.getLimit(address(GHO_GSM_4626)); + ghoAvailableToMint = ghoLimit - ghoLevel; assertEq(ghoAvailableToMint, 0); assertEq(GHO_GSM_4626.getAccruedFees(), 0, 'Unexpected GSM accrued fees'); @@ -629,7 +633,7 @@ contract TestGsm4626Edge is TestGhoBase { GHO_GSM_4626.getAvailableLiquidity(), false ); - (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 totalMintedGho = GHO_GSM_4626.getUsedGho(); uint256 yieldInGho = totalBackedGho - totalMintedGho; assertEq(yieldInGho, DEFAULT_GSM_GHO_AMOUNT); @@ -724,7 +728,7 @@ contract TestGsm4626Edge is TestGhoBase { GHO_GSM_4626.getAvailableLiquidity(), false ); - (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 totalMintedGho = GHO_GSM_4626.getUsedGho(); uint256 yieldInGho = totalBackedGho - totalMintedGho; assertEq(yieldInGho, DEFAULT_GSM_GHO_AMOUNT); @@ -736,7 +740,7 @@ contract TestGsm4626Edge is TestGhoBase { assertEq(GHO_GSM_4626.getAccruedFees(), ongoingAccruedFees, 'Unexpected GSM accrued fees'); // Bucket capacity of GSM set to 0 so no more GHO can be minted (including yield in form of GHO) - GHO_TOKEN.setFacilitatorBucketCapacity(address(GHO_GSM_4626), 0); + GHO_RESERVE.setLimit(address(GHO_GSM_4626), 0); // Fee distribution uint256 treasuryBalanceBefore = GHO_TOKEN.balanceOf(address(TREASURY)); @@ -760,7 +764,7 @@ contract TestGsm4626Edge is TestGhoBase { * 3. Alice buyAsset of the current exposure. There is a mint of GHO before the action so the level is updated. */ - (, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, 0); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), 0); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), 0); @@ -772,7 +776,7 @@ contract TestGsm4626Edge is TestGhoBase { uint256 calcExposure = DEFAULT_GSM_USDC_AMOUNT; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, sellAssetAmount); - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), sellAssetAmount); assertEq( @@ -811,7 +815,7 @@ contract TestGsm4626Edge is TestGhoBase { GHO_GSM_4626.getAvailableLiquidity(), false ); - (, uint256 totalMintedGho) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 totalMintedGho = GHO_GSM_4626.getUsedGho(); assertEq(totalBackedGho, totalMintedGho + DEFAULT_GSM_GHO_AMOUNT); calcGhoMinted = 0; @@ -819,7 +823,7 @@ contract TestGsm4626Edge is TestGhoBase { GHO_GSM_4626.buyAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); vm.stopPrank(); - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); assertEq( @@ -838,7 +842,7 @@ contract TestGsm4626Edge is TestGhoBase { * 4. Exposure is 0 but level is not 0, so there is unbacked GHO */ - (, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + uint256 ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, 0); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), 0); assertEq(USDC_4626_TOKEN.previewRedeem(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626))), 0); @@ -850,7 +854,7 @@ contract TestGsm4626Edge is TestGhoBase { uint256 calcExposure = DEFAULT_GSM_USDC_AMOUNT; _sellAsset(GHO_GSM_4626, USDC_4626_TOKEN, USDC_TOKEN, ALICE, sellAssetAmount); - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), sellAssetAmount); assertEq( @@ -890,7 +894,7 @@ contract TestGsm4626Edge is TestGhoBase { vm.stopPrank(); // 0 exposure, but non-zero level - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); assertTrue(ghoLevel != 0); assertEq(ghoLevel, calcGhoMinted); assertEq(USDC_4626_TOKEN.balanceOf(address(GHO_GSM_4626)), calcExposure); @@ -923,8 +927,9 @@ contract TestGsm4626Edge is TestGhoBase { GHO_GSM_4626.sellAsset(DEFAULT_GSM_USDC_EXPOSURE, ALICE); vm.stopPrank(); - (uint256 ghoCapacity, uint256 ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); - assertEq(ghoLevel, ghoCapacity, 'Unexpected GHO bucket level after initial sell'); + uint256 ghoLevel = GHO_GSM_4626.getUsedGho(); + uint256 ghoLimit = GHO_RESERVE.getLimit(address(GHO_GSM_4626)); + assertEq(ghoLevel, ghoLimit, 'Unexpected GHO bucket level after initial sell'); // Simulate a gain _changeExchangeRate(USDC_4626_TOKEN, USDC_TOKEN, DEFAULT_GSM_USDC_EXPOSURE / 4, true); @@ -944,8 +949,8 @@ contract TestGsm4626Edge is TestGhoBase { assertEq(GHO_TOKEN.balanceOf(ALICE), 0, 'Unexpected final GHO balance'); // Ensure GHO level is at 0, but that excess is unchanged - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); - (ghoCapacity, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); + ghoLimit = GHO_RESERVE.getLimit(address(GHO_GSM_4626)); assertEq(ghoLevel, 0, 'Unexpected GHO bucket level after initial sell'); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, (DEFAULT_GSM_USDC_EXPOSURE / 4) * 1e12, 'Unexpected excess'); @@ -961,7 +966,7 @@ contract TestGsm4626Edge is TestGhoBase { vm.stopPrank(); // Ensure GHO level is at 2e12, but that excess is unchanged - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq(ghoLevel, 2e12, 'Unexpected GHO bucket level after initial sell'); (excess, deficit) = GHO_GSM_4626.getCurrentBacking(); assertEq(excess, (DEFAULT_GSM_USDC_EXPOSURE / 4) * 1e12, 'Unexpected excess'); @@ -976,7 +981,7 @@ contract TestGsm4626Edge is TestGhoBase { vm.stopPrank(); // Ensure GHO level is at the previous amount of excess, and excess is now 1e12 - (, ghoLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM_4626)); + ghoLevel = GHO_GSM_4626.getUsedGho(); assertEq( ghoLevel, (DEFAULT_GSM_USDC_EXPOSURE / 4) * 1e12, diff --git a/src/test/TestGsmFullFlow.t.sol b/src/test/TestGsmFullFlow.t.sol new file mode 100644 index 00000000..2b2e7ac1 --- /dev/null +++ b/src/test/TestGsmFullFlow.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; + +contract TestGsmFullFlow is TestGhoBase { + function testGsmFull() public { + OwnableFacilitator facilitator = new OwnableFacilitator(address(this), address(GHO_TOKEN)); + GHO_TOKEN.addFacilitator(address(facilitator), 'OwnableFacilitatorFlow', DEFAULT_CAPACITY); + + GhoReserve reserve = new GhoReserve(address(GHO_TOKEN)); + reserve.initialize(address(this)); + + Gsm gsm = new Gsm( + address(GHO_TOKEN), + address(USDC_TOKEN), + address(GHO_GSM_FIXED_PRICE_STRATEGY) + ); + gsm.initialize(address(this), TREASURY, DEFAULT_GSM_USDC_EXPOSURE, address(reserve)); + + reserve.addEntity(address(gsm)); + reserve.setLimit(address(gsm), 5_000_000 ether); + + uint256 mintAmount = 10_000_000 ether; + assertEq(GHO_TOKEN.balanceOf(address(reserve)), 0); + (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(capacity, DEFAULT_CAPACITY, 'Unexpected initial capacity'); + assertEq(level, 0, 'Unexpected initial level'); + + facilitator.mint(address(reserve), mintAmount); + + (, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(GHO_TOKEN.balanceOf(address(reserve)), mintAmount, 'Unexpected balanceOf GHO'); + assertEq(level, mintAmount, 'Unexpected level after mint'); + + // Use zero fees for simplicity + vm.prank(FAUCET); + USDC_TOKEN.mint(ALICE, DEFAULT_GSM_USDC_AMOUNT); + + vm.startPrank(ALICE); + USDC_TOKEN.approve(address(gsm), DEFAULT_GSM_USDC_AMOUNT); + vm.expectEmit(true, true, true, true, address(gsm)); + emit SellAsset(ALICE, ALICE, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, 0); + (uint256 assetAmount, uint256 ghoBought) = gsm.sellAsset(DEFAULT_GSM_USDC_AMOUNT, ALICE); + vm.stopPrank(); + + assertEq(ghoBought, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected GHO amount bought'); + assertEq(assetAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount sold'); + assertEq(USDC_TOKEN.balanceOf(ALICE), 0, 'Unexpected final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), DEFAULT_GSM_GHO_AMOUNT, 'Unexpected final GHO balance'); + assertEq(gsm.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); + assertEq(ghoBought, gsm.getUsedGho(), 'Unexpected amount of used GHO'); + + (uint256 limit, uint256 used) = reserve.getUsage(address(gsm)); + + assertEq( + limit - used, + reserve.getLimit(address(gsm)) - ghoBought, + 'Unexpected amount of available capacity' + ); + + // Buy assets as another user + ghoFaucet(BOB, DEFAULT_GSM_GHO_AMOUNT); + vm.startPrank(BOB); + GHO_TOKEN.approve(address(gsm), DEFAULT_GSM_GHO_AMOUNT); + vm.expectEmit(true, true, true, true, address(gsm)); + emit BuyAsset(BOB, BOB, DEFAULT_GSM_USDC_AMOUNT, DEFAULT_GSM_GHO_AMOUNT, 0); + (uint256 assetAmountBought, uint256 ghoSold) = gsm.buyAsset(DEFAULT_GSM_USDC_AMOUNT, BOB); + vm.stopPrank(); + + assertEq(ghoSold, DEFAULT_GSM_GHO_AMOUNT, 'Unexpected GHO amount sold'); + assertEq(assetAmountBought, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected asset amount bought'); + assertEq(USDC_TOKEN.balanceOf(BOB), DEFAULT_GSM_USDC_AMOUNT, 'Unexpected final USDC balance'); + assertEq(GHO_TOKEN.balanceOf(ALICE), DEFAULT_GSM_GHO_AMOUNT, 'Unexpected final GHO balance'); + assertEq(gsm.getExposureCap(), DEFAULT_GSM_USDC_EXPOSURE, 'Unexpected exposure capacity'); + assertEq(0, gsm.getUsedGho(), 'Unexpected amount of used GHO'); + + (limit, used) = reserve.getUsage(address(gsm)); + + assertEq( + limit - used, + reserve.getLimit(address(gsm)), + 'Unexpected amount of available capacity' + ); + + reserve.transfer(address(facilitator), GHO_TOKEN.balanceOf(address(reserve))); + + assertEq(GHO_TOKEN.balanceOf(address(reserve)), 0); + facilitator.burn(mintAmount); + + (, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(level, 0, 'Unexpected level after burn'); + + vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); + emit FacilitatorRemoved(address(facilitator)); + GHO_TOKEN.removeFacilitator(address(facilitator)); + } +} diff --git a/src/test/TestGsmSampleLiquidator.t.sol b/src/test/TestGsmSampleLiquidator.t.sol index 76f3ac1d..15efde75 100644 --- a/src/test/TestGsmSampleLiquidator.t.sol +++ b/src/test/TestGsmSampleLiquidator.t.sol @@ -39,7 +39,7 @@ contract TestGsmSampleLiquidator is TestGhoBase { assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seize amount returned'); // Mint the current bucket level - (, uint256 bucketLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); + uint256 bucketLevel = GHO_GSM.getUsedGho(); assertGt(bucketLevel, 0, 'Unexpected 0 minted GHO'); ghoFaucet(address(this), bucketLevel); @@ -67,7 +67,7 @@ contract TestGsmSampleLiquidator is TestGhoBase { assertEq(seizedAmount, DEFAULT_GSM_USDC_AMOUNT, 'Unexpected seize amount returned'); // Mint the current bucket level + 1, to have more GHO than necessary - (, uint256 bucketLevel) = GHO_TOKEN.getFacilitatorBucket(address(GHO_GSM)); + uint256 bucketLevel = GHO_GSM.getUsedGho(); assertGt(bucketLevel, 0, 'Unexpected 0 minted GHO'); ghoFaucet(address(this), bucketLevel + 1); diff --git a/src/test/TestGsmSwapEdge.t.sol b/src/test/TestGsmSwapEdge.t.sol index 318bcec8..10f4d433 100644 --- a/src/test/TestGsmSwapEdge.t.sol +++ b/src/test/TestGsmSwapEdge.t.sol @@ -19,8 +19,9 @@ contract TestGsmSwapEdge is TestGhoBase { 5 ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), type(uint256).max); // Sell 2 assets for 2e11 GHO vm.prank(FAUCET); @@ -76,9 +77,10 @@ contract TestGsmSwapEdge is TestGhoBase { 18 ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, 100_000_000e18); + gsm.initialize(address(this), TREASURY, 100_000_000e18, address(GHO_RESERVE)); gsm.updateFeeStrategy(address(newFeeStrategy)); - GHO_TOKEN.addFacilitator(address(gsm), 'Test GSM', DEFAULT_CAPACITY); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), 100_000_000 ether); // Get asset amount required to receive 1 GHO (uint256 assetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = gsm @@ -110,9 +112,10 @@ contract TestGsmSwapEdge is TestGhoBase { 18 ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, 100_000_000e18); + gsm.initialize(address(this), TREASURY, 100_000_000e18, address(GHO_RESERVE)); gsm.updateFeeStrategy(address(newFeeStrategy)); - GHO_TOKEN.addFacilitator(address(gsm), 'Test GSM', DEFAULT_CAPACITY); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), 100_000_000 ether); // Get asset amount required to receive 10000 GHO (uint256 assetAmount, uint256 ghoBought, uint256 grossAmount, uint256 fee) = gsm @@ -154,8 +157,9 @@ contract TestGsmSwapEdge is TestGhoBase { 24 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(ALICE, TREASURY, 1_000_000e24); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); + gsm.initialize(ALICE, TREASURY, 1_000_000e24, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), 100_000_000 ether); vm.startPrank(ALICE); newToken.approve(address(gsm), type(uint256).max); @@ -203,8 +207,9 @@ contract TestGsmSwapEdge is TestGhoBase { 24 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(ALICE, TREASURY, 1_000_000e24); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); + gsm.initialize(ALICE, TREASURY, 1_000_000e24, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), 100_000_000 ether); vm.startPrank(ALICE); // Alice sells some asset to the GSM @@ -247,8 +252,9 @@ contract TestGsmSwapEdge is TestGhoBase { 6 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(ALICE, TREASURY, 1_000_000e6); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); + gsm.initialize(ALICE, TREASURY, 1_000_000e6, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), 100_000_000 ether); // User wants to know how much asset must sell to get 1.9e12 GHO vm.startPrank(ALICE); @@ -297,8 +303,9 @@ contract TestGsmSwapEdge is TestGhoBase { 24 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(ALICE, TREASURY, 1_000_000e24); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); + gsm.initialize(ALICE, TREASURY, 1_000_000e24, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), 100_000_000 ether); // User wants to know how much asset must sell to get 1 GHO vm.startPrank(ALICE); @@ -347,8 +354,9 @@ contract TestGsmSwapEdge is TestGhoBase { 6 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(ALICE, TREASURY, 1_000_000e6); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); + gsm.initialize(ALICE, TREASURY, 1_000_000e6, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), 100_000_000 ether); vm.startPrank(ALICE); // Alice sells some asset to the GSM @@ -396,8 +404,9 @@ contract TestGsmSwapEdge is TestGhoBase { 24 // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(ALICE, TREASURY, 1_000_000e24); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', DEFAULT_CAPACITY); + gsm.initialize(ALICE, TREASURY, 1_000_000e24, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), 100_000_000 ether); vm.startPrank(ALICE); // Alice sells some asset to the GSM diff --git a/src/test/TestGsmSwapFuzz.t.sol b/src/test/TestGsmSwapFuzz.t.sol index d926d435..21cfc846 100644 --- a/src/test/TestGsmSwapFuzz.t.sol +++ b/src/test/TestGsmSwapFuzz.t.sol @@ -89,8 +89,10 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), type(uint128).max); + deal(address(GHO_TOKEN), address(GHO_RESERVE), type(uint256).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -173,7 +175,7 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); // Get gho amount for selling assets (uint256 assetSold, , uint256 ghoMinted, ) = gsm.getGhoAmountForSellAsset(amount); @@ -210,7 +212,7 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); GHO_TOKEN.addFacilitator(address(gsm), 'Test GSM', type(uint128).max); if (buyFeeBps > 0 || sellFeeBps > 0) { @@ -251,8 +253,10 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); - GHO_TOKEN.addFacilitator(address(gsm), 'Test GSM', type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), type(uint256).max); + deal(address(GHO_TOKEN), address(GHO_RESERVE), type(uint256).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -301,7 +305,9 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), type(uint256).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -344,7 +350,9 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), type(uint256).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -387,7 +395,9 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), type(uint256).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -431,7 +441,7 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -477,7 +487,7 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, uint128(assetAmount)); + gsm.initialize(address(this), TREASURY, uint128(assetAmount), address(GHO_RESERVE)); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -548,7 +558,7 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, uint128(assetAmount)); + gsm.initialize(address(this), TREASURY, uint128(assetAmount), address(GHO_RESERVE)); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -615,8 +625,9 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, uint128(assetAmount)); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', type(uint128).max); + gsm.initialize(address(this), TREASURY, uint128(assetAmount), address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), type(uint256).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -634,6 +645,7 @@ contract TestGsmSwapFuzz is TestGhoBase { assetAmount ); vm.assume(vars.estGhoAmount1 > 0); + deal(address(GHO_TOKEN), address(GHO_RESERVE), type(uint256).max); (vars.exactAssetAmount, vars.exactGhoAmount) = gsm.sellAsset(assetAmount, ALICE); @@ -712,8 +724,9 @@ contract TestGsmSwapFuzz is TestGhoBase { underlyingDecimals // decimals ); Gsm gsm = new Gsm(address(GHO_TOKEN), address(newToken), address(newPriceStrategy)); - gsm.initialize(address(this), TREASURY, type(uint128).max); - GHO_TOKEN.addFacilitator(address(gsm), 'GSM TINY', type(uint128).max); + gsm.initialize(address(this), TREASURY, type(uint128).max, address(GHO_RESERVE)); + GHO_RESERVE.addEntity(address(gsm)); + GHO_RESERVE.setLimit(address(gsm), type(uint256).max); if (buyFeeBps > 0 || sellFeeBps > 0) { FixedFeeStrategy newFeeStrategy = new FixedFeeStrategy(buyFeeBps, sellFeeBps); @@ -726,6 +739,8 @@ contract TestGsmSwapFuzz is TestGhoBase { sellAssetAmount = type(uint128).max; } + deal(address(GHO_TOKEN), address(GHO_RESERVE), type(uint256).max); + vm.prank(FAUCET); newToken.mint(ALICE, sellAssetAmount); @@ -738,6 +753,8 @@ contract TestGsmSwapFuzz is TestGhoBase { (, uint256 estGhoBought, , ) = gsm.getGhoAmountForBuyAsset(assetAmount); ghoFaucet(ALICE, estGhoBought * 20); + deal(address(GHO_TOKEN), address(GHO_RESERVE), estGhoBought); + // Buy vm.startPrank(ALICE); uint256 userGhoBefore = GHO_TOKEN.balanceOf(ALICE); diff --git a/src/test/TestOwnableFacilitator.t.sol b/src/test/TestOwnableFacilitator.t.sol new file mode 100644 index 00000000..172555f2 --- /dev/null +++ b/src/test/TestOwnableFacilitator.t.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import './TestGhoBase.t.sol'; + +contract TestOwnableFacilitator is TestGhoBase { + function testConstructor() public { + OwnableFacilitator facilitator = new OwnableFacilitator(address(this), address(GHO_TOKEN)); + assertEq(facilitator.GHO_TOKEN(), address(GHO_TOKEN)); + assertEq(facilitator.owner(), address(this)); + } + + function testRevertConstructorInvalidOwner() public { + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new OwnableFacilitator(address(0), address(GHO_TOKEN)); + } + + function testRevertConstructorInvalidGhoToken() public { + vm.expectRevert('ZERO_ADDRESS_NOT_VALID'); + new OwnableFacilitator(address(this), address(0)); + } + + function testMint() public { + OwnableFacilitator facilitator = _deployFacilitator(); + uint256 amount = 50_000_000 ether; + uint256 ghoBalanceBefore = GHO_TOKEN.balanceOf(address(this)); + (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, 0); + assertEq(ghoBalanceBefore, 0); + + facilitator.mint(address(this), amount); + + (capacity, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + uint256 ghoBalanceAfter = GHO_TOKEN.balanceOf(address(this)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, amount); + assertEq(amount, ghoBalanceAfter); + } + + function testMintFizz(uint256 amount) public { + vm.assume(amount > 0 && amount <= DEFAULT_CAPACITY); + + OwnableFacilitator facilitator = _deployFacilitator(); + uint256 ghoBalanceBefore = GHO_TOKEN.balanceOf(address(this)); + (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, 0); + assertEq(ghoBalanceBefore, 0); + + facilitator.mint(address(this), amount); + + (capacity, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + uint256 ghoBalanceAfter = GHO_TOKEN.balanceOf(address(this)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, amount); + assertEq(amount, ghoBalanceAfter); + } + + function testRevertMintIfMintIsTooHigh() public { + OwnableFacilitator facilitator = _deployFacilitator(); + vm.expectRevert('FACILITATOR_BUCKET_CAPACITY_EXCEEDED'); + facilitator.mint(address(this), DEFAULT_CAPACITY + 1); + } + + function testBurn() public { + OwnableFacilitator facilitator = _deployFacilitator(); + uint256 amount = 50_000_000 ether; + uint256 ghoBalanceBefore = GHO_TOKEN.balanceOf(address(this)); + (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, 0); + assertEq(ghoBalanceBefore, 0); + + facilitator.mint(address(this), amount); + + (capacity, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + uint256 ghoBalanceAfter = GHO_TOKEN.balanceOf(address(this)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, amount); + assertEq(amount, ghoBalanceAfter); + + GHO_TOKEN.transfer(address(facilitator), amount / 2); + facilitator.burn(amount / 2); + + (capacity, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + ghoBalanceAfter = GHO_TOKEN.balanceOf(address(this)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, amount / 2); + assertEq(amount / 2, ghoBalanceAfter); + } + + function testBurnFuzz(uint256 amount) public { + vm.assume(amount > 1 && amount <= DEFAULT_CAPACITY); + + OwnableFacilitator facilitator = _deployFacilitator(); + uint256 ghoBalanceBefore = GHO_TOKEN.balanceOf(address(this)); + (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, 0); + assertEq(ghoBalanceBefore, 0); + + facilitator.mint(address(this), amount); + + (capacity, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + uint256 ghoBalanceAfter = GHO_TOKEN.balanceOf(address(this)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, amount); + assertEq(amount, ghoBalanceAfter); + + GHO_TOKEN.transfer(address(facilitator), amount / 2); + facilitator.burn(amount / 2); + + (capacity, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + ghoBalanceAfter = GHO_TOKEN.balanceOf(address(this)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertApproxEqAbs(level, amount / 2, 1); + assertApproxEqAbs(amount / 2, ghoBalanceAfter, 1); + } + + function testRevertBurnIfNoBalance() public { + vm.expectRevert(); + OWNABLE_FACILITATOR.burn(1); + } + + function testOffboardFacilitator() public { + OwnableFacilitator facilitator = _deployFacilitator(); + (uint256 capacity, uint256 level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(capacity, DEFAULT_CAPACITY); + assertEq(level, 0); + + vm.expectEmit(true, false, false, true, address(GHO_TOKEN)); + emit FacilitatorRemoved(address(facilitator)); + GHO_TOKEN.removeFacilitator(address(facilitator)); + + (capacity, level) = GHO_TOKEN.getFacilitatorBucket(address(facilitator)); + + assertEq(capacity, 0); + assertEq(level, 0); + } + + function _deployFacilitator() internal returns (OwnableFacilitator) { + OwnableFacilitator facilitator = new OwnableFacilitator(address(this), address(GHO_TOKEN)); + GHO_TOKEN.addFacilitator(address(facilitator), 'OwnableFacilitatorTest', DEFAULT_CAPACITY); + + return facilitator; + } +} diff --git a/src/test/helpers/Events.sol b/src/test/helpers/Events.sol index c4fdc9fb..057cb879 100644 --- a/src/test/helpers/Events.sol +++ b/src/test/helpers/Events.sol @@ -127,4 +127,12 @@ interface Events { // Upgrades event Upgraded(address indexed implementation); + + // GhoReserve + event EntityAdded(address indexed entity); + event EntityRemoved(address indexed entity); + event GhoRestored(address indexed entity, uint256 amount); + event GhoTransferred(address indexed to, uint256 amount); + event GhoUsed(address indexed entity, uint256 amount); + event GhoLimitUpdated(address indexed entity, uint256 limit); }