@@ -411,6 +411,14 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
411
411
(FIXED_POINT_PRECISION);
412
412
prov.tokens = prov.tokens - providerTokensSlashed;
413
413
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
+
414
422
// Service provider accounting
415
423
_serviceProviders[serviceProvider].tokensProvisioned =
416
424
_serviceProviders[serviceProvider].tokensProvisioned -
@@ -438,6 +446,16 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
438
446
(pool.tokensThawing * (FIXED_POINT_PRECISION - delegationFractionSlashed)) /
439
447
FIXED_POINT_PRECISION;
440
448
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
+
441
459
emit DelegationSlashed (serviceProvider, verifier, tokensToSlash);
442
460
} else {
443
461
emit DelegationSlashingSkipped (serviceProvider, verifier, tokensToSlash);
@@ -644,7 +662,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
644
662
thawingPeriod: _thawingPeriod,
645
663
createdAt: uint64 (block .timestamp ),
646
664
maxVerifierCutPending: _maxVerifierCut,
647
- thawingPeriodPending: _thawingPeriod
665
+ thawingPeriodPending: _thawingPeriod,
666
+ thawingNonce: 0
648
667
});
649
668
650
669
ServiceProviderInternal storage sp = _serviceProviders[_serviceProvider];
@@ -672,25 +691,34 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
672
691
673
692
/**
674
693
* @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.
675
697
*/
676
698
function _thaw (address _serviceProvider , address _verifier , uint256 _tokens ) private returns (bytes32 ) {
677
699
require (_tokens != 0 , HorizonStakingInvalidZeroTokens ());
678
700
uint256 tokensAvailable = _getProviderTokensAvailable (_serviceProvider, _verifier);
679
701
require (tokensAvailable >= _tokens, HorizonStakingInsufficientTokens (tokensAvailable, _tokens));
680
702
681
703
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);
683
710
uint64 thawingUntil = uint64 (block .timestamp + uint256 (prov.thawingPeriod));
684
711
685
- prov.tokensThawing = prov.tokensThawing + _tokens;
686
712
prov.sharesThawing = prov.sharesThawing + thawingShares;
713
+ prov.tokensThawing = prov.tokensThawing + _tokens;
687
714
688
715
bytes32 thawRequestId = _createThawRequest (
689
716
_serviceProvider,
690
717
_verifier,
691
718
_serviceProvider,
692
719
thawingShares,
693
- thawingUntil
720
+ thawingUntil,
721
+ prov.thawingNonce
694
722
);
695
723
emit ProvisionThawed (_serviceProvider, _verifier, _tokens);
696
724
return thawRequestId;
@@ -715,7 +743,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
715
743
_serviceProvider,
716
744
tokensThawing,
717
745
sharesThawing,
718
- _nThawRequests
746
+ _nThawRequests,
747
+ prov.thawingNonce
719
748
);
720
749
721
750
prov.tokens = prov.tokens - tokensThawed_;
@@ -741,15 +770,19 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
741
770
DelegationPoolInternal storage pool = _getDelegationPool (_serviceProvider, _verifier);
742
771
DelegationInternal storage delegation = pool.delegators[msg .sender ];
743
772
773
+ // An invalid delegation pool has shares but no tokens
744
774
require (
745
- pool.tokens != 0 || ( pool.shares == 0 && pool.sharesThawing == 0 ) ,
775
+ pool.tokens != 0 || pool.shares == 0 ,
746
776
HorizonStakingInvalidDelegationPoolState (_serviceProvider, _verifier)
747
777
);
748
778
749
779
// 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));
753
786
require (shares != 0 && shares >= _minSharesOut, HorizonStakingSlippageProtection (shares, _minSharesOut));
754
787
755
788
pool.tokens = pool.tokens + _tokens;
@@ -764,6 +797,12 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
764
797
* @notice See {IHorizonStakingMain-undelegate}.
765
798
* @dev To allow delegation to be slashable even while thawing without breaking accounting
766
799
* 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.
767
806
*/
768
807
function _undelegate (
769
808
address _serviceProvider ,
@@ -775,23 +814,30 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
775
814
DelegationPoolInternal storage pool = _getDelegationPool (_serviceProvider, _verifier);
776
815
DelegationInternal storage delegation = pool.delegators[msg .sender ];
777
816
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)
778
819
require (pool.tokens != 0 , HorizonStakingInvalidDelegationPoolState (_serviceProvider, _verifier));
779
820
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
780
824
uint256 tokens = (_shares * (pool.tokens - pool.tokensThawing)) / pool.shares;
781
825
uint256 thawingShares = pool.tokensThawing == 0 ? tokens : ((tokens * pool.sharesThawing) / pool.tokensThawing);
782
826
uint64 thawingUntil = uint64 (block .timestamp + uint256 (_provisions[_serviceProvider][_verifier].thawingPeriod));
783
- pool.shares = pool.shares - _shares;
827
+
784
828
pool.tokensThawing = pool.tokensThawing + tokens;
785
829
pool.sharesThawing = pool.sharesThawing + thawingShares;
786
830
831
+ pool.shares = pool.shares - _shares;
787
832
delegation.shares = delegation.shares - _shares;
788
833
789
834
bytes32 thawRequestId = _createThawRequest (
790
835
_serviceProvider,
791
836
_verifier,
792
837
beneficiary,
793
838
thawingShares,
794
- thawingUntil
839
+ thawingUntil,
840
+ pool.thawingNonce
795
841
);
796
842
797
843
emit TokensUndelegated (_serviceProvider, _verifier, msg .sender , tokens);
@@ -809,7 +855,12 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
809
855
uint256 _nThawRequests
810
856
) private {
811
857
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
+ );
813
864
814
865
uint256 tokensThawed = 0 ;
815
866
uint256 sharesThawing = pool.sharesThawing;
@@ -820,9 +871,12 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
820
871
msg .sender ,
821
872
tokensThawing,
822
873
sharesThawing,
823
- _nThawRequests
874
+ _nThawRequests,
875
+ pool.thawingNonce
824
876
);
825
877
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.
826
880
pool.tokens = pool.tokens - tokensThawed;
827
881
pool.sharesThawing = sharesThawing;
828
882
pool.tokensThawing = tokensThawing;
@@ -849,20 +903,27 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
849
903
* @param _owner The address of the owner of the thaw request
850
904
* @param _shares The number of shares to thaw
851
905
* @param _thawingUntil The timestamp until which the shares are thawing
906
+ * @param _thawingNonce Owner's validity nonce for the thaw request
852
907
* @return The ID of the thaw request
853
908
*/
854
909
function _createThawRequest (
855
910
address _serviceProvider ,
856
911
address _verifier ,
857
912
address _owner ,
858
913
uint256 _shares ,
859
- uint64 _thawingUntil
914
+ uint64 _thawingUntil ,
915
+ uint256 _thawingNonce
860
916
) private returns (bytes32 ) {
861
917
LinkedList.List storage thawRequestList = _thawRequestLists[_serviceProvider][_verifier][_owner];
862
918
require (thawRequestList.count < MAX_THAW_REQUESTS, HorizonStakingTooManyThawRequests ());
863
919
864
920
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
+ });
866
927
867
928
if (thawRequestList.count != 0 ) _thawRequests[thawRequestList.tail].next = thawRequestId;
868
929
thawRequestList.addTail (thawRequestId);
@@ -880,6 +941,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
880
941
* @param _tokensThawing The current amount of tokens already thawing
881
942
* @param _sharesThawing The current amount of shares already thawing
882
943
* @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.
883
945
* @return The amount of thawed tokens
884
946
* @return The amount of tokens still thawing
885
947
* @return The amount of shares still thawing
@@ -890,7 +952,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
890
952
address _owner ,
891
953
uint256 _tokensThawing ,
892
954
uint256 _sharesThawing ,
893
- uint256 _nThawRequests
955
+ uint256 _nThawRequests ,
956
+ uint256 _thawingNonce
894
957
) private returns (uint256 , uint256 , uint256 ) {
895
958
LinkedList.List storage thawRequestList = _thawRequestLists[_serviceProvider][_verifier][_owner];
896
959
require (thawRequestList.count > 0 , HorizonStakingNothingThawing ());
@@ -900,7 +963,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
900
963
_getNextThawRequest,
901
964
_fulfillThawRequest,
902
965
_deleteThawRequest,
903
- abi.encode (tokensThawed, _tokensThawing, _sharesThawing),
966
+ abi.encode (tokensThawed, _tokensThawing, _sharesThawing, _thawingNonce ),
904
967
_nThawRequests
905
968
);
906
969
@@ -929,20 +992,30 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
929
992
}
930
993
931
994
// decode
932
- (uint256 tokensThawed , uint256 tokensThawing , uint256 sharesThawing ) = abi.decode (
995
+ (uint256 tokensThawed , uint256 tokensThawing , uint256 sharesThawing , uint256 thawingNonce ) = abi.decode (
933
996
_acc,
934
- (uint256 , uint256 , uint256 )
997
+ (uint256 , uint256 , uint256 , uint256 )
935
998
);
936
999
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
+ );
943
1016
944
1017
// encode
945
- _acc = abi.encode (tokensThawed, tokensThawing, sharesThawing);
1018
+ _acc = abi.encode (tokensThawed, tokensThawing, sharesThawing, thawingNonce );
946
1019
return (false , _acc);
947
1020
}
948
1021
0 commit comments