Skip to content

Commit 942920c

Browse files
committed
chore(sgho): set supply cap at initialization
1 parent 778a45c commit 942920c

File tree

3 files changed

+124
-24
lines changed

3 files changed

+124
-24
lines changed

src/contracts/extensions/sgho/interfaces/IsGHO.sol

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ interface IsGHO {
4545
*/
4646
error RateMustBeLessThanMaxRate();
4747

48+
/**
49+
* @notice Thrown when a deposit or mint would exceed the total supply cap.
50+
*/
51+
error SupplyCapExceeded();
52+
4853
// --- Events ---
4954

5055
/**
@@ -53,6 +58,20 @@ interface IsGHO {
5358
*/
5459
event TargetRateUpdated(uint256 newRate);
5560

61+
/**
62+
* @notice Emitted when ERC20 tokens are rescued from the contract.
63+
* @param caller The address that initiated the rescue operation.
64+
* @param token The address of the rescued ERC20 token.
65+
* @param to The recipient address of the rescued tokens.
66+
* @param amount The amount of tokens rescued.
67+
*/
68+
event ERC20Rescued(
69+
address indexed caller,
70+
address indexed token,
71+
address indexed to,
72+
uint256 amount
73+
);
74+
5675
// --- State Variables (as view functions) ---
5776

5877
/**
@@ -61,6 +80,12 @@ interface IsGHO {
6180
*/
6281
function gho() external view returns (address);
6382

83+
/**
84+
* @notice Returns the total supply cap of the vault.
85+
* @return The total supply cap.
86+
*/
87+
function supplyCap() external view returns (uint256);
88+
6489
/**
6590
* @notice Returns the chain ID of the network where the contract is deployed.
6691
* @dev This is used for EIP-712 signature validation to prevent replay attacks across different chains.
@@ -116,7 +141,12 @@ interface IsGHO {
116141
* @dev This function can only be called once. It sets up initial roles and configurations.
117142
* While the function is marked as `payable`, it is designed to reject any attached Ether value.
118143
*/
119-
function initialize(address gho_, address aclManager_, uint256 maxTargetRate_) external payable;
144+
function initialize(
145+
address gho_,
146+
address aclManager_,
147+
uint256 maxTargetRate_,
148+
uint256 supplyCap_
149+
) external payable;
120150

121151
/**
122152
* @notice Overload of the standard ERC20Permit `permit` function.
@@ -166,20 +196,6 @@ interface IsGHO {
166196

167197
// --- Events ---
168198

169-
/**
170-
* @notice Emitted when ERC20 tokens are rescued from the contract.
171-
* @param caller The address that initiated the rescue operation.
172-
* @param token The address of the rescued ERC20 token.
173-
* @param to The recipient address of the rescued tokens.
174-
* @param amount The amount of tokens rescued.
175-
*/
176-
event ERC20Rescued(
177-
address indexed caller,
178-
address indexed token,
179-
address indexed to,
180-
uint256 amount
181-
);
182-
183199
/**
184200
* @notice The receive function is implemented to reject direct Ether transfers to the contract.
185201
*/

src/contracts/extensions/sgho/sGHO.sol

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ contract sGHO is Initializable, ERC4626Upgradeable, ERC20PermitUpgradeable, IsGH
2727

2828
uint256 public targetRate;
2929
uint256 public maxTargetRate;
30+
uint256 public supplyCap;
3031
uint256 public yieldIndex;
3132
uint256 public lastUpdate;
3233
uint256 internal constant RATE_PRECISION = 1e10;
@@ -51,11 +52,13 @@ contract sGHO is Initializable, ERC4626Upgradeable, ERC20PermitUpgradeable, IsGH
5152
* @param _gho Address of the underlying GHO token.
5253
* @param _aclmanager Address of the Aave ACL Manager.
5354
* @param _maxTargetRate The maximum allowable target rate.
55+
* @param _supplyCap The total supply cap for the vault.
5456
*/
5557
function initialize(
5658
address _gho,
5759
address _aclmanager,
58-
uint256 _maxTargetRate
60+
uint256 _maxTargetRate,
61+
uint256 _supplyCap
5962
) public payable initializer {
6063
__ERC20_init('sGHO', 'sGHO');
6164
__ERC4626_init(IERC20(_gho));
@@ -64,6 +67,7 @@ contract sGHO is Initializable, ERC4626Upgradeable, ERC20PermitUpgradeable, IsGH
6467
gho = _gho;
6568
aclManager = IAccessControl(_aclmanager);
6669
maxTargetRate = _maxTargetRate;
70+
supplyCap = _supplyCap;
6771

6872
deploymentChainId = block.chainid;
6973
yieldIndex = WadRayMath.RAY;
@@ -178,6 +182,22 @@ contract sGHO is Initializable, ERC4626Upgradeable, ERC20PermitUpgradeable, IsGH
178182
);
179183
}
180184

185+
function maxDeposit(address) public view override(ERC4626Upgradeable) returns (uint256) {
186+
uint256 currentAssets = totalAssets();
187+
if (currentAssets >= supplyCap) {
188+
return 0;
189+
}
190+
return supplyCap - currentAssets;
191+
}
192+
193+
function maxMint(address) public view override(ERC4626Upgradeable) returns (uint256) {
194+
uint256 currentAssets = totalAssets();
195+
if (currentAssets >= supplyCap) {
196+
return 0;
197+
}
198+
return convertToShares(supplyCap - currentAssets);
199+
}
200+
181201
/**
182202
* @notice Deposits GHO into the vault and mints sGHO shares to the receiver.
183203
* @dev The yield index is updated before the deposit to ensure correct share calculation.

tests/extensions/sgho/sGHO.t.sol

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ contract sGhoTest is TestnetProcedures {
3636
address internal yManager; // Yield manager user
3737

3838
uint256 internal constant MAX_TARGET_RATE = 5000; // 50%
39+
uint256 internal constant SUPPLY_CAP = 1_000_000 ether; // 1M GHO
3940

4041
// Permit constants
4142
string internal constant VERSION = '1'; // Matches sGHO constructor
@@ -66,7 +67,8 @@ contract sGhoTest is TestnetProcedures {
6667
sGHO.initialize.selector,
6768
address(gho),
6869
address(contracts.aclManager),
69-
MAX_TARGET_RATE
70+
MAX_TARGET_RATE,
71+
SUPPLY_CAP
7072
)
7173
)
7274
)
@@ -302,18 +304,28 @@ contract sGhoTest is TestnetProcedures {
302304
}
303305

304306
function test_4626_maxMethods() external {
305-
// Deposit max checks (no limits implemented in this sGHO version)
306-
assertEq(sgho.maxDeposit(user1), type(uint256).max, 'maxDeposit should be max');
307-
assertEq(sgho.maxMint(user1), type(uint256).max, 'maxMint should be max');
307+
// Max deposit should be the supply cap initially
308+
assertEq(sgho.maxDeposit(user1), SUPPLY_CAP, 'maxDeposit should be supply cap');
308309

309-
// Withdraw max checks
310+
// Max mint should correspond to the supply cap
311+
uint256 expectedMaxMint = sgho.convertToShares(SUPPLY_CAP);
312+
assertEq(sgho.maxMint(user1), expectedMaxMint, 'maxMint should be supply cap in shares');
313+
314+
// Deposit some amount and check max withdraw/redeem
310315
vm.startPrank(user1);
311316
uint256 depositAmount = 100 ether;
312317
sgho.deposit(depositAmount, user1);
313318
uint256 shares = sgho.balanceOf(user1);
314319

315320
assertEq(sgho.maxWithdraw(user1), depositAmount, 'maxWithdraw mismatch');
316321
assertEq(sgho.maxRedeem(user1), shares, 'maxRedeem mismatch');
322+
323+
// Max deposit should be reduced by the deposited amount
324+
assertEq(
325+
sgho.maxDeposit(user1),
326+
SUPPLY_CAP - depositAmount,
327+
'maxDeposit should be reduced'
328+
);
317329
vm.stopPrank();
318330
}
319331

@@ -359,6 +371,51 @@ contract sGhoTest is TestnetProcedures {
359371
vm.stopPrank();
360372
}
361373

374+
// --- Supply Cap Tests ---
375+
function test_revert_deposit_exceedsCap() external {
376+
vm.startPrank(user1);
377+
uint256 amount = SUPPLY_CAP + 1;
378+
vm.expectRevert(
379+
abi.encodeWithSelector(
380+
ERC4626.ERC4626ExceededMaxDeposit.selector,
381+
user1,
382+
amount,
383+
SUPPLY_CAP
384+
)
385+
);
386+
sgho.deposit(amount, user1);
387+
vm.stopPrank();
388+
}
389+
390+
function test_revert_mint_exceedsCap() external {
391+
vm.startPrank(user1);
392+
uint256 shares = sgho.convertToShares(SUPPLY_CAP) + 1;
393+
uint256 maxShares = sgho.maxMint(user1);
394+
vm.expectRevert(
395+
abi.encodeWithSelector(
396+
ERC4626.ERC4626ExceededMaxMint.selector,
397+
user1,
398+
shares,
399+
maxShares
400+
)
401+
);
402+
sgho.mint(shares, user1);
403+
vm.stopPrank();
404+
}
405+
406+
function test_deposit_atCap() external {
407+
vm.startPrank(user1);
408+
sgho.deposit(SUPPLY_CAP, user1);
409+
assertEq(sgho.totalAssets(), SUPPLY_CAP, 'Total assets should equal supply cap');
410+
// The contract balance will be the supply cap plus the 1 GHO donated in setUp
411+
assertEq(
412+
gho.balanceOf(address(sgho)),
413+
SUPPLY_CAP + 1 ether,
414+
'Contract balance should be supply cap + initial donation'
415+
);
416+
vm.stopPrank();
417+
}
418+
362419
// --- Yield Integration Tests (_updateVault) ---
363420

364421
function test_yield_claimSavingsIntegration(uint256 depositAmount, uint64 timeSkip) external {
@@ -790,7 +847,8 @@ contract sGhoTest is TestnetProcedures {
790847
sGHO.initialize.selector,
791848
address(gho),
792849
address(contracts.aclManager),
793-
MAX_TARGET_RATE
850+
MAX_TARGET_RATE,
851+
SUPPLY_CAP
794852
)
795853
)
796854
)
@@ -811,15 +869,21 @@ contract sGhoTest is TestnetProcedures {
811869
sGHO.initialize.selector,
812870
address(gho),
813871
address(contracts.aclManager),
814-
MAX_TARGET_RATE
872+
MAX_TARGET_RATE,
873+
SUPPLY_CAP
815874
)
816875
);
817876

818877
sGHO newSgho = sGHO(payable(address(proxy)));
819878

820879
// Should revert on second initialization via proxy
821880
vm.expectRevert();
822-
newSgho.initialize(address(gho), address(contracts.aclManager), MAX_TARGET_RATE);
881+
newSgho.initialize(
882+
address(gho),
883+
address(contracts.aclManager),
884+
MAX_TARGET_RATE,
885+
SUPPLY_CAP
886+
);
823887
}
824888

825889
function _wadPow(uint256 base, uint256 exp) internal pure returns (uint256) {

0 commit comments

Comments
 (0)