Skip to content

Commit ebd830d

Browse files
committed
feat: update timelock logic
1 parent dd67b36 commit ebd830d

File tree

6 files changed

+195
-68
lines changed

6 files changed

+195
-68
lines changed

contracts/0.8.25/Accounting.sol

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ contract Accounting is VaultHub {
6969
uint256 postTotalPooledEther;
7070
/// @notice amount of ether to be locked in the vaults
7171
uint256[] vaultsLockedEther;
72+
/// @notice amount of ether to be locked in the vaults
73+
uint256[] vaultsThresholdEther;
7274
/// @notice amount of shares to be minted as vault fees to the treasury
7375
uint256[] vaultsTreasuryFeeShares;
7476
/// @notice total amount of shares to be minted as vault fees to the treasury
@@ -225,7 +227,12 @@ contract Accounting is VaultHub {
225227

226228
// Calculate the amount of ether locked in the vaults to back external balance of stETH
227229
// and the amount of shares to mint as fees to the treasury for each vaults
228-
(update.vaultsLockedEther, update.vaultsTreasuryFeeShares, update.totalVaultsTreasuryFeeShares) =
230+
(
231+
update.vaultsLockedEther,
232+
update.vaultsThresholdEther,
233+
update.vaultsTreasuryFeeShares,
234+
update.totalVaultsTreasuryFeeShares
235+
) =
229236
_calculateVaultsRebase(
230237
update.postTotalShares,
231238
update.postTotalPooledEther,
@@ -339,6 +346,7 @@ contract Accounting is VaultHub {
339346
_report.vaultValues,
340347
_report.inOutDeltas,
341348
_update.vaultsLockedEther,
349+
_update.vaultsThresholdEther,
342350
_update.vaultsTreasuryFeeShares
343351
);
344352

contracts/0.8.25/vaults/Dashboard.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,11 @@ contract Dashboard is Permissions {
168168
}
169169

170170
/**
171-
* @notice Returns the force withdrawal unlock time of the vault.
172-
* @return The force withdrawal unlock time as a uint40.
171+
* @notice Returns the time when the vault became unbalanced.
172+
* @return The time when the vault became unbalanced as a uint40.
173173
*/
174-
function forceWithdrawalUnlockTime() external view returns (uint40) {
175-
return vaultSocket().forceWithdrawalUnlockTime;
174+
function unbalancedSince() external view returns (uint40) {
175+
return vaultSocket().unbalancedSince;
176176
}
177177

178178
/**

contracts/0.8.25/vaults/StakingVault.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,6 @@ contract StakingVault is IStakingVault, OwnableUpgradeable {
307307

308308
ERC7201Storage storage $ = _getStorage();
309309
if (owner() == msg.sender || (_valuation < $.locked && msg.sender == address(VAULT_HUB))) {
310-
311310
$.inOutDelta -= int128(int256(_ether));
312311

313312
emit Withdrawn(msg.sender, address(VAULT_HUB), _ether);

contracts/0.8.25/vaults/VaultHub.sol

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ abstract contract VaultHub is PausableUntilWithRoles {
4949
uint16 treasuryFeeBP;
5050
/// @notice if true, vault is disconnected and fee is not accrued
5151
bool isDisconnected;
52-
/// @notice timestamp when the vault can force withdraw in case it is unbalanced
52+
/// @notice timestamp when the vault became unbalanced
5353
/// @dev 0 if the vault is currently balanced
54-
uint40 forceWithdrawalUnlockTime;
54+
uint40 unbalancedSince;
5555
// ### we have 64 bits left in this slot
5656
}
5757

@@ -73,7 +73,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
7373
uint256 internal constant CONNECT_DEPOSIT = 1 ether;
7474

7575
/// @notice Time-lock for force validator withdrawal
76-
uint256 public constant FORCE_WITHDRAWAL_TIMELOCK = 3 days;
76+
uint40 public constant FORCE_WITHDRAWAL_TIMELOCK = 3 days;
7777

7878
/// @notice Lido stETH contract
7979
IStETH public immutable STETH;
@@ -166,7 +166,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
166166
uint16(_reserveRatioThresholdBP),
167167
uint16(_treasuryFeeBP),
168168
false, // isDisconnected
169-
0 // forceWithdrawalUnlockTime
169+
0 // unbalancedSince
170170
);
171171
$.vaultIndex[_vault] = $.sockets.length;
172172
$.sockets.push(vr);
@@ -233,10 +233,11 @@ abstract contract VaultHub is PausableUntilWithRoles {
233233
if (vaultSharesAfterMint > shareLimit) revert ShareLimitExceeded(_vault, shareLimit);
234234

235235
uint256 reserveRatioBP = socket.reserveRatioBP;
236-
uint256 maxMintableShares = _maxMintableShares(_vault, reserveRatioBP, shareLimit);
236+
uint256 valuation = IStakingVault(_vault).valuation();
237+
uint256 maxMintableShares = _maxMintableShares(valuation, reserveRatioBP, shareLimit);
237238

238239
if (vaultSharesAfterMint > maxMintableShares) {
239-
revert InsufficientValuationToMint(_vault, IStakingVault(_vault).valuation());
240+
revert InsufficientValuationToMint(_vault, valuation);
240241
}
241242

242243
socket.sharesMinted = uint96(vaultSharesAfterMint);
@@ -272,7 +273,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
272273

273274
STETH.burnExternalShares(_amountOfShares);
274275

275-
_updateUnbalancedState(_vault, socket);
276+
_vaultAssessment(_vault, socket);
276277

277278
emit BurnedSharesOnVault(_vault, _amountOfShares);
278279
}
@@ -293,7 +294,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
293294

294295
VaultSocket storage socket = _connectedSocket(_vault);
295296

296-
uint256 threshold = _maxMintableShares(_vault, socket.reserveRatioThresholdBP, socket.shareLimit);
297+
uint256 valuation = IStakingVault(_vault).valuation();
298+
uint256 threshold = _maxMintableShares(valuation, socket.reserveRatioThresholdBP, socket.shareLimit);
297299
uint256 sharesMinted = socket.sharesMinted;
298300
if (sharesMinted <= threshold) {
299301
// NOTE!: on connect vault is always balanced
@@ -316,13 +318,12 @@ abstract contract VaultHub is PausableUntilWithRoles {
316318
// reserveRatio = BPS_BASE - maxMintableRatio
317319
// X = (mintedStETH * BPS_BASE - vault.valuation() * maxMintableRatio) / reserveRatio
318320

319-
uint256 amountToRebalance = (mintedStETH * TOTAL_BASIS_POINTS -
320-
IStakingVault(_vault).valuation() * maxMintableRatio) / reserveRatioBP;
321+
uint256 amountToRebalance = (mintedStETH * TOTAL_BASIS_POINTS - valuation * maxMintableRatio) / reserveRatioBP;
321322

322323
// TODO: add some gas compensation here
323324
IStakingVault(_vault).rebalance(amountToRebalance);
324325

325-
// NB: check _updateUnbalancedState is calculated in rebalance() triggered from the `StakingVault`.
326+
// NB: check _updateUnbalancedSince is calculated in rebalance() triggered from the `StakingVault`.
326327
}
327328

328329
/// @notice rebalances the vault by writing off the amount of ether equal
@@ -341,8 +342,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
341342

342343
STETH.rebalanceExternalEtherToInternal{value: msg.value}();
343344

344-
// Check if vault is still unbalanced after rebalance
345-
_updateUnbalancedState(msg.sender, socket);
345+
_vaultAssessment(msg.sender, socket);
346346

347347
emit VaultRebalanced(msg.sender, sharesToBurn);
348348
}
@@ -351,29 +351,31 @@ abstract contract VaultHub is PausableUntilWithRoles {
351351
/// @param _vault vault address
352352
/// @return bool whether the vault can force withdraw
353353
function canForceValidatorWithdrawal(address _vault) public view returns (bool) {
354-
uint40 forceWithdrawalUnlockTime = _connectedSocket(_vault).forceWithdrawalUnlockTime;
354+
uint40 unbalancedSince = _connectedSocket(_vault).unbalancedSince;
355355

356-
if (forceWithdrawalUnlockTime == 0) return false;
356+
if (unbalancedSince == 0) return false;
357357

358-
return block.timestamp >= forceWithdrawalUnlockTime;
358+
return block.timestamp >= unbalancedSince + FORCE_WITHDRAWAL_TIMELOCK;
359359
}
360360

361361
/// @notice forces validator withdrawal from the beacon chain in case the vault is unbalanced
362362
/// @param _vault vault address
363363
/// @param _pubkeys pubkeys of the validators to withdraw
364364
function forceValidatorWithdrawal(address _vault, bytes calldata _pubkeys) external payable {
365+
if (msg.value == 0) revert ZeroArgument("msg.value");
365366
if (_vault == address(0)) revert ZeroArgument("_vault");
366367
if (_pubkeys.length == 0) revert ZeroArgument("_pubkeys");
367368

368369
VaultSocket storage socket = _connectedSocket(_vault);
369370

370-
uint256 threshold = _maxMintableShares(_vault, socket.reserveRatioThresholdBP, socket.shareLimit);
371+
uint256 valuation = IStakingVault(_vault).valuation();
372+
uint256 threshold = _maxMintableShares(valuation, socket.reserveRatioThresholdBP, socket.shareLimit);
371373
if (socket.sharesMinted <= threshold) {
372374
revert AlreadyBalanced(_vault, socket.sharesMinted, threshold);
373375
}
374376

375377
if (!canForceValidatorWithdrawal(_vault)) {
376-
revert ForceWithdrawalTimelockActive(_vault, socket.forceWithdrawalUnlockTime);
378+
revert ForceWithdrawalTimelockActive(_vault, socket.unbalancedSince + FORCE_WITHDRAWAL_TIMELOCK);
377379
}
378380

379381
IStakingVault(_vault).forceValidatorWithdrawal{value: msg.value}(_pubkeys);
@@ -403,7 +405,12 @@ abstract contract VaultHub is PausableUntilWithRoles {
403405
uint256 _preTotalShares,
404406
uint256 _preTotalPooledEther,
405407
uint256 _sharesToMintAsFees
406-
) internal view returns (uint256[] memory lockedEther, uint256[] memory treasuryFeeShares, uint256 totalTreasuryFeeShares) {
408+
) internal view returns (
409+
uint256[] memory lockedEther,
410+
uint256[] memory thresholdEther,
411+
uint256[] memory treasuryFeeShares,
412+
uint256 totalTreasuryFeeShares
413+
) {
407414
/// HERE WILL BE ACCOUNTING DRAGON
408415

409416
// \||/
@@ -424,6 +431,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
424431

425432
treasuryFeeShares = new uint256[](length);
426433
lockedEther = new uint256[](length);
434+
thresholdEther = new uint256[](length);
427435

428436
for (uint256 i = 0; i < length; ++i) {
429437
VaultSocket memory socket = $.sockets[i + 1];
@@ -444,6 +452,9 @@ abstract contract VaultHub is PausableUntilWithRoles {
444452
(mintedStETH * TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - socket.reserveRatioBP),
445453
CONNECT_DEPOSIT
446454
);
455+
456+
// Minimum amount of ether that should be in the vault to avoid unbalanced state
457+
thresholdEther[i] = (mintedStETH * TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - socket.reserveRatioThresholdBP);
447458
}
448459
}
449460
}
@@ -472,7 +483,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
472483

473484
// TODO: optimize potential rewards calculation
474485
uint256 potentialRewards = ((chargeableValue * (_postTotalPooledEther * _preTotalShares)) /
475-
(_postTotalSharesNoFees * _preTotalPooledEther) - chargeableValue);
486+
(_postTotalSharesNoFees * _preTotalPooledEther) - chargeableValue);
476487
uint256 treasuryFee = (potentialRewards * _socket.treasuryFeeBP) / TOTAL_BASIS_POINTS;
477488

478489
treasuryFeeShares = (treasuryFee * _preTotalShares) / _preTotalPooledEther;
@@ -482,6 +493,7 @@ abstract contract VaultHub is PausableUntilWithRoles {
482493
uint256[] memory _valuations,
483494
int256[] memory _inOutDeltas,
484495
uint256[] memory _locked,
496+
uint256[] memory _thresholds,
485497
uint256[] memory _treasureFeeShares
486498
) internal {
487499
VaultHubStorage storage $ = _getVaultHubStorage();
@@ -496,7 +508,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
496508
socket.sharesMinted += uint96(treasuryFeeShares);
497509
}
498510

499-
_updateUnbalancedState(socket.vault, socket);
511+
_epicrisis(_valuations[i], _thresholds[i], socket);
512+
500513

501514
IStakingVault(socket.vault).report(_valuations[i], _inOutDeltas[i], _locked[i]);
502515
}
@@ -517,21 +530,25 @@ abstract contract VaultHub is PausableUntilWithRoles {
517530
}
518531
}
519532

520-
function _updateUnbalancedState(address _vault, VaultSocket storage _socket) internal {
521-
uint256 threshold = _maxMintableShares(_vault, _socket.reserveRatioThresholdBP, _socket.shareLimit);
522-
bool isUnbalanced = _socket.sharesMinted > threshold;
523-
uint40 currentUnlockTime = _socket.forceWithdrawalUnlockTime;
533+
/// @notice Evaluates if vault's valuation meets minimum threshold and marks it as unbalanced if below threshold
534+
function _vaultAssessment(address _vault, VaultSocket storage _socket) internal {
535+
uint256 valuation = IStakingVault(_vault).valuation();
536+
uint256 threshold = (_socket.sharesMinted * TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - _socket.reserveRatioThresholdBP);
537+
538+
_epicrisis(valuation, threshold, _socket);
539+
}
524540

525-
if (isUnbalanced) {
526-
if (currentUnlockTime == 0) {
527-
uint40 newUnlockTime = uint40(block.timestamp + FORCE_WITHDRAWAL_TIMELOCK);
528-
_socket.forceWithdrawalUnlockTime = newUnlockTime;
529-
emit VaultBecameUnbalanced(_vault, newUnlockTime);
541+
/// @notice Updates vault's unbalanced state based on if valuation is above/below threshold
542+
function _epicrisis(uint256 _valuation, uint256 _threshold, VaultSocket storage _socket) internal {
543+
if (_valuation < _threshold) {
544+
if (_socket.unbalancedSince == 0) {
545+
_socket.unbalancedSince = uint40(block.timestamp);
546+
emit VaultBecameUnbalanced(address(_socket.vault), _socket.unbalancedSince + FORCE_WITHDRAWAL_TIMELOCK);
530547
}
531548
} else {
532-
if (currentUnlockTime != 0) {
533-
_socket.forceWithdrawalUnlockTime = 0;
534-
emit VaultBecameBalanced(_vault);
549+
if (_socket.unbalancedSince != 0) {
550+
_socket.unbalancedSince = 0;
551+
emit VaultBecameBalanced(address(_socket.vault));
535552
}
536553
}
537554
}
@@ -549,9 +566,8 @@ abstract contract VaultHub is PausableUntilWithRoles {
549566

550567
/// @dev returns total number of stETH shares that is possible to mint on the provided vault with provided reserveRatio
551568
/// it does not count shares that is already minted, but does count shareLimit on the vault
552-
function _maxMintableShares(address _vault, uint256 _reserveRatio, uint256 _shareLimit) internal view returns (uint256) {
553-
uint256 maxStETHMinted = (IStakingVault(_vault).valuation() * (TOTAL_BASIS_POINTS - _reserveRatio)) /
554-
TOTAL_BASIS_POINTS;
569+
function _maxMintableShares(uint256 _valuation, uint256 _reserveRatio, uint256 _shareLimit) internal view returns (uint256) {
570+
uint256 maxStETHMinted = (_valuation * (TOTAL_BASIS_POINTS - _reserveRatio)) / TOTAL_BASIS_POINTS;
555571

556572
return Math256.min(STETH.getSharesByPooledEth(maxStETHMinted), _shareLimit);
557573
}

test/0.8.25/vaults/dashboard/dashboard.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe("Dashboard.sol", () => {
179179
reserveRatioThresholdBP: 800n,
180180
treasuryFeeBP: 500n,
181181
isDisconnected: false,
182-
forceWithdrawalUnlockTime: 0n,
182+
unbalancedSince: 0n,
183183
};
184184

185185
await hub.mock__setVaultSocket(vault, sockets);
@@ -190,7 +190,7 @@ describe("Dashboard.sol", () => {
190190
expect(await dashboard.reserveRatioBP()).to.equal(sockets.reserveRatioBP);
191191
expect(await dashboard.thresholdReserveRatioBP()).to.equal(sockets.reserveRatioThresholdBP);
192192
expect(await dashboard.treasuryFee()).to.equal(sockets.treasuryFeeBP);
193-
expect(await dashboard.forceWithdrawalUnlockTime()).to.equal(sockets.forceWithdrawalUnlockTime);
193+
expect(await dashboard.unbalancedSince()).to.equal(sockets.unbalancedSince);
194194
});
195195
});
196196

@@ -217,7 +217,7 @@ describe("Dashboard.sol", () => {
217217
reserveRatioThresholdBP: 800n,
218218
treasuryFeeBP: 500n,
219219
isDisconnected: false,
220-
forceWithdrawalUnlockTime: 0n,
220+
unbalancedSince: 0n,
221221
};
222222

223223
await hub.mock__setVaultSocket(vault, sockets);
@@ -240,7 +240,7 @@ describe("Dashboard.sol", () => {
240240
reserveRatioThresholdBP: 800n,
241241
treasuryFeeBP: 500n,
242242
isDisconnected: false,
243-
forceWithdrawalUnlockTime: 0n,
243+
unbalancedSince: 0n,
244244
};
245245

246246
await hub.mock__setVaultSocket(vault, sockets);
@@ -261,7 +261,7 @@ describe("Dashboard.sol", () => {
261261
reserveRatioThresholdBP: 800n,
262262
treasuryFeeBP: 500n,
263263
isDisconnected: false,
264-
forceWithdrawalUnlockTime: 0n,
264+
unbalancedSince: 0n,
265265
};
266266

267267
await hub.mock__setVaultSocket(vault, sockets);
@@ -282,7 +282,7 @@ describe("Dashboard.sol", () => {
282282
reserveRatioThresholdBP: 0n,
283283
treasuryFeeBP: 500n,
284284
isDisconnected: false,
285-
forceWithdrawalUnlockTime: 0n,
285+
unbalancedSince: 0n,
286286
};
287287

288288
await hub.mock__setVaultSocket(vault, sockets);
@@ -311,7 +311,7 @@ describe("Dashboard.sol", () => {
311311
reserveRatioThresholdBP: 800n,
312312
treasuryFeeBP: 500n,
313313
isDisconnected: false,
314-
forceWithdrawalUnlockTime: 0n,
314+
unbalancedSince: 0n,
315315
};
316316

317317
await hub.mock__setVaultSocket(vault, sockets);
@@ -338,7 +338,7 @@ describe("Dashboard.sol", () => {
338338
reserveRatioThresholdBP: 800n,
339339
treasuryFeeBP: 500n,
340340
isDisconnected: false,
341-
forceWithdrawalUnlockTime: 0n,
341+
unbalancedSince: 0n,
342342
};
343343

344344
await hub.mock__setVaultSocket(vault, sockets);
@@ -362,7 +362,7 @@ describe("Dashboard.sol", () => {
362362
reserveRatioThresholdBP: 800n,
363363
treasuryFeeBP: 500n,
364364
isDisconnected: false,
365-
forceWithdrawalUnlockTime: 0n,
365+
unbalancedSince: 0n,
366366
};
367367

368368
await hub.mock__setVaultSocket(vault, sockets);
@@ -384,7 +384,7 @@ describe("Dashboard.sol", () => {
384384
reserveRatioThresholdBP: 800n,
385385
treasuryFeeBP: 500n,
386386
isDisconnected: false,
387-
forceWithdrawalUnlockTime: 0n,
387+
unbalancedSince: 0n,
388388
};
389389

390390
await hub.mock__setVaultSocket(vault, sockets);
@@ -409,7 +409,7 @@ describe("Dashboard.sol", () => {
409409
reserveRatioThresholdBP: 800n,
410410
treasuryFeeBP: 500n,
411411
isDisconnected: false,
412-
forceWithdrawalUnlockTime: 0n,
412+
unbalancedSince: 0n,
413413
};
414414

415415
await hub.mock__setVaultSocket(vault, sockets);

0 commit comments

Comments
 (0)