Skip to content

Commit 7035403

Browse files
authored
fix: tweaks to thawing pool management (#1048)
* fix: wip tweaks to thawing pool management Signed-off-by: Tomás Migone <[email protected]> * fix: handle edge cases for thawing pools Signed-off-by: Tomás Migone <[email protected]> * fix: simplify some conditions and checks Signed-off-by: Tomás Migone <[email protected]> * chore: fix test comment Signed-off-by: Tomás Migone <[email protected]> * fix: typos and variable rename Signed-off-by: Tomás Migone <[email protected]> * fix: merge conflicts Signed-off-by: Tomás Migone <[email protected]> * fix: improve thawing pool reset condition Signed-off-by: Tomás Migone <[email protected]> --------- Signed-off-by: Tomás Migone <[email protected]>
1 parent f0294b0 commit 7035403

File tree

12 files changed

+651
-109
lines changed

12 files changed

+651
-109
lines changed

packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,15 @@ interface IHorizonStakingMain {
254254
* @param tokens The amount of tokens being released
255255
* @param shares The amount of shares being released
256256
* @param thawingUntil The timestamp until the stake has thawed
257+
* @param valid Whether the thaw request was valid at the time of fulfillment
257258
*/
258-
event ThawRequestFulfilled(bytes32 indexed thawRequestId, uint256 tokens, uint256 shares, uint64 thawingUntil);
259+
event ThawRequestFulfilled(
260+
bytes32 indexed thawRequestId,
261+
uint256 tokens,
262+
uint256 shares,
263+
uint64 thawingUntil,
264+
bool valid
265+
);
259266

260267
/**
261268
* @notice Emitted when a series of thaw requests are fulfilled.
@@ -742,6 +749,8 @@ interface IHorizonStakingMain {
742749
* Tokens can be automatically re-delegated to another provision by setting `newServiceProvider`.
743750
* @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw
744751
* requests in the event that fulfilling all of them results in a gas limit error.
752+
* @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill
753+
* the thaw requests with an amount equal to zero.
745754
*
746755
* Requirements:
747756
* - Must have previously initiated a thaw request using {undelegate}.

packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ interface IHorizonStakingTypes {
3232
uint32 maxVerifierCutPending;
3333
// Pending value for `thawingPeriod`. Verifier needs to accept it before it becomes active.
3434
uint64 thawingPeriodPending;
35+
// Value of the current thawing nonce. Thaw requests with older nonces are invalid.
36+
uint256 thawingNonce;
3537
}
3638

3739
/**
@@ -75,6 +77,8 @@ interface IHorizonStakingTypes {
7577
uint256 tokensThawing;
7678
// Shares representing the thawing tokens
7779
uint256 sharesThawing;
80+
// Value of the current thawing nonce. Thaw requests with older nonces are invalid.
81+
uint256 thawingNonce;
7882
}
7983

8084
/**
@@ -101,6 +105,8 @@ interface IHorizonStakingTypes {
101105
uint256 tokensThawing;
102106
// Shares representing the thawing tokens
103107
uint256 sharesThawing;
108+
// Value of the current thawing nonce. Thaw requests with older nonces are invalid.
109+
uint256 thawingNonce;
104110
}
105111

106112
/**
@@ -137,5 +143,7 @@ interface IHorizonStakingTypes {
137143
uint64 thawingUntil;
138144
// Id of the next thaw request in the linked list
139145
bytes32 next;
146+
// Used to invalidate unfulfilled thaw requests
147+
uint256 thawingNonce;
140148
}
141149
}

packages/horizon/contracts/staking/HorizonStaking.sol

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,14 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
411411
(FIXED_POINT_PRECISION);
412412
prov.tokens = prov.tokens - providerTokensSlashed;
413413

414+
// If the slashing leaves the thawing shares with no thawing tokens, cancel pending thawings by:
415+
// - deleting all thawing shares
416+
// - incrementing the nonce to invalidate pending thaw requests
417+
if (prov.sharesThawing != 0 && prov.tokensThawing == 0) {
418+
prov.sharesThawing = 0;
419+
prov.thawingNonce++;
420+
}
421+
414422
// Service provider accounting
415423
_serviceProviders[serviceProvider].tokensProvisioned =
416424
_serviceProviders[serviceProvider].tokensProvisioned -
@@ -438,6 +446,16 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
438446
(pool.tokensThawing * (FIXED_POINT_PRECISION - delegationFractionSlashed)) /
439447
FIXED_POINT_PRECISION;
440448

449+
// If the slashing leaves the thawing shares with no thawing tokens, cancel pending thawings by:
450+
// - deleting all thawing shares
451+
// - incrementing the nonce to invalidate pending thaw requests
452+
// Note that thawing shares are completely lost, delegators won't get back the corresponding
453+
// delegation pool shares.
454+
if (pool.sharesThawing != 0 && pool.tokensThawing == 0) {
455+
pool.sharesThawing = 0;
456+
pool.thawingNonce++;
457+
}
458+
441459
emit DelegationSlashed(serviceProvider, verifier, tokensToSlash);
442460
} else {
443461
emit DelegationSlashingSkipped(serviceProvider, verifier, tokensToSlash);
@@ -644,7 +662,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
644662
thawingPeriod: _thawingPeriod,
645663
createdAt: uint64(block.timestamp),
646664
maxVerifierCutPending: _maxVerifierCut,
647-
thawingPeriodPending: _thawingPeriod
665+
thawingPeriodPending: _thawingPeriod,
666+
thawingNonce: 0
648667
});
649668

650669
ServiceProviderInternal storage sp = _serviceProviders[_serviceProvider];
@@ -672,25 +691,34 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
672691

673692
/**
674693
* @notice See {IHorizonStakingMain-thaw}.
694+
* @dev We use a thawing pool to keep track of tokens thawing for multiple thaw requests.
695+
* If due to slashing the thawing pool loses all of its tokens, the pool is reset and all pending thaw
696+
* requests are invalidated.
675697
*/
676698
function _thaw(address _serviceProvider, address _verifier, uint256 _tokens) private returns (bytes32) {
677699
require(_tokens != 0, HorizonStakingInvalidZeroTokens());
678700
uint256 tokensAvailable = _getProviderTokensAvailable(_serviceProvider, _verifier);
679701
require(tokensAvailable >= _tokens, HorizonStakingInsufficientTokens(tokensAvailable, _tokens));
680702

681703
Provision storage prov = _provisions[_serviceProvider][_verifier];
682-
uint256 thawingShares = prov.sharesThawing == 0 ? _tokens : (prov.sharesThawing * _tokens) / prov.tokensThawing;
704+
705+
// Calculate shares to issue
706+
// Thawing pool is reset/initialized when the pool is empty: prov.tokensThawing == 0
707+
uint256 thawingShares = prov.tokensThawing == 0
708+
? _tokens
709+
: ((prov.sharesThawing * _tokens) / prov.tokensThawing);
683710
uint64 thawingUntil = uint64(block.timestamp + uint256(prov.thawingPeriod));
684711

685-
prov.tokensThawing = prov.tokensThawing + _tokens;
686712
prov.sharesThawing = prov.sharesThawing + thawingShares;
713+
prov.tokensThawing = prov.tokensThawing + _tokens;
687714

688715
bytes32 thawRequestId = _createThawRequest(
689716
_serviceProvider,
690717
_verifier,
691718
_serviceProvider,
692719
thawingShares,
693-
thawingUntil
720+
thawingUntil,
721+
prov.thawingNonce
694722
);
695723
emit ProvisionThawed(_serviceProvider, _verifier, _tokens);
696724
return thawRequestId;
@@ -715,7 +743,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
715743
_serviceProvider,
716744
tokensThawing,
717745
sharesThawing,
718-
_nThawRequests
746+
_nThawRequests,
747+
prov.thawingNonce
719748
);
720749

721750
prov.tokens = prov.tokens - tokensThawed_;
@@ -741,15 +770,19 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
741770
DelegationPoolInternal storage pool = _getDelegationPool(_serviceProvider, _verifier);
742771
DelegationInternal storage delegation = pool.delegators[msg.sender];
743772

773+
// An invalid delegation pool has shares but no tokens
744774
require(
745-
pool.tokens != 0 || (pool.shares == 0 && pool.sharesThawing == 0),
775+
pool.tokens != 0 || pool.shares == 0,
746776
HorizonStakingInvalidDelegationPoolState(_serviceProvider, _verifier)
747777
);
748778

749779
// Calculate shares to issue
750-
uint256 shares = (pool.tokens == 0 || pool.tokens == pool.tokensThawing)
751-
? _tokens
752-
: ((_tokens * pool.shares) / (pool.tokens - pool.tokensThawing));
780+
// Delegation pool is reset/initialized in any of the following cases:
781+
// - pool.tokens == 0 and pool.shares == 0, pool is completely empty. Note that we don't test shares == 0 because
782+
// the invalid delegation pool check already ensures shares are 0 if tokens are 0
783+
// - pool.tokens == pool.tokensThawing, the entire pool is thawing
784+
bool initializePool = pool.tokens == 0 || pool.tokens == pool.tokensThawing;
785+
uint256 shares = initializePool ? _tokens : ((_tokens * pool.shares) / (pool.tokens - pool.tokensThawing));
753786
require(shares != 0 && shares >= _minSharesOut, HorizonStakingSlippageProtection(shares, _minSharesOut));
754787

755788
pool.tokens = pool.tokens + _tokens;
@@ -764,6 +797,12 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
764797
* @notice See {IHorizonStakingMain-undelegate}.
765798
* @dev To allow delegation to be slashable even while thawing without breaking accounting
766799
* the delegation pool shares are burned and replaced with thawing pool shares.
800+
* @dev Note that due to slashing the delegation pool can enter an invalid state if all it's tokens are slashed.
801+
* An invalid pool can only be recovered by adding back tokens into the pool with {IHorizonStakingMain-addToDelegationPool}.
802+
* Any time the delegation pool is invalidated, the thawing pool is also reset and any pending undelegate requests get
803+
* invalidated.
804+
* Note that delegation that is caught thawing when the pool is invalidated will be completely lost! However delegation shares
805+
* that were not thawing will be preserved.
767806
*/
768807
function _undelegate(
769808
address _serviceProvider,
@@ -775,23 +814,30 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
775814
DelegationPoolInternal storage pool = _getDelegationPool(_serviceProvider, _verifier);
776815
DelegationInternal storage delegation = pool.delegators[msg.sender];
777816
require(delegation.shares >= _shares, HorizonStakingInsufficientShares(delegation.shares, _shares));
817+
818+
// An invalid delegation pool has shares but no tokens (previous require check ensures shares > 0)
778819
require(pool.tokens != 0, HorizonStakingInvalidDelegationPoolState(_serviceProvider, _verifier));
779820

821+
// Calculate thawing shares to issue - convert delegation pool shares to thawing pool shares
822+
// delegation pool shares -> delegation pool tokens -> thawing pool shares
823+
// Thawing pool is reset/initialized when the pool is empty: prov.tokensThawing == 0
780824
uint256 tokens = (_shares * (pool.tokens - pool.tokensThawing)) / pool.shares;
781825
uint256 thawingShares = pool.tokensThawing == 0 ? tokens : ((tokens * pool.sharesThawing) / pool.tokensThawing);
782826
uint64 thawingUntil = uint64(block.timestamp + uint256(_provisions[_serviceProvider][_verifier].thawingPeriod));
783-
pool.shares = pool.shares - _shares;
827+
784828
pool.tokensThawing = pool.tokensThawing + tokens;
785829
pool.sharesThawing = pool.sharesThawing + thawingShares;
786830

831+
pool.shares = pool.shares - _shares;
787832
delegation.shares = delegation.shares - _shares;
788833

789834
bytes32 thawRequestId = _createThawRequest(
790835
_serviceProvider,
791836
_verifier,
792837
beneficiary,
793838
thawingShares,
794-
thawingUntil
839+
thawingUntil,
840+
pool.thawingNonce
795841
);
796842

797843
emit TokensUndelegated(_serviceProvider, _verifier, msg.sender, tokens);
@@ -809,7 +855,12 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
809855
uint256 _nThawRequests
810856
) private {
811857
DelegationPoolInternal storage pool = _getDelegationPool(_serviceProvider, _verifier);
812-
require(pool.tokens != 0, HorizonStakingInvalidDelegationPoolState(_serviceProvider, _verifier));
858+
859+
// An invalid delegation pool has shares but no tokens
860+
require(
861+
pool.tokens != 0 || pool.shares == 0,
862+
HorizonStakingInvalidDelegationPoolState(_serviceProvider, _verifier)
863+
);
813864

814865
uint256 tokensThawed = 0;
815866
uint256 sharesThawing = pool.sharesThawing;
@@ -820,9 +871,12 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
820871
msg.sender,
821872
tokensThawing,
822873
sharesThawing,
823-
_nThawRequests
874+
_nThawRequests,
875+
pool.thawingNonce
824876
);
825877

878+
// The next subtraction should never revert becase: pool.tokens >= pool.tokensThawing and pool.tokensThawing >= tokensThawed
879+
// In the event the pool gets completely slashed tokensThawed will fulfil to 0.
826880
pool.tokens = pool.tokens - tokensThawed;
827881
pool.sharesThawing = sharesThawing;
828882
pool.tokensThawing = tokensThawing;
@@ -849,20 +903,27 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
849903
* @param _owner The address of the owner of the thaw request
850904
* @param _shares The number of shares to thaw
851905
* @param _thawingUntil The timestamp until which the shares are thawing
906+
* @param _thawingNonce Owner's validity nonce for the thaw request
852907
* @return The ID of the thaw request
853908
*/
854909
function _createThawRequest(
855910
address _serviceProvider,
856911
address _verifier,
857912
address _owner,
858913
uint256 _shares,
859-
uint64 _thawingUntil
914+
uint64 _thawingUntil,
915+
uint256 _thawingNonce
860916
) private returns (bytes32) {
861917
LinkedList.List storage thawRequestList = _thawRequestLists[_serviceProvider][_verifier][_owner];
862918
require(thawRequestList.count < MAX_THAW_REQUESTS, HorizonStakingTooManyThawRequests());
863919

864920
bytes32 thawRequestId = keccak256(abi.encodePacked(_serviceProvider, _verifier, _owner, thawRequestList.nonce));
865-
_thawRequests[thawRequestId] = ThawRequest({ shares: _shares, thawingUntil: _thawingUntil, next: bytes32(0) });
921+
_thawRequests[thawRequestId] = ThawRequest({
922+
shares: _shares,
923+
thawingUntil: _thawingUntil,
924+
next: bytes32(0),
925+
thawingNonce: _thawingNonce
926+
});
866927

867928
if (thawRequestList.count != 0) _thawRequests[thawRequestList.tail].next = thawRequestId;
868929
thawRequestList.addTail(thawRequestId);
@@ -880,6 +941,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
880941
* @param _tokensThawing The current amount of tokens already thawing
881942
* @param _sharesThawing The current amount of shares already thawing
882943
* @param _nThawRequests The number of thaw requests to fulfill. If set to 0, all thaw requests are fulfilled.
944+
* @param _thawingNonce The current valid thawing nonce. Any thaw request with a different nonce is invalid and should be ignored.
883945
* @return The amount of thawed tokens
884946
* @return The amount of tokens still thawing
885947
* @return The amount of shares still thawing
@@ -890,7 +952,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
890952
address _owner,
891953
uint256 _tokensThawing,
892954
uint256 _sharesThawing,
893-
uint256 _nThawRequests
955+
uint256 _nThawRequests,
956+
uint256 _thawingNonce
894957
) private returns (uint256, uint256, uint256) {
895958
LinkedList.List storage thawRequestList = _thawRequestLists[_serviceProvider][_verifier][_owner];
896959
require(thawRequestList.count > 0, HorizonStakingNothingThawing());
@@ -900,7 +963,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
900963
_getNextThawRequest,
901964
_fulfillThawRequest,
902965
_deleteThawRequest,
903-
abi.encode(tokensThawed, _tokensThawing, _sharesThawing),
966+
abi.encode(tokensThawed, _tokensThawing, _sharesThawing, _thawingNonce),
904967
_nThawRequests
905968
);
906969

@@ -929,20 +992,30 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
929992
}
930993

931994
// decode
932-
(uint256 tokensThawed, uint256 tokensThawing, uint256 sharesThawing) = abi.decode(
995+
(uint256 tokensThawed, uint256 tokensThawing, uint256 sharesThawing, uint256 thawingNonce) = abi.decode(
933996
_acc,
934-
(uint256, uint256, uint256)
997+
(uint256, uint256, uint256, uint256)
935998
);
936999

937-
// process
938-
uint256 tokens = (thawRequest.shares * tokensThawing) / sharesThawing;
939-
tokensThawing = tokensThawing - tokens;
940-
sharesThawing = sharesThawing - thawRequest.shares;
941-
tokensThawed = tokensThawed + tokens;
942-
emit ThawRequestFulfilled(_thawRequestId, tokens, thawRequest.shares, thawRequest.thawingUntil);
1000+
// process - only fulfill thaw requests for the current valid nonce
1001+
uint256 tokens = 0;
1002+
bool validThawRequest = thawRequest.thawingNonce == thawingNonce;
1003+
if (validThawRequest) {
1004+
tokens = (thawRequest.shares * tokensThawing) / sharesThawing;
1005+
tokensThawing = tokensThawing - tokens;
1006+
sharesThawing = sharesThawing - thawRequest.shares;
1007+
tokensThawed = tokensThawed + tokens;
1008+
}
1009+
emit ThawRequestFulfilled(
1010+
_thawRequestId,
1011+
tokens,
1012+
thawRequest.shares,
1013+
thawRequest.thawingUntil,
1014+
validThawRequest
1015+
);
9431016

9441017
// encode
945-
_acc = abi.encode(tokensThawed, tokensThawing, sharesThawing);
1018+
_acc = abi.encode(tokensThawed, tokensThawing, sharesThawing, thawingNonce);
9461019
return (false, _acc);
9471020
}
9481021

packages/horizon/contracts/staking/HorizonStakingBase.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ abstract contract HorizonStakingBase is
9696
pool.shares = poolInternal.shares;
9797
pool.tokensThawing = poolInternal.tokensThawing;
9898
pool.sharesThawing = poolInternal.sharesThawing;
99+
pool.thawingNonce = poolInternal.thawingNonce;
99100
return pool;
100101
}
101102

0 commit comments

Comments
 (0)