Skip to content

Commit bbd23f5

Browse files
committed
fix: add minimum tokens amount for undelegate with beneficiary (TRST-H03)
1 parent 57aea44 commit bbd23f5

File tree

7 files changed

+33
-13
lines changed

7 files changed

+33
-13
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,13 @@ interface IHorizonStakingMain {
433433
*/
434434
error HorizonStakingInsufficientDelegationTokens(uint256 tokens, uint256 minTokens);
435435

436+
/**
437+
* @notice Thrown when the minimum token amount required for delegation with beneficiary is not met.
438+
* @param tokens The actual token amount
439+
* @param minTokens The minimum required token amount
440+
*/
441+
error HorizonStakingInsufficientUndelegationTokens(uint256 tokens, uint256 minTokens);
442+
436443
/**
437444
* @notice Thrown when attempting to undelegate with a beneficiary that is the zero address.
438445
*/

packages/horizon/contracts/staking/HorizonStaking.sol

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,17 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
3636
uint256 private constant FIXED_POINT_PRECISION = 1e18;
3737

3838
/// @dev Maximum number of simultaneous stake thaw requests (per provision) or undelegations (per delegation)
39-
uint256 private constant MAX_THAW_REQUESTS = 100;
39+
uint256 private constant MAX_THAW_REQUESTS = 1_000;
4040

4141
/// @dev Address of the staking extension contract
4242
address private immutable STAKING_EXTENSION_ADDRESS;
4343

4444
/// @dev Minimum amount of delegation.
4545
uint256 private constant MIN_DELEGATION = 1e18;
4646

47+
/// @dev Minimum amount of undelegation with beneficiary.
48+
uint256 private constant MIN_UNDELEGATION_WITH_BENEFICIARY = 10e18;
49+
4750
/**
4851
* @notice Checks that the caller is authorized to operate over a provision.
4952
* @param serviceProvider The address of the service provider.
@@ -931,6 +934,17 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
931934
// delegation pool shares -> delegation pool tokens -> thawing pool shares
932935
// Thawing pool is reset/initialized when the pool is empty: prov.tokensThawing == 0
933936
uint256 tokens = (_shares * (pool.tokens - pool.tokensThawing)) / pool.shares;
937+
938+
// Since anyone can undelegate for any beneficiary, we require a minimum amount to prevent
939+
// malicious actors from flooding the thaw request list with tiny amounts and causing a
940+
// denial of service attack by hitting the MAX_THAW_REQUESTS limit
941+
if (_requestType == ThawRequestType.DelegationWithBeneficiary) {
942+
require(
943+
tokens >= MIN_UNDELEGATION_WITH_BENEFICIARY,
944+
HorizonStakingInsufficientUndelegationTokens(tokens, MIN_UNDELEGATION_WITH_BENEFICIARY)
945+
);
946+
}
947+
934948
// Thawing shares are rounded down to protect the pool and avoid taking extra tokens from other participants.
935949
uint256 thawingShares = pool.tokensThawing == 0 ? tokens : ((tokens * pool.sharesThawing) / pool.tokensThawing);
936950
uint64 thawingUntil = uint64(block.timestamp + uint256(_provisions[_serviceProvider][_verifier].thawingPeriod));

packages/horizon/test/staking/delegation/undelegate.t.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest {
6363
address beneficiary
6464
) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) {
6565
vm.assume(beneficiary != address(0));
66+
vm.assume(delegationAmount >= MIN_UNDELEGATION_WITH_BENEFICIARY);
6667
resetPrank(users.delegator);
6768
DelegationInternal memory delegation = _getStorage_Delegation(users.indexer, subgraphDataServiceAddress, users.delegator, false);
6869
_undelegateWithBeneficiary(users.indexer, subgraphDataServiceAddress, delegation.shares, beneficiary);
@@ -95,7 +96,7 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest {
9596
public
9697
useIndexer
9798
useProvision(1000 ether, 0, 0)
98-
useDelegation(1000 ether)
99+
useDelegation(10000 ether)
99100
{
100101
resetPrank(users.delegator);
101102

packages/horizon/test/staking/delegation/withdraw.t.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest {
167167
{
168168
vm.assume(beneficiary != address(0));
169169
vm.assume(beneficiary != address(staking));
170+
vm.assume(delegationAmount >= MIN_UNDELEGATION_WITH_BENEFICIARY);
170171
// Skip beneficiary if balance will overflow
171172
vm.assume(token.balanceOf(beneficiary) < type(uint256).max - delegationAmount);
172173

@@ -196,6 +197,7 @@ contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest {
196197
{
197198
vm.assume(beneficiary != address(0));
198199
vm.assume(beneficiary != users.delegator);
200+
vm.assume(delegationAmount >= MIN_UNDELEGATION_WITH_BENEFICIARY);
199201

200202
// Delegator undelegates to beneficiary
201203
resetPrank(users.delegator);

packages/horizon/test/staking/provision/deprovision.t.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ contract HorizonStakingDeprovisionTest is HorizonStakingTest {
1717
uint256 thawCount,
1818
uint256 deprovisionCount
1919
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) {
20-
thawCount = bound(thawCount, 1, MAX_THAW_REQUESTS);
20+
thawCount = bound(thawCount, 1, 100);
2121
deprovisionCount = bound(deprovisionCount, 0, thawCount);
2222
vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step
2323
uint256 individualThawAmount = amount / thawCount;
@@ -37,7 +37,7 @@ contract HorizonStakingDeprovisionTest is HorizonStakingTest {
3737
uint64 thawingPeriod,
3838
uint256 thawCount
3939
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) {
40-
thawCount = bound(thawCount, 2, MAX_THAW_REQUESTS);
40+
thawCount = bound(thawCount, 2, 100);
4141
vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step
4242
uint256 individualThawAmount = amount / thawCount;
4343

packages/horizon/test/staking/provision/thaw.t.sol

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ contract HorizonStakingThawTest is HorizonStakingTest {
2626
uint64 thawingPeriod,
2727
uint256 thawCount
2828
) public useIndexer useProvision(amount, 0, thawingPeriod) {
29-
thawCount = bound(thawCount, 1, MAX_THAW_REQUESTS);
29+
thawCount = bound(thawCount, 1, 100);
3030
vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step
3131
uint256 individualThawAmount = amount / thawCount;
3232

@@ -72,13 +72,8 @@ contract HorizonStakingThawTest is HorizonStakingTest {
7272
staking.thaw(users.indexer, subgraphDataServiceAddress, thawAmount);
7373
}
7474

75-
function testThaw_RevertWhen_OverMaxThawRequests(
76-
uint256 amount,
77-
uint64 thawingPeriod,
78-
uint256 thawAmount
79-
) public useIndexer useProvision(amount, 0, thawingPeriod) {
80-
vm.assume(amount >= MAX_THAW_REQUESTS + 1);
81-
thawAmount = bound(thawAmount, 1, amount / (MAX_THAW_REQUESTS + 1));
75+
function testThaw_RevertWhen_OverMaxThawRequests() public useIndexer useProvision(10000 ether, 0, 0) {
76+
uint256 thawAmount = 1 ether;
8277

8378
for (uint256 i = 0; i < MAX_THAW_REQUESTS; i++) {
8479
_thaw(users.indexer, subgraphDataServiceAddress, thawAmount);

packages/horizon/test/utils/Constants.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ abstract contract Constants {
1111
// GraphPayments parameters
1212
uint256 internal constant protocolPaymentCut = 10000;
1313
// Staking constants
14-
uint256 internal constant MAX_THAW_REQUESTS = 100;
14+
uint256 internal constant MAX_THAW_REQUESTS = 1_000;
1515
uint64 internal constant MAX_THAWING_PERIOD = 28 days;
1616
uint32 internal constant THAWING_PERIOD_IN_BLOCKS = 300;
1717
uint256 internal constant MIN_DELEGATION = 1e18;
18+
uint256 internal constant MIN_UNDELEGATION_WITH_BENEFICIARY = 10e18;
1819
// Epoch manager
1920
uint256 internal constant EPOCH_LENGTH = 1;
2021
// Rewards manager

0 commit comments

Comments
 (0)