diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingBase.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingBase.sol index e221dc2cf..0145dac16 100644 --- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingBase.sol +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingBase.sol @@ -24,6 +24,11 @@ interface IHorizonStakingBase { */ event StakeDeposited(address indexed serviceProvider, uint256 tokens); + /** + * @notice Thrown when using an invalid thaw request type. + */ + error HorizonStakingInvalidThawRequestType(); + /** * @notice Gets the details of a service provider. * @param serviceProvider The address of the service provider. @@ -134,21 +139,27 @@ interface IHorizonStakingBase { /** * @notice Gets a thaw request. + * @param thawRequestType The type of thaw request. * @param thawRequestId The id of the thaw request. * @return The thaw request details. */ - function getThawRequest(bytes32 thawRequestId) external view returns (IHorizonStakingTypes.ThawRequest memory); + function getThawRequest( + IHorizonStakingTypes.ThawRequestType thawRequestType, + bytes32 thawRequestId + ) external view returns (IHorizonStakingTypes.ThawRequest memory); /** * @notice Gets the metadata of a thaw request list. * Service provider and delegators each have their own thaw request list per provision. * Metadata includes the head and tail of the list, plus the total number of thaw requests. + * @param thawRequestType The type of thaw request. * @param serviceProvider The address of the service provider. * @param verifier The address of the verifier. * @param owner The owner of the thaw requests. Use either the service provider or delegator address. * @return The thaw requests list metadata. */ function getThawRequestList( + IHorizonStakingTypes.ThawRequestType thawRequestType, address serviceProvider, address verifier, address owner @@ -156,12 +167,18 @@ interface IHorizonStakingBase { /** * @notice Gets the amount of thawed tokens for a given provision. + * @param thawRequestType The type of thaw request. * @param serviceProvider The address of the service provider. * @param verifier The address of the verifier. * @param owner The owner of the thaw requests. Use either the service provider or delegator address. * @return The amount of thawed tokens. */ - function getThawedTokens(address serviceProvider, address verifier, address owner) external view returns (uint256); + function getThawedTokens( + IHorizonStakingTypes.ThawRequestType thawRequestType, + address serviceProvider, + address verifier, + address owner + ) external view returns (uint256); /** * @notice Gets the maximum allowed thawing period for a provision. diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingExtension.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingExtension.sol index a0b2dc1af..84318f536 100644 --- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingExtension.sol +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingExtension.sol @@ -77,6 +77,12 @@ interface IHorizonStakingExtension is IRewardsIssuer { uint256 delegationRewards ); + /** + * @dev Emitted when `indexer` was slashed for a total of `tokens` amount. + * Tracks `reward` amount of tokens given to `beneficiary`. + */ + event StakeSlashed(address indexed indexer, uint256 tokens, uint256 reward, address beneficiary); + /** * @notice Close an allocation and free the staked tokens. * To be eligible for rewards a proof of indexing must be presented. @@ -148,4 +154,14 @@ interface IHorizonStakingExtension is IRewardsIssuer { */ // solhint-disable-next-line func-name-mixedcase function __DEPRECATED_getThawingPeriod() external view returns (uint64); + + /** + * @notice Slash the indexer stake. Delegated tokens are not subject to slashing. + * @dev Can only be called by the slasher role. + * @param indexer Address of indexer to slash + * @param tokens Amount of tokens to slash from the indexer stake + * @param reward Amount of reward tokens to send to a beneficiary + * @param beneficiary Address of a beneficiary to receive a reward for the slashing + */ + function legacySlash(address indexer, uint256 tokens, uint256 reward, address beneficiary) external; } diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol index b144b0ce6..50ca90fc3 100644 --- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingMain.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; import { IGraphPayments } from "../../interfaces/IGraphPayments.sol"; +import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; /** * @title Inferface for the {HorizonStaking} contract. @@ -205,6 +206,15 @@ interface IHorizonStakingMain { uint256 tokens ); + /** + * @notice Emitted when `delegator` withdrew delegated `tokens` from `indexer` using `withdrawDelegated`. + * @dev This event is for the legacy `withdrawDelegated` function. + * @param indexer The address of the indexer + * @param delegator The address of the delegator + * @param tokens The amount of tokens withdrawn + */ + event StakeDelegatedWithdrawn(address indexed indexer, address indexed delegator, uint256 tokens); + /** * @notice Emitted when tokens are added to a delegation pool's reserve. * @param serviceProvider The address of the service provider @@ -271,13 +281,15 @@ interface IHorizonStakingMain { * @param owner The address of the owner of the thaw requests * @param thawRequestsFulfilled The number of thaw requests fulfilled * @param tokens The total amount of tokens being released + * @param requestType The type of thaw request */ event ThawRequestsFulfilled( address indexed serviceProvider, address indexed verifier, address indexed owner, uint256 thawRequestsFulfilled, - uint256 tokens + uint256 tokens, + IHorizonStakingTypes.ThawRequestType requestType ); // -- Events: governance -- @@ -303,9 +315,8 @@ interface IHorizonStakingMain { /** * @notice Emitted when the delegation slashing global flag is set. - * @param enabled Whether delegation slashing is enabled or disabled. */ - event DelegationSlashingEnabled(bool enabled); + event DelegationSlashingEnabled(); // -- Errors: tokens @@ -415,6 +426,20 @@ interface IHorizonStakingMain { */ error HorizonStakingInvalidDelegationPool(address serviceProvider, address verifier); + /** + * @notice Thrown when the minimum token amount required for delegation is not met. + * @param tokens The actual token amount + * @param minTokens The minimum required token amount + */ + error HorizonStakingInsufficientDelegationTokens(uint256 tokens, uint256 minTokens); + + /** + * @notice Thrown when the minimum token amount required for undelegation with beneficiary is not met. + * @param tokens The actual token amount + * @param minTokens The minimum required token amount + */ + error HorizonStakingInsufficientUndelegationTokens(uint256 tokens, uint256 minTokens); + /** * @notice Thrown when attempting to undelegate with a beneficiary that is the zero address. */ @@ -515,6 +540,8 @@ interface IHorizonStakingMain { * - During the transition period it's locked for a period of time before it can be withdrawn * by calling {withdraw}. * - After the transition period it's immediately withdrawn. + * Note that after the transition period if there are tokens still locked they will have to be + * withdrawn by calling {withdraw}. * @dev Requirements: * - `_tokens` cannot be zero. * - `_serviceProvider` must have enough idle stake to cover the staking amount and any @@ -747,7 +774,7 @@ interface IHorizonStakingMain { * @param beneficiary The address where the tokens will be withdrawn after thawing * @return The ID of the thaw request */ - function undelegate( + function undelegateWithBeneficiary( address serviceProvider, address verifier, uint256 shares, @@ -772,6 +799,28 @@ interface IHorizonStakingMain { */ function withdrawDelegated(address serviceProvider, address verifier, uint256 nThawRequests) external; + /** + * @notice Withdraw undelegated with beneficiary tokens from a provision after thawing. + * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw + * requests in the event that fulfilling all of them results in a gas limit error. + * @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill + * the thaw requests with an amount equal to zero. + * + * Requirements: + * - Must have previously initiated a thaw request using {undelegateWithBeneficiary}. + * + * Emits {ThawRequestFulfilled}, {ThawRequestsFulfilled} and {DelegatedTokensWithdrawn} events. + * + * @param serviceProvider The service provider address + * @param verifier The verifier address + * @param nThawRequests The number of thaw requests to fulfill. Set to 0 to fulfill all thaw requests. + */ + function withdrawDelegatedWithBeneficiary( + address serviceProvider, + address verifier, + uint256 nThawRequests + ) external; + /** * @notice Re-delegate undelegated tokens from a provision after thawing to a `newServiceProvider` and `newVerifier`. * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw @@ -838,13 +887,14 @@ interface IHorizonStakingMain { /** * @notice Withdraw undelegated tokens from the subgraph data service provision after thawing. * This function is for backwards compatibility with the legacy staking contract. - * It only allows withdrawing from the subgraph data service and DOES NOT have slippage protection in - * case the caller opts for re-delegating. + * It only allows withdrawing tokens undelegated before horizon upgrade. * @dev See {delegate}. * @param serviceProvider The service provider address - * @param newServiceProvider The address of a new service provider, if the delegator wants to re-delegate */ - function withdrawDelegated(address serviceProvider, address newServiceProvider) external; + function withdrawDelegated( + address serviceProvider, + address // newServiceProvider, deprecated + ) external returns (uint256); /** * @notice Slash a service provider. This can only be called by a verifier to which diff --git a/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol b/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol index 0dfc6c774..e2376bf18 100644 --- a/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol +++ b/packages/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol @@ -131,6 +131,17 @@ interface IHorizonStakingTypes { uint256 __DEPRECATED_tokensLockedUntil; } + /** + * @dev Enum to specify the type of thaw request. + * @param Provision Represents a thaw request for a provision. + * @param Delegation Represents a thaw request for a delegation. + */ + enum ThawRequestType { + Provision, + Delegation, + DelegationWithBeneficiary + } + /** * @notice Details of a stake thawing operation. * @dev ThawRequests are stored in linked lists by service provider/delegator, @@ -146,4 +157,42 @@ interface IHorizonStakingTypes { // Used to invalidate unfulfilled thaw requests uint256 thawingNonce; } + + /** + * @notice Parameters to fulfill thaw requests. + * @dev This struct is used to avoid stack too deep error in the `fulfillThawRequests` function. + * @param requestType The type of thaw request (Provision or Delegation) + * @param serviceProvider The address of the service provider + * @param verifier The address of the verifier + * @param owner The address of the owner of the thaw request + * @param tokensThawing The current amount of tokens already thawing + * @param sharesThawing The current amount of shares already thawing + * @param nThawRequests The number of thaw requests to fulfill. If set to 0, all thaw requests are fulfilled. + * @param thawingNonce The current valid thawing nonce. Any thaw request with a different nonce is invalid and should be ignored. + */ + struct FulfillThawRequestsParams { + ThawRequestType requestType; + address serviceProvider; + address verifier; + address owner; + uint256 tokensThawing; + uint256 sharesThawing; + uint256 nThawRequests; + uint256 thawingNonce; + } + + /** + * @notice Results of the traversal of thaw requests. + * @dev This struct is used to avoid stack too deep error in the `fulfillThawRequests` function. + * @param requestsFulfilled The number of thaw requests fulfilled + * @param tokensThawed The total amount of tokens thawed + * @param tokensThawing The total amount of tokens thawing + * @param sharesThawing The total amount of shares thawing + */ + struct TraverseThawRequestsResults { + uint256 requestsFulfilled; + uint256 tokensThawed; + uint256 tokensThawing; + uint256 sharesThawing; + } } diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index 8df8f20f6..3414fe555 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.27; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStakingMain } from "../interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingExtension } from "../interfaces/internal/IHorizonStakingExtension.sol"; import { IGraphPayments } from "../interfaces/IGraphPayments.sol"; import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; @@ -35,11 +36,17 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { uint256 private constant FIXED_POINT_PRECISION = 1e18; /// @dev Maximum number of simultaneous stake thaw requests (per provision) or undelegations (per delegation) - uint256 private constant MAX_THAW_REQUESTS = 100; + uint256 private constant MAX_THAW_REQUESTS = 1_000; /// @dev Address of the staking extension contract address private immutable STAKING_EXTENSION_ADDRESS; + /// @dev Minimum amount of delegation. + uint256 private constant MIN_DELEGATION = 1e18; + + /// @dev Minimum amount of undelegation with beneficiary. + uint256 private constant MIN_UNDELEGATION_WITH_BENEFICIARY = 10e18; + /** * @notice Checks that the caller is authorized to operate over a provision. * @param serviceProvider The address of the service provider. @@ -297,20 +304,20 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { address verifier, uint256 shares ) external override notPaused returns (bytes32) { - return _undelegate(serviceProvider, verifier, shares, msg.sender); + return _undelegate(ThawRequestType.Delegation, serviceProvider, verifier, shares, msg.sender); } /** * @notice See {IHorizonStakingMain-undelegate}. */ - function undelegate( + function undelegateWithBeneficiary( address serviceProvider, address verifier, uint256 shares, address beneficiary ) external override notPaused returns (bytes32) { require(beneficiary != address(0), HorizonStakingInvalidBeneficiaryZeroAddress()); - return _undelegate(serviceProvider, verifier, shares, beneficiary); + return _undelegate(ThawRequestType.DelegationWithBeneficiary, serviceProvider, verifier, shares, beneficiary); } /** @@ -321,7 +328,34 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { address verifier, uint256 nThawRequests ) external override notPaused { - _withdrawDelegated(serviceProvider, verifier, address(0), address(0), 0, nThawRequests); + _withdrawDelegated( + ThawRequestType.Delegation, + serviceProvider, + verifier, + address(0), + address(0), + 0, + nThawRequests + ); + } + + /** + * @notice See {IHorizonStakingMain-withdrawDelegatedWithBeneficiary}. + */ + function withdrawDelegatedWithBeneficiary( + address serviceProvider, + address verifier, + uint256 nThawRequests + ) external override notPaused { + _withdrawDelegated( + ThawRequestType.DelegationWithBeneficiary, + serviceProvider, + verifier, + address(0), + address(0), + 0, + nThawRequests + ); } /** @@ -338,6 +372,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { require(newServiceProvider != address(0), HorizonStakingInvalidServiceProviderZeroAddress()); require(newVerifier != address(0), HorizonStakingInvalidVerifierZeroAddress()); _withdrawDelegated( + ThawRequestType.Delegation, oldServiceProvider, oldVerifier, newServiceProvider, @@ -374,21 +409,43 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * @notice See {IHorizonStakingMain-undelegate}. */ function undelegate(address serviceProvider, uint256 shares) external override notPaused { - _undelegate(serviceProvider, SUBGRAPH_DATA_SERVICE_ADDRESS, shares, msg.sender); + _undelegate(ThawRequestType.Delegation, serviceProvider, SUBGRAPH_DATA_SERVICE_ADDRESS, shares, msg.sender); } /** * @notice See {IHorizonStakingMain-withdrawDelegated}. */ - function withdrawDelegated(address serviceProvider, address newServiceProvider) external override notPaused { - _withdrawDelegated( - serviceProvider, - SUBGRAPH_DATA_SERVICE_ADDRESS, - newServiceProvider, - SUBGRAPH_DATA_SERVICE_ADDRESS, - 0, - 0 - ); + function withdrawDelegated( + address serviceProvider, + address // newServiceProvider, deprecated + ) external override notPaused returns (uint256) { + // Get the delegation pool of the indexer + address delegator = msg.sender; + DelegationPoolInternal storage pool = _legacyDelegationPools[serviceProvider]; + DelegationInternal storage delegation = pool.delegators[delegator]; + + // Validation + uint256 tokensToWithdraw = 0; + uint256 currentEpoch = _graphEpochManager().currentEpoch(); + if ( + delegation.__DEPRECATED_tokensLockedUntil > 0 && currentEpoch >= delegation.__DEPRECATED_tokensLockedUntil + ) { + tokensToWithdraw = delegation.__DEPRECATED_tokensLocked; + } + require(tokensToWithdraw > 0, "!tokens"); + + // Reset lock + delegation.__DEPRECATED_tokensLocked = 0; + delegation.__DEPRECATED_tokensLockedUntil = 0; + + emit StakeDelegatedWithdrawn(serviceProvider, delegator, tokensToWithdraw); + + // -- Interactions -- + + // Return tokens to the delegator + _graphToken().pushTokens(delegator, tokensToWithdraw); + + return tokensToWithdraw; } /* @@ -404,6 +461,23 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { uint256 tokensVerifier, address verifierDestination ) external override notPaused { + // TODO remove after the transition period + // Check if sender is authorized to slash on the deprecated list + if (__DEPRECATED_slashers[msg.sender]) { + // Forward call to staking extension + (bool success, ) = STAKING_EXTENSION_ADDRESS.delegatecall( + abi.encodeWithSelector( + IHorizonStakingExtension.legacySlash.selector, + serviceProvider, + tokens, + tokensVerifier, + verifierDestination + ) + ); + require(success, "Delegatecall to legacySlash failed"); + return; + } + address verifier = msg.sender; Provision storage prov = _provisions[serviceProvider][verifier]; DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier); @@ -432,8 +506,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { _graphToken().burnTokens(providerTokensSlashed - tokensVerifier); // Provision accounting - // TODO check for rounding issues - uint256 provisionFractionSlashed = (providerTokensSlashed * FIXED_POINT_PRECISION) / prov.tokens; + uint256 provisionFractionSlashed = (providerTokensSlashed * FIXED_POINT_PRECISION + prov.tokens - 1) / + prov.tokens; prov.tokensThawing = (prov.tokensThawing * (FIXED_POINT_PRECISION - provisionFractionSlashed)) / (FIXED_POINT_PRECISION); @@ -468,7 +542,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { _graphToken().burnTokens(tokensToSlash); // Delegation pool accounting - uint256 delegationFractionSlashed = (tokensToSlash * FIXED_POINT_PRECISION) / pool.tokens; + uint256 delegationFractionSlashed = (tokensToSlash * FIXED_POINT_PRECISION + pool.tokens - 1) / + pool.tokens; pool.tokens = pool.tokens - tokensToSlash; pool.tokensThawing = (pool.tokensThawing * (FIXED_POINT_PRECISION - delegationFractionSlashed)) / @@ -534,7 +609,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { */ function setDelegationSlashingEnabled() external override onlyGovernor { _delegationSlashingEnabled = true; - emit DelegationSlashingEnabled(_delegationSlashingEnabled); + emit DelegationSlashingEnabled(); } /** @@ -663,7 +738,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { } /** - * @notice See {IHorizonStakingMain-createProvision}. + * @notice See {IHorizonStakingMain-provision}. */ function _createProvision( address _serviceProvider, @@ -732,15 +807,17 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { // Calculate shares to issue // Thawing pool is reset/initialized when the pool is empty: prov.tokensThawing == 0 + // Round thawing shares up to ensure fairness and avoid undervaluing the shares due to rounding down. uint256 thawingShares = prov.tokensThawing == 0 ? _tokens - : ((prov.sharesThawing * _tokens) / prov.tokensThawing); + : ((prov.sharesThawing * _tokens + prov.tokensThawing - 1) / prov.tokensThawing); uint64 thawingUntil = uint64(block.timestamp + uint256(prov.thawingPeriod)); prov.sharesThawing = prov.sharesThawing + thawingShares; prov.tokensThawing = prov.tokensThawing + _tokens; bytes32 thawRequestId = _createThawRequest( + ThawRequestType.Provision, _serviceProvider, _verifier, _serviceProvider, @@ -765,15 +842,18 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { uint256 tokensThawed_ = 0; uint256 sharesThawing = prov.sharesThawing; uint256 tokensThawing = prov.tokensThawing; - (tokensThawed_, tokensThawing, sharesThawing) = _fulfillThawRequests( - _serviceProvider, - _verifier, - _serviceProvider, - tokensThawing, - sharesThawing, - _nThawRequests, - prov.thawingNonce - ); + + FulfillThawRequestsParams memory params = FulfillThawRequestsParams({ + requestType: ThawRequestType.Provision, + serviceProvider: _serviceProvider, + verifier: _verifier, + owner: _serviceProvider, + tokensThawing: tokensThawing, + sharesThawing: sharesThawing, + nThawRequests: _nThawRequests, + thawingNonce: prov.thawingNonce + }); + (tokensThawed_, tokensThawing, sharesThawing) = _fulfillThawRequests(params); prov.tokens = prov.tokens - tokensThawed_; prov.sharesThawing = sharesThawing; @@ -790,6 +870,9 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * have been done before calling this function. */ function _delegate(address _serviceProvider, address _verifier, uint256 _tokens, uint256 _minSharesOut) private { + // Enforces a minimum delegation amount to prevent share manipulation attacks. + // This stops attackers from inflating share value and blocking other delegators. + require(_tokens >= MIN_DELEGATION, HorizonStakingInsufficientDelegationTokens(_tokens, MIN_DELEGATION)); require( _provisions[_serviceProvider][_verifier].createdAt != 0, HorizonStakingInvalidProvision(_serviceProvider, _verifier) @@ -833,6 +916,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * that were not thawing will be preserved. */ function _undelegate( + ThawRequestType _requestType, address _serviceProvider, address _verifier, uint256 _shares, @@ -850,6 +934,18 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { // delegation pool shares -> delegation pool tokens -> thawing pool shares // Thawing pool is reset/initialized when the pool is empty: prov.tokensThawing == 0 uint256 tokens = (_shares * (pool.tokens - pool.tokensThawing)) / pool.shares; + + // Since anyone can undelegate for any beneficiary, we require a minimum amount to prevent + // malicious actors from flooding the thaw request list with tiny amounts and causing a + // denial of service attack by hitting the MAX_THAW_REQUESTS limit + if (_requestType == ThawRequestType.DelegationWithBeneficiary) { + require( + tokens >= MIN_UNDELEGATION_WITH_BENEFICIARY, + HorizonStakingInsufficientUndelegationTokens(tokens, MIN_UNDELEGATION_WITH_BENEFICIARY) + ); + } + + // Thawing shares are rounded down to protect the pool and avoid taking extra tokens from other participants. uint256 thawingShares = pool.tokensThawing == 0 ? tokens : ((tokens * pool.sharesThawing) / pool.tokensThawing); uint64 thawingUntil = uint64(block.timestamp + uint256(_provisions[_serviceProvider][_verifier].thawingPeriod)); @@ -858,8 +954,16 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { pool.shares = pool.shares - _shares; delegation.shares = delegation.shares - _shares; + if (delegation.shares != 0) { + uint256 remainingTokens = (delegation.shares * (pool.tokens - pool.tokensThawing)) / pool.shares; + require( + remainingTokens >= MIN_DELEGATION, + HorizonStakingInsufficientTokens(remainingTokens, MIN_DELEGATION) + ); + } bytes32 thawRequestId = _createThawRequest( + _requestType, _serviceProvider, _verifier, _beneficiary, @@ -876,6 +980,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * @notice See {IHorizonStakingMain-withdrawDelegated}. */ function _withdrawDelegated( + ThawRequestType _requestType, address _serviceProvider, address _verifier, address _newServiceProvider, @@ -894,15 +999,18 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { uint256 tokensThawed = 0; uint256 sharesThawing = pool.sharesThawing; uint256 tokensThawing = pool.tokensThawing; - (tokensThawed, tokensThawing, sharesThawing) = _fulfillThawRequests( - _serviceProvider, - _verifier, - msg.sender, - tokensThawing, - sharesThawing, - _nThawRequests, - pool.thawingNonce - ); + + FulfillThawRequestsParams memory params = FulfillThawRequestsParams({ + requestType: _requestType, + serviceProvider: _serviceProvider, + verifier: _verifier, + owner: msg.sender, + tokensThawing: tokensThawing, + sharesThawing: sharesThawing, + nThawRequests: _nThawRequests, + thawingNonce: pool.thawingNonce + }); + (tokensThawed, tokensThawing, sharesThawing) = _fulfillThawRequests(params); // The next subtraction should never revert becase: pool.tokens >= pool.tokensThawing and pool.tokensThawing >= tokensThawed // In the event the pool gets completely slashed tokensThawed will fulfil to 0. @@ -936,6 +1044,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * @return The ID of the thaw request */ function _createThawRequest( + ThawRequestType _requestType, address _serviceProvider, address _verifier, address _owner, @@ -943,18 +1052,23 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { uint64 _thawingUntil, uint256 _thawingNonce ) private returns (bytes32) { - LinkedList.List storage thawRequestList = _thawRequestLists[_serviceProvider][_verifier][_owner]; + require(_shares != 0, HorizonStakingInvalidZeroShares()); + LinkedList.List storage thawRequestList = _getThawRequestList( + _requestType, + _serviceProvider, + _verifier, + _owner + ); require(thawRequestList.count < MAX_THAW_REQUESTS, HorizonStakingTooManyThawRequests()); bytes32 thawRequestId = keccak256(abi.encodePacked(_serviceProvider, _verifier, _owner, thawRequestList.nonce)); - _thawRequests[thawRequestId] = ThawRequest({ - shares: _shares, - thawingUntil: _thawingUntil, - next: bytes32(0), - thawingNonce: _thawingNonce - }); + ThawRequest storage thawRequest = _getThawRequest(_requestType, thawRequestId); + thawRequest.shares = _shares; + thawRequest.thawingUntil = _thawingUntil; + thawRequest.next = bytes32(0); + thawRequest.thawingNonce = _thawingNonce; - if (thawRequestList.count != 0) _thawRequests[thawRequestList.tail].next = thawRequestId; + if (thawRequestList.count != 0) _getThawRequest(_requestType, thawRequestList.tail).next = thawRequestId; thawRequestList.addTail(thawRequestId); emit ThawRequestCreated(_serviceProvider, _verifier, _owner, _shares, _thawingUntil, thawRequestId); @@ -964,41 +1078,74 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { /** * @notice Traverses a thaw request list and fulfills expired thaw requests. * @dev Emits a {ThawRequestsFulfilled} event and a {ThawRequestFulfilled} event for each thaw request fulfilled. - * @param _serviceProvider The address of the service provider - * @param _verifier The address of the verifier - * @param _owner The address of the owner of the thaw request - * @param _tokensThawing The current amount of tokens already thawing - * @param _sharesThawing The current amount of shares already thawing - * @param _nThawRequests The number of thaw requests to fulfill. If set to 0, all thaw requests are fulfilled. - * @param _thawingNonce The current valid thawing nonce. Any thaw request with a different nonce is invalid and should be ignored. + * @param params The parameters for fulfilling thaw requests * @return The amount of thawed tokens * @return The amount of tokens still thawing * @return The amount of shares still thawing */ - function _fulfillThawRequests( - address _serviceProvider, - address _verifier, - address _owner, - uint256 _tokensThawing, - uint256 _sharesThawing, - uint256 _nThawRequests, - uint256 _thawingNonce - ) private returns (uint256, uint256, uint256) { - LinkedList.List storage thawRequestList = _thawRequestLists[_serviceProvider][_verifier][_owner]; + function _fulfillThawRequests(FulfillThawRequestsParams memory params) private returns (uint256, uint256, uint256) { + LinkedList.List storage thawRequestList = _getThawRequestList( + params.requestType, + params.serviceProvider, + params.verifier, + params.owner + ); require(thawRequestList.count > 0, HorizonStakingNothingThawing()); - uint256 tokensThawed = 0; + TraverseThawRequestsResults memory results = _traverseThawRequests(params, thawRequestList); + + emit ThawRequestsFulfilled( + params.serviceProvider, + params.verifier, + params.owner, + results.requestsFulfilled, + results.tokensThawed, + params.requestType + ); + + return (results.tokensThawed, results.tokensThawing, results.sharesThawing); + } + + /** + * @notice Traverses a thaw request list and fulfills expired thaw requests. + * @param params The parameters for fulfilling thaw requests + * @param thawRequestList The list of thaw requests to traverse + * @return The results of the traversal + */ + function _traverseThawRequests( + FulfillThawRequestsParams memory params, + LinkedList.List storage thawRequestList + ) private returns (TraverseThawRequestsResults memory) { + function(bytes32) view returns (bytes32) getNextItem = _getNextThawRequest(params.requestType); + function(bytes32) deleteItem = _getDeleteThawRequest(params.requestType); + + bytes memory acc = abi.encode( + params.requestType, + uint256(0), + params.tokensThawing, + params.sharesThawing, + params.thawingNonce + ); (uint256 thawRequestsFulfilled, bytes memory data) = thawRequestList.traverse( - _getNextThawRequest, + getNextItem, _fulfillThawRequest, - _deleteThawRequest, - abi.encode(tokensThawed, _tokensThawing, _sharesThawing, _thawingNonce), - _nThawRequests + deleteItem, + acc, + params.nThawRequests ); - (tokensThawed, _tokensThawing, _sharesThawing) = abi.decode(data, (uint256, uint256, uint256)); - emit ThawRequestsFulfilled(_serviceProvider, _verifier, _owner, thawRequestsFulfilled, tokensThawed); - return (tokensThawed, _tokensThawing, _sharesThawing); + (, uint256 tokensThawed, uint256 tokensThawing, uint256 sharesThawing) = abi.decode( + data, + (ThawRequestType, uint256, uint256, uint256) + ); + + return + TraverseThawRequestsResults({ + requestsFulfilled: thawRequestsFulfilled, + tokensThawed: tokensThawed, + tokensThawing: tokensThawing, + sharesThawing: sharesThawing + }); } /** @@ -1013,19 +1160,22 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * @return The updated accumulator data */ function _fulfillThawRequest(bytes32 _thawRequestId, bytes memory _acc) private returns (bool, bytes memory) { - ThawRequest storage thawRequest = _thawRequests[_thawRequestId]; + // decode + ( + ThawRequestType requestType, + uint256 tokensThawed, + uint256 tokensThawing, + uint256 sharesThawing, + uint256 thawingNonce + ) = abi.decode(_acc, (ThawRequestType, uint256, uint256, uint256, uint256)); + + ThawRequest storage thawRequest = _getThawRequest(requestType, _thawRequestId); // early exit if (thawRequest.thawingUntil > block.timestamp) { return (true, LinkedList.NULL_BYTES); } - // decode - (uint256 tokensThawed, uint256 tokensThawing, uint256 sharesThawing, uint256 thawingNonce) = abi.decode( - _acc, - (uint256, uint256, uint256, uint256) - ); - // process - only fulfill thaw requests for the current valid nonce uint256 tokens = 0; bool validThawRequest = thawRequest.thawingNonce == thawingNonce; @@ -1044,17 +1194,49 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { ); // encode - _acc = abi.encode(tokensThawed, tokensThawing, sharesThawing, thawingNonce); + _acc = abi.encode(requestType, tokensThawed, tokensThawing, sharesThawing, thawingNonce); return (false, _acc); } /** - * @notice Deletes a ThawRequest. - * @dev This function is used as a callback in the thaw requests linked list traversal. - * @param _thawRequestId The ID of the thaw request to delete + * @notice Determines the correct callback function for `deleteItem` based on the request type. + * @param _requestType The type of thaw request (Provision or Delegation). + * @return A function pointer to the appropriate `deleteItem` callback. + */ + function _getDeleteThawRequest(ThawRequestType _requestType) private pure returns (function(bytes32)) { + if (_requestType == ThawRequestType.Provision) { + return _deleteProvisionThawRequest; + } else if (_requestType == ThawRequestType.Delegation) { + return _deleteDelegationThawRequest; + } else if (_requestType == ThawRequestType.DelegationWithBeneficiary) { + return _deleteDelegationWithBeneficiaryThawRequest; + } else { + revert HorizonStakingInvalidThawRequestType(); + } + } + + /** + * @notice Deletes a thaw request for a provision. + * @param _thawRequestId The ID of the thaw request to delete. + */ + function _deleteProvisionThawRequest(bytes32 _thawRequestId) private { + delete _thawRequests[ThawRequestType.Provision][_thawRequestId]; + } + + /** + * @notice Deletes a thaw request for a delegation. + * @param _thawRequestId The ID of the thaw request to delete. + */ + function _deleteDelegationThawRequest(bytes32 _thawRequestId) private { + delete _thawRequests[ThawRequestType.Delegation][_thawRequestId]; + } + + /** + * @notice Deletes a thaw request for a delegation with a beneficiary. + * @param _thawRequestId The ID of the thaw request to delete. */ - function _deleteThawRequest(bytes32 _thawRequestId) private { - delete _thawRequests[_thawRequestId]; + function _deleteDelegationWithBeneficiaryThawRequest(bytes32 _thawRequestId) private { + delete _thawRequests[ThawRequestType.DelegationWithBeneficiary][_thawRequestId]; } /** diff --git a/packages/horizon/contracts/staking/HorizonStakingBase.sol b/packages/horizon/contracts/staking/HorizonStakingBase.sol index d29d3edec..5f2808255 100644 --- a/packages/horizon/contracts/staking/HorizonStakingBase.sol +++ b/packages/horizon/contracts/staking/HorizonStakingBase.sol @@ -173,48 +173,58 @@ abstract contract HorizonStakingBase is /** * @notice See {IHorizonStakingBase-getThawRequest}. */ - function getThawRequest(bytes32 thawRequestId) external view override returns (ThawRequest memory) { - return _thawRequests[thawRequestId]; + function getThawRequest( + ThawRequestType requestType, + bytes32 thawRequestId + ) external view override returns (ThawRequest memory) { + return _getThawRequest(requestType, thawRequestId); } /** * @notice See {IHorizonStakingBase-getThawRequestList}. */ function getThawRequestList( + ThawRequestType requestType, address serviceProvider, address verifier, address owner ) external view override returns (LinkedList.List memory) { - return _thawRequestLists[serviceProvider][verifier][owner]; + return _getThawRequestList(requestType, serviceProvider, verifier, owner); } /** * @notice See {IHorizonStakingBase-getThawedTokens}. */ function getThawedTokens( + ThawRequestType requestType, address serviceProvider, address verifier, address owner ) external view override returns (uint256) { - LinkedList.List storage thawRequestList = _thawRequestLists[serviceProvider][verifier][owner]; + LinkedList.List storage thawRequestList = _getThawRequestList(requestType, serviceProvider, verifier, owner); if (thawRequestList.count == 0) { return 0; } - uint256 tokens = 0; + uint256 thawedTokens = 0; Provision storage prov = _provisions[serviceProvider][verifier]; + uint256 tokensThawing = prov.tokensThawing; + uint256 sharesThawing = prov.sharesThawing; bytes32 thawRequestId = thawRequestList.head; while (thawRequestId != bytes32(0)) { - ThawRequest storage thawRequest = _thawRequests[thawRequestId]; + ThawRequest storage thawRequest = _getThawRequest(requestType, thawRequestId); if (thawRequest.thawingUntil <= block.timestamp) { - tokens += (thawRequest.shares * prov.tokensThawing) / prov.sharesThawing; + uint256 tokens = (thawRequest.shares * tokensThawing) / sharesThawing; + tokensThawing = tokensThawing - tokens; + sharesThawing = sharesThawing - thawRequest.shares; + thawedTokens = thawedTokens + tokens; } else { break; } thawRequestId = thawRequest.next; } - return tokens; + return thawedTokens; } /** @@ -258,11 +268,11 @@ abstract contract HorizonStakingBase is * TODO: update the calculation after the transition period. */ function _getIdleStake(address _serviceProvider) internal view returns (uint256) { - return - _serviceProviders[_serviceProvider].tokensStaked - - _serviceProviders[_serviceProvider].tokensProvisioned - - _serviceProviders[_serviceProvider].__DEPRECATED_tokensAllocated - + uint256 tokensUsed = _serviceProviders[_serviceProvider].tokensProvisioned + + _serviceProviders[_serviceProvider].__DEPRECATED_tokensAllocated + _serviceProviders[_serviceProvider].__DEPRECATED_tokensLocked; + uint256 tokensStaked = _serviceProviders[_serviceProvider].tokensStaked; + return tokensStaked > tokensUsed ? tokensStaked - tokensUsed : 0; } /** @@ -289,11 +299,83 @@ abstract contract HorizonStakingBase is } /** - * @notice Gets the next thaw request after `_thawRequestId`. - * @dev This function is used as a callback in the thaw requests linked list traversal. + * @notice Determines the correct callback function for `getNextItem` based on the request type. + * @param _requestType The type of thaw request (Provision or Delegation). + * @return A function pointer to the appropriate `getNextItem` callback. */ - function _getNextThawRequest(bytes32 _thawRequestId) internal view returns (bytes32) { - return _thawRequests[_thawRequestId].next; + function _getNextThawRequest( + ThawRequestType _requestType + ) internal pure returns (function(bytes32) view returns (bytes32)) { + if (_requestType == ThawRequestType.Provision) { + return _getNextProvisionThawRequest; + } else if (_requestType == ThawRequestType.Delegation) { + return _getNextDelegationThawRequest; + } else if (_requestType == ThawRequestType.DelegationWithBeneficiary) { + return _getNextDelegationWithBeneficiaryThawRequest; + } else { + revert HorizonStakingInvalidThawRequestType(); + } + } + + /** + * @notice Retrieves the next thaw request for a provision. + * @param _thawRequestId The ID of the current thaw request. + * @return The ID of the next thaw request in the list. + */ + function _getNextProvisionThawRequest(bytes32 _thawRequestId) internal view returns (bytes32) { + return _thawRequests[ThawRequestType.Provision][_thawRequestId].next; + } + + /** + * @notice Retrieves the next thaw request for a delegation. + * @param _thawRequestId The ID of the current thaw request. + * @return The ID of the next thaw request in the list. + */ + function _getNextDelegationThawRequest(bytes32 _thawRequestId) internal view returns (bytes32) { + return _thawRequests[ThawRequestType.Delegation][_thawRequestId].next; + } + + /** + * @notice Retrieves the next thaw request for a delegation with a beneficiary. + * @param _thawRequestId The ID of the current thaw request. + * @return The ID of the next thaw request in the list. + */ + function _getNextDelegationWithBeneficiaryThawRequest(bytes32 _thawRequestId) internal view returns (bytes32) { + return _thawRequests[ThawRequestType.DelegationWithBeneficiary][_thawRequestId].next; + } + + /** + * @notice Retrieves the thaw request list for the given request type. + * @dev Uses the `ThawRequestType` to determine which mapping to access. + * Reverts if the request type is unknown. + * @param _requestType The type of thaw request (Provision or Delegation). + * @param _serviceProvider The address of the service provider. + * @param _verifier The address of the verifier. + * @param _owner The address of the owner of the thaw request. + * @return The linked list of thaw requests for the specified request type. + */ + function _getThawRequestList( + ThawRequestType _requestType, + address _serviceProvider, + address _verifier, + address _owner + ) internal view returns (LinkedList.List storage) { + return _thawRequestLists[_requestType][_serviceProvider][_verifier][_owner]; + } + + /** + * @notice Retrieves a specific thaw request for the given request type. + * @dev Uses the `ThawRequestType` to determine which mapping to access. + * Reverts if the request type is unknown. + * @param _requestType The type of thaw request (Provision or Delegation). + * @param _thawRequestId The unique ID of the thaw request. + * @return The thaw request data for the specified request type and ID. + */ + function _getThawRequest( + ThawRequestType _requestType, + bytes32 _thawRequestId + ) internal view returns (IHorizonStakingTypes.ThawRequest storage) { + return _thawRequests[_requestType][_thawRequestId]; } /** diff --git a/packages/horizon/contracts/staking/HorizonStakingExtension.sol b/packages/horizon/contracts/staking/HorizonStakingExtension.sol index bc878a4f5..3b42ebf1a 100644 --- a/packages/horizon/contracts/staking/HorizonStakingExtension.sol +++ b/packages/horizon/contracts/staking/HorizonStakingExtension.sol @@ -19,8 +19,8 @@ import { HorizonStakingBase } from "./HorizonStakingBase.sol"; * to the Horizon Staking contract. It allows indexers to close allocations and collect pending query fees, but it * does not allow for the creation of new allocations. This should allow indexers to migrate to a subgraph data service * without losing rewards or having service interruptions. - * @dev TODO: Once the transition period passes this contract can be removed. It's expected the transition period to - * last for a full allocation cycle (28 epochs). + * @dev TODO: Once the transition period passes this contract can be removed (note that an upgrade to the RewardsManager + * will also be required). It's expected the transition period to last for a full allocation cycle (28 epochs). * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ @@ -36,6 +36,14 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension _; } + /** + * @dev Check if the caller is the slasher. + */ + modifier onlySlasher() { + require(__DEPRECATED_slashers[msg.sender] == true, "!slasher"); + _; + } + /** * @dev The staking contract is upgradeable however we still use the constructor to set * a few immutable variables. @@ -255,6 +263,54 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension return __DEPRECATED_thawingPeriod; } + function legacySlash( + address indexer, + uint256 tokens, + uint256 reward, + address beneficiary + ) external override onlySlasher notPaused { + ServiceProviderInternal storage indexerStake = _serviceProviders[indexer]; + + // Only able to slash a non-zero number of tokens + require(tokens > 0, "!tokens"); + + // Rewards comes from tokens slashed balance + require(tokens >= reward, "rewards>slash"); + + // Cannot slash stake of an indexer without any or enough stake + require(indexerStake.tokensStaked > 0, "!stake"); + require(tokens <= indexerStake.tokensStaked, "slash>stake"); + + // Validate beneficiary of slashed tokens + require(beneficiary != address(0), "!beneficiary"); + + // Slashing more tokens than freely available (over allocation condition) + // Unlock locked tokens to avoid the indexer to withdraw them + uint256 tokensUsed = indexerStake.__DEPRECATED_tokensAllocated + indexerStake.__DEPRECATED_tokensLocked; + uint256 tokensAvailable = tokensUsed > indexerStake.tokensStaked ? 0 : indexerStake.tokensStaked - tokensUsed; + if (tokens > tokensAvailable && indexerStake.__DEPRECATED_tokensLocked > 0) { + uint256 tokensOverAllocated = tokens - tokensAvailable; + uint256 tokensToUnlock = MathUtils.min(tokensOverAllocated, indexerStake.__DEPRECATED_tokensLocked); + indexerStake.__DEPRECATED_tokensLocked = indexerStake.__DEPRECATED_tokensLocked - tokensToUnlock; + if (indexerStake.__DEPRECATED_tokensLocked == 0) { + indexerStake.__DEPRECATED_tokensLockedUntil = 0; + } + } + + // Remove tokens to slash from the stake + indexerStake.tokensStaked = indexerStake.tokensStaked - tokens; + + // -- Interactions -- + + // Set apart the reward for the beneficiary and burn remaining slashed stake + _graphToken().burnTokens(tokens - reward); + + // Give the beneficiary a reward for slashing + _graphToken().pushTokens(beneficiary, reward); + + emit StakeSlashed(indexer, tokens, reward, beneficiary); + } + /** * @notice (Legacy) Return true if operator is allowed for the service provider on the subgraph data service. * @dev TODO: Delete after the transition period @@ -349,8 +405,7 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension // Anyone is allowed to close ONLY under two concurrent conditions // - After maxAllocationEpochs passed // - When the allocation is for non-zero amount of tokens - bool isIndexerOrOperator = msg.sender == alloc.indexer || - isOperator(alloc.indexer, SUBGRAPH_DATA_SERVICE_ADDRESS); + bool isIndexerOrOperator = msg.sender == alloc.indexer || isOperator(msg.sender, alloc.indexer); if (epochs <= __DEPRECATED_maxAllocationEpochs || alloc.tokens == 0) { require(isIndexerOrOperator, "!auth"); } diff --git a/packages/horizon/contracts/staking/HorizonStakingStorage.sol b/packages/horizon/contracts/staking/HorizonStakingStorage.sol index ce2755468..a470ac363 100644 --- a/packages/horizon/contracts/staking/HorizonStakingStorage.sol +++ b/packages/horizon/contracts/staking/HorizonStakingStorage.sol @@ -149,11 +149,12 @@ abstract contract HorizonStakingV1Storage { /// @dev Thaw requests /// Details for each thawing operation in the staking contract (for both service providers and delegators). - mapping(bytes32 thawRequestId => IHorizonStakingTypes.ThawRequest thawRequest) internal _thawRequests; + mapping(IHorizonStakingTypes.ThawRequestType thawRequestType => mapping(bytes32 thawRequestId => IHorizonStakingTypes.ThawRequest thawRequest)) + internal _thawRequests; /// @dev Thaw request lists /// Metadata defining linked lists of thaw requests for each service provider or delegator (owner) - mapping(address serviceProvider => mapping(address verifier => mapping(address owner => LinkedList.List list))) + mapping(IHorizonStakingTypes.ThawRequestType thawRequestType => mapping(address serviceProvider => mapping(address verifier => mapping(address owner => LinkedList.List list)))) internal _thawRequestLists; /// @dev Operator allow list diff --git a/packages/horizon/test/GraphBase.t.sol b/packages/horizon/test/GraphBase.t.sol index b2d43ba63..b2eef0dd9 100644 --- a/packages/horizon/test/GraphBase.t.sol +++ b/packages/horizon/test/GraphBase.t.sol @@ -71,7 +71,8 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { operator: createUser("operator"), gateway: createUser("gateway"), verifier: createUser("verifier"), - delegator: createUser("delegator") + delegator: createUser("delegator"), + legacySlasher: createUser("legacySlasher") }); // Deploy protocol contracts diff --git a/packages/horizon/test/escrow/collect.t.sol b/packages/horizon/test/escrow/collect.t.sol index 106582beb..55f378b3a 100644 --- a/packages/horizon/test/escrow/collect.t.sol +++ b/packages/horizon/test/escrow/collect.t.sol @@ -24,7 +24,7 @@ contract GraphEscrowCollectTest is GraphEscrowTest { useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { dataServiceCut = bound(dataServiceCut, 0, MAX_PPM); - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); diff --git a/packages/horizon/test/payments/GraphPayments.t.sol b/packages/horizon/test/payments/GraphPayments.t.sol index 559180bf1..494a7912a 100644 --- a/packages/horizon/test/payments/GraphPayments.t.sol +++ b/packages/horizon/test/payments/GraphPayments.t.sol @@ -122,7 +122,7 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { address escrowAddress = address(escrow); // Delegate tokens - tokensDelegate = bound(tokensDelegate, 1, MAX_STAKING_TOKENS); + tokensDelegate = bound(tokensDelegate, MIN_DELEGATION, MAX_STAKING_TOKENS); vm.startPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, tokensDelegate, 0); diff --git a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol index 1fe4ceba3..3be6633fb 100644 --- a/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol +++ b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol @@ -8,6 +8,7 @@ import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol import { IHorizonStakingBase } from "../../../contracts/interfaces/internal/IHorizonStakingBase.sol"; import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol"; +import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; import { LinkedList } from "../../../contracts/libraries/LinkedList.sol"; import { MathUtils } from "../../../contracts/libraries/MathUtils.sol"; @@ -400,6 +401,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { // before Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier); LinkedList.List memory beforeThawRequestList = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, serviceProvider, verifier, serviceProvider @@ -428,13 +430,9 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { // after Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); - ThawRequest memory afterThawRequest = staking.getThawRequest(thawRequestId); - LinkedList.List memory afterThawRequestList = staking.getThawRequestList( - serviceProvider, - verifier, - serviceProvider - ); - ThawRequest memory afterPreviousTailThawRequest = staking.getThawRequest(beforeThawRequestList.tail); + ThawRequest memory afterThawRequest = staking.getThawRequest(IHorizonStakingTypes.ThawRequestType.Provision, thawRequestId); + LinkedList.List memory afterThawRequestList = _getThawRequestList(IHorizonStakingTypes.ThawRequestType.Provision, serviceProvider, verifier, serviceProvider); + ThawRequest memory afterPreviousTailThawRequest = staking.getThawRequest(IHorizonStakingTypes.ThawRequestType.Provision, beforeThawRequestList.tail); // assert assertEq(afterProvision.tokens, beforeProvision.tokens); @@ -472,18 +470,21 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier); ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); LinkedList.List memory beforeThawRequestList = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, serviceProvider, verifier, serviceProvider ); - CalcValues_ThawRequestData memory calcValues = calcThawRequestData( - serviceProvider, - verifier, - serviceProvider, - nThawRequests, - false - ); + Params_CalcThawRequestData memory params = Params_CalcThawRequestData({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider: serviceProvider, + verifier: verifier, + owner: serviceProvider, + iterations: nThawRequests, + delegation: false + }); + CalcValues_ThawRequestData memory calcValues = calcThawRequestData(params); // deprovision for (uint i = 0; i < calcValues.thawRequestsFulfilledList.length; i++) { @@ -503,7 +504,8 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { verifier, serviceProvider, calcValues.thawRequestsFulfilledList.length, - calcValues.tokensThawed + calcValues.tokensThawed, + IHorizonStakingTypes.ThawRequestType.Provision ); vm.expectEmit(address(staking)); emit IHorizonStakingMain.TokensDeprovisioned(serviceProvider, verifier, calcValues.tokensThawed); @@ -513,6 +515,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { Provision memory afterProvision = staking.getProvision(serviceProvider, verifier); ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); LinkedList.List memory afterThawRequestList = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, serviceProvider, verifier, serviceProvider @@ -540,7 +543,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { beforeServiceProvider.__DEPRECATED_tokensLockedUntil ); for (uint i = 0; i < calcValues.thawRequestsFulfilledListIds.length; i++) { - ThawRequest memory thawRequest = staking.getThawRequest(calcValues.thawRequestsFulfilledListIds[i]); + ThawRequest memory thawRequest = staking.getThawRequest(IHorizonStakingTypes.ThawRequestType.Provision, calcValues.thawRequestsFulfilledListIds[i]); assertEq(thawRequest.shares, 0); assertEq(thawRequest.thawingUntil, 0); assertEq(thawRequest.next, bytes32(0)); @@ -583,17 +586,19 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { provision: staking.getProvision(serviceProvider, verifier), provisionNewVerifier: staking.getProvision(serviceProvider, newVerifier), serviceProvider: _getStorage_ServiceProviderInternal(serviceProvider), - thawRequestList: staking.getThawRequestList(serviceProvider, verifier, serviceProvider) + thawRequestList: staking.getThawRequestList(IHorizonStakingTypes.ThawRequestType.Provision, serviceProvider, verifier, serviceProvider) }); // calc - CalcValues_ThawRequestData memory calcValues = calcThawRequestData( - serviceProvider, - verifier, - serviceProvider, - nThawRequests, - false - ); + Params_CalcThawRequestData memory params = Params_CalcThawRequestData({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.Provision, + serviceProvider: serviceProvider, + verifier: verifier, + owner: serviceProvider, + iterations: nThawRequests, + delegation: false + }); + CalcValues_ThawRequestData memory calcValues = calcThawRequestData(params); // reprovision for (uint i = 0; i < calcValues.thawRequestsFulfilledList.length; i++) { @@ -613,7 +618,8 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { verifier, serviceProvider, calcValues.thawRequestsFulfilledList.length, - calcValues.tokensThawed + calcValues.tokensThawed, + IHorizonStakingTypes.ThawRequestType.Provision ); vm.expectEmit(address(staking)); emit IHorizonStakingMain.TokensDeprovisioned(serviceProvider, verifier, calcValues.tokensThawed); @@ -626,6 +632,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { Provision memory afterProvisionNewVerifier = staking.getProvision(serviceProvider, newVerifier); ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider); LinkedList.List memory afterThawRequestList = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Provision, serviceProvider, verifier, serviceProvider @@ -680,7 +687,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { // assert: thaw request list old verifier for (uint i = 0; i < calcValues.thawRequestsFulfilledListIds.length; i++) { - ThawRequest memory thawRequest = staking.getThawRequest(calcValues.thawRequestsFulfilledListIds[i]); + ThawRequest memory thawRequest = staking.getThawRequest(IHorizonStakingTypes.ThawRequestType.Provision, calcValues.thawRequestsFulfilledListIds[i]); assertEq(thawRequest.shares, 0); assertEq(thawRequest.thawingUntil, 0); assertEq(thawRequest.next, bytes32(0)); @@ -882,8 +889,8 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { uint256 deltaShares = afterDelegation.shares - beforeDelegation.shares; // assertions - assertEq(beforePool.tokens + tokens, afterPool.tokens); - assertEq(beforePool.shares + calcShares, afterPool.shares); + assertEq(beforePool.tokens + tokens, afterPool.tokens, "afterPool.tokens FAIL"); + assertEq(beforePool.shares + calcShares, afterPool.shares, "afterPool.shares FAIL"); assertEq(beforePool.tokensThawing, afterPool.tokensThawing); assertEq(beforePool.sharesThawing, afterPool.sharesThawing); assertEq(beforePool.thawingNonce, afterPool.thawingNonce); @@ -898,16 +905,30 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { function _undelegate(address serviceProvider, address verifier, uint256 shares) internal { (, address caller, ) = vm.readCallers(); - __undelegate(serviceProvider, verifier, shares, false, caller); + __undelegate(IHorizonStakingTypes.ThawRequestType.Delegation, serviceProvider, verifier, shares, false, caller); } - function _undelegate(address serviceProvider, address verifier, uint256 shares, address beneficiary) internal { - __undelegate(serviceProvider, verifier, shares, false, beneficiary); + function _undelegateWithBeneficiary(address serviceProvider, address verifier, uint256 shares, address beneficiary) internal { + __undelegate( + IHorizonStakingTypes.ThawRequestType.DelegationWithBeneficiary, + serviceProvider, + verifier, + shares, + false, + beneficiary + ); } function _undelegate(address serviceProvider, uint256 shares) internal { (, address caller, ) = vm.readCallers(); - __undelegate(serviceProvider, subgraphDataServiceLegacyAddress, shares, true, caller); + __undelegate( + IHorizonStakingTypes.ThawRequestType.Delegation, + serviceProvider, + subgraphDataServiceLegacyAddress, + shares, + true, + caller + ); } struct BeforeValues_Undelegate { @@ -923,14 +944,21 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { bytes32 thawRequestId; } - function __undelegate(address serviceProvider, address verifier, uint256 shares, bool legacy, address beneficiary) private { + function __undelegate( + IHorizonStakingTypes.ThawRequestType thawRequestType, + address serviceProvider, + address verifier, + uint256 shares, + bool legacy, + address beneficiary + ) private { (, address delegator, ) = vm.readCallers(); // before BeforeValues_Undelegate memory beforeValues; beforeValues.pool = _getStorage_DelegationPoolInternal(serviceProvider, verifier, legacy); beforeValues.delegation = _getStorage_Delegation(serviceProvider, verifier, delegator, legacy); - beforeValues.thawRequestList = staking.getThawRequestList(serviceProvider, verifier, delegator); + beforeValues.thawRequestList = staking.getThawRequestList(thawRequestType, serviceProvider, verifier, delegator); beforeValues.delegatedTokens = staking.getDelegatedTokensAvailable(serviceProvider, verifier); // calc @@ -962,8 +990,12 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { emit IHorizonStakingMain.TokensUndelegated(serviceProvider, verifier, delegator, calcValues.tokens); if (legacy) { staking.undelegate(serviceProvider, shares); + } else if (thawRequestType == IHorizonStakingTypes.ThawRequestType.Delegation) { + staking.undelegate(serviceProvider, verifier, shares); + } else if (thawRequestType == IHorizonStakingTypes.ThawRequestType.DelegationWithBeneficiary) { + staking.undelegateWithBeneficiary(serviceProvider, verifier, shares, beneficiary); } else { - staking.undelegate(serviceProvider, verifier, shares, beneficiary); + revert("Invalid thaw request type"); } // after @@ -978,8 +1010,8 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { beneficiary, legacy ); - LinkedList.List memory afterThawRequestList = staking.getThawRequestList(serviceProvider, verifier, beneficiary); - ThawRequest memory afterThawRequest = staking.getThawRequest(calcValues.thawRequestId); + LinkedList.List memory afterThawRequestList = staking.getThawRequestList(thawRequestType, serviceProvider, verifier, beneficiary); + ThawRequest memory afterThawRequest = staking.getThawRequest(thawRequestType, calcValues.thawRequestId); uint256 afterDelegatedTokens = staking.getDelegatedTokensAvailable(serviceProvider, verifier); // assertions @@ -1008,15 +1040,35 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { address verifier, uint256 nThawRequests ) internal { - __withdrawDelegated( - serviceProvider, - verifier, - address(0), - address(0), - 0, - nThawRequests, - false - ); + Params_WithdrawDelegated memory params = Params_WithdrawDelegated({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.Delegation, + serviceProvider: serviceProvider, + verifier: verifier, + newServiceProvider: address(0), + newVerifier: address(0), + minSharesForNewProvider: 0, + nThawRequests: nThawRequests, + legacy: verifier == subgraphDataServiceLegacyAddress + }); + __withdrawDelegated(params); + } + + function _withdrawDelegatedWithBeneficiary( + address serviceProvider, + address verifier, + uint256 nThawRequests + ) internal { + Params_WithdrawDelegated memory params = Params_WithdrawDelegated({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.DelegationWithBeneficiary, + serviceProvider: serviceProvider, + verifier: verifier, + newServiceProvider: address(0), + newVerifier: address(0), + minSharesForNewProvider: 0, + nThawRequests: nThawRequests, + legacy: false + }); + __withdrawDelegated(params); } function _redelegate( @@ -1027,19 +1079,17 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { uint256 minSharesForNewProvider, uint256 nThawRequests ) internal { - __withdrawDelegated( - serviceProvider, - verifier, - newServiceProvider, - newVerifier, - minSharesForNewProvider, - nThawRequests, - false - ); - } - - function _withdrawDelegated(address serviceProvider, address newServiceProvider) internal { - __withdrawDelegated(serviceProvider, subgraphDataServiceLegacyAddress, newServiceProvider, subgraphDataServiceLegacyAddress, 0, 0, true); + Params_WithdrawDelegated memory params = Params_WithdrawDelegated({ + thawRequestType: IHorizonStakingTypes.ThawRequestType.Delegation, + serviceProvider: serviceProvider, + verifier: verifier, + newServiceProvider: newServiceProvider, + newVerifier: newVerifier, + minSharesForNewProvider: minSharesForNewProvider, + nThawRequests: nThawRequests, + legacy: false + }); + __withdrawDelegated(params); } struct BeforeValues_WithdrawDelegated { @@ -1059,35 +1109,40 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { uint256 stakingBalance; } - function __withdrawDelegated( - address _serviceProvider, - address _verifier, - address _newServiceProvider, - address _newVerifier, - uint256 _minSharesForNewProvider, - uint256 _nThawRequests, - bool legacy - ) private { + struct Params_WithdrawDelegated { + IHorizonStakingTypes.ThawRequestType thawRequestType; + address serviceProvider; + address verifier; + address newServiceProvider; + address newVerifier; + uint256 minSharesForNewProvider; + uint256 nThawRequests; + bool legacy; + } + + function __withdrawDelegated(Params_WithdrawDelegated memory params) private { (, address msgSender, ) = vm.readCallers(); - bool reDelegate = _newServiceProvider != address(0) && _newVerifier != address(0); + bool reDelegate = params.newServiceProvider != address(0) && params.newVerifier != address(0); // before BeforeValues_WithdrawDelegated memory beforeValues; - beforeValues.pool = _getStorage_DelegationPoolInternal(_serviceProvider, _verifier, legacy); - beforeValues.newPool = _getStorage_DelegationPoolInternal(_newServiceProvider, _newVerifier, legacy); - beforeValues.newDelegation = _getStorage_Delegation(_newServiceProvider, _newVerifier, msgSender, legacy); - beforeValues.thawRequestList = staking.getThawRequestList(_serviceProvider, _verifier, msgSender); + beforeValues.pool = _getStorage_DelegationPoolInternal(params.serviceProvider, params.verifier, params.legacy); + beforeValues.newPool = _getStorage_DelegationPoolInternal(params.newServiceProvider, params.newVerifier, params.legacy); + beforeValues.newDelegation = _getStorage_Delegation(params.newServiceProvider, params.newVerifier, msgSender, params.legacy); + beforeValues.thawRequestList = staking.getThawRequestList(params.thawRequestType, params.serviceProvider, params.verifier, msgSender); beforeValues.senderBalance = token.balanceOf(msgSender); beforeValues.stakingBalance = token.balanceOf(address(staking)); - CalcValues_ThawRequestData memory calcValues = calcThawRequestData( - _serviceProvider, - _verifier, - msgSender, - _nThawRequests, - true - ); + Params_CalcThawRequestData memory paramsCalc = Params_CalcThawRequestData({ + thawRequestType: params.thawRequestType, + serviceProvider: params.serviceProvider, + verifier: params.verifier, + owner: msgSender, + iterations: params.nThawRequests, + delegation: true + }); + CalcValues_ThawRequestData memory calcValues = calcThawRequestData(paramsCalc); // withdrawDelegated for (uint i = 0; i < calcValues.thawRequestsFulfilledList.length; i++) { @@ -1103,18 +1158,19 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { } vm.expectEmit(address(staking)); emit IHorizonStakingMain.ThawRequestsFulfilled( - _serviceProvider, - _verifier, + params.serviceProvider, + params.verifier, msgSender, calcValues.thawRequestsFulfilledList.length, - calcValues.tokensThawed + calcValues.tokensThawed, + params.thawRequestType ); if (calcValues.tokensThawed != 0) { vm.expectEmit(); if (reDelegate) { emit IHorizonStakingMain.TokensDelegated( - _newServiceProvider, - _newVerifier, + params.newServiceProvider, + params.newVerifier, msgSender, calcValues.tokensThawed ); @@ -1125,32 +1181,34 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { vm.expectEmit(); emit IHorizonStakingMain.DelegatedTokensWithdrawn( - _serviceProvider, - _verifier, + params.serviceProvider, + params.verifier, msgSender, calcValues.tokensThawed ); - if (legacy) { - staking.withdrawDelegated(_serviceProvider, _newServiceProvider); - } else if (reDelegate) { + if (reDelegate) { staking.redelegate( - _serviceProvider, - _verifier, - _newServiceProvider, - _newVerifier, - _minSharesForNewProvider, - _nThawRequests + params.serviceProvider, + params.verifier, + params.newServiceProvider, + params.newVerifier, + params.minSharesForNewProvider, + params.nThawRequests ); + } else if (params.thawRequestType == IHorizonStakingTypes.ThawRequestType.Delegation) { + staking.withdrawDelegated(params.serviceProvider, params.verifier, params.nThawRequests); + } else if (params.thawRequestType == IHorizonStakingTypes.ThawRequestType.DelegationWithBeneficiary) { + staking.withdrawDelegatedWithBeneficiary(params.serviceProvider, params.verifier, params.nThawRequests); } else { - staking.withdrawDelegated(_serviceProvider, _verifier, _nThawRequests); + revert("Invalid thaw request type"); } // after AfterValues_WithdrawDelegated memory afterValues; - afterValues.pool = _getStorage_DelegationPoolInternal(_serviceProvider, _verifier, legacy); - afterValues.newPool = _getStorage_DelegationPoolInternal(_newServiceProvider, _newVerifier, legacy); - afterValues.newDelegation = _getStorage_Delegation(_newServiceProvider, _newVerifier, msgSender, legacy); - afterValues.thawRequestList = staking.getThawRequestList(_serviceProvider, _verifier, msgSender); + afterValues.pool = _getStorage_DelegationPoolInternal(params.serviceProvider, params.verifier, params.legacy); + afterValues.newPool = _getStorage_DelegationPoolInternal(params.newServiceProvider, params.newVerifier, params.legacy); + afterValues.newDelegation = _getStorage_Delegation(params.newServiceProvider, params.newVerifier, msgSender, params.legacy); + afterValues.thawRequestList = staking.getThawRequestList(params.thawRequestType, params.serviceProvider, params.verifier, msgSender); afterValues.senderBalance = token.balanceOf(msgSender); afterValues.stakingBalance = token.balanceOf(address(staking)); @@ -1162,7 +1220,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { assertEq(afterValues.pool.thawingNonce, beforeValues.pool.thawingNonce); for (uint i = 0; i < calcValues.thawRequestsFulfilledListIds.length; i++) { - ThawRequest memory thawRequest = staking.getThawRequest(calcValues.thawRequestsFulfilledListIds[i]); + ThawRequest memory thawRequest = staking.getThawRequest(params.thawRequestType, calcValues.thawRequestsFulfilledListIds[i]); assertEq(thawRequest.shares, 0); assertEq(thawRequest.thawingUntil, 0); assertEq(thawRequest.next, bytes32(0)); @@ -1210,7 +1268,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { afterValues.newDelegation.__DEPRECATED_tokensLockedUntil, beforeValues.newDelegation.__DEPRECATED_tokensLockedUntil ); - assertGe(deltaShares, _minSharesForNewProvider); + assertGe(deltaShares, params.minSharesForNewProvider); assertEq(calcShares, deltaShares); assertEq(afterValues.senderBalance - beforeValues.senderBalance, 0); assertEq(beforeValues.stakingBalance - afterValues.stakingBalance, 0); @@ -1296,7 +1354,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { function _setDelegationSlashingEnabled() internal { // setDelegationSlashingEnabled vm.expectEmit(); - emit IHorizonStakingMain.DelegationSlashingEnabled(true); + emit IHorizonStakingMain.DelegationSlashingEnabled(); staking.setDelegationSlashingEnabled(); // after @@ -1417,7 +1475,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { uint256 tokensSlashed = calcValues.providerTokensSlashed + (isDelegationSlashingEnabled ? calcValues.delegationTokensSlashed : 0); uint256 provisionThawingTokens = (before.provision.tokensThawing * - (1e18 - ((calcValues.providerTokensSlashed * 1e18) / before.provision.tokens))) / (1e18); + (1e18 - ((calcValues.providerTokensSlashed * 1e18 + before.provision.tokens - 1) / before.provision.tokens))) / (1e18); // assert assertEq(afterProvision.tokens + calcValues.providerTokensSlashed, before.provision.tokens); @@ -1435,7 +1493,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { (before.provision.sharesThawing != 0 && afterProvision.sharesThawing == 0) ? before.provision.thawingNonce + 1 : before.provision.thawingNonce); if (isDelegationSlashingEnabled) { uint256 poolThawingTokens = (before.pool.tokensThawing * - (1e18 - ((calcValues.delegationTokensSlashed * 1e18) / before.pool.tokens))) / (1e18); + (1e18 - ((calcValues.delegationTokensSlashed * 1e18 + before.pool.tokens - 1) / before.pool.tokens))) / (1e18); assertEq(afterPool.tokens + calcValues.delegationTokensSlashed, before.pool.tokens); assertEq(afterPool.shares, before.pool.shares); assertEq(afterPool.tokensThawing, poolThawingTokens); @@ -2207,34 +2265,49 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { uint256[] thawRequestsFulfilledListTokens; } - function calcThawRequestData( - address serviceProvider, - address verifier, - address owner, - uint256 iterations, - bool delegation - ) private view returns (CalcValues_ThawRequestData memory) { - LinkedList.List memory thawRequestList = staking.getThawRequestList(serviceProvider, verifier, owner); + struct ThawingData { + uint256 tokensThawed; + uint256 tokensThawing; + uint256 sharesThawing; + uint256 thawRequestsFulfilled; + } + + struct Params_CalcThawRequestData { + IHorizonStakingTypes.ThawRequestType thawRequestType; + address serviceProvider; + address verifier; + address owner; + uint256 iterations; + bool delegation; + } + + function calcThawRequestData(Params_CalcThawRequestData memory params) private view returns (CalcValues_ThawRequestData memory) { + LinkedList.List memory thawRequestList = _getThawRequestList( + params.thawRequestType, + params.serviceProvider, + params.verifier, + params.owner + ); if (thawRequestList.count == 0) { return CalcValues_ThawRequestData(0, 0, 0, new ThawRequest[](0), new bytes32[](0), new uint256[](0)); } - Provision memory prov = staking.getProvision(serviceProvider, verifier); - DelegationPool memory pool = staking.getDelegationPool(serviceProvider, verifier); + Provision memory prov = staking.getProvision(params.serviceProvider, params.verifier); + DelegationPool memory pool = staking.getDelegationPool(params.serviceProvider, params.verifier); uint256 tokensThawed = 0; - uint256 tokensThawing = delegation ? pool.tokensThawing : prov.tokensThawing; - uint256 sharesThawing = delegation ? pool.sharesThawing : prov.sharesThawing; + uint256 tokensThawing = params.delegation ? pool.tokensThawing : prov.tokensThawing; + uint256 sharesThawing = params.delegation ? pool.sharesThawing : prov.sharesThawing; uint256 thawRequestsFulfilled = 0; bytes32 thawRequestId = thawRequestList.head; - while (thawRequestId != bytes32(0) && (iterations == 0 || thawRequestsFulfilled < iterations)) { - ThawRequest memory thawRequest = staking.getThawRequest(thawRequestId); - bool isThawRequestValid = thawRequest.thawingNonce == (delegation ? pool.thawingNonce : prov.thawingNonce); + while (thawRequestId != bytes32(0) && (params.iterations == 0 || thawRequestsFulfilled < params.iterations)) { + ThawRequest memory thawRequest = _getThawRequest(params.thawRequestType, thawRequestId); + bool isThawRequestValid = thawRequest.thawingNonce == (params.delegation ? pool.thawingNonce : prov.thawingNonce); if (thawRequest.thawingUntil <= block.timestamp) { thawRequestsFulfilled++; if (isThawRequestValid) { - uint256 tokens = delegation + uint256 tokens = params.delegation ? (thawRequest.shares * pool.tokensThawing) / pool.sharesThawing : (thawRequest.shares * prov.tokensThawing) / prov.sharesThawing; tokensThawed += tokens; @@ -2248,24 +2321,28 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { } // we need to do a second pass because solidity doesnt allow dynamic arrays on memory - ThawRequest[] memory thawRequestsFulfilledList = new ThawRequest[](thawRequestsFulfilled); - bytes32[] memory thawRequestsFulfilledListIds = new bytes32[](thawRequestsFulfilled); - uint256[] memory thawRequestsFulfilledListTokens = new uint256[](thawRequestsFulfilled); + CalcValues_ThawRequestData memory thawRequestData; + thawRequestData.tokensThawed = tokensThawed; + thawRequestData.tokensThawing = tokensThawing; + thawRequestData.sharesThawing = sharesThawing; + thawRequestData.thawRequestsFulfilledList = new ThawRequest[](thawRequestsFulfilled); + thawRequestData.thawRequestsFulfilledListIds = new bytes32[](thawRequestsFulfilled); + thawRequestData.thawRequestsFulfilledListTokens = new uint256[](thawRequestsFulfilled); uint256 i = 0; thawRequestId = thawRequestList.head; - while (thawRequestId != bytes32(0) && (iterations == 0 || i < iterations)) { - ThawRequest memory thawRequest = staking.getThawRequest(thawRequestId); - bool isThawRequestValid = thawRequest.thawingNonce == (delegation ? pool.thawingNonce : prov.thawingNonce); + while (thawRequestId != bytes32(0) && (params.iterations == 0 || i < params.iterations)) { + ThawRequest memory thawRequest = _getThawRequest(params.thawRequestType, thawRequestId); + bool isThawRequestValid = thawRequest.thawingNonce == (params.delegation ? pool.thawingNonce : prov.thawingNonce); if (thawRequest.thawingUntil <= block.timestamp) { if (isThawRequestValid) { - thawRequestsFulfilledListTokens[i] = delegation + thawRequestData.thawRequestsFulfilledListTokens[i] = params.delegation ? (thawRequest.shares * pool.tokensThawing) / pool.sharesThawing : (thawRequest.shares * prov.tokensThawing) / prov.sharesThawing; } - thawRequestsFulfilledListIds[i] = thawRequestId; - thawRequestsFulfilledList[i] = staking.getThawRequest(thawRequestId); - thawRequestId = thawRequestsFulfilledList[i].next; + thawRequestData.thawRequestsFulfilledListIds[i] = thawRequestId; + thawRequestData.thawRequestsFulfilledList[i] = _getThawRequest(params.thawRequestType, thawRequestId); + thawRequestId = thawRequestData.thawRequestsFulfilledList[i].next; i++; } else { break; @@ -2273,18 +2350,34 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { thawRequestId = thawRequest.next; } - assertEq(thawRequestsFulfilled, thawRequestsFulfilledList.length); - assertEq(thawRequestsFulfilled, thawRequestsFulfilledListIds.length); - assertEq(thawRequestsFulfilled, thawRequestsFulfilledListTokens.length); - - return - CalcValues_ThawRequestData( - tokensThawed, - tokensThawing, - sharesThawing, - thawRequestsFulfilledList, - thawRequestsFulfilledListIds, - thawRequestsFulfilledListTokens - ); + assertEq(thawRequestsFulfilled, thawRequestData.thawRequestsFulfilledList.length); + assertEq(thawRequestsFulfilled, thawRequestData.thawRequestsFulfilledListIds.length); + assertEq(thawRequestsFulfilled, thawRequestData.thawRequestsFulfilledListTokens.length); + + return thawRequestData; + } + + function _getThawRequestList( + IHorizonStakingTypes.ThawRequestType thawRequestType, + address serviceProvider, + address verifier, + address owner + ) private view returns (LinkedList.List memory) { + return staking.getThawRequestList( + thawRequestType, + serviceProvider, + verifier, + owner + ); + } + + function _getThawRequest( + IHorizonStakingTypes.ThawRequestType thawRequestType, + bytes32 thawRequestId + ) private view returns (ThawRequest memory) { + return staking.getThawRequest( + thawRequestType, + thawRequestId + ); } } diff --git a/packages/horizon/test/staking/HorizonStaking.t.sol b/packages/horizon/test/staking/HorizonStaking.t.sol index b1b45d118..d57b1c1b8 100644 --- a/packages/horizon/test/staking/HorizonStaking.t.sol +++ b/packages/horizon/test/staking/HorizonStaking.t.sol @@ -31,7 +31,7 @@ contract HorizonStakingTest is HorizonStakingSharedTest { modifier useDelegation(uint256 delegationAmount) { address msgSender; (, msgSender, ) = vm.readCallers(); - vm.assume(delegationAmount > 1); + vm.assume(delegationAmount >= MIN_DELEGATION); vm.assume(delegationAmount <= MAX_STAKING_TOKENS); vm.startPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); @@ -56,4 +56,30 @@ contract HorizonStakingTest is HorizonStakingSharedTest { resetPrank(msgSender); _; } + + modifier useUndelegate(uint256 shares) { + resetPrank(users.delegator); + + DelegationPoolInternalTest memory pool = _getStorage_DelegationPoolInternal( + users.indexer, + subgraphDataServiceAddress, + false + ); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + + shares = bound(shares, 1, delegation.shares); + uint256 tokens = (shares * (pool.tokens - pool.tokensThawing)) / pool.shares; + if (shares < delegation.shares) { + uint256 remainingTokens = (shares * (pool.tokens - pool.tokensThawing - tokens)) / pool.shares; + vm.assume(remainingTokens >= MIN_DELEGATION); + } + + _undelegate(users.indexer, subgraphDataServiceAddress, shares); + _; + } } diff --git a/packages/horizon/test/staking/allocation/close.t.sol b/packages/horizon/test/staking/allocation/close.t.sol index ce3cab273..6257e30ec 100644 --- a/packages/horizon/test/staking/allocation/close.t.sol +++ b/packages/horizon/test/staking/allocation/close.t.sol @@ -12,6 +12,18 @@ contract HorizonStakingCloseAllocationTest is HorizonStakingTest { bytes32 internal constant _poi = keccak256("poi"); + /* + * MODIFIERS + */ + + modifier useLegacyOperator() { + resetPrank(users.indexer); + _setOperator(subgraphDataServiceLegacyAddress, users.operator, true); + vm.startPrank(users.operator); + _; + vm.stopPrank(); + } + /* * TESTS */ @@ -26,6 +38,16 @@ contract HorizonStakingCloseAllocationTest is HorizonStakingTest { _closeAllocation(_allocationId, _poi); } + function testCloseAllocation_Operator(uint256 tokens) public useLegacyOperator() useAllocation(1 ether) { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + // Skip 15 epochs + vm.roll(15); + + _closeAllocation(_allocationId, _poi); + } + function testCloseAllocation_WithBeneficiaryAddress(uint256 tokens) public useIndexer useAllocation(1 ether) { tokens = bound(tokens, 1, MAX_STAKING_TOKENS); _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); diff --git a/packages/horizon/test/staking/delegation/addToPool.t.sol b/packages/horizon/test/staking/delegation/addToPool.t.sol index ff5b957ca..2bd73f28f 100644 --- a/packages/horizon/test/staking/delegation/addToPool.t.sol +++ b/packages/horizon/test/staking/delegation/addToPool.t.sol @@ -10,6 +10,7 @@ contract HorizonStakingDelegationAddToPoolTest is HorizonStakingTest { modifier useValidDelegationAmount(uint256 tokens) { vm.assume(tokens <= MAX_STAKING_TOKENS); + vm.assume(tokens >= MIN_DELEGATION); _; } @@ -93,7 +94,7 @@ contract HorizonStakingDelegationAddToPoolTest is HorizonStakingTest { uint256 recoverAmount ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing() { recoverAmount = bound(recoverAmount, 1, MAX_STAKING_TOKENS); - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); // create delegation pool resetPrank(users.delegator); @@ -116,7 +117,7 @@ contract HorizonStakingDelegationAddToPoolTest is HorizonStakingTest { uint256 recoverAmount ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing() { recoverAmount = bound(recoverAmount, 1, MAX_STAKING_TOKENS); - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); // create delegation pool resetPrank(users.delegator); diff --git a/packages/horizon/test/staking/delegation/delegate.t.sol b/packages/horizon/test/staking/delegation/delegate.t.sol index ab58e4bde..043453e10 100644 --- a/packages/horizon/test/staking/delegation/delegate.t.sol +++ b/packages/horizon/test/staking/delegation/delegate.t.sol @@ -59,9 +59,25 @@ contract HorizonStakingDelegateTest is HorizonStakingTest { staking.delegate(users.indexer, subgraphDataServiceAddress, 0, 0); } + function testDelegate_RevertWhen_UnderMinDelegation( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) { + delegationAmount = bound(delegationAmount, 1, MIN_DELEGATION - 1); + vm.startPrank(users.delegator); + token.approve(address(staking), delegationAmount); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInsufficientDelegationTokens.selector, + delegationAmount, + MIN_DELEGATION + ); + vm.expectRevert(expectedError); + staking.delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + } + function testDelegate_LegacySubgraphService(uint256 amount, uint256 delegationAmount) public useIndexer { amount = bound(amount, 1 ether, MAX_STAKING_TOKENS); - delegationAmount = bound(delegationAmount, 1, MAX_STAKING_TOKENS); + delegationAmount = bound(delegationAmount, MIN_DELEGATION, MAX_STAKING_TOKENS); _createProvision(users.indexer, subgraphDataServiceLegacyAddress, amount, 0, 0); resetPrank(users.delegator); @@ -72,7 +88,7 @@ contract HorizonStakingDelegateTest is HorizonStakingTest { uint256 tokens, uint256 delegationTokens ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing() { - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); @@ -96,7 +112,7 @@ contract HorizonStakingDelegateTest is HorizonStakingTest { uint256 tokens, uint256 delegationTokens ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing() { - delegationTokens = bound(delegationTokens, 2, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION * 2, MAX_STAKING_TOKENS); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); @@ -126,7 +142,7 @@ contract HorizonStakingDelegateTest is HorizonStakingTest { uint256 recoverAmount ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing() { recoverAmount = bound(recoverAmount, 1, MAX_STAKING_TOKENS); - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); // create delegation pool resetPrank(users.delegator); diff --git a/packages/horizon/test/staking/delegation/legacyWithdraw.t.sol b/packages/horizon/test/staking/delegation/legacyWithdraw.t.sol new file mode 100644 index 000000000..c90fa1200 --- /dev/null +++ b/packages/horizon/test/staking/delegation/legacyWithdraw.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol"; +import { LinkedList } from "../../../contracts/libraries/LinkedList.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingLegacyWithdrawDelegationTest is HorizonStakingTest { + /* + * MODIFIERS + */ + + modifier useDelegator() { + resetPrank(users.delegator); + _; + } + + /* + * HELPERS + */ + + function _setLegacyDelegation( + address _indexer, + address _delegator, + uint256 _shares, + uint256 __DEPRECATED_tokensLocked, + uint256 __DEPRECATED_tokensLockedUntil + ) public { + // Calculate the base storage slot for the serviceProvider in the mapping + bytes32 baseSlot = keccak256(abi.encode(_indexer, uint256(20))); + + // Calculate the slot for the delegator's DelegationInternal struct + bytes32 delegatorSlot = keccak256(abi.encode(_delegator, bytes32(uint256(baseSlot) + 4))); + + // Use vm.store to set each field of the struct + vm.store(address(staking), bytes32(uint256(delegatorSlot)), bytes32(_shares)); + vm.store(address(staking), bytes32(uint256(delegatorSlot) + 1), bytes32(__DEPRECATED_tokensLocked)); + vm.store(address(staking), bytes32(uint256(delegatorSlot) + 2), bytes32(__DEPRECATED_tokensLockedUntil)); + } + + /* + * ACTIONS + */ + + function _legacyWithdrawDelegated(address _indexer) internal { + (, address delegator, ) = vm.readCallers(); + IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool(_indexer, subgraphDataServiceLegacyAddress); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + uint256 beforeDelegatorBalance = token.balanceOf(users.delegator); + + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.StakeDelegatedWithdrawn(_indexer, delegator, pool.tokens); + staking.withdrawDelegated(users.indexer, address(0)); + + uint256 afterStakingBalance = token.balanceOf(address(staking)); + uint256 afterDelegatorBalance = token.balanceOf(users.delegator); + + assertEq(afterStakingBalance, beforeStakingBalance - pool.tokens); + assertEq(afterDelegatorBalance - pool.tokens, beforeDelegatorBalance); + + DelegationInternal memory delegation = _getStorage_Delegation( + _indexer, + subgraphDataServiceLegacyAddress, + delegator, + true + ); + assertEq(delegation.shares, 0); + assertEq(delegation.__DEPRECATED_tokensLocked, 0); + assertEq(delegation.__DEPRECATED_tokensLockedUntil, 0); + } + + /* + * TESTS + */ + + function testWithdraw_Legacy(uint256 tokensLocked) public useDelegator { + vm.assume(tokensLocked > 0); + + _setStorage_DelegationPool(users.indexer, tokensLocked, 0, 0); + _setLegacyDelegation(users.indexer, users.delegator, 0, tokensLocked, 1); + token.transfer(address(staking), tokensLocked); + + _legacyWithdrawDelegated(users.indexer); + } + + function testWithdraw_Legacy_RevertWhen_NoTokens() public useDelegator { + _setStorage_DelegationPool(users.indexer, 0, 0, 0); + _setLegacyDelegation(users.indexer, users.delegator, 0, 0, 0); + + vm.expectRevert("!tokens"); + staking.withdrawDelegated(users.indexer, address(0)); + } +} diff --git a/packages/horizon/test/staking/delegation/redelegate.t.sol b/packages/horizon/test/staking/delegation/redelegate.t.sol index 605e6601f..6e30348c4 100644 --- a/packages/horizon/test/staking/delegation/redelegate.t.sol +++ b/packages/horizon/test/staking/delegation/redelegate.t.sol @@ -9,19 +9,6 @@ import { HorizonStakingTest } from "../HorizonStaking.t.sol"; contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { - /* - * MODIFIERS - */ - - modifier useUndelegate(uint256 shares) { - resetPrank(users.delegator); - DelegationInternal memory delegation = _getStorage_Delegation(users.indexer, subgraphDataServiceAddress, users.delegator, false); - shares = bound(shares, 1, delegation.shares); - - _undelegate(users.indexer, subgraphDataServiceAddress, shares); - _; - } - /* * HELPERS */ diff --git a/packages/horizon/test/staking/delegation/undelegate.t.sol b/packages/horizon/test/staking/delegation/undelegate.t.sol index 4cad2e0c3..e23fdadb8 100644 --- a/packages/horizon/test/staking/delegation/undelegate.t.sol +++ b/packages/horizon/test/staking/delegation/undelegate.t.sol @@ -32,7 +32,7 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest { uint256 undelegateSteps ) public useIndexer useProvision(amount, 0, 0) { undelegateSteps = bound(undelegateSteps, 1, 10); - delegationAmount = bound(delegationAmount, 10 wei, MAX_STAKING_TOKENS); + delegationAmount = bound(delegationAmount, MIN_DELEGATION * undelegateSteps, MAX_STAKING_TOKENS); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); @@ -44,9 +44,17 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest { ); uint256 undelegateAmount = delegation.shares / undelegateSteps; - for (uint i = 0; i < undelegateSteps; i++) { + for (uint i = 0; i < undelegateSteps - 1; i++) { _undelegate(users.indexer, subgraphDataServiceAddress, undelegateAmount); } + + delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); } function testUndelegate_WithBeneficiary( @@ -55,16 +63,40 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest { address beneficiary ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { vm.assume(beneficiary != address(0)); + vm.assume(delegationAmount >= MIN_UNDELEGATION_WITH_BENEFICIARY); resetPrank(users.delegator); DelegationInternal memory delegation = _getStorage_Delegation(users.indexer, subgraphDataServiceAddress, users.delegator, false); - _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares, beneficiary); + _undelegateWithBeneficiary(users.indexer, subgraphDataServiceAddress, delegation.shares, beneficiary); + } + + function testUndelegate_RevertWhen_InsuficientTokens( + uint256 amount, + uint256 delegationAmount, + uint256 undelegateAmount + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + undelegateAmount = bound(undelegateAmount, 1, delegationAmount); + resetPrank(users.delegator); + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + undelegateAmount = bound(undelegateAmount, delegation.shares - MIN_DELEGATION + 1, delegation.shares - 1); + bytes memory expectedError = abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInsufficientTokens.selector, + delegation.shares - undelegateAmount, + MIN_DELEGATION + ); + vm.expectRevert(expectedError); + staking.undelegate(users.indexer, subgraphDataServiceAddress, undelegateAmount); } function testUndelegate_RevertWhen_TooManyUndelegations() public useIndexer useProvision(1000 ether, 0, 0) - useDelegation(1000 ether) + useDelegation(10000 ether) { resetPrank(users.delegator); @@ -112,7 +144,7 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest { function testUndelegate_LegacySubgraphService(uint256 amount, uint256 delegationAmount) public useIndexer { amount = bound(amount, 1, MAX_STAKING_TOKENS); - delegationAmount = bound(delegationAmount, 1, MAX_STAKING_TOKENS); + delegationAmount = bound(delegationAmount, MIN_DELEGATION, MAX_STAKING_TOKENS); _createProvision(users.indexer, subgraphDataServiceLegacyAddress, amount, 0, 0); resetPrank(users.delegator); @@ -131,7 +163,7 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest { uint256 tokens, uint256 delegationTokens ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing() { - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); @@ -162,7 +194,7 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest { uint256 tokens, uint256 delegationTokens ) public useIndexer useProvision(tokens, 0, 0) useDelegationSlashing { - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); // delegate resetPrank(users.delegator); @@ -232,6 +264,6 @@ contract HorizonStakingUndelegateTest is HorizonStakingTest { DelegationInternal memory delegation = _getStorage_Delegation(users.indexer, subgraphDataServiceAddress, users.delegator, false); bytes memory expectedError = abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidBeneficiaryZeroAddress.selector); vm.expectRevert(expectedError); - staking.undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares, address(0)); + staking.undelegateWithBeneficiary(users.indexer, subgraphDataServiceAddress, delegation.shares, address(0)); } } diff --git a/packages/horizon/test/staking/delegation/withdraw.t.sol b/packages/horizon/test/staking/delegation/withdraw.t.sol index c9aa04dbc..ab286c279 100644 --- a/packages/horizon/test/staking/delegation/withdraw.t.sol +++ b/packages/horizon/test/staking/delegation/withdraw.t.sol @@ -4,28 +4,12 @@ pragma solidity 0.8.27; import "forge-std/Test.sol"; import { IHorizonStakingMain } from "../../../contracts/interfaces/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; import { LinkedList } from "../../../contracts/libraries/LinkedList.sol"; import { HorizonStakingTest } from "../HorizonStaking.t.sol"; contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { - /* - * MODIFIERS - */ - - modifier useUndelegate(uint256 shares) { - resetPrank(users.delegator); - DelegationInternal memory delegation = _getStorage_Delegation( - users.indexer, - subgraphDataServiceAddress, - users.delegator, - false - ); - shares = bound(shares, 1, delegation.shares); - - _undelegate(users.indexer, subgraphDataServiceAddress, shares); - _; - } /* * TESTS @@ -42,11 +26,12 @@ contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { useUndelegate(withdrawShares) { LinkedList.List memory thawingRequests = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Delegation, users.indexer, subgraphDataServiceAddress, users.delegator ); - ThawRequest memory thawRequest = staking.getThawRequest(thawingRequests.tail); + ThawRequest memory thawRequest = staking.getThawRequest(IHorizonStakingTypes.ThawRequestType.Delegation, thawingRequests.tail); skip(thawRequest.thawingUntil + 1); @@ -79,7 +64,7 @@ contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { } function testWithdrawDelegation_LegacySubgraphService(uint256 delegationAmount) public useIndexer { - delegationAmount = bound(delegationAmount, 1, MAX_STAKING_TOKENS); + delegationAmount = bound(delegationAmount, MIN_DELEGATION, MAX_STAKING_TOKENS); _createProvision(users.indexer, subgraphDataServiceLegacyAddress, 10_000_000 ether, 0, MAX_THAWING_PERIOD); resetPrank(users.delegator); @@ -93,22 +78,23 @@ contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { _undelegate(users.indexer, delegation.shares); LinkedList.List memory thawingRequests = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Delegation, users.indexer, subgraphDataServiceLegacyAddress, users.delegator ); - ThawRequest memory thawRequest = staking.getThawRequest(thawingRequests.tail); + ThawRequest memory thawRequest = staking.getThawRequest(IHorizonStakingTypes.ThawRequestType.Delegation, thawingRequests.tail); skip(thawRequest.thawingUntil + 1); - _withdrawDelegated(users.indexer, address(0)); + _withdrawDelegated(users.indexer, subgraphDataServiceLegacyAddress, 0); } function testWithdrawDelegation_RevertWhen_InvalidPool( uint256 tokens, uint256 delegationTokens ) public useIndexer useProvision(tokens, 0, MAX_THAWING_PERIOD) useDelegationSlashing() { - delegationTokens = bound(delegationTokens, 2, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION * 2, MAX_STAKING_TOKENS); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); @@ -180,22 +166,24 @@ contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { useDelegation(delegationAmount) { vm.assume(beneficiary != address(0)); + vm.assume(beneficiary != address(staking)); + vm.assume(delegationAmount >= MIN_UNDELEGATION_WITH_BENEFICIARY); // Skip beneficiary if balance will overflow vm.assume(token.balanceOf(beneficiary) < type(uint256).max - delegationAmount); // Delegator undelegates to beneficiary resetPrank(users.delegator); DelegationInternal memory delegation = _getStorage_Delegation(users.indexer, subgraphDataServiceAddress, users.delegator, false); - _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares, beneficiary); + _undelegateWithBeneficiary(users.indexer, subgraphDataServiceAddress, delegation.shares, beneficiary); // Thawing period ends - LinkedList.List memory thawingRequests = staking.getThawRequestList(users.indexer, subgraphDataServiceAddress, beneficiary); - ThawRequest memory thawRequest = staking.getThawRequest(thawingRequests.tail); + LinkedList.List memory thawingRequests = staking.getThawRequestList(IHorizonStakingTypes.ThawRequestType.Delegation, users.indexer, subgraphDataServiceAddress, beneficiary); + ThawRequest memory thawRequest = staking.getThawRequest(IHorizonStakingTypes.ThawRequestType.Delegation, thawingRequests.tail); skip(thawRequest.thawingUntil + 1); // Beneficiary withdraws delegated tokens resetPrank(beneficiary); - _withdrawDelegated(users.indexer, subgraphDataServiceAddress, 1); + _withdrawDelegatedWithBeneficiary(users.indexer, subgraphDataServiceAddress, 1); } function testWithdrawDelegation_RevertWhen_PreviousOwnerAttemptsToWithdraw( @@ -209,15 +197,16 @@ contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { { vm.assume(beneficiary != address(0)); vm.assume(beneficiary != users.delegator); + vm.assume(delegationAmount >= MIN_UNDELEGATION_WITH_BENEFICIARY); // Delegator undelegates to beneficiary resetPrank(users.delegator); DelegationInternal memory delegation = _getStorage_Delegation(users.indexer, subgraphDataServiceAddress, users.delegator, false); - _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares, beneficiary); + _undelegateWithBeneficiary(users.indexer, subgraphDataServiceAddress, delegation.shares, beneficiary); // Thawing period ends - LinkedList.List memory thawingRequests = staking.getThawRequestList(users.indexer, subgraphDataServiceAddress, users.delegator); - ThawRequest memory thawRequest = staking.getThawRequest(thawingRequests.tail); + LinkedList.List memory thawingRequests = staking.getThawRequestList(IHorizonStakingTypes.ThawRequestType.Delegation, users.indexer, subgraphDataServiceAddress, users.delegator); + ThawRequest memory thawRequest = staking.getThawRequest(IHorizonStakingTypes.ThawRequestType.Delegation, thawingRequests.tail); skip(thawRequest.thawingUntil + 1); // Delegator attempts to withdraw delegated tokens, should revert since beneficiary is the thaw request owner diff --git a/packages/horizon/test/staking/provision/deprovision.t.sol b/packages/horizon/test/staking/provision/deprovision.t.sol index ff022e1aa..4fa97da6c 100644 --- a/packages/horizon/test/staking/provision/deprovision.t.sol +++ b/packages/horizon/test/staking/provision/deprovision.t.sol @@ -17,7 +17,7 @@ contract HorizonStakingDeprovisionTest is HorizonStakingTest { uint256 thawCount, uint256 deprovisionCount ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { - thawCount = bound(thawCount, 1, MAX_THAW_REQUESTS); + thawCount = bound(thawCount, 1, 100); deprovisionCount = bound(deprovisionCount, 0, thawCount); vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step uint256 individualThawAmount = amount / thawCount; @@ -37,7 +37,7 @@ contract HorizonStakingDeprovisionTest is HorizonStakingTest { uint64 thawingPeriod, uint256 thawCount ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { - thawCount = bound(thawCount, 2, MAX_THAW_REQUESTS); + thawCount = bound(thawCount, 2, 100); vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step uint256 individualThawAmount = amount / thawCount; diff --git a/packages/horizon/test/staking/provision/thaw.t.sol b/packages/horizon/test/staking/provision/thaw.t.sol index c3b4d6903..eb58e8e86 100644 --- a/packages/horizon/test/staking/provision/thaw.t.sol +++ b/packages/horizon/test/staking/provision/thaw.t.sol @@ -26,7 +26,7 @@ contract HorizonStakingThawTest is HorizonStakingTest { uint64 thawingPeriod, uint256 thawCount ) public useIndexer useProvision(amount, 0, thawingPeriod) { - thawCount = bound(thawCount, 1, MAX_THAW_REQUESTS); + thawCount = bound(thawCount, 1, 100); vm.assume(amount >= thawCount); // ensure the provision has at least 1 token for each thaw step uint256 individualThawAmount = amount / thawCount; @@ -72,13 +72,8 @@ contract HorizonStakingThawTest is HorizonStakingTest { staking.thaw(users.indexer, subgraphDataServiceAddress, thawAmount); } - function testThaw_RevertWhen_OverMaxThawRequests( - uint256 amount, - uint64 thawingPeriod, - uint256 thawAmount - ) public useIndexer useProvision(amount, 0, thawingPeriod) { - vm.assume(amount >= MAX_THAW_REQUESTS + 1); - thawAmount = bound(thawAmount, 1, amount / (MAX_THAW_REQUESTS + 1)); + function testThaw_RevertWhen_OverMaxThawRequests() public useIndexer useProvision(10000 ether, 0, 0) { + uint256 thawAmount = 1 ether; for (uint256 i = 0; i < MAX_THAW_REQUESTS; i++) { _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); @@ -139,4 +134,28 @@ contract HorizonStakingThawTest is HorizonStakingTest { resetPrank(users.indexer); _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); } + + function testThaw_GetThawedTokens( + uint256 amount, + uint64 thawingPeriod, + uint256 thawSteps + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + thawSteps = bound(thawSteps, 1, 10); + + uint256 thawAmount = amount / thawSteps; + vm.assume(thawAmount > 0); + for (uint256 i = 0; i < thawSteps; i++) { + _thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + skip(thawingPeriod + 1); + + uint256 thawedTokens = staking.getThawedTokens( + ThawRequestType.Provision, + users.indexer, + subgraphDataServiceAddress, + users.indexer + ); + vm.assertEq(thawedTokens, thawAmount * thawSteps); + } } diff --git a/packages/horizon/test/staking/slash/legacySlash.t.sol b/packages/horizon/test/staking/slash/legacySlash.t.sol new file mode 100644 index 000000000..f5595fdac --- /dev/null +++ b/packages/horizon/test/staking/slash/legacySlash.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingExtension } from "../../../contracts/interfaces/internal/IHorizonStakingExtension.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingLegacySlashTest is HorizonStakingTest { + + /* + * MODIFIERS + */ + + modifier useLegacySlasher(address slasher) { + bytes32 storageKey = keccak256(abi.encode(slasher, 18)); + vm.store(address(staking), storageKey, bytes32(uint256(1))); + _; + } + + /* + * HELPERS + */ + + function _setIndexer( + address _indexer, + uint256 _tokensStaked, + uint256 _tokensAllocated, + uint256 _tokensLocked, + uint256 _tokensLockedUntil + ) public { + bytes32 baseSlot = keccak256(abi.encode(_indexer, 14)); + + vm.store(address(staking), bytes32(uint256(baseSlot)), bytes32(_tokensStaked)); + vm.store(address(staking), bytes32(uint256(baseSlot) + 1), bytes32(_tokensAllocated)); + vm.store(address(staking), bytes32(uint256(baseSlot) + 2), bytes32(_tokensLocked)); + vm.store(address(staking), bytes32(uint256(baseSlot) + 3), bytes32(_tokensLockedUntil)); + } + + /* + * ACTIONS + */ + + function _legacySlash(address _indexer, uint256 _tokens, uint256 _rewards, address _beneficiary) internal { + // before + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + uint256 beforeRewardsDestinationBalance = token.balanceOf(_beneficiary); + + // slash + vm.expectEmit(address(staking)); + emit IHorizonStakingExtension.StakeSlashed(_indexer, _tokens, _rewards, _beneficiary); + staking.slash(_indexer, _tokens, _rewards, _beneficiary); + + // after + uint256 afterStakingBalance = token.balanceOf(address(staking)); + uint256 afterRewardsDestinationBalance = token.balanceOf(_beneficiary); + + assertEq(beforeStakingBalance - _tokens, afterStakingBalance); + assertEq(beforeRewardsDestinationBalance, afterRewardsDestinationBalance - _rewards); + } + + /* + * TESTS + */ + + function testSlash_Legacy( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 1); + slashTokens = bound(slashTokens, 1, tokens); + reward = bound(reward, 0, slashTokens); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + resetPrank(users.legacySlasher); + _legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_UsingLockedTokens( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 1); + slashTokens = bound(slashTokens, 1, tokens); + reward = bound(reward, 0, slashTokens); + + _setIndexer(users.indexer, tokens, 0, tokens, block.timestamp + 1); + // Send tokens manually to staking + token.transfer(address(staking), tokens); + + resetPrank(users.legacySlasher); + _legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_UsingAllocatedTokens( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 1); + slashTokens = bound(slashTokens, 1, tokens); + reward = bound(reward, 0, slashTokens); + + _setIndexer(users.indexer, tokens, 0, tokens, 0); + // Send tokens manually to staking + token.transfer(address(staking), tokens); + + resetPrank(users.legacySlasher); + staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_CallerNotSlasher( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer { + vm.assume(tokens > 0); + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + vm.expectRevert("!slasher"); + staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_RewardsOverSlashTokens( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 0); + vm.assume(slashTokens > 0); + vm.assume(reward > slashTokens); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + resetPrank(users.legacySlasher); + vm.expectRevert("rewards>slash"); + staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_NoStake( + uint256 slashTokens, + uint256 reward + ) public useLegacySlasher(users.legacySlasher) { + vm.assume(slashTokens > 0); + reward = bound(reward, 0, slashTokens); + + resetPrank(users.legacySlasher); + vm.expectRevert("!stake"); + staking.legacySlash(users.indexer, slashTokens, reward, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_ZeroTokens( + uint256 tokens + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 0); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + resetPrank(users.legacySlasher); + vm.expectRevert("!tokens"); + staking.legacySlash(users.indexer, 0, 0, makeAddr("fisherman")); + } + + function testSlash_Legacy_RevertWhen_NoBeneficiary( + uint256 tokens, + uint256 slashTokens, + uint256 reward + ) public useIndexer useLegacySlasher(users.legacySlasher) { + vm.assume(tokens > 0); + slashTokens = bound(slashTokens, 1, tokens); + reward = bound(reward, 0, slashTokens); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); + + resetPrank(users.legacySlasher); + vm.expectRevert("!beneficiary"); + staking.legacySlash(users.indexer, slashTokens, reward, address(0)); + } + + function test_LegacySlash_WhenTokensAllocatedGreaterThanStake() + public + useIndexer + useLegacySlasher(users.legacySlasher) + { + // Setup indexer with: + // - tokensStaked = 1000 GRT + // - tokensAllocated = 800 GRT + // - tokensLocked = 300 GRT + // This means tokensUsed (1100 GRT) > tokensStaked (1000 GRT) + _setIndexer( + users.indexer, + 1000 ether, // tokensStaked + 800 ether, // tokensAllocated + 300 ether, // tokensLocked + 0 // tokensLockedUntil + ); + + // Send tokens manually to staking + token.transfer(address(staking), 1100 ether); + + resetPrank(users.legacySlasher); + _legacySlash(users.indexer, 1000 ether, 500 ether, makeAddr("fisherman")); + } +} diff --git a/packages/horizon/test/staking/slash/slash.t.sol b/packages/horizon/test/staking/slash/slash.t.sol index 7c7933419..fc5676b0f 100644 --- a/packages/horizon/test/staking/slash/slash.t.sol +++ b/packages/horizon/test/staking/slash/slash.t.sol @@ -54,7 +54,7 @@ contract HorizonStakingSlashTest is HorizonStakingTest { uint256 delegationTokens ) public useIndexer useProvision(tokens, MAX_PPM, 0) { vm.assume(slashTokens > tokens); - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); verifierCutAmount = bound(verifierCutAmount, 0, MAX_PPM); vm.assume(verifierCutAmount <= tokens); @@ -71,7 +71,7 @@ contract HorizonStakingSlashTest is HorizonStakingTest { uint256 verifierCutAmount, uint256 delegationTokens ) public useIndexer useProvision(tokens, MAX_PPM, 0) useDelegationSlashing() { - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); slashTokens = bound(slashTokens, tokens + 1, tokens + 1 + delegationTokens); verifierCutAmount = bound(verifierCutAmount, 0, tokens); @@ -110,7 +110,7 @@ contract HorizonStakingSlashTest is HorizonStakingTest { uint256 tokens, uint256 delegationTokens ) public useIndexer useProvision(tokens, MAX_PPM, 0) useDelegationSlashing { - delegationTokens = bound(delegationTokens, 1, MAX_STAKING_TOKENS); + delegationTokens = bound(delegationTokens, MIN_DELEGATION, MAX_STAKING_TOKENS); resetPrank(users.delegator); _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); @@ -140,4 +140,46 @@ contract HorizonStakingSlashTest is HorizonStakingTest { vm.startPrank(subgraphDataServiceAddress); _slash(users.indexer, subgraphDataServiceAddress, tokens + delegationTokens, 0); } + + function testSlash_RoundDown_TokensThawing_Provision() public useIndexer { + uint256 tokens = 1 ether + 1; + _useProvision(subgraphDataServiceAddress, tokens, MAX_PPM, MAX_THAWING_PERIOD); + + _thaw(users.indexer, subgraphDataServiceAddress, tokens); + + resetPrank(subgraphDataServiceAddress); + _slash(users.indexer, subgraphDataServiceAddress, 1, 0); + + resetPrank(users.indexer); + Provision memory provision = staking.getProvision(users.indexer, subgraphDataServiceAddress); + assertEq(provision.tokens, tokens - 1); + // Tokens thawing should be rounded down + assertEq(provision.tokensThawing, tokens - 2); + } + + function testSlash_RoundDown_TokensThawing_Delegation( + uint256 tokens + ) public useIndexer useProvision(tokens, MAX_PPM, 0) useDelegationSlashing { + resetPrank(users.delegator); + uint256 delegationTokens = 1 ether + 1; + _delegate(users.indexer, subgraphDataServiceAddress, delegationTokens, 0); + + DelegationInternal memory delegation = _getStorage_Delegation( + users.indexer, + subgraphDataServiceAddress, + users.delegator, + false + ); + _undelegate(users.indexer, subgraphDataServiceAddress, delegation.shares); + + resetPrank(subgraphDataServiceAddress); + // Slash 1 token from delegation + _slash(users.indexer, subgraphDataServiceAddress, tokens + 1, 0); + + resetPrank(users.delegator); + DelegationPool memory pool = staking.getDelegationPool(users.indexer, subgraphDataServiceAddress); + assertEq(pool.tokens, delegationTokens - 1); + // Tokens thawing should be rounded down + assertEq(pool.tokensThawing, delegationTokens - 2); + } } diff --git a/packages/horizon/test/utils/Constants.sol b/packages/horizon/test/utils/Constants.sol index e9ad5c2e9..e74e3b0d1 100644 --- a/packages/horizon/test/utils/Constants.sol +++ b/packages/horizon/test/utils/Constants.sol @@ -10,9 +10,11 @@ abstract contract Constants { // GraphPayments parameters uint256 internal constant protocolPaymentCut = 10000; // Staking constants - uint256 internal constant MAX_THAW_REQUESTS = 100; + uint256 internal constant MAX_THAW_REQUESTS = 1_000; uint64 internal constant MAX_THAWING_PERIOD = 28 days; uint32 internal constant THAWING_PERIOD_IN_BLOCKS = 300; + uint256 internal constant MIN_DELEGATION = 1e18; + uint256 internal constant MIN_UNDELEGATION_WITH_BENEFICIARY = 10e18; // Epoch manager uint256 internal constant EPOCH_LENGTH = 1; // Rewards manager diff --git a/packages/horizon/test/utils/Users.sol b/packages/horizon/test/utils/Users.sol index b26329f91..ecd3927ab 100644 --- a/packages/horizon/test/utils/Users.sol +++ b/packages/horizon/test/utils/Users.sol @@ -9,4 +9,5 @@ struct Users { address gateway; address verifier; address delegator; + address legacySlasher; } \ No newline at end of file