diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 767449026..5e1908064 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -21,11 +21,10 @@ import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/r * @title Rewards Manager Contract * @author Edge & Node * @notice Manages rewards distribution for indexers and delegators in the Graph Protocol - * @dev Tracks how inflationary GRT rewards should be handed out. Relies on the Curation contract - * and the Staking contract. Signaled GRT in Curation determine what percentage of the tokens go - * towards each subgraph. Then each Subgraph can have multiple Indexers Staked on it. Thus, the - * total rewards for the Subgraph are split up for each Indexer based on much they have Staked on - * that Subgraph. + * @dev Tracks how inflationary GRT rewards should be handed out. Signaled GRT in Curation determine + * what percentage of the tokens go towards each subgraph. Then each Subgraph can have multiple + * Indexers Staked on it. Thus, the total rewards for the Subgraph are split up for each Indexer based + * on much they have Staked on that Subgraph. * * Note: * The contract provides getter functions to query the state of accrued rewards: @@ -249,17 +248,10 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa subgraph.accRewardsForSubgraphSnapshot ); - // There are two contributors to subgraph allocated tokens: - // - the legacy allocations on the legacy staking contract - // - the new allocations on the subgraph service uint256 subgraphAllocatedTokens = 0; - address[2] memory rewardsIssuers = [address(staking()), address(subgraphService)]; - for (uint256 i = 0; i < rewardsIssuers.length; i++) { - if (rewardsIssuers[i] != address(0)) { - subgraphAllocatedTokens += IRewardsIssuer(rewardsIssuers[i]).getSubgraphAllocatedTokens( - _subgraphDeploymentID - ); - } + address rewardsIssuer = address(subgraphService); + if (rewardsIssuer != address(0)) { + subgraphAllocatedTokens += IRewardsIssuer(rewardsIssuer).getSubgraphAllocatedTokens(_subgraphDeploymentID); } if (subgraphAllocatedTokens == 0) { @@ -303,7 +295,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa /** * @inheritdoc IRewardsManager - * @dev Hook called from the Staking contract on allocate() and close() + * @dev Hook called from the IRewardsIssuer contract on allocate() and close() */ function onSubgraphAllocationUpdate(bytes32 _subgraphDeploymentID) public override returns (uint256) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; @@ -317,10 +309,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa /// @inheritdoc IRewardsManager function getRewards(address _rewardsIssuer, address _allocationID) external view override returns (uint256) { - require( - _rewardsIssuer == address(staking()) || _rewardsIssuer == address(subgraphService), - "Not a rewards issuer" - ); + require(_rewardsIssuer == address(subgraphService), "Not a rewards issuer"); ( bool isActive, @@ -372,15 +361,12 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa /** * @inheritdoc IRewardsManager * @dev This function can only be called by an authorized rewards issuer which are - * the staking contract (for legacy allocations), and the subgraph service (for new allocations). + * - the subgraph service (for allocations). * Mints 0 tokens if the allocation is not active. */ function takeRewards(address _allocationID) external override returns (uint256) { address rewardsIssuer = msg.sender; - require( - rewardsIssuer == address(staking()) || rewardsIssuer == address(subgraphService), - "Caller must be a rewards issuer" - ); + require(rewardsIssuer == address(subgraphService), "Caller must be a rewards issuer"); ( bool isActive, diff --git a/packages/contracts/test/tests/unit/disputes/poi.test.ts b/packages/contracts/test/tests/unit/disputes/poi.test.ts index b465f5986..b391dd0d4 100644 --- a/packages/contracts/test/tests/unit/disputes/poi.test.ts +++ b/packages/contracts/test/tests/unit/disputes/poi.test.ts @@ -1,4 +1,4 @@ -import { DisputeManager } from '@graphprotocol/contracts' +import { DisputeManager, IRewardsManager } from '@graphprotocol/contracts' import { EpochManager } from '@graphprotocol/contracts' import { GraphToken } from '@graphprotocol/contracts' import { IStaking } from '@graphprotocol/contracts' @@ -30,6 +30,7 @@ describe('DisputeManager:POI', () => { let epochManager: EpochManager let grt: GraphToken let staking: IStaking + let rewardsManager: IRewardsManager // Derive some channel keys for each indexer used to sign attestations const indexerChannelKey = deriveChannelKey() @@ -92,10 +93,15 @@ describe('DisputeManager:POI', () => { epochManager = contracts.EpochManager as EpochManager grt = contracts.GraphToken as GraphToken staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager as IRewardsManager // Give some funds to the fisherman await grt.connect(governor).mint(fisherman.address, fishermanTokens) await grt.connect(fisherman).approve(disputeManager.address, fishermanTokens) + + // HACK: we set the staking contract as the subgraph service to make tests pass. + // This is due to the test suite being outdated. + await rewardsManager.connect(governor).setSubgraphService(staking.address) }) beforeEach(async function () { diff --git a/packages/contracts/test/tests/unit/disputes/query.test.ts b/packages/contracts/test/tests/unit/disputes/query.test.ts index 73238b4e0..e411bd028 100644 --- a/packages/contracts/test/tests/unit/disputes/query.test.ts +++ b/packages/contracts/test/tests/unit/disputes/query.test.ts @@ -1,5 +1,5 @@ import { createAttestation, Receipt } from '@graphprotocol/common-ts' -import { DisputeManager } from '@graphprotocol/contracts' +import { DisputeManager, IRewardsManager } from '@graphprotocol/contracts' import { EpochManager } from '@graphprotocol/contracts' import { GraphToken } from '@graphprotocol/contracts' import { IStaking } from '@graphprotocol/contracts' @@ -35,6 +35,7 @@ describe('DisputeManager:Query', () => { let epochManager: EpochManager let grt: GraphToken let staking: IStaking + let rewardsManager: IRewardsManager // Derive some channel keys for each indexer used to sign attestations const indexer1ChannelKey = deriveChannelKey() @@ -121,6 +122,7 @@ describe('DisputeManager:Query', () => { epochManager = contracts.EpochManager as EpochManager grt = contracts.GraphToken as GraphToken staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager as IRewardsManager // Give some funds to the fisherman for (const dst of [fisherman, fisherman2]) { @@ -139,6 +141,10 @@ describe('DisputeManager:Query', () => { indexerAddress: indexer.address, receipt, } + + // HACK: we set the staking contract as the subgraph service to make tests pass. + // This is due to the test suite being outdated. + await rewardsManager.connect(governor).setSubgraphService(staking.address) }) beforeEach(async function () { diff --git a/packages/contracts/test/tests/unit/l2/l2Staking.test.ts b/packages/contracts/test/tests/unit/l2/l2Staking.test.ts index 39dc75e7a..cf22eaba0 100644 --- a/packages/contracts/test/tests/unit/l2/l2Staking.test.ts +++ b/packages/contracts/test/tests/unit/l2/l2Staking.test.ts @@ -1,4 +1,4 @@ -import { IL2Staking } from '@graphprotocol/contracts' +import { IL2Staking, IRewardsManager } from '@graphprotocol/contracts' import { L2GraphTokenGateway } from '@graphprotocol/contracts' import { GraphToken } from '@graphprotocol/contracts' import { EpochManager, L1GNS, L1GraphTokenGateway, L1Staking } from '@graphprotocol/contracts' @@ -35,6 +35,7 @@ describe('L2Staking', () => { let l2GraphTokenGateway: L2GraphTokenGateway let staking: IL2Staking let grt: GraphToken + let rewardsManager: IRewardsManager const tokens10k = toGRT('10000') const tokens100k = toGRT('100000') @@ -88,6 +89,7 @@ describe('L2Staking', () => { l1StakingMock = l1MockContracts.L1Staking as L1Staking l1GNSMock = l1MockContracts.L1GNS as L1GNS l1GRTGatewayMock = l1MockContracts.L1GraphTokenGateway as L1GraphTokenGateway + rewardsManager = fixtureContracts.RewardsManager as IRewardsManager // Deploy L2 arbitrum bridge await fixture.loadL2ArbitrumBridge(governor) @@ -99,6 +101,10 @@ describe('L2Staking', () => { await grt.connect(me).approve(staking.address, tokens1m) await grt.connect(governor).mint(other.address, tokens1m) await grt.connect(other).approve(staking.address, tokens1m) + + // HACK: we set the staking contract as the subgraph service to make tests pass. + // This is due to the test suite being outdated. + await rewardsManager.connect(governor).setSubgraphService(staking.address) }) beforeEach(async function () { diff --git a/packages/contracts/test/tests/unit/rewards/rewards.test.ts b/packages/contracts/test/tests/unit/rewards/rewards.test.ts index e6171cc13..fbf0912eb 100644 --- a/packages/contracts/test/tests/unit/rewards/rewards.test.ts +++ b/packages/contracts/test/tests/unit/rewards/rewards.test.ts @@ -151,6 +151,10 @@ describe('Rewards', () => { await grt.connect(wallet).approve(staking.address, toGRT('1000000')) await grt.connect(wallet).approve(curation.address, toGRT('1000000')) } + + // HACK: we set the staking contract as the subgraph service to make tests pass. + // This is due to the test suite being outdated. + await rewardsManager.connect(governor).setSubgraphService(staking.address) }) beforeEach(async function () { diff --git a/packages/contracts/test/tests/unit/staking/allocation.test.ts b/packages/contracts/test/tests/unit/staking/allocation.test.ts index dd28aa73d..76de77a35 100644 --- a/packages/contracts/test/tests/unit/staking/allocation.test.ts +++ b/packages/contracts/test/tests/unit/staking/allocation.test.ts @@ -379,6 +379,10 @@ describe('Staking:Allocation', () => { // Give some funds to the delegator and approve staking contract to use funds on delegator behalf await grt.connect(governor).mint(delegator.address, tokensToDelegate) await grt.connect(delegator).approve(staking.address, tokensToDelegate) + + // HACK: we set the staking contract as the subgraph service to make tests pass. + // This is due to the test suite being outdated. + await rewardsManager.connect(governor).setSubgraphService(staking.address) }) beforeEach(async function () { diff --git a/packages/contracts/test/tests/unit/staking/delegation.test.ts b/packages/contracts/test/tests/unit/staking/delegation.test.ts index 71f911006..3542e817e 100644 --- a/packages/contracts/test/tests/unit/staking/delegation.test.ts +++ b/packages/contracts/test/tests/unit/staking/delegation.test.ts @@ -1,4 +1,4 @@ -import { EpochManager } from '@graphprotocol/contracts' +import { EpochManager, IRewardsManager } from '@graphprotocol/contracts' import { GraphToken } from '@graphprotocol/contracts' import { IStaking } from '@graphprotocol/contracts' import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toBN, toGRT } from '@graphprotocol/sdk' @@ -29,6 +29,7 @@ describe('Staking::Delegation', () => { let epochManager: EpochManager let grt: GraphToken let staking: IStaking + let rewardsManager: IRewardsManager // Test values const poi = randomHexBytes() @@ -159,6 +160,7 @@ describe('Staking::Delegation', () => { epochManager = contracts.EpochManager as EpochManager grt = contracts.GraphToken as GraphToken staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager as IRewardsManager // Distribute test funds for (const wallet of [delegator, delegator2]) { @@ -173,6 +175,10 @@ describe('Staking::Delegation', () => { } await grt.connect(governor).mint(assetHolder.address, tokensToCollect) await grt.connect(assetHolder).approve(staking.address, tokensToCollect) + + // HACK: we set the staking contract as the subgraph service to make tests pass. + // This is due to the test suite being outdated. + await rewardsManager.connect(governor).setSubgraphService(staking.address) }) beforeEach(async function () { diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index 73f48c354..57ef2453f 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -9,7 +9,6 @@ pragma solidity 0.8.27; import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStakingMain } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol"; -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { ILinkedList } from "@graphprotocol/interfaces/contracts/horizon/internal/ILinkedList.sol"; @@ -28,9 +27,6 @@ import { HorizonStakingBase } from "./HorizonStakingBase.sol"; * @dev Implements the {IHorizonStakingMain} interface. * @dev This is the main Staking contract in The Graph protocol after the Horizon upgrade. * It is designed to be deployed as an upgrade to the L2Staking contract from the legacy contracts package. - * @dev It uses a {HorizonStakingExtension} contract to implement the full {IHorizonStaking} interface through delegatecalls. - * This is due to the contract size limit on Arbitrum (24kB). The extension contract implements functionality to support - * the legacy staking functions. It can be eventually removed without affecting the main staking contract. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ @@ -42,9 +38,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { /// @dev Maximum number of simultaneous stake thaw requests (per provision) or undelegations (per delegation) 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; @@ -77,50 +70,12 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { /** * @notice The staking contract is upgradeable however we still use the constructor to set a few immutable variables * @param controller The address of the Graph controller contract - * @param stakingExtensionAddress The address of the staking extension contract * @param subgraphDataServiceAddress The address of the subgraph data service */ constructor( address controller, - address stakingExtensionAddress, address subgraphDataServiceAddress - ) HorizonStakingBase(controller, subgraphDataServiceAddress) { - STAKING_EXTENSION_ADDRESS = stakingExtensionAddress; - } - - /** - * @notice Delegates the current call to the StakingExtension implementation. - * @dev This function does not return to its internal call site, it will return directly to the - * external caller. - */ - fallback() external { - // solhint-disable-previous-line payable-fallback, no-complex-fallback - address extensionImpl = STAKING_EXTENSION_ADDRESS; - // solhint-disable-next-line no-inline-assembly - assembly { - // (a) get free memory pointer - let ptr := mload(0x40) - - // (1) copy incoming call data - calldatacopy(ptr, 0, calldatasize()) - - // (2) forward call to logic contract - let result := delegatecall(gas(), extensionImpl, ptr, calldatasize(), 0, 0) - let size := returndatasize() - - // (3) retrieve return data - returndatacopy(ptr, 0, size) - - // (4) forward return data back to caller - switch result - case 0 { - revert(ptr, size) - } - default { - return(ptr, size) - } - } - } + ) HorizonStakingBase(controller, subgraphDataServiceAddress) {} /* * STAKING @@ -156,6 +111,11 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { _withdraw(msg.sender); } + /// @inheritdoc IHorizonStakingMain + function forceWithdraw(address serviceProvider) external override notPaused { + _withdraw(serviceProvider); + } + /* * PROVISIONS */ @@ -367,33 +327,15 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { address serviceProvider, address // deprecated - kept for backwards compatibility ) 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, HorizonStakingNothingToWithdraw()); - - // 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 _withdrawDelegatedLegacy(serviceProvider, msg.sender); + } - return tokensToWithdraw; + /// @inheritdoc IHorizonStakingMain + function forceWithdrawDelegated( + address serviceProvider, + address delegator + ) external override notPaused returns (uint256) { + return _withdrawDelegatedLegacy(serviceProvider, delegator); } /* @@ -407,21 +349,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { uint256 tokensVerifier, address verifierDestination ) external override notPaused { - // TRANSITION PERIOD: 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 - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = STAKING_EXTENSION_ADDRESS.delegatecall( - abi.encodeCall( - IHorizonStakingExtension.legacySlash, - (serviceProvider, tokens, tokensVerifier, verifierDestination) - ) - ); - require(success, HorizonStakingLegacySlashFailed()); - return; - } - address verifier = msg.sender; Provision storage prov = _provisions[serviceProvider][verifier]; DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier); @@ -538,12 +465,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { emit DelegationSlashingEnabled(); } - /// @inheritdoc IHorizonStakingMain - function clearThawingPeriod() external override onlyGovernor { - __DEPRECATED_thawingPeriod = 0; - emit ThawingPeriodCleared(); - } - /// @inheritdoc IHorizonStakingMain function setMaxThawingPeriod(uint64 maxThawingPeriod) external override onlyGovernor { _maxThawingPeriod = maxThawingPeriod; @@ -569,17 +490,19 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { } /* - * GETTERS + * PRIVATE FUNCTIONS */ - /// @inheritdoc IHorizonStakingMain - function getStakingExtension() external view override returns (address) { - return STAKING_EXTENSION_ADDRESS; - } - - /* - * PRIVATE FUNCTIONS + /** + * @notice Deposit tokens into the service provider stake. + * Emits a {HorizonStakeDeposited} event. + * @param _serviceProvider The address of the service provider. + * @param _tokens The amount of tokens to deposit. */ + function _stake(address _serviceProvider, uint256 _tokens) internal { + _serviceProviders[_serviceProvider].tokensStaked = _serviceProviders[_serviceProvider].tokensStaked + _tokens; + emit HorizonStakeDeposited(_serviceProvider, _tokens); + } /** * @notice Deposit tokens on the service provider stake, on behalf of the service provider. @@ -599,12 +522,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { /** * @notice Move idle stake back to the owner's account. - * Stake is removed from the protocol: - * - 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}. + * Stake is immediately removed from the protocol. * @param _tokens Amount of tokens to unstake */ function _unstake(uint256 _tokens) private { @@ -614,45 +532,19 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { require(_tokens <= tokensIdle, HorizonStakingInsufficientIdleStake(_tokens, tokensIdle)); ServiceProviderInternal storage sp = _serviceProviders[serviceProvider]; - uint256 stakedTokens = sp.tokensStaked; - - // This is also only during the transition period: we need - // to ensure tokens stay locked after closing legacy allocations. - // After sufficient time (56 days?) we should remove the closeAllocation function - // and set the thawing period to 0. - uint256 lockingPeriod = __DEPRECATED_thawingPeriod; - if (lockingPeriod == 0) { - sp.tokensStaked = stakedTokens - _tokens; - _graphToken().pushTokens(serviceProvider, _tokens); - emit HorizonStakeWithdrawn(serviceProvider, _tokens); - } else { - // Before locking more tokens, withdraw any unlocked ones if possible - if (sp.__DEPRECATED_tokensLocked != 0 && block.number >= sp.__DEPRECATED_tokensLockedUntil) { - _withdraw(serviceProvider); - } - // TRANSITION PERIOD: remove after the transition period - // Take into account period averaging for multiple unstake requests - if (sp.__DEPRECATED_tokensLocked > 0) { - lockingPeriod = MathUtils.weightedAverageRoundingUp( - MathUtils.diffOrZero(sp.__DEPRECATED_tokensLockedUntil, block.number), // Remaining thawing period - sp.__DEPRECATED_tokensLocked, // Weighted by remaining unstaked tokens - lockingPeriod, // Thawing period - _tokens // Weighted by new tokens to unstake - ); - } + sp.tokensStaked -= _tokens; - // Update balances - sp.__DEPRECATED_tokensLocked = sp.__DEPRECATED_tokensLocked + _tokens; - sp.__DEPRECATED_tokensLockedUntil = block.number + lockingPeriod; - emit HorizonStakeLocked(serviceProvider, sp.__DEPRECATED_tokensLocked, sp.__DEPRECATED_tokensLockedUntil); - } + _graphToken().pushTokens(serviceProvider, _tokens); + emit HorizonStakeWithdrawn(serviceProvider, _tokens); } /** * @notice Withdraw service provider tokens once the thawing period (initiated by {unstake}) has passed. * All thawed tokens are withdrawn. - * @dev TRANSITION PERIOD: This is only needed during the transition period while we still have - * a global lock. After that, unstake() will automatically withdraw. + * This function is for backwards compatibility with the legacy staking contract. + * It only allows withdrawing tokens unstaked before horizon upgrade. + * @dev This function can't be removed in case there are still pre-horizon unstakes. + * Note that it's assumed unstakes have already passed their thawing period. * @param _serviceProvider Address of service provider to withdraw funds from */ function _withdraw(address _serviceProvider) private { @@ -660,10 +552,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { ServiceProviderInternal storage sp = _serviceProviders[_serviceProvider]; uint256 tokensToWithdraw = sp.__DEPRECATED_tokensLocked; require(tokensToWithdraw != 0, HorizonStakingInvalidZeroTokens()); - require( - block.number >= sp.__DEPRECATED_tokensLockedUntil, - HorizonStakingStillThawing(sp.__DEPRECATED_tokensLockedUntil) - ); // Reset locked tokens sp.__DEPRECATED_tokensLocked = 0; @@ -683,8 +571,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * service, where the data service is the verifier. * This function can be called by the service provider or by an operator authorized by the provider * for this specific verifier. - * @dev TRANSITION PERIOD: During the transition period, only the subgraph data service can be used as a verifier. This - * prevents an escape hatch for legacy allocation stake. * @param _serviceProvider The service provider address * @param _tokens The amount of tokens that will be locked and slashable * @param _verifier The verifier address for which the tokens are provisioned (who will be able to slash the tokens) @@ -699,11 +585,6 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { uint64 _thawingPeriod ) private { require(_tokens > 0, HorizonStakingInvalidZeroTokens()); - // TRANSITION PERIOD: Remove this after the transition period - it prevents an early escape hatch for legacy allocations - require( - _verifier == SUBGRAPH_DATA_SERVICE_ADDRESS || __DEPRECATED_thawingPeriod == 0, - HorizonStakingInvalidVerifier(_verifier) - ); require(PPMMath.isValidPPM(_maxVerifierCut), HorizonStakingInvalidMaxVerifierCut(_maxVerifierCut)); require( _thawingPeriod <= _maxThawingPeriod, @@ -1228,6 +1109,42 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { emit OperatorSet(msg.sender, _verifier, _operator, _allowed); } + /** + * @notice Withdraw legacy undelegated tokens for a delegator. + * @dev This function handles pre-Horizon undelegations where tokens are locked + * in the legacy delegation pool. + * @param _serviceProvider The service provider address + * @param _delegator The delegator address + * @return The amount of tokens withdrawn + */ + function _withdrawDelegatedLegacy(address _serviceProvider, address _delegator) private returns (uint256) { + 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, HorizonStakingNothingToWithdraw()); + + // 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; + } + /** * @notice Check if an operator is authorized for the caller on a specific verifier / data service. * @dev Note that this function handles the special case where the verifier is the subgraph data service, diff --git a/packages/horizon/contracts/staking/HorizonStakingBase.sol b/packages/horizon/contracts/staking/HorizonStakingBase.sol index 9c52a2171..75e2cd37c 100644 --- a/packages/horizon/contracts/staking/HorizonStakingBase.sol +++ b/packages/horizon/contracts/staking/HorizonStakingBase.sol @@ -23,9 +23,7 @@ import { HorizonStakingV1Storage } from "./HorizonStakingStorage.sol"; * @author Edge & Node * @notice This contract is the base staking contract implementing storage getters for both internal * and external use. - * @dev Implementation of the {IHorizonStakingBase} interface. - * @dev It's meant to be inherited by the {HorizonStaking} and {HorizonStakingExtension} - * contracts so some internal functions are also included here. + * @dev Implementation of the {IHorizonStakingBase} interface, meant to be inherited by {HorizonStaking}. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ @@ -54,6 +52,11 @@ abstract contract HorizonStakingBase is SUBGRAPH_DATA_SERVICE_ADDRESS = subgraphDataServiceAddress; } + /// @inheritdoc IHorizonStakingBase + function getSubgraphService() external view override returns (address) { + return SUBGRAPH_DATA_SERVICE_ADDRESS; + } + /// @inheritdoc IHorizonStakingBase /// @dev Removes deprecated fields from the return value. function getServiceProvider(address serviceProvider) external view override returns (ServiceProvider memory) { @@ -218,31 +221,18 @@ abstract contract HorizonStakingBase is return _delegationSlashingEnabled; } - /** - * @notice Deposit tokens into the service provider stake. - * @dev TRANSITION PERIOD: After transition period move to IHorizonStakingMain. Temporarily it - * needs to be here since it's used by both {HorizonStaking} and {HorizonStakingExtension}. - * - * Emits a {HorizonStakeDeposited} event. - * @param _serviceProvider The address of the service provider. - * @param _tokens The amount of tokens to deposit. - */ - function _stake(address _serviceProvider, uint256 _tokens) internal { - _serviceProviders[_serviceProvider].tokensStaked = _serviceProviders[_serviceProvider].tokensStaked + _tokens; - emit HorizonStakeDeposited(_serviceProvider, _tokens); - } - /** * @notice Gets the service provider's idle stake which is the stake that is not being * used for any provision. Note that this only includes service provider's self stake. - * @dev Note that the calculation considers tokens that were locked in the legacy staking contract. - * @dev TRANSITION PERIOD: update the calculation after the transition period. + * @dev Note that the calculation: + * - assumes tokens that were allocated to a subgraph deployment pre-horizon were all unallocated. + * - considers tokens that were locked in the legacy staking contract and never withdrawn. + * * @param _serviceProvider The address of the service provider. * @return The amount of tokens that are idle. */ function _getIdleStake(address _serviceProvider) internal view returns (uint256) { uint256 tokensUsed = _serviceProviders[_serviceProvider].tokensProvisioned + - _serviceProviders[_serviceProvider].__DEPRECATED_tokensAllocated + _serviceProviders[_serviceProvider].__DEPRECATED_tokensLocked; uint256 tokensStaked = _serviceProviders[_serviceProvider].tokensStaked; return tokensStaked > tokensUsed ? tokensStaked - tokensUsed : 0; diff --git a/packages/horizon/contracts/staking/HorizonStakingExtension.sol b/packages/horizon/contracts/staking/HorizonStakingExtension.sol deleted file mode 100644 index b1adcde0d..000000000 --- a/packages/horizon/contracts/staking/HorizonStakingExtension.sol +++ /dev/null @@ -1,484 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity 0.8.27; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable function-max-lines, gas-strict-inequalities - -import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; -import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; -import { IRewardsIssuer } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol"; - -import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol"; -import { MathUtils } from "../libraries/MathUtils.sol"; -import { ExponentialRebates } from "./libraries/ExponentialRebates.sol"; -import { PPMMath } from "../libraries/PPMMath.sol"; - -import { HorizonStakingBase } from "./HorizonStakingBase.sol"; - -/** - * @title Horizon Staking extension contract - * @author Edge & Node - * @notice The {HorizonStakingExtension} contract implements the legacy functionality required to support the transition - * 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 TRANSITION PERIOD: 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 at least 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. - */ -contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension { - using TokenUtils for IGraphToken; - using PPMMath for uint256; - - /** - * @dev Check if the caller is the slasher. - */ - modifier onlySlasher() { - require(__DEPRECATED_slashers[msg.sender], "!slasher"); - _; - } - - /** - * @notice The staking contract is upgradeable however we still use the constructor to set a few immutable variables - * @param controller The address of the Graph controller contract - * @param subgraphDataServiceAddress The address of the subgraph data service - */ - constructor( - address controller, - address subgraphDataServiceAddress - ) HorizonStakingBase(controller, subgraphDataServiceAddress) {} - - /// @inheritdoc IHorizonStakingExtension - function closeAllocation(address allocationID, bytes32 poi) external override notPaused { - _closeAllocation(allocationID, poi); - } - - /// @inheritdoc IHorizonStakingExtension - function collect(uint256 tokens, address allocationID) external override notPaused { - // Allocation identifier validation - require(allocationID != address(0), "!alloc"); - - // Allocation must exist - AllocationState allocState = _getAllocationState(allocationID); - require(allocState != AllocationState.Null, "!collect"); - - // If the query fees are zero, we don't want to revert - // but we also don't need to do anything, so just return - if (tokens == 0) { - return; - } - - Allocation storage alloc = __DEPRECATED_allocations[allocationID]; - bytes32 subgraphDeploymentID = alloc.subgraphDeploymentID; - - uint256 queryFees = tokens; // Tokens collected from the channel - uint256 protocolTax = 0; // Tokens burnt as protocol tax - uint256 curationFees = 0; // Tokens distributed to curators as curation fees - uint256 queryRebates = 0; // Tokens to distribute to indexer - uint256 delegationRewards = 0; // Tokens to distribute to delegators - - { - // -- Pull tokens from the sender -- - _graphToken().pullTokens(msg.sender, queryFees); - - // -- Collect protocol tax -- - protocolTax = _collectTax(queryFees, __DEPRECATED_protocolPercentage); - queryFees = queryFees - protocolTax; - - // -- Collect curation fees -- - // Only if the subgraph deployment is curated - curationFees = _collectCurationFees(subgraphDeploymentID, queryFees, __DEPRECATED_curationPercentage); - queryFees = queryFees - curationFees; - - // -- Process rebate reward -- - // Using accumulated fees and subtracting previously distributed rebates - // allows for multiple vouchers to be collected while following the rebate formula - alloc.collectedFees = alloc.collectedFees + queryFees; - - // No rebates if indexer has no stake or if lambda is zero - uint256 newRebates = (alloc.tokens == 0 || __DEPRECATED_lambdaNumerator == 0) - ? 0 - : ExponentialRebates.exponentialRebates( - alloc.collectedFees, - alloc.tokens, - __DEPRECATED_alphaNumerator, - __DEPRECATED_alphaDenominator, - __DEPRECATED_lambdaNumerator, - __DEPRECATED_lambdaDenominator - ); - - // -- Ensure rebates to distribute are within bounds -- - // Indexers can become under or over rebated if rebate parameters (alpha, lambda) - // change between successive collect calls for the same allocation - - // Ensure rebates to distribute are not negative (indexer is over-rebated) - queryRebates = MathUtils.diffOrZero(newRebates, alloc.distributedRebates); - - // Ensure rebates to distribute are not greater than available (indexer is under-rebated) - queryRebates = MathUtils.min(queryRebates, queryFees); - - // -- Burn rebates remanent -- - _graphToken().burnTokens(queryFees - queryRebates); - - // -- Distribute rebates -- - if (queryRebates > 0) { - alloc.distributedRebates = alloc.distributedRebates + queryRebates; - - // -- Collect delegation rewards into the delegation pool -- - delegationRewards = _collectDelegationQueryRewards(alloc.indexer, queryRebates); - queryRebates = queryRebates - delegationRewards; - - // -- Transfer or restake rebates -- - _sendRewards(queryRebates, alloc.indexer, __DEPRECATED_rewardsDestination[alloc.indexer] == address(0)); - } - } - - emit RebateCollected( - msg.sender, - alloc.indexer, - subgraphDeploymentID, - allocationID, - _graphEpochManager().currentEpoch(), - tokens, - protocolTax, - curationFees, - queryFees, - queryRebates, - delegationRewards - ); - } - - /// @inheritdoc IHorizonStakingExtension - 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 tokens that are already provisioned would break provision accounting, we need to limit - // the slash amount. This can be compensated for, by slashing with the main slash function if needed. - uint256 slashableStake = indexerStake.tokensStaked - indexerStake.tokensProvisioned; - if (slashableStake == 0) { - emit StakeSlashed(indexer, 0, 0, beneficiary); - return; - } - if (tokens > slashableStake) { - reward = (reward * slashableStake) / tokens; - tokens = slashableStake; - } - - // 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); - } - - /// @inheritdoc IHorizonStakingExtension - function isAllocation(address allocationID) external view override returns (bool) { - return _getAllocationState(allocationID) != AllocationState.Null; - } - - /// @inheritdoc IHorizonStakingExtension - function getAllocation(address allocationID) external view override returns (Allocation memory) { - return __DEPRECATED_allocations[allocationID]; - } - - /// @inheritdoc IRewardsIssuer - function getAllocationData( - address allocationID - ) external view override returns (bool, address, bytes32, uint256, uint256, uint256) { - Allocation memory allo = __DEPRECATED_allocations[allocationID]; - bool isActive = _getAllocationState(allocationID) == AllocationState.Active; - return (isActive, allo.indexer, allo.subgraphDeploymentID, allo.tokens, allo.accRewardsPerAllocatedToken, 0); - } - - /// @inheritdoc IHorizonStakingExtension - function getAllocationState(address allocationID) external view override returns (AllocationState) { - return _getAllocationState(allocationID); - } - - /// @inheritdoc IRewardsIssuer - function getSubgraphAllocatedTokens(bytes32 subgraphDeploymentID) external view override returns (uint256) { - return __DEPRECATED_subgraphAllocations[subgraphDeploymentID]; - } - - /// @inheritdoc IHorizonStakingExtension - function getIndexerStakedTokens(address indexer) external view override returns (uint256) { - return _serviceProviders[indexer].tokensStaked; - } - - /// @inheritdoc IHorizonStakingExtension - function getSubgraphService() external view override returns (address) { - return SUBGRAPH_DATA_SERVICE_ADDRESS; - } - - /// @inheritdoc IHorizonStakingExtension - function hasStake(address indexer) external view override returns (bool) { - return _serviceProviders[indexer].tokensStaked > 0; - } - - /// @inheritdoc IHorizonStakingExtension - function __DEPRECATED_getThawingPeriod() external view returns (uint64) { - return __DEPRECATED_thawingPeriod; - } - - /// @inheritdoc IHorizonStakingExtension - function isOperator(address operator, address serviceProvider) public view override returns (bool) { - return _legacyOperatorAuth[serviceProvider][operator]; - } - - /** - * @notice Collect tax to burn for an amount of tokens - * @param _tokens Total tokens received used to calculate the amount of tax to collect - * @param _percentage Percentage of tokens to burn as tax - * @return Amount of tax charged - */ - function _collectTax(uint256 _tokens, uint256 _percentage) private returns (uint256) { - uint256 tax = _tokens.mulPPMRoundUp(_percentage); - _graphToken().burnTokens(tax); // Burn tax if any - return tax; - } - - /** - * @notice Triggers an update of rewards due to a change in allocations - * @param _subgraphDeploymentID Subgraph deployment updated - */ - function _updateRewards(bytes32 _subgraphDeploymentID) private { - _graphRewardsManager().onSubgraphAllocationUpdate(_subgraphDeploymentID); - } - - /** - * @notice Assign rewards for the closed allocation to indexer and delegators - * @param _allocationID Allocation - * @param _indexer Address of the indexer that did the allocation - */ - function _distributeRewards(address _allocationID, address _indexer) private { - // Automatically triggers update of rewards snapshot as allocation will change - // after this call. Take rewards mint tokens for the Staking contract to distribute - // between indexer and delegators - uint256 totalRewards = _graphRewardsManager().takeRewards(_allocationID); - if (totalRewards == 0) { - return; - } - - // Calculate delegation rewards and add them to the delegation pool - uint256 delegationRewards = _collectDelegationIndexingRewards(_indexer, totalRewards); - uint256 indexerRewards = totalRewards - delegationRewards; - - // Send the indexer rewards - _sendRewards(indexerRewards, _indexer, __DEPRECATED_rewardsDestination[_indexer] == address(0)); - } - - /** - * @notice Send rewards to the appropriate destination - * @param _tokens Number of rewards tokens - * @param _beneficiary Address of the beneficiary of rewards - * @param _restake Whether to restake or not - */ - function _sendRewards(uint256 _tokens, address _beneficiary, bool _restake) private { - if (_tokens == 0) return; - - if (_restake) { - // Restake to place fees into the indexer stake - _stake(_beneficiary, _tokens); - } else { - // Transfer funds to the beneficiary's designated rewards destination if set - address destination = __DEPRECATED_rewardsDestination[_beneficiary]; - _graphToken().pushTokens(destination == address(0) ? _beneficiary : destination, _tokens); - } - } - - /** - * @notice Close an allocation and free the staked tokens - * @param _allocationID The allocation identifier - * @param _poi Proof of indexing submitted for the allocated period - */ - function _closeAllocation(address _allocationID, bytes32 _poi) private { - // Allocation must exist and be active - AllocationState allocState = _getAllocationState(_allocationID); - require(allocState == AllocationState.Active, "!active"); - - // Get allocation - Allocation memory alloc = __DEPRECATED_allocations[_allocationID]; - - // Validate that an allocation cannot be closed before one epoch - alloc.closedAtEpoch = _graphEpochManager().currentEpoch(); - uint256 epochs = MathUtils.diffOrZero(alloc.closedAtEpoch, alloc.createdAtEpoch); - - // Indexer or operator can close an allocation - // 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(msg.sender, alloc.indexer); - if (epochs <= __DEPRECATED_maxAllocationEpochs || alloc.tokens == 0) { - require(isIndexerOrOperator, "!auth"); - } - - // -- Rewards Distribution -- - - // Process non-zero-allocation rewards tracking - if (alloc.tokens > 0) { - // Distribute rewards if proof of indexing was presented by the indexer or operator - if (isIndexerOrOperator && _poi != 0 && epochs > 0) { - _distributeRewards(_allocationID, alloc.indexer); - } else { - _updateRewards(alloc.subgraphDeploymentID); - } - - // Free allocated tokens from use - _serviceProviders[alloc.indexer].__DEPRECATED_tokensAllocated = - _serviceProviders[alloc.indexer].__DEPRECATED_tokensAllocated - alloc.tokens; - - // Track total allocations per subgraph - // Used for rewards calculations - __DEPRECATED_subgraphAllocations[alloc.subgraphDeploymentID] = - __DEPRECATED_subgraphAllocations[alloc.subgraphDeploymentID] - alloc.tokens; - } - - // Close the allocation - // Note that this breaks CEI pattern. We update after the rewards distribution logic as it expects the allocation - // to still be active. There shouldn't be reentrancy risk here as all internal calls are to trusted contracts. - __DEPRECATED_allocations[_allocationID].closedAtEpoch = alloc.closedAtEpoch; - - emit AllocationClosed( - alloc.indexer, - alloc.subgraphDeploymentID, - alloc.closedAtEpoch, - alloc.tokens, - _allocationID, - msg.sender, - _poi, - !isIndexerOrOperator - ); - } - - /** - * @notice Collect the delegation rewards for query fees - * @dev This function will assign the collected fees to the delegation pool - * @param _indexer Indexer to which the tokens to distribute are related - * @param _tokens Total tokens received used to calculate the amount of fees to collect - * @return Amount of delegation rewards - */ - function _collectDelegationQueryRewards(address _indexer, uint256 _tokens) private returns (uint256) { - uint256 delegationRewards = 0; - DelegationPoolInternal storage pool = _legacyDelegationPools[_indexer]; - if (pool.tokens > 0 && uint256(pool.__DEPRECATED_queryFeeCut).isValidPPM()) { - uint256 indexerCut = uint256(pool.__DEPRECATED_queryFeeCut).mulPPM(_tokens); - delegationRewards = _tokens - indexerCut; - pool.tokens = pool.tokens + delegationRewards; - } - return delegationRewards; - } - - /** - * @notice Collect the delegation rewards for indexing - * @dev This function will assign the collected fees to the delegation pool - * @param _indexer Indexer to which the tokens to distribute are related - * @param _tokens Total tokens received used to calculate the amount of fees to collect - * @return Amount of delegation rewards - */ - function _collectDelegationIndexingRewards(address _indexer, uint256 _tokens) private returns (uint256) { - uint256 delegationRewards = 0; - DelegationPoolInternal storage pool = _legacyDelegationPools[_indexer]; - if (pool.tokens > 0 && uint256(pool.__DEPRECATED_indexingRewardCut).isValidPPM()) { - uint256 indexerCut = uint256(pool.__DEPRECATED_indexingRewardCut).mulPPM(_tokens); - delegationRewards = _tokens - indexerCut; - pool.tokens = pool.tokens + delegationRewards; - } - return delegationRewards; - } - - /** - * @notice Collect the curation fees for a subgraph deployment from an amount of tokens - * @dev This function transfer curation fees to the Curation contract by calling Curation.collect - * @param _subgraphDeploymentID Subgraph deployment to which the curation fees are related - * @param _tokens Total tokens received used to calculate the amount of fees to collect - * @param _curationCut Percentage of tokens to collect as fees - * @return Amount of curation fees - */ - function _collectCurationFees( - bytes32 _subgraphDeploymentID, - uint256 _tokens, - uint256 _curationCut - ) private returns (uint256) { - if (_tokens == 0) { - return 0; - } - - ICuration curation = _graphCuration(); - bool isCurationEnabled = _curationCut > 0 && address(curation) != address(0); - - if (isCurationEnabled && curation.isCurated(_subgraphDeploymentID)) { - uint256 curationFees = _tokens.mulPPMRoundUp(_curationCut); - if (curationFees > 0) { - // Transfer and call collect() - // This function transfer tokens to a trusted protocol contracts - // Then we call collect() to do the transfer Bookkeeping - _graphRewardsManager().onSubgraphSignalUpdate(_subgraphDeploymentID); - _graphToken().pushTokens(address(curation), curationFees); - curation.collect(_subgraphDeploymentID, curationFees); - } - return curationFees; - } - return 0; - } - - /** - * @notice Return the current state of an allocation - * @param _allocationID Allocation identifier - * @return AllocationState enum with the state of the allocation - */ - function _getAllocationState(address _allocationID) private view returns (AllocationState) { - Allocation storage alloc = __DEPRECATED_allocations[_allocationID]; - - if (alloc.indexer == address(0)) { - return AllocationState.Null; - } - - if (alloc.createdAtEpoch != 0 && alloc.closedAtEpoch == 0) { - return AllocationState.Active; - } - - return AllocationState.Closed; - } -} diff --git a/packages/horizon/contracts/staking/HorizonStakingStorage.sol b/packages/horizon/contracts/staking/HorizonStakingStorage.sol index 5f63af9df..a10b853a8 100644 --- a/packages/horizon/contracts/staking/HorizonStakingStorage.sol +++ b/packages/horizon/contracts/staking/HorizonStakingStorage.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.27; -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { ILinkedList } from "@graphprotocol/interfaces/contracts/horizon/internal/ILinkedList.sol"; @@ -62,7 +61,7 @@ abstract contract HorizonStakingV1Storage { /// @dev Allocation details. /// Deprecated, now applied on the subgraph data service - mapping(address allocationId => IHorizonStakingExtension.Allocation allocation) internal __DEPRECATED_allocations; + mapping(address allocationId => bytes32 __DEPRECATED_allocation) internal __DEPRECATED_allocations; /// @dev Subgraph allocations, tracks the tokens allocated to a subgraph deployment /// Deprecated, now applied on the SubgraphService @@ -89,7 +88,7 @@ abstract contract HorizonStakingV1Storage { uint32 internal __DEPRECATED_delegationParametersCooldown; /// @dev Time in epochs a delegator needs to wait to withdraw delegated stake - /// Deprecated, now only enforced during a transition period + /// Deprecated, enforced by each data service as needed. uint32 internal __DEPRECATED_delegationUnbondingPeriod; /// @dev Percentage of tokens to tax a delegation deposit diff --git a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol deleted file mode 100644 index 974e7197b..000000000 --- a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity 0.8.27; - -import { LibFixedMath } from "../../libraries/LibFixedMath.sol"; - -/** - * @title ExponentialRebates library - * @author Edge & Node - * @notice A library to compute query fee rebates using an exponential formula - * @dev This is only used for backwards compatibility in HorizonStaking, and should - * be removed after the transition period. - * @custom:security-contact Please email security+contracts@thegraph.com if you find any - * bugs. We may have an active bug bounty program. - */ -library ExponentialRebates { - /// @dev Maximum value of the exponent for which to compute the exponential before clamping to zero. - uint32 private constant MAX_EXPONENT = 15; - - /** - * @notice The exponential formula used to compute fee-based rewards for staking pools in a given epoch - * @dev This function does not perform bounds checking on the inputs, but the following conditions - * need to be true: - * 0 <= alphaNumerator / alphaDenominator <= 1 - * 0 < lambdaNumerator / lambdaDenominator - * The exponential rebates function has the form: - * `(1 - alpha * exp ^ (-lambda * stake / fees)) * fees` - * @param fees Fees generated by indexer in the staking pool - * @param stake Stake attributed to the indexer in the staking pool - * @param alphaNumerator Numerator of `alpha` in the rebates function - * @param alphaDenominator Denominator of `alpha` in the rebates function - * @param lambdaNumerator Numerator of `lambda` in the rebates function - * @param lambdaDenominator Denominator of `lambda` in the rebates function - * @return rewards Rewards owed to the staking pool - */ - function exponentialRebates( - uint256 fees, - uint256 stake, - uint32 alphaNumerator, - uint32 alphaDenominator, - uint32 lambdaNumerator, - uint32 lambdaDenominator - ) external pure returns (uint256) { - // If alpha is zero indexer gets 100% fees rebate - int256 alpha = LibFixedMath.toFixed(int32(alphaNumerator), int32(alphaDenominator)); - if (alpha == 0) { - return fees; - } - - // No rebates if no fees... - if (fees == 0) { - return 0; - } - - // Award all fees as rebate if the exponent is too large - int256 lambda = LibFixedMath.toFixed(int32(lambdaNumerator), int32(lambdaDenominator)); - int256 exponent = LibFixedMath.mulDiv(lambda, int256(stake), int256(fees)); - if (LibFixedMath.toInteger(exponent) > int256(uint256(MAX_EXPONENT))) { - return fees; - } - - // Compute `1 - alpha * exp ^(-exponent)` - int256 factor = LibFixedMath.sub(LibFixedMath.one(), LibFixedMath.mul(alpha, LibFixedMath.exp(-exponent))); - - // Weight the fees by the factor - return LibFixedMath.uintMul(factor, fees); - } -} diff --git a/packages/horizon/contracts/utilities/GraphDirectory.sol b/packages/horizon/contracts/utilities/GraphDirectory.sol index 6e657c6d7..f5f86841e 100644 --- a/packages/horizon/contracts/utilities/GraphDirectory.sol +++ b/packages/horizon/contracts/utilities/GraphDirectory.sol @@ -13,8 +13,6 @@ import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/r import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; import { IGraphProxyAdmin } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol"; -import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; - /** * @title GraphDirectory contract * @author Edge & Node @@ -55,13 +53,6 @@ abstract contract GraphDirectory { /// @notice The Graph Proxy Admin contract address IGraphProxyAdmin private immutable GRAPH_PROXY_ADMIN; - // -- Legacy Graph contracts -- - // These are required for backwards compatibility on HorizonStakingExtension - // TRANSITION PERIOD: remove these once HorizonStakingExtension is removed - - /// @notice The Curation contract address - ICuration private immutable GRAPH_CURATION; - /** * @notice Emitted when the GraphDirectory is initialized * @param graphToken The Graph Token contract address @@ -73,7 +64,6 @@ abstract contract GraphDirectory { * @param graphRewardsManager The Rewards Manager contract address * @param graphTokenGateway The Token Gateway contract address * @param graphProxyAdmin The Graph Proxy Admin contract address - * @param graphCuration The Curation contract address */ event GraphDirectoryInitialized( address indexed graphToken, @@ -84,8 +74,7 @@ abstract contract GraphDirectory { address graphEpochManager, address graphRewardsManager, address graphTokenGateway, - address graphProxyAdmin, - address graphCuration + address graphProxyAdmin ); /** @@ -116,7 +105,6 @@ abstract contract GraphDirectory { GRAPH_REWARDS_MANAGER = IRewardsManager(_getContractFromController("RewardsManager")); GRAPH_TOKEN_GATEWAY = ITokenGateway(_getContractFromController("GraphTokenGateway")); GRAPH_PROXY_ADMIN = IGraphProxyAdmin(_getContractFromController("GraphProxyAdmin")); - GRAPH_CURATION = ICuration(_getContractFromController("Curation")); emit GraphDirectoryInitialized( address(GRAPH_TOKEN), @@ -127,8 +115,7 @@ abstract contract GraphDirectory { address(GRAPH_EPOCH_MANAGER), address(GRAPH_REWARDS_MANAGER), address(GRAPH_TOKEN_GATEWAY), - address(GRAPH_PROXY_ADMIN), - address(GRAPH_CURATION) + address(GRAPH_PROXY_ADMIN) ); } @@ -204,14 +191,6 @@ abstract contract GraphDirectory { return GRAPH_PROXY_ADMIN; } - /** - * @notice Get the Curation contract - * @return The Curation contract - */ - function _graphCuration() internal view returns (ICuration) { - return GRAPH_CURATION; - } - /** * @notice Get a contract address from the controller * @dev Requirements: diff --git a/packages/horizon/ignition/modules/core/HorizonStaking.ts b/packages/horizon/ignition/modules/core/HorizonStaking.ts index c4044b0af..a7bec9076 100644 --- a/packages/horizon/ignition/modules/core/HorizonStaking.ts +++ b/packages/horizon/ignition/modules/core/HorizonStaking.ts @@ -3,8 +3,6 @@ import GraphProxyAdminArtifact from '@graphprotocol/contracts/artifacts/contract import { buildModule } from '@nomicfoundation/hardhat-ignition/modules' import HorizonStakingArtifact from '../../../build/contracts/contracts/staking/HorizonStaking.sol/HorizonStaking.json' -import HorizonStakingExtensionArtifact from '../../../build/contracts/contracts/staking/HorizonStakingExtension.sol/HorizonStakingExtension.json' -import ExponentialRebatesArtifact from '../../../build/contracts/contracts/staking/libraries/ExponentialRebates.sol/ExponentialRebates.json' import GraphPeripheryModule, { MigratePeripheryModule } from '../periphery/periphery' import { upgradeGraphProxy } from '../proxy/GraphProxy' import { deployImplementation } from '../proxy/implementation' @@ -17,25 +15,11 @@ export default buildModule('HorizonStaking', (m) => { const subgraphServiceAddress = m.getParameter('subgraphServiceAddress') const maxThawingPeriod = m.getParameter('maxThawingPeriod') - // Deploy HorizonStakingExtension - requires periphery and proxies to be registered in the controller - const ExponentialRebates = m.library('ExponentialRebates', ExponentialRebatesArtifact) - const HorizonStakingExtension = m.contract( - 'HorizonStakingExtension', - HorizonStakingExtensionArtifact, - [Controller, subgraphServiceAddress], - { - libraries: { - ExponentialRebates: ExponentialRebates, - }, - after: [GraphPeripheryModule, HorizonProxiesModule], - }, - ) - // Deploy HorizonStaking implementation const HorizonStakingImplementation = deployImplementation(m, { name: 'HorizonStaking', artifact: HorizonStakingArtifact, - constructorArgs: [Controller, HorizonStakingExtension, subgraphServiceAddress], + constructorArgs: [Controller, subgraphServiceAddress], }) // Upgrade proxy to implementation contract @@ -61,24 +45,11 @@ export const MigrateHorizonStakingDeployerModule = buildModule('HorizonStakingDe const HorizonStakingProxy = m.contractAt('HorizonStakingProxy', GraphProxyArtifact, horizonStakingAddress) - // Deploy HorizonStakingExtension - requires periphery and proxies to be registered in the controller - const ExponentialRebates = m.library('ExponentialRebates', ExponentialRebatesArtifact) - const HorizonStakingExtension = m.contract( - 'HorizonStakingExtension', - HorizonStakingExtensionArtifact, - [Controller, subgraphServiceAddress], - { - libraries: { - ExponentialRebates: ExponentialRebates, - }, - }, - ) - // Deploy HorizonStaking implementation const HorizonStakingImplementation = deployImplementation(m, { name: 'HorizonStaking', artifact: HorizonStakingArtifact, - constructorArgs: [Controller, HorizonStakingExtension, subgraphServiceAddress], + constructorArgs: [Controller, subgraphServiceAddress], }) return { HorizonStakingProxy, HorizonStakingImplementation } diff --git a/packages/horizon/scripts/integration b/packages/horizon/scripts/integration index baf48cf5e..c92a85ee8 100755 --- a/packages/horizon/scripts/integration +++ b/packages/horizon/scripts/integration @@ -100,12 +100,6 @@ npx hardhat deploy:migrate --network localhost --horizon-config integration --st # Step 4 - Governor npx hardhat deploy:migrate --network localhost --horizon-config integration --step 4 --patch-config --account-index 1 --hide-banner --standalone -# Run integration tests - During transition period -npx hardhat test:integration --phase during-transition-period --network localhost - -# Clear thawing period -npx hardhat transition:clear-thawing --network localhost - # Run integration tests - After transition period npx hardhat test:integration --phase after-transition-period --network localhost diff --git a/packages/horizon/tasks/test/integration.ts b/packages/horizon/tasks/test/integration.ts index 95b2ea230..bba9fa1c2 100644 --- a/packages/horizon/tasks/test/integration.ts +++ b/packages/horizon/tasks/test/integration.ts @@ -4,13 +4,9 @@ import { TASK_TEST } from 'hardhat/builtin-tasks/task-names' import { task } from 'hardhat/config' task('test:integration', 'Runs all integration tests') - .addParam( - 'phase', - 'Test phase to run: "during-transition-period", "after-transition-period", "after-delegation-slashing-enabled"', - ) + .addParam('phase', 'Test phase to run: "after-transition-period", "after-delegation-slashing-enabled"') .setAction(async (taskArgs, hre) => { // Get test files for each phase - const duringTransitionPeriodFiles = await glob('test/integration/during-transition-period/**/*.{js,ts}') const afterTransitionPeriodFiles = await glob('test/integration/after-transition-period/**/*.{js,ts}') const afterDelegationSlashingEnabledFiles = await glob( 'test/integration/after-delegation-slashing-enabled/**/*.{js,ts}', @@ -20,9 +16,6 @@ task('test:integration', 'Runs all integration tests') printBanner(taskArgs.phase, 'INTEGRATION TESTS: ') switch (taskArgs.phase) { - case 'during-transition-period': - await hre.run(TASK_TEST, { testFiles: duringTransitionPeriodFiles }) - break case 'after-transition-period': await hre.run(TASK_TEST, { testFiles: afterTransitionPeriodFiles }) break @@ -31,7 +24,7 @@ task('test:integration', 'Runs all integration tests') break default: throw new Error( - 'Invalid phase. Must be "during-transition-period", "after-transition-period", "after-delegation-slashing-enabled", or "all"', + 'Invalid phase. Must be "after-transition-period", "after-delegation-slashing-enabled", or "all"', ) } }) diff --git a/packages/horizon/tasks/transitions/thawing-period.ts b/packages/horizon/tasks/transitions/thawing-period.ts deleted file mode 100644 index e21e2bad2..000000000 --- a/packages/horizon/tasks/transitions/thawing-period.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { requireLocalNetwork } from '@graphprotocol/toolshed/hardhat' -import { printBanner } from '@graphprotocol/toolshed/utils' -import { task, types } from 'hardhat/config' - -task('transition:clear-thawing', 'Clears the thawing period in HorizonStaking') - .addOptionalParam('governorIndex', 'Derivation path index for the governor account', 1, types.int) - .addFlag('skipNetworkCheck', 'Skip the network check (use with caution)') - .setAction(async (taskArgs, hre) => { - printBanner('CLEARING THAWING PERIOD') - - if (!taskArgs.skipNetworkCheck) { - requireLocalNetwork(hre) - } - - const graph = hre.graph() - const governor = await graph.accounts.getGovernor(taskArgs.governorIndex) - const horizonStaking = graph.horizon.contracts.HorizonStaking - - console.log('Clearing thawing period...') - await horizonStaking.connect(governor).clearThawingPeriod() - console.log('Thawing period cleared') - }) diff --git a/packages/horizon/test/deployment/HorizonStaking.test.ts b/packages/horizon/test/deployment/HorizonStaking.test.ts index fed2af75f..f60d92b52 100644 --- a/packages/horizon/test/deployment/HorizonStaking.test.ts +++ b/packages/horizon/test/deployment/HorizonStaking.test.ts @@ -1,5 +1,5 @@ import { loadConfig } from '@graphprotocol/toolshed/hardhat' -import { assert, expect } from 'chai' +import { expect } from 'chai' import hre from 'hardhat' import { graphProxyTests } from './lib/GraphProxy.test' @@ -27,16 +27,6 @@ describe('HorizonStaking', function () { expect(delegationSlashingEnabled).to.equal(false) }) - testIf(4)('should set a non zero thawing period', async function () { - if (process.env.IGNITION_DEPLOYMENT_TYPE === 'protocol') { - assert.fail('Deployment type "protocol": no historical state available') - } - const thawingPeriod = await HorizonStaking.__DEPRECATED_getThawingPeriod() - expect(thawingPeriod).to.not.equal(0) - }) - - it.skip('should set the right staking extension address') - testIf(4)('should set the right subgraph data service address', async function () { const subgraphDataServiceAddress = await HorizonStaking.getSubgraphService() expect(subgraphDataServiceAddress).to.equal(config.$global.subgraphServiceAddress) diff --git a/packages/horizon/test/integration/during-transition-period/delegator.test.ts b/packages/horizon/test/integration/during-transition-period/delegator.test.ts deleted file mode 100644 index 352599f18..000000000 --- a/packages/horizon/test/integration/during-transition-period/delegator.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { ZERO_ADDRESS } from '@graphprotocol/toolshed' -import { delegators } from '@graphprotocol/toolshed/fixtures' -import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' -import { ethers } from 'hardhat' - -describe('Delegator', () => { - let snapshotId: string - - const thawingPeriod = 2419200n // 28 days - - // Subgraph service address is not set for integration tests - const subgraphServiceAddress = '0x0000000000000000000000000000000000000000' - - const graph = hre.graph() - const horizonStaking = graph.horizon.contracts.HorizonStaking - const graphToken = graph.horizon.contracts.L2GraphToken - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('Existing Protocol Users', () => { - describe('User undelegated before horizon was deployed', () => { - let indexer: HardhatEthersSigner - let delegator: HardhatEthersSigner - let tokens: bigint - - before(async () => { - const delegatorFixture = delegators[2] - const delegationFixture = delegatorFixture.delegations[0] - - // Verify delegator is undelegated - expect(delegatorFixture.undelegate).to.be.true - - // Get signers - indexer = await ethers.getSigner(delegationFixture.indexerAddress) - delegator = await ethers.getSigner(delegatorFixture.address) - - // Get tokens - tokens = delegationFixture.tokens - }) - - it('should be able to withdraw their tokens after the thawing period', async () => { - // Get the thawing period - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Mine remaining blocks to complete thawing period - for (let i = 0; i < Number(thawingPeriod) + 1; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Get delegator balance before withdrawing - const balanceBefore = await graphToken.balanceOf(delegator.address) - - // Withdraw tokens - await horizonStaking.connect(delegator)['withdrawDelegated(address,address)'](indexer.address, ZERO_ADDRESS) - - // Get delegator balance after withdrawing - const balanceAfter = await graphToken.balanceOf(delegator.address) - - // Expected balance after is the balance before plus the tokens minus the 0.5% delegation tax - const expectedBalanceAfter = balanceBefore + tokens - (tokens * 5000n) / 1000000n - - // Verify tokens are withdrawn - expect(balanceAfter).to.equal(expectedBalanceAfter) - }) - - it('should revert if the thawing period has not passed', async () => { - // Withdraw tokens - await expect( - horizonStaking.connect(delegator)['withdrawDelegated(address,address)'](indexer.address, ZERO_ADDRESS), - ).to.be.revertedWithCustomError(horizonStaking, 'HorizonStakingNothingToWithdraw') - }) - }) - - describe('Transition period is over', () => { - let governor: HardhatEthersSigner - let indexer: HardhatEthersSigner - let delegator: HardhatEthersSigner - let tokens: bigint - - before(async () => { - const delegatorFixture = delegators[0] - const delegationFixture = delegatorFixture.delegations[0] - - // Get signers - governor = await graph.accounts.getGovernor() - indexer = await ethers.getSigner(delegationFixture.indexerAddress) - delegator = await ethers.getSigner(delegatorFixture.address) - - // Get tokens - tokens = delegationFixture.tokens - }) - - it('should be able to undelegate during transition period and withdraw after transition period', async () => { - // Get delegator's delegation - const delegation = await horizonStaking.getDelegation( - indexer.address, - subgraphServiceAddress, - delegator.address, - ) - - // Undelegate tokens - await horizonStaking - .connect(delegator) - ['undelegate(address,address,uint256)'](indexer.address, subgraphServiceAddress, delegation.shares) - - // Wait for thawing period - await ethers.provider.send('evm_increaseTime', [Number(thawingPeriod) + 1]) - await ethers.provider.send('evm_mine', []) - - // Clear thawing period - await horizonStaking.connect(governor).clearThawingPeriod() - - // Get delegator balance before withdrawing - const balanceBefore = await graphToken.balanceOf(delegator.address) - - // Withdraw tokens - await horizonStaking - .connect(delegator) - ['withdrawDelegated(address,address,uint256)'](indexer.address, ZERO_ADDRESS, BigInt(1)) - - // Get delegator balance after withdrawing - const balanceAfter = await graphToken.balanceOf(delegator.address) - - // Expected balance after is the balance before plus the tokens minus the 0.5% delegation tax - // because the delegation was before the horizon upgrade, after the upgrade there is no tax - const expectedBalanceAfter = balanceBefore + tokens - (tokens * 5000n) / 1000000n - - // Verify tokens are withdrawn - expect(balanceAfter).to.equal(expectedBalanceAfter) - }) - }) - }) -}) diff --git a/packages/horizon/test/integration/during-transition-period/multicall.test.ts b/packages/horizon/test/integration/during-transition-period/multicall.test.ts deleted file mode 100644 index 948cd8f5f..000000000 --- a/packages/horizon/test/integration/during-transition-period/multicall.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { ONE_MILLION, PaymentTypes } from '@graphprotocol/toolshed' -import { setGRTBalance } from '@graphprotocol/toolshed/hardhat' -import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' -import { ethers } from 'hardhat' - -describe('Service Provider', () => { - let snapshotId: string - - const maxVerifierCut = 50_000n - const thawingPeriod = 2419200n - - const graph = hre.graph() - const horizonStaking = graph.horizon.contracts.HorizonStaking - const graphToken = graph.horizon.contracts.L2GraphToken - - const subgraphServiceAddress = '0x0000000000000000000000000000000000000000' - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('New Protocol Users', () => { - let serviceProvider: HardhatEthersSigner - - before(async () => { - ;[, , serviceProvider] = await graph.accounts.getTestAccounts() - await setGRTBalance(graph.provider, graphToken.target, serviceProvider.address, ONE_MILLION) - }) - - it('should allow multicalling stake+provision calls', async () => { - const tokensToStake = ethers.parseEther('1000') - const tokensToProvision = ethers.parseEther('100') - - // check state before - const beforeProvision = await horizonStaking.getProvision(serviceProvider.address, subgraphServiceAddress) - expect(beforeProvision.tokens).to.equal(0) - expect(beforeProvision.maxVerifierCut).to.equal(0) - expect(beforeProvision.thawingPeriod).to.equal(0) - expect(beforeProvision.createdAt).to.equal(0) - - // multicall - await graphToken.connect(serviceProvider).approve(horizonStaking.target, tokensToStake) - const stakeCalldata = horizonStaking.interface.encodeFunctionData('stake', [tokensToStake]) - const provisionCalldata = horizonStaking.interface.encodeFunctionData('provision', [ - serviceProvider.address, - subgraphServiceAddress, - tokensToProvision, - maxVerifierCut, - thawingPeriod, - ]) - await horizonStaking.connect(serviceProvider).multicall([stakeCalldata, provisionCalldata]) - - // check state after - const block = await graph.provider.getBlock('latest') - const afterProvision = await horizonStaking.getProvision(serviceProvider.address, subgraphServiceAddress) - expect(afterProvision.tokens).to.equal(tokensToProvision) - expect(afterProvision.maxVerifierCut).to.equal(maxVerifierCut) - expect(afterProvision.thawingPeriod).to.equal(thawingPeriod) - expect(afterProvision.createdAt).to.equal(block?.timestamp) - }) - - it('should allow multicalling delegation parameter set calls', async () => { - // check state before - const beforeIndexingRewards = await horizonStaking.getDelegationFeeCut( - serviceProvider.address, - subgraphServiceAddress, - PaymentTypes.IndexingRewards, - ) - const beforeQueryFee = await horizonStaking.getDelegationFeeCut( - serviceProvider.address, - subgraphServiceAddress, - PaymentTypes.QueryFee, - ) - expect(beforeIndexingRewards).to.equal(0) - expect(beforeQueryFee).to.equal(0) - - // multicall - const indexingRewardsCalldata = horizonStaking.interface.encodeFunctionData('setDelegationFeeCut', [ - serviceProvider.address, - subgraphServiceAddress, - PaymentTypes.IndexingRewards, - 10_000n, - ]) - const queryFeeCalldata = horizonStaking.interface.encodeFunctionData('setDelegationFeeCut', [ - serviceProvider.address, - subgraphServiceAddress, - PaymentTypes.QueryFee, - 12_345n, - ]) - await horizonStaking.connect(serviceProvider).multicall([indexingRewardsCalldata, queryFeeCalldata]) - - // check state after - const afterIndexingRewards = await horizonStaking.getDelegationFeeCut( - serviceProvider.address, - subgraphServiceAddress, - PaymentTypes.IndexingRewards, - ) - const afterQueryFee = await horizonStaking.getDelegationFeeCut( - serviceProvider.address, - subgraphServiceAddress, - PaymentTypes.QueryFee, - ) - expect(afterIndexingRewards).to.equal(10_000n) - expect(afterQueryFee).to.equal(12_345n) - }) - }) -}) diff --git a/packages/horizon/test/integration/during-transition-period/operator.test.ts b/packages/horizon/test/integration/during-transition-period/operator.test.ts deleted file mode 100644 index ab5b26ebf..000000000 --- a/packages/horizon/test/integration/during-transition-period/operator.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { generatePOI } from '@graphprotocol/toolshed' -import { indexers } from '@graphprotocol/toolshed/fixtures' -import { getEventData } from '@graphprotocol/toolshed/hardhat' -import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' -import { ethers } from 'hardhat' - -describe('Operator', () => { - let snapshotId: string - - // Subgraph service address is not set for integration tests - const subgraphServiceAddress = '0x0000000000000000000000000000000000000000' - - const graph = hre.graph() - const horizonStaking = graph.horizon.contracts.HorizonStaking - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('Existing Protocol Users', () => { - let indexer: HardhatEthersSigner - let operator: HardhatEthersSigner - let allocationID: string - let allocationTokens: bigint - let delegationIndexingCut: number - - before(async () => { - const indexerFixture = indexers[0] - const allocationFixture = indexerFixture.allocations[0] - - // Get signers - indexer = await ethers.getSigner(indexerFixture.address) - ;[operator] = await graph.accounts.getTestAccounts() - - // Get allocation details - allocationID = allocationFixture.allocationID - allocationTokens = allocationFixture.tokens - delegationIndexingCut = indexerFixture.indexingRewardCut - - // Set the operator - await horizonStaking.connect(indexer).setOperator(subgraphServiceAddress, operator.address, true) - }) - - it('should allow the operator to close an open legacy allocation and collect rewards', async () => { - // Use a non-zero POI - const poi = generatePOI('poi') - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Get delegation pool before closing allocation - const delegationPoolBefore = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensBefore = delegationPoolBefore.tokens - - // Mine blocks to simulate time passing - const halfThawingPeriod = Number(thawingPeriod) / 2 - for (let i = 0; i < halfThawingPeriod; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Get idle stake before closing allocation - const idleStakeBefore = await horizonStaking.getIdleStake(indexer.address) - - // Close allocation - const tx = await horizonStaking.connect(operator).closeAllocation(allocationID, poi) - const eventData = await getEventData( - tx, - 'event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount)', - ) - const rewards = eventData[2] - - // Verify rewards are not zero - expect(rewards).to.not.equal(0, 'Rewards were not transferred to service provider') - - // Verify rewards minus delegation cut are restaked - const idleStakeAfter = await horizonStaking.getIdleStake(indexer.address) - const idleStakeRewardsTokens = (rewards * BigInt(delegationIndexingCut)) / 1000000n - expect(idleStakeAfter).to.equal( - idleStakeBefore + allocationTokens + idleStakeRewardsTokens, - 'Rewards were not restaked', - ) - - // Verify delegators cut is added to delegation pool - const delegationPool = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensAfter = delegationPool.tokens - const delegationRewardsTokens = rewards - idleStakeRewardsTokens - expect(delegationPoolTokensAfter).to.equal( - delegationPoolTokensBefore + delegationRewardsTokens, - 'Delegators cut was not added to delegation pool', - ) - }) - }) -}) diff --git a/packages/horizon/test/integration/during-transition-period/permissionless.test.ts b/packages/horizon/test/integration/during-transition-period/permissionless.test.ts deleted file mode 100644 index a7d13e302..000000000 --- a/packages/horizon/test/integration/during-transition-period/permissionless.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { generatePOI } from '@graphprotocol/toolshed' -import { indexers } from '@graphprotocol/toolshed/fixtures' -import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' -import { ethers } from 'hardhat' - -describe('Permissionless', () => { - let snapshotId: string - - const graph = hre.graph() - const horizonStaking = graph.horizon.contracts.HorizonStaking - const epochManager = graph.horizon.contracts.EpochManager - const subgraphServiceAddress = '0x0000000000000000000000000000000000000000' - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('After max allocation epochs', () => { - let indexer: HardhatEthersSigner - let anySigner: HardhatEthersSigner - let allocationID: string - let allocationTokens: bigint - - before(async () => { - // Get signers - indexer = await ethers.getSigner(indexers[0].address) - ;[anySigner] = await graph.accounts.getTestAccounts() - - // ensure anySigner is not operator for the indexer - await horizonStaking.connect(indexer).setOperator(subgraphServiceAddress, anySigner.address, false) - - // Get allocation details - allocationID = indexers[0].allocations[0].allocationID - allocationTokens = indexers[0].allocations[0].tokens - }) - - it('should allow any user to close an allocation after 28 epochs', async () => { - // Get indexer's idle stake before closing allocation - const idleStakeBefore = await horizonStaking.getIdleStake(indexer.address) - - // Mine blocks to simulate 28 epochs passing - const startingEpoch = await epochManager.currentEpoch() - while ((await epochManager.currentEpoch()) - startingEpoch < 28) { - await ethers.provider.send('evm_mine', []) - } - - // Close allocation - const poi = generatePOI('poi') - await horizonStaking.connect(anySigner).closeAllocation(allocationID, poi) - - // Get indexer's idle stake after closing allocation - const idleStakeAfter = await horizonStaking.getIdleStake(indexer.address) - - // Verify allocation tokens were added to indexer's idle stake but no rewards were collected - expect(idleStakeAfter).to.be.equal(idleStakeBefore + allocationTokens) - }) - }) -}) diff --git a/packages/horizon/test/integration/during-transition-period/service-provider.test.ts b/packages/horizon/test/integration/during-transition-period/service-provider.test.ts deleted file mode 100644 index 0be3c6112..000000000 --- a/packages/horizon/test/integration/during-transition-period/service-provider.test.ts +++ /dev/null @@ -1,521 +0,0 @@ -import { generatePOI, ONE_MILLION } from '@graphprotocol/toolshed' -import { indexers } from '@graphprotocol/toolshed/fixtures' -import { getEventData, setGRTBalance } from '@graphprotocol/toolshed/hardhat' -import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' -import { ethers } from 'hardhat' - -describe('Service Provider', () => { - let snapshotId: string - - const graph = hre.graph() - const { stake, collect } = graph.horizon.actions - const horizonStaking = graph.horizon.contracts.HorizonStaking - const graphToken = graph.horizon.contracts.L2GraphToken - - // Subgraph service address is not set for integration tests - const subgraphServiceAddress = '0x0000000000000000000000000000000000000000' - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('New Protocol Users', () => { - let serviceProvider: HardhatEthersSigner - let tokensToStake = ethers.parseEther('1000') - - before(async () => { - ;[, , serviceProvider] = await graph.accounts.getTestAccounts() - await setGRTBalance(graph.provider, graphToken.target, serviceProvider.address, ONE_MILLION) - - // Stake tokens to service provider - await stake(serviceProvider, [tokensToStake]) - }) - - it('should allow service provider to unstake and withdraw after thawing period', async () => { - const tokensToUnstake = ethers.parseEther('100') - const balanceBefore = await graphToken.balanceOf(serviceProvider.address) - - // First unstake request - await horizonStaking.connect(serviceProvider).unstake(tokensToUnstake) - - // During transition period, tokens are locked by thawing period - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Mine remaining blocks to complete thawing period - for (let i = 0; i < Number(thawingPeriod) + 1; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Now we can withdraw - await horizonStaking.connect(serviceProvider).withdraw() - const balanceAfter = await graphToken.balanceOf(serviceProvider.address) - - expect(balanceAfter).to.equal( - balanceBefore + tokensToUnstake, - 'Tokens were not transferred back to service provider', - ) - }) - - it('should handle multiple unstake requests correctly', async () => { - // Make multiple unstake requests - const request1 = ethers.parseEther('50') - const request2 = ethers.parseEther('75') - - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // First unstake request - await horizonStaking.connect(serviceProvider).unstake(request1) - - // Mine half of thawing period blocks - const halfThawingPeriod = Number(thawingPeriod) / 2 - for (let i = 0; i < halfThawingPeriod; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Second unstake request - await horizonStaking.connect(serviceProvider).unstake(request2) - - // Mine remaining blocks to complete first unstake thawing period - for (let i = 0; i < halfThawingPeriod; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Check that withdraw reverts since thawing period is not complete - await expect(horizonStaking.connect(serviceProvider).withdraw()).to.be.revertedWithCustomError( - horizonStaking, - 'HorizonStakingStillThawing', - ) - - // Mine remaining blocks to complete thawing period - for (let i = 0; i < halfThawingPeriod + 1; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Get balance before withdrawing - const balanceBefore = await graphToken.balanceOf(serviceProvider.address) - - // Withdraw all thawed tokens - await horizonStaking.connect(serviceProvider).withdraw() - - // Verify all tokens are withdrawn and transferred back to service provider - const balanceAfter = await graphToken.balanceOf(serviceProvider.address) - expect(balanceAfter).to.equal( - balanceBefore + request1 + request2, - 'Tokens were not transferred back to service provider', - ) - }) - - describe('Transition period is over', () => { - let governor: HardhatEthersSigner - let tokensToUnstake: bigint - - before(async () => { - // Get governor - governor = await graph.accounts.getGovernor() - - // Set tokens - tokensToStake = ethers.parseEther('100000') - tokensToUnstake = ethers.parseEther('10000') - }) - - it('should be able to withdraw tokens that were unstaked during transition period', async () => { - // Stake tokens - await stake(serviceProvider, [tokensToStake]) - - // Unstake tokens - await horizonStaking.connect(serviceProvider).unstake(tokensToUnstake) - - // Get balance before withdrawing - const balanceBefore = await graphToken.balanceOf(serviceProvider.address) - - // Get thawing period - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Clear thawing period - await horizonStaking.connect(governor).clearThawingPeriod() - - // Mine blocks to complete thawing period - for (let i = 0; i < Number(thawingPeriod) + 1; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Withdraw tokens - await horizonStaking.connect(serviceProvider).withdraw() - - // Get balance after withdrawing - const balanceAfter = await graphToken.balanceOf(serviceProvider.address) - expect(balanceAfter).to.equal( - balanceBefore + tokensToUnstake, - 'Tokens were not transferred back to service provider', - ) - }) - - it('should be able to unstake tokens without a thawing period', async () => { - // Stake tokens - await stake(serviceProvider, [tokensToStake]) - - // Clear thawing period - await horizonStaking.connect(governor).clearThawingPeriod() - - // Get balance before withdrawing - const balanceBefore = await graphToken.balanceOf(serviceProvider.address) - - // Unstake tokens - await horizonStaking.connect(serviceProvider).unstake(tokensToUnstake) - - // Get balance after withdrawing - const balanceAfter = await graphToken.balanceOf(serviceProvider.address) - expect(balanceAfter).to.equal( - balanceBefore + tokensToUnstake, - 'Tokens were not transferred back to service provider', - ) - }) - }) - }) - - describe('Existing Protocol Users', () => { - let indexer: HardhatEthersSigner - let tokensUnstaked: bigint - - before(async () => { - const indexerFixture = indexers[0] - indexer = await ethers.getSigner(indexerFixture.address) - tokensUnstaked = indexerFixture.tokensToUnstake || 0n - - await setGRTBalance(graph.provider, graphToken.target, indexer.address, ONE_MILLION) - }) - - it('should allow service provider to withdraw their locked tokens after thawing period passes', async () => { - // Get balance before withdrawing - const balanceBefore = await graphToken.balanceOf(indexer.address) - - // Get thawing period - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Mine blocks to complete thawing period - for (let i = 0; i < Number(thawingPeriod) + 1; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Withdraw tokens - await horizonStaking.connect(indexer).withdraw() - - // Verify tokens are transferred back to service provider - const balanceAfter = await graphToken.balanceOf(indexer.address) - expect(balanceAfter).to.equal( - balanceBefore + tokensUnstaked, - 'Tokens were not transferred back to service provider', - ) - }) - - describe('Legacy allocations', () => { - describe('Restaking', () => { - let delegationIndexingCut: number - let delegationQueryFeeCut: number - let allocationID: string - let allocationTokens: bigint - let gateway: HardhatEthersSigner - - beforeEach(async () => { - const indexerFixture = indexers[0] - indexer = await ethers.getSigner(indexerFixture.address) - delegationIndexingCut = indexerFixture.indexingRewardCut - delegationQueryFeeCut = indexerFixture.queryFeeCut - allocationID = indexerFixture.allocations[0].allocationID - allocationTokens = indexerFixture.allocations[0].tokens - gateway = await graph.accounts.getGateway() - await setGRTBalance(graph.provider, graphToken.target, gateway.address, ONE_MILLION) - }) - - it('should be able to close an open legacy allocation and collect rewards', async () => { - // Use a non-zero POI - const poi = generatePOI('poi') - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Get delegation pool before closing allocation - const delegationPoolBefore = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensBefore = delegationPoolBefore.tokens - - // Mine blocks to simulate time passing - const halfThawingPeriod = Number(thawingPeriod) / 2 - for (let i = 0; i < halfThawingPeriod; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Get idle stake before closing allocation - const idleStakeBefore = await horizonStaking.getIdleStake(indexer.address) - - // Close allocation - const tx = await horizonStaking.connect(indexer).closeAllocation(allocationID, poi) - const eventData = await getEventData( - tx, - 'event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount)', - ) - const rewards = eventData[2] - - // Verify rewards are not zero - expect(rewards).to.not.equal(0, 'Rewards were not transferred to service provider') - - // Verify rewards minus delegation cut are restaked - const idleStakeAfter = await horizonStaking.getIdleStake(indexer.address) - const idleStakeRewardsTokens = (rewards * BigInt(delegationIndexingCut)) / 1000000n - expect(idleStakeAfter).to.equal( - idleStakeBefore + allocationTokens + idleStakeRewardsTokens, - 'Rewards were not restaked', - ) - - // Verify delegators cut is added to delegation pool - const delegationPool = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensAfter = delegationPool.tokens - const delegationRewardsTokens = rewards - idleStakeRewardsTokens - expect(delegationPoolTokensAfter).to.equal( - delegationPoolTokensBefore + delegationRewardsTokens, - 'Delegators cut was not added to delegation pool', - ) - }) - - it('should be able to collect query fees', async () => { - const tokensToCollect = ethers.parseEther('1000') - - // Get idle stake before collecting - const idleStakeBefore = await horizonStaking.getIdleStake(indexer.address) - - // Get delegation pool before collecting - const delegationPoolBefore = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensBefore = delegationPoolBefore.tokens - - // Collect query fees - await collect(gateway, [tokensToCollect, allocationID]) - - // Get idle stake after collecting - const idleStakeAfter = await horizonStaking.getIdleStake(indexer.address) - - // Subtract protocol tax (1%) and curation fees (10% after the protocol tax deduction) - const protocolTax = (tokensToCollect * 1n) / 100n - const curationFees = (tokensToCollect * 99n) / 1000n - const remainingTokens = tokensToCollect - protocolTax - curationFees - - // Verify tokens minus delegators cut are restaked - const indexerCutTokens = (remainingTokens * BigInt(delegationQueryFeeCut)) / 1000000n - expect(idleStakeAfter).to.equal(idleStakeBefore + indexerCutTokens, 'Indexer cut was not restaked') - - // Verify delegators cut is added to delegation pool - const delegationPool = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensAfter = delegationPool.tokens - const delegationCutTokens = remainingTokens - indexerCutTokens - expect(delegationPoolTokensAfter).to.equal( - delegationPoolTokensBefore + delegationCutTokens, - 'Delegators cut was not added to delegation pool', - ) - }) - - it('should be able to close an allocation and collect query fees for the closed allocation', async () => { - // Use a non-zero POI - const poi = generatePOI('poi') - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Mine blocks to simulate time passing - const halfThawingPeriod = Number(thawingPeriod) / 2 - for (let i = 0; i < halfThawingPeriod; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Close allocation - await horizonStaking.connect(indexer).closeAllocation(allocationID, poi) - - // Tokens to collect - const tokensToCollect = ethers.parseEther('1000') - - // Get idle stake before collecting - const idleStakeBefore = await horizonStaking.getIdleStake(indexer.address) - - // Get delegation pool before collecting - const delegationPoolBefore = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensBefore = delegationPoolBefore.tokens - - // Collect query fees - await collect(gateway, [tokensToCollect, allocationID]) - - // Get idle stake after collecting - const idleStakeAfter = await horizonStaking.getIdleStake(indexer.address) - - // Subtract protocol tax (1%) and curation fees (10% after the protocol tax deduction) - const protocolTax = (tokensToCollect * 1n) / 100n - const curationFees = (tokensToCollect * 99n) / 1000n - const remainingTokens = tokensToCollect - protocolTax - curationFees - - // Verify tokens minus delegators cut are restaked - const indexerCutTokens = (remainingTokens * BigInt(delegationQueryFeeCut)) / 1000000n - expect(idleStakeAfter).to.equal(idleStakeBefore + indexerCutTokens, 'Indexer cut was not restaked') - - // Verify delegators cut is added to delegation pool - const delegationPool = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensAfter = delegationPool.tokens - const delegationCutTokens = remainingTokens - indexerCutTokens - expect(delegationPoolTokensAfter).to.equal( - delegationPoolTokensBefore + delegationCutTokens, - 'Delegators cut was not added to delegation pool', - ) - }) - }) - - describe('With rewardsDestination set', () => { - let delegationIndexingCut: number - let delegationQueryFeeCut: number - let rewardsDestination: string - let allocationID: string - let gateway: HardhatEthersSigner - - beforeEach(async () => { - const indexerFixture = indexers[1] - indexer = await ethers.getSigner(indexerFixture.address) - delegationIndexingCut = indexerFixture.indexingRewardCut - delegationQueryFeeCut = indexerFixture.queryFeeCut - rewardsDestination = indexerFixture.rewardsDestination! - allocationID = indexerFixture.allocations[0].allocationID - gateway = await graph.accounts.getGateway() - await setGRTBalance(graph.provider, graphToken.target, gateway.address, ONE_MILLION) - }) - - it('should be able to close an open allocation and collect rewards', async () => { - // Use a non-zero POI - const poi = generatePOI('poi') - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Get delegation tokens before - const delegationPoolBefore = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensBefore = delegationPoolBefore.tokens - - // Mine blocks to simulate time passing - const halfThawingPeriod = Number(thawingPeriod) / 2 - for (let i = 0; i < halfThawingPeriod; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Get rewards destination balance before closing allocation - const balanceBefore = await graphToken.balanceOf(rewardsDestination) - - // Close allocation - const tx = await horizonStaking.connect(indexer).closeAllocation(allocationID, poi) - const eventData = await getEventData( - tx, - 'event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount)', - ) - const rewards = eventData[2] - - // Verify rewards are not zero - expect(rewards).to.not.equal(0, 'Rewards were not transferred to rewards destination') - - // Verify indexer rewards cut is transferred to rewards destination - const balanceAfter = await graphToken.balanceOf(rewardsDestination) - const indexerCutTokens = (rewards * BigInt(delegationIndexingCut)) / 1000000n - expect(balanceAfter).to.equal( - balanceBefore + indexerCutTokens, - 'Indexer cut was not transferred to rewards destination', - ) - - // Verify delegators cut is added to delegation pool - const delegationPoolAfter = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensAfter = delegationPoolAfter.tokens - const delegationCutTokens = rewards - indexerCutTokens - expect(delegationPoolTokensAfter).to.equal( - delegationPoolTokensBefore + delegationCutTokens, - 'Delegators cut was not added to delegation pool', - ) - }) - - it('should be able to collect query fees', async () => { - const tokensToCollect = ethers.parseEther('1000') - - // Get rewards destination balance before collecting - const balanceBefore = await graphToken.balanceOf(rewardsDestination) - - // Get delegation tokens before - const delegationPoolBefore = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensBefore = delegationPoolBefore.tokens - - // Collect query fees - await collect(gateway, [tokensToCollect, allocationID]) - - // Get rewards destination balance after collecting - const balanceAfter = await graphToken.balanceOf(rewardsDestination) - - // Subtract protocol tax (1%) and curation fees (10% after the protocol tax deduction) - const protocolTax = (tokensToCollect * 1n) / 100n - const curationFees = (tokensToCollect * 99n) / 1000n - const remainingTokens = tokensToCollect - protocolTax - curationFees - - // Verify indexer cut is transferred to rewards destination - const indexerCutTokens = (remainingTokens * BigInt(delegationQueryFeeCut)) / 1000000n - expect(balanceAfter).to.equal( - balanceBefore + indexerCutTokens, - 'Indexer cut was not transferred to rewards destination', - ) - - // Verify delegators cut is added to delegation pool - const delegationPoolAfter = await horizonStaking.getDelegationPool(indexer.address, subgraphServiceAddress) - const delegationPoolTokensAfter = delegationPoolAfter.tokens - const delegationCutTokens = remainingTokens - indexerCutTokens - expect(delegationPoolTokensAfter).to.equal( - delegationPoolTokensBefore + delegationCutTokens, - 'Delegators cut was not added to delegation pool', - ) - }) - }) - }) - - describe('Transition period is over', () => { - let governor: HardhatEthersSigner - let tokensToUnstake: bigint - - before(async () => { - // Get governor - governor = await graph.accounts.getGovernor() - - // Get indexer - const indexerFixture = indexers[2] - indexer = await ethers.getSigner(indexerFixture.address) - - // Set tokens - tokensToUnstake = ethers.parseEther('10000') - }) - - it('should be able to withdraw tokens that were unstaked during transition period', async () => { - // Unstake tokens during transition period - await horizonStaking.connect(indexer).unstake(tokensToUnstake) - - // Get thawing period - const thawingPeriod = await horizonStaking.__DEPRECATED_getThawingPeriod() - - // Clear thawing period - await horizonStaking.connect(governor).clearThawingPeriod() - - // Mine blocks to complete thawing period - for (let i = 0; i < Number(thawingPeriod) + 1; i++) { - await ethers.provider.send('evm_mine', []) - } - - // Get balance before withdrawing - const balanceBefore = await graphToken.balanceOf(indexer.address) - - // Withdraw tokens - await horizonStaking.connect(indexer).withdraw() - - // Get balance after withdrawing - const balanceAfter = await graphToken.balanceOf(indexer.address) - expect(balanceAfter).to.equal( - balanceBefore + tokensToUnstake, - 'Tokens were not transferred back to service provider', - ) - }) - }) - }) -}) diff --git a/packages/horizon/test/integration/during-transition-period/slasher.test.ts b/packages/horizon/test/integration/during-transition-period/slasher.test.ts deleted file mode 100644 index 47ced0883..000000000 --- a/packages/horizon/test/integration/during-transition-period/slasher.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { indexers } from '@graphprotocol/toolshed/fixtures' -import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import hre from 'hardhat' -import { ethers } from 'hardhat' - -describe('Slasher', () => { - let snapshotId: string - - let indexer: string - let slasher: HardhatEthersSigner - let tokensToSlash: bigint - - const graph = hre.graph() - const horizonStaking = graph.horizon.contracts.HorizonStaking - const graphToken = graph.horizon.contracts.L2GraphToken - - before(async () => { - slasher = await graph.accounts.getArbitrator() - }) - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('Available tokens', () => { - before(() => { - const indexerFixture = indexers[0] - indexer = indexerFixture.address - tokensToSlash = ethers.parseEther('10000') - }) - - it('should be able to slash indexer stake', async () => { - // Before slash state - const idleStakeBeforeSlash = await horizonStaking.getIdleStake(indexer) - const tokensVerifier = tokensToSlash / 2n - const slasherBeforeBalance = await graphToken.balanceOf(slasher.address) - - // Slash tokens - await horizonStaking.connect(slasher).slash(indexer, tokensToSlash, tokensVerifier, slasher.address) - - // Indexer's stake should have decreased - const idleStakeAfterSlash = await horizonStaking.getIdleStake(indexer) - expect(idleStakeAfterSlash).to.equal(idleStakeBeforeSlash - tokensToSlash, 'Indexer stake should have decreased') - - // Slasher should have received the tokens - const slasherAfterBalance = await graphToken.balanceOf(slasher.address) - expect(slasherAfterBalance).to.equal( - slasherBeforeBalance + tokensVerifier, - 'Slasher should have received the tokens', - ) - }) - }) - - describe('Locked tokens', () => { - before(() => { - const indexerFixture = indexers[1] - indexer = indexerFixture.address - tokensToSlash = indexerFixture.stake - }) - - it('should be able to slash locked tokens', async () => { - // Before slash state - const tokensVerifier = tokensToSlash / 2n - const slasherBeforeBalance = await graphToken.balanceOf(slasher.address) - - // Slash tokens - await horizonStaking.connect(slasher).slash(indexer, tokensToSlash, tokensVerifier, slasher.address) - - // Indexer's entire stake should have been slashed - const indexerStakeAfterSlash = await horizonStaking.getServiceProvider(indexer) - expect(indexerStakeAfterSlash.tokensStaked).to.equal(0n, 'Indexer stake should have been slashed') - - // Slasher should have received the tokens - const slasherAfterBalance = await graphToken.balanceOf(slasher.address) - expect(slasherAfterBalance).to.equal( - slasherBeforeBalance + tokensVerifier, - 'Slasher should have received the tokens', - ) - }) - }) -}) diff --git a/packages/horizon/test/unit/GraphBase.t.sol b/packages/horizon/test/unit/GraphBase.t.sol index f3f55b96a..efd1533d8 100644 --- a/packages/horizon/test/unit/GraphBase.t.sol +++ b/packages/horizon/test/unit/GraphBase.t.sol @@ -14,7 +14,6 @@ import { GraphPayments } from "contracts/payments/GraphPayments.sol"; import { GraphTallyCollector } from "contracts/payments/collectors/GraphTallyCollector.sol"; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { HorizonStaking } from "contracts/staking/HorizonStaking.sol"; -import { HorizonStakingExtension } from "contracts/staking/HorizonStakingExtension.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; import { MockGRTToken } from "../../contracts/mocks/MockGRTToken.sol"; import { EpochManagerMock } from "contracts/mocks/EpochManagerMock.sol"; @@ -43,7 +42,6 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { GraphTallyCollector graphTallyCollector; HorizonStaking private stakingBase; - HorizonStakingExtension private stakingExtension; address subgraphDataServiceLegacyAddress = makeAddr("subgraphDataServiceLegacyAddress"); address subgraphDataServiceAddress = makeAddr("subgraphDataServiceAddress"); @@ -71,8 +69,7 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { operator: createUser("operator"), gateway: createUser("gateway"), verifier: createUser("verifier"), - delegator: createUser("delegator"), - legacySlasher: createUser("legacySlasher") + delegator: createUser("delegator") }); // Deploy protocol contracts @@ -86,7 +83,6 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { vm.label({ account: address(payments), newLabel: "GraphPayments" }); vm.label({ account: address(escrow), newLabel: "PaymentsEscrow" }); vm.label({ account: address(staking), newLabel: "HorizonStaking" }); - vm.label({ account: address(stakingExtension), newLabel: "HorizonStakingExtension" }); vm.label({ account: address(graphTallyCollector), newLabel: "GraphTallyCollector" }); // Ensure caller is back to the original msg.sender @@ -194,12 +190,7 @@ abstract contract GraphBaseTest is IHorizonStakingTypes, Utils, Constants { escrow = PaymentsEscrow(escrowProxyAddress); } - stakingExtension = new HorizonStakingExtension(address(controller), subgraphDataServiceLegacyAddress); - stakingBase = new HorizonStaking( - address(controller), - address(stakingExtension), - subgraphDataServiceLegacyAddress - ); + stakingBase = new HorizonStaking(address(controller), subgraphDataServiceLegacyAddress); graphTallyCollector = new GraphTallyCollector( "GraphTallyCollector", diff --git a/packages/horizon/test/unit/escrow/getters.t.sol b/packages/horizon/test/unit/escrow/getters.t.sol index 262192125..84f5661ac 100644 --- a/packages/horizon/test/unit/escrow/getters.t.sol +++ b/packages/horizon/test/unit/escrow/getters.t.sol @@ -35,11 +35,10 @@ contract GraphEscrowGettersTest is GraphEscrowTest { uint256 amountThawing, uint256 amountCollected ) public useGateway useDeposit(amountDeposit) { - vm.assume(amountThawing > 0); - vm.assume(amountDeposit > 0); - vm.assume(amountDeposit >= amountThawing); - vm.assume(amountDeposit >= amountCollected); - vm.assume(amountDeposit - amountCollected < amountThawing); + // amountCollected must be >= 1 for valid range, and <= amountDeposit + amountCollected = bound(amountCollected, 1, amountDeposit); + // amountThawing must be in (amountDeposit - amountCollected, amountDeposit] + amountThawing = bound(amountThawing, amountDeposit - amountCollected + 1, amountDeposit); // thaw some funds _thawEscrow(users.verifier, users.indexer, amountThawing); diff --git a/packages/horizon/test/unit/shared/horizon-staking/HorizonStakingShared.t.sol b/packages/horizon/test/unit/shared/horizon-staking/HorizonStakingShared.t.sol index f89a7fafa..babaaca09 100644 --- a/packages/horizon/test/unit/shared/horizon-staking/HorizonStakingShared.t.sol +++ b/packages/horizon/test/unit/shared/horizon-staking/HorizonStakingShared.t.sol @@ -7,14 +7,12 @@ import { GraphBaseTest } from "../../GraphBase.t.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IHorizonStakingBase } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol"; import { IHorizonStakingMain } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol"; -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; import { ILinkedList } from "@graphprotocol/interfaces/contracts/horizon/internal/ILinkedList.sol"; import { LinkedList } from "../../../../contracts/libraries/LinkedList.sol"; import { MathUtils } from "../../../../contracts/libraries/MathUtils.sol"; import { PPMMath } from "../../../../contracts/libraries/PPMMath.sol"; -import { ExponentialRebates } from "../../../../contracts/staking/libraries/ExponentialRebates.sol"; abstract contract HorizonStakingSharedTest is GraphBaseTest { using LinkedList for ILinkedList.List; @@ -23,13 +21,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { event Transfer(address indexed from, address indexed to, uint tokens); address internal _allocationId = makeAddr("allocationId"); - bytes32 internal constant _subgraphDeploymentID = keccak256("subgraphDeploymentID"); - uint256 internal constant MAX_ALLOCATION_EPOCHS = 28; - - uint32 internal alphaNumerator = 100; - uint32 internal alphaDenominator = 100; - uint32 internal lambdaNumerator = 60; - uint32 internal lambdaDenominator = 100; /* * MODIFIERS @@ -80,17 +71,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { _createProvision(users.indexer, dataService, tokens, maxVerifierCut, thawingPeriod); } - modifier useAllocation(uint256 tokens) { - vm.assume(tokens <= MAX_STAKING_TOKENS); - _createAllocation(users.indexer, _allocationId, _subgraphDeploymentID, tokens); - _; - } - - modifier useRebateParameters() { - _setStorage_RebateParameters(alphaNumerator, alphaDenominator, lambdaNumerator, lambdaDenominator); - _; - } - /* * HELPERS: these are shortcuts to perform common actions that often involve multiple contract calls */ @@ -105,34 +85,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { _provision(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod); } - // This allows setting up contract state with legacy allocations - function _createAllocation( - address serviceProvider, - address allocationId, - bytes32 subgraphDeploymentID, - uint256 tokens - ) internal { - _setStorage_MaxAllocationEpochs(MAX_ALLOCATION_EPOCHS); - - IHorizonStakingExtension.Allocation memory _allocation = IHorizonStakingExtension.Allocation({ - indexer: serviceProvider, - subgraphDeploymentID: subgraphDeploymentID, - tokens: tokens, - createdAtEpoch: block.timestamp, - closedAtEpoch: 0, - collectedFees: 0, - __DEPRECATED_effectiveAllocation: 0, - accRewardsPerAllocatedToken: 0, - distributedRebates: 0 - }); - _setStorage_allocation(_allocation, allocationId, tokens); - - // delegation pool initialized - _setStorage_DelegationPool(serviceProvider, 0, uint32(PPMMath.MAX_PPM), uint32(PPMMath.MAX_PPM)); - - token.transfer(address(staking), tokens); - } - /* * ACTIONS: these are individual contract calls wrapped in assertion blocks to ensure they work as expected */ @@ -152,7 +104,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { // stakeTo token.approve(address(staking), tokens); vm.expectEmit(); - emit IHorizonStakingBase.HorizonStakeDeposited(serviceProvider, tokens); + emit IHorizonStakingMain.HorizonStakeDeposited(serviceProvider, tokens); staking.stakeTo(serviceProvider, tokens); // after @@ -185,7 +137,7 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { // stakeTo token.approve(address(staking), tokens); vm.expectEmit(); - emit IHorizonStakingBase.HorizonStakeDeposited(serviceProvider, tokens); + emit IHorizonStakingMain.HorizonStakeDeposited(serviceProvider, tokens); vm.expectEmit(); emit IHorizonStakingMain.ProvisionIncreased(serviceProvider, verifier, tokens); staking.stakeToProvision(serviceProvider, verifier, tokens); @@ -232,48 +184,15 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { function _unstake(uint256 _tokens) internal { (, address msgSender, ) = vm.readCallers(); - uint256 deprecatedThawingPeriod = staking.__DEPRECATED_getThawingPeriod(); - // before uint256 beforeSenderBalance = token.balanceOf(msgSender); uint256 beforeStakingBalance = token.balanceOf(address(staking)); ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(msgSender); - bool withdrawCalled = beforeServiceProvider.__DEPRECATED_tokensLocked != 0 && - block.number >= beforeServiceProvider.__DEPRECATED_tokensLockedUntil; - - if (deprecatedThawingPeriod != 0 && beforeServiceProvider.__DEPRECATED_tokensLocked > 0) { - deprecatedThawingPeriod = MathUtils.weightedAverageRoundingUp( - MathUtils.diffOrZero( - withdrawCalled ? 0 : beforeServiceProvider.__DEPRECATED_tokensLockedUntil, - block.number - ), - withdrawCalled ? 0 : beforeServiceProvider.__DEPRECATED_tokensLocked, - deprecatedThawingPeriod, - _tokens - ); - } - // unstake - if (deprecatedThawingPeriod == 0) { - vm.expectEmit(address(staking)); - emit IHorizonStakingMain.HorizonStakeWithdrawn(msgSender, _tokens); - } else { - if (withdrawCalled) { - vm.expectEmit(address(staking)); - emit IHorizonStakingMain.HorizonStakeWithdrawn( - msgSender, - beforeServiceProvider.__DEPRECATED_tokensLocked - ); - } + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.HorizonStakeWithdrawn(msgSender, _tokens); - vm.expectEmit(address(staking)); - emit IHorizonStakingMain.HorizonStakeLocked( - msgSender, - withdrawCalled ? _tokens : beforeServiceProvider.__DEPRECATED_tokensLocked + _tokens, - block.number + deprecatedThawingPeriod - ); - } staking.unstake(_tokens); // after @@ -282,41 +201,16 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(msgSender); // assert - if (deprecatedThawingPeriod == 0) { - assertEq(afterSenderBalance, _tokens + beforeSenderBalance); - assertEq(afterStakingBalance, beforeStakingBalance - _tokens); - assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked - _tokens); - assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned); - assertEq( - afterServiceProvider.__DEPRECATED_tokensAllocated, - beforeServiceProvider.__DEPRECATED_tokensAllocated - ); - assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked); - assertEq( - afterServiceProvider.__DEPRECATED_tokensLockedUntil, - beforeServiceProvider.__DEPRECATED_tokensLockedUntil - ); - } else { - assertEq( - afterServiceProvider.tokensStaked, - withdrawCalled - ? beforeServiceProvider.tokensStaked - beforeServiceProvider.__DEPRECATED_tokensLocked - : beforeServiceProvider.tokensStaked - ); - assertEq( - afterServiceProvider.__DEPRECATED_tokensLocked, - _tokens + (withdrawCalled ? 0 : beforeServiceProvider.__DEPRECATED_tokensLocked) - ); - assertEq(afterServiceProvider.__DEPRECATED_tokensLockedUntil, block.number + deprecatedThawingPeriod); - assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned); - assertEq( - afterServiceProvider.__DEPRECATED_tokensAllocated, - beforeServiceProvider.__DEPRECATED_tokensAllocated - ); - uint256 tokensTransferred = (withdrawCalled ? beforeServiceProvider.__DEPRECATED_tokensLocked : 0); - assertEq(afterSenderBalance, beforeSenderBalance + tokensTransferred); - assertEq(afterStakingBalance, beforeStakingBalance - tokensTransferred); - } + assertEq(afterSenderBalance, _tokens + beforeSenderBalance); + assertEq(afterStakingBalance, beforeStakingBalance - _tokens); + assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked - _tokens); + assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned); + assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq( + afterServiceProvider.__DEPRECATED_tokensLockedUntil, + beforeServiceProvider.__DEPRECATED_tokensLockedUntil + ); } function _withdraw() internal { @@ -1460,19 +1354,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { assertEq(afterEnabled, true); } - function _clearThawingPeriod() internal { - // clearThawingPeriod - vm.expectEmit(address(staking)); - emit IHorizonStakingMain.ThawingPeriodCleared(); - staking.clearThawingPeriod(); - - // after - uint64 afterThawingPeriod = staking.__DEPRECATED_getThawingPeriod(); - - // assert - assertEq(afterThawingPeriod, 0); - } - function _setMaxThawingPeriod(uint64 maxThawingPeriod) internal { // setMaxThawingPeriod vm.expectEmit(address(staking)); @@ -1619,318 +1500,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { } } - // use struct to avoid 'stack too deep' error - struct CalcValues_CloseAllocation { - uint256 rewards; - uint256 delegatorRewards; - uint256 indexerRewards; - } - struct BeforeValues_CloseAllocation { - IHorizonStakingExtension.Allocation allocation; - DelegationPoolInternalTest pool; - ServiceProviderInternal serviceProvider; - uint256 subgraphAllocations; - uint256 stakingBalance; - uint256 indexerBalance; - uint256 beneficiaryBalance; - } - - // Current rewards manager is mocked and assumed to mint fixed rewards - function _closeAllocation(address allocationId, bytes32 poi) internal { - (, address msgSender, ) = vm.readCallers(); - - // before - BeforeValues_CloseAllocation memory beforeValues; - beforeValues.allocation = staking.getAllocation(allocationId); - beforeValues.pool = _getStorage_DelegationPoolInternal( - beforeValues.allocation.indexer, - subgraphDataServiceLegacyAddress, - true - ); - beforeValues.serviceProvider = _getStorage_ServiceProviderInternal(beforeValues.allocation.indexer); - beforeValues.subgraphAllocations = _getStorage_SubgraphAllocations( - beforeValues.allocation.subgraphDeploymentID - ); - beforeValues.stakingBalance = token.balanceOf(address(staking)); - beforeValues.indexerBalance = token.balanceOf(beforeValues.allocation.indexer); - beforeValues.beneficiaryBalance = token.balanceOf( - _getStorage_RewardsDestination(beforeValues.allocation.indexer) - ); - - bool isAuth = staking.isAuthorized( - beforeValues.allocation.indexer, - subgraphDataServiceLegacyAddress, - msgSender - ); - address rewardsDestination = _getStorage_RewardsDestination(beforeValues.allocation.indexer); - - CalcValues_CloseAllocation memory calcValues = CalcValues_CloseAllocation({ - rewards: ALLOCATIONS_REWARD_CUT, - delegatorRewards: ALLOCATIONS_REWARD_CUT - - uint256(beforeValues.pool.__DEPRECATED_indexingRewardCut).mulPPM(ALLOCATIONS_REWARD_CUT), - indexerRewards: 0 - }); - calcValues.indexerRewards = - ALLOCATIONS_REWARD_CUT - (beforeValues.pool.tokens > 0 ? calcValues.delegatorRewards : 0); - - // closeAllocation - vm.expectEmit(address(staking)); - emit IHorizonStakingExtension.AllocationClosed( - beforeValues.allocation.indexer, - beforeValues.allocation.subgraphDeploymentID, - epochManager.currentEpoch(), - beforeValues.allocation.tokens, - allocationId, - msgSender, - poi, - !isAuth - ); - staking.closeAllocation(allocationId, poi); - - // after - IHorizonStakingExtension.Allocation memory afterAllocation = staking.getAllocation(allocationId); - DelegationPoolInternalTest memory afterPool = _getStorage_DelegationPoolInternal( - beforeValues.allocation.indexer, - subgraphDataServiceLegacyAddress, - true - ); - ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal( - beforeValues.allocation.indexer - ); - uint256 afterSubgraphAllocations = _getStorage_SubgraphAllocations( - beforeValues.allocation.subgraphDeploymentID - ); - uint256 afterStakingBalance = token.balanceOf(address(staking)); - uint256 afterIndexerBalance = token.balanceOf(beforeValues.allocation.indexer); - uint256 afterBeneficiaryBalance = token.balanceOf(rewardsDestination); - - if (beforeValues.allocation.tokens > 0) { - if (isAuth && poi != 0) { - if (rewardsDestination != address(0)) { - assertEq( - beforeValues.stakingBalance + calcValues.rewards - calcValues.indexerRewards, - afterStakingBalance - ); - assertEq(beforeValues.indexerBalance, afterIndexerBalance); - assertEq(beforeValues.beneficiaryBalance + calcValues.indexerRewards, afterBeneficiaryBalance); - } else { - assertEq(beforeValues.stakingBalance + calcValues.rewards, afterStakingBalance); - assertEq(beforeValues.indexerBalance, afterIndexerBalance); - assertEq(beforeValues.beneficiaryBalance, afterBeneficiaryBalance); - } - } else { - assertEq(beforeValues.stakingBalance, afterStakingBalance); - assertEq(beforeValues.indexerBalance, afterIndexerBalance); - assertEq(beforeValues.beneficiaryBalance, afterBeneficiaryBalance); - } - } else { - assertEq(beforeValues.stakingBalance, afterStakingBalance); - assertEq(beforeValues.indexerBalance, afterIndexerBalance); - assertEq(beforeValues.beneficiaryBalance, afterBeneficiaryBalance); - } - - assertEq(afterAllocation.indexer, beforeValues.allocation.indexer); - assertEq(afterAllocation.subgraphDeploymentID, beforeValues.allocation.subgraphDeploymentID); - assertEq(afterAllocation.tokens, beforeValues.allocation.tokens); - assertEq(afterAllocation.createdAtEpoch, beforeValues.allocation.createdAtEpoch); - assertEq(afterAllocation.closedAtEpoch, epochManager.currentEpoch()); - assertEq(afterAllocation.collectedFees, beforeValues.allocation.collectedFees); - assertEq( - afterAllocation.__DEPRECATED_effectiveAllocation, - beforeValues.allocation.__DEPRECATED_effectiveAllocation - ); - assertEq(afterAllocation.accRewardsPerAllocatedToken, beforeValues.allocation.accRewardsPerAllocatedToken); - assertEq(afterAllocation.distributedRebates, beforeValues.allocation.distributedRebates); - - if (beforeValues.allocation.tokens > 0 && isAuth && poi != 0 && rewardsDestination == address(0)) { - assertEq( - afterServiceProvider.tokensStaked, - beforeValues.serviceProvider.tokensStaked + calcValues.indexerRewards - ); - } else { - assertEq(afterServiceProvider.tokensStaked, beforeValues.serviceProvider.tokensStaked); - } - assertEq(afterServiceProvider.tokensProvisioned, beforeValues.serviceProvider.tokensProvisioned); - assertEq( - afterServiceProvider.__DEPRECATED_tokensAllocated + beforeValues.allocation.tokens, - beforeValues.serviceProvider.__DEPRECATED_tokensAllocated - ); - assertEq( - afterServiceProvider.__DEPRECATED_tokensLocked, - beforeValues.serviceProvider.__DEPRECATED_tokensLocked - ); - assertEq( - afterServiceProvider.__DEPRECATED_tokensLockedUntil, - beforeValues.serviceProvider.__DEPRECATED_tokensLockedUntil - ); - - assertEq(afterSubgraphAllocations + beforeValues.allocation.tokens, beforeValues.subgraphAllocations); - - if (beforeValues.allocation.tokens > 0 && isAuth && poi != 0 && beforeValues.pool.tokens > 0) { - assertEq(afterPool.tokens, beforeValues.pool.tokens + calcValues.delegatorRewards); - } else { - assertEq(afterPool.tokens, beforeValues.pool.tokens); - } - } - - // use struct to avoid 'stack too deep' error - struct BeforeValues_Collect { - IHorizonStakingExtension.Allocation allocation; - DelegationPoolInternalTest pool; - ServiceProviderInternal serviceProvider; - uint256 stakingBalance; - uint256 senderBalance; - uint256 curationBalance; - uint256 beneficiaryBalance; - } - struct CalcValues_Collect { - uint256 protocolTaxTokens; - uint256 queryFees; - uint256 curationCutTokens; - uint256 newRebates; - uint256 payment; - uint256 delegationFeeCut; - } - struct AfterValues_Collect { - IHorizonStakingExtension.Allocation allocation; - DelegationPoolInternalTest pool; - ServiceProviderInternal serviceProvider; - uint256 stakingBalance; - uint256 senderBalance; - uint256 curationBalance; - uint256 beneficiaryBalance; - } - - function _collect(uint256 tokens, address allocationId) internal { - (, address msgSender, ) = vm.readCallers(); - - // before - BeforeValues_Collect memory beforeValues; - beforeValues.allocation = staking.getAllocation(allocationId); - beforeValues.pool = _getStorage_DelegationPoolInternal( - beforeValues.allocation.indexer, - subgraphDataServiceLegacyAddress, - true - ); - beforeValues.serviceProvider = _getStorage_ServiceProviderInternal(beforeValues.allocation.indexer); - - (uint32 curationPercentage, uint32 protocolPercentage) = _getStorage_ProtocolTaxAndCuration(); - address rewardsDestination = _getStorage_RewardsDestination(beforeValues.allocation.indexer); - - beforeValues.stakingBalance = token.balanceOf(address(staking)); - beforeValues.senderBalance = token.balanceOf(msgSender); - beforeValues.curationBalance = token.balanceOf(address(curation)); - beforeValues.beneficiaryBalance = token.balanceOf(rewardsDestination); - - // calc some stuff - CalcValues_Collect memory calcValues; - calcValues.protocolTaxTokens = tokens.mulPPMRoundUp(protocolPercentage); - calcValues.queryFees = tokens - calcValues.protocolTaxTokens; - calcValues.curationCutTokens = 0; - if (curation.isCurated(beforeValues.allocation.subgraphDeploymentID)) { - calcValues.curationCutTokens = calcValues.queryFees.mulPPMRoundUp(curationPercentage); - calcValues.queryFees -= calcValues.curationCutTokens; - } - calcValues.newRebates = ExponentialRebates.exponentialRebates( - calcValues.queryFees + beforeValues.allocation.collectedFees, - beforeValues.allocation.tokens, - alphaNumerator, - alphaDenominator, - lambdaNumerator, - lambdaDenominator - ); - calcValues.payment = calcValues.newRebates > calcValues.queryFees - ? calcValues.queryFees - : calcValues.newRebates; - calcValues.delegationFeeCut = 0; - if (beforeValues.pool.tokens > 0) { - calcValues.delegationFeeCut = - calcValues.payment - calcValues.payment.mulPPM(beforeValues.pool.__DEPRECATED_queryFeeCut); - calcValues.payment -= calcValues.delegationFeeCut; - } - - // staking.collect() - if (tokens > 0) { - vm.expectEmit(address(staking)); - emit IHorizonStakingExtension.RebateCollected( - msgSender, - beforeValues.allocation.indexer, - beforeValues.allocation.subgraphDeploymentID, - allocationId, - epochManager.currentEpoch(), - tokens, - calcValues.protocolTaxTokens, - calcValues.curationCutTokens, - calcValues.queryFees, - calcValues.payment, - calcValues.delegationFeeCut - ); - } - staking.collect(tokens, allocationId); - - // after - AfterValues_Collect memory afterValues; - afterValues.allocation = staking.getAllocation(allocationId); - afterValues.pool = _getStorage_DelegationPoolInternal( - beforeValues.allocation.indexer, - subgraphDataServiceLegacyAddress, - true - ); - afterValues.serviceProvider = _getStorage_ServiceProviderInternal(beforeValues.allocation.indexer); - afterValues.stakingBalance = token.balanceOf(address(staking)); - afterValues.senderBalance = token.balanceOf(msgSender); - afterValues.curationBalance = token.balanceOf(address(curation)); - afterValues.beneficiaryBalance = token.balanceOf(rewardsDestination); - - // assert - assertEq(afterValues.senderBalance + tokens, beforeValues.senderBalance); - assertEq(afterValues.curationBalance, beforeValues.curationBalance + calcValues.curationCutTokens); - if (rewardsDestination != address(0)) { - assertEq(afterValues.beneficiaryBalance, beforeValues.beneficiaryBalance + calcValues.payment); - assertEq(afterValues.stakingBalance, beforeValues.stakingBalance + calcValues.delegationFeeCut); - } else { - assertEq(afterValues.beneficiaryBalance, beforeValues.beneficiaryBalance); - assertEq( - afterValues.stakingBalance, - beforeValues.stakingBalance + calcValues.delegationFeeCut + calcValues.payment - ); - } - - assertEq( - afterValues.allocation.collectedFees, - beforeValues.allocation.collectedFees + tokens - calcValues.protocolTaxTokens - calcValues.curationCutTokens - ); - assertEq(afterValues.allocation.indexer, beforeValues.allocation.indexer); - assertEq(afterValues.allocation.subgraphDeploymentID, beforeValues.allocation.subgraphDeploymentID); - assertEq(afterValues.allocation.tokens, beforeValues.allocation.tokens); - assertEq(afterValues.allocation.createdAtEpoch, beforeValues.allocation.createdAtEpoch); - assertEq(afterValues.allocation.closedAtEpoch, beforeValues.allocation.closedAtEpoch); - assertEq( - afterValues.allocation.accRewardsPerAllocatedToken, - beforeValues.allocation.accRewardsPerAllocatedToken - ); - assertEq( - afterValues.allocation.distributedRebates, - beforeValues.allocation.distributedRebates + calcValues.newRebates - ); - - assertEq(afterValues.pool.tokens, beforeValues.pool.tokens + calcValues.delegationFeeCut); - assertEq(afterValues.pool.shares, beforeValues.pool.shares); - assertEq(afterValues.pool.tokensThawing, beforeValues.pool.tokensThawing); - assertEq(afterValues.pool.sharesThawing, beforeValues.pool.sharesThawing); - assertEq(afterValues.pool.thawingNonce, beforeValues.pool.thawingNonce); - - assertEq(afterValues.serviceProvider.tokensProvisioned, beforeValues.serviceProvider.tokensProvisioned); - if (rewardsDestination != address(0)) { - assertEq(afterValues.serviceProvider.tokensStaked, beforeValues.serviceProvider.tokensStaked); - } else { - assertEq( - afterValues.serviceProvider.tokensStaked, - beforeValues.serviceProvider.tokensStaked + calcValues.payment - ); - } - } - /* * STORAGE HELPERS */ @@ -1975,22 +1544,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { return vm.load(address(staking), bytes32(slot)) == bytes32(uint256(1)); } - function _setStorage_DeprecatedThawingPeriod(uint32 _thawingPeriod) internal { - uint256 slot = 13; - - // Read the current value of the slot - uint256 currentSlotValue = uint256(vm.load(address(staking), bytes32(slot))); - - // Create a mask to clear the bits for __DEPRECATED_thawingPeriod (bits 0-31) - uint256 mask = ~(uint256(0xFFFFFFFF)); // Mask to clear the first 32 bits - - // Clear the bits for __DEPRECATED_thawingPeriod and set the new value - uint256 newSlotValue = (currentSlotValue & mask) | uint256(_thawingPeriod); - - // Store the updated value back into the slot - vm.store(address(staking), bytes32(slot), bytes32(newSlotValue)); - } - function _setStorage_ServiceProvider( address _indexer, uint256 _tokensStaked, @@ -2096,59 +1649,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { return delegation; } - function _setStorage_allocation( - IHorizonStakingExtension.Allocation memory allocation, - address allocationId, - uint256 tokens - ) internal { - // __DEPRECATED_allocations - uint256 allocationsSlot = 15; - bytes32 allocationBaseSlot = keccak256(abi.encode(allocationId, allocationsSlot)); - vm.store(address(staking), allocationBaseSlot, bytes32(uint256(uint160(allocation.indexer)))); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 1), allocation.subgraphDeploymentID); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 2), bytes32(tokens)); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 3), bytes32(allocation.createdAtEpoch)); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 4), bytes32(allocation.closedAtEpoch)); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 5), bytes32(allocation.collectedFees)); - vm.store( - address(staking), - bytes32(uint256(allocationBaseSlot) + 6), - bytes32(allocation.__DEPRECATED_effectiveAllocation) - ); - vm.store( - address(staking), - bytes32(uint256(allocationBaseSlot) + 7), - bytes32(allocation.accRewardsPerAllocatedToken) - ); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 8), bytes32(allocation.distributedRebates)); - - // _serviceProviders - uint256 serviceProviderSlot = 14; - bytes32 serviceProviderBaseSlot = keccak256(abi.encode(allocation.indexer, serviceProviderSlot)); - uint256 currentTokensStaked = uint256(vm.load(address(staking), serviceProviderBaseSlot)); - uint256 currentTokensProvisioned = uint256( - vm.load(address(staking), bytes32(uint256(serviceProviderBaseSlot) + 1)) - ); - vm.store( - address(staking), - bytes32(uint256(serviceProviderBaseSlot) + 0), - bytes32(currentTokensStaked + tokens) - ); - vm.store( - address(staking), - bytes32(uint256(serviceProviderBaseSlot) + 1), - bytes32(currentTokensProvisioned + tokens) - ); - - // __DEPRECATED_subgraphAllocations - uint256 subgraphsAllocationsSlot = 16; - bytes32 subgraphAllocationsBaseSlot = keccak256( - abi.encode(allocation.subgraphDeploymentID, subgraphsAllocationsSlot) - ); - uint256 currentAllocatedTokens = uint256(vm.load(address(staking), subgraphAllocationsBaseSlot)); - vm.store(address(staking), subgraphAllocationsBaseSlot, bytes32(currentAllocatedTokens + tokens)); - } - function _getStorage_SubgraphAllocations(bytes32 subgraphDeploymentID) internal view returns (uint256) { uint256 subgraphsAllocationsSlot = 16; bytes32 subgraphAllocationsBaseSlot = keccak256(abi.encode(subgraphDeploymentID, subgraphsAllocationsSlot)); @@ -2167,40 +1667,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { return address(uint160(uint256(vm.load(address(staking), rewardsDestinationSlotBaseSlot)))); } - function _setStorage_MaxAllocationEpochs(uint256 maxAllocationEpochs) internal { - uint256 slot = 13; - - // Read the current value of the storage slot - uint256 currentSlotValue = uint256(vm.load(address(staking), bytes32(slot))); - - // Mask to clear the specific bits for __DEPRECATED_maxAllocationEpochs (bits 128-159) - uint256 mask = ~(uint256(0xFFFFFFFF) << 128); - - // Clear the bits and set the new maxAllocationEpochs value - uint256 newSlotValue = (currentSlotValue & mask) | (uint256(maxAllocationEpochs) << 128); - - // Store the updated value back into the slot - vm.store(address(staking), bytes32(slot), bytes32(newSlotValue)); - - uint256 readMaxAllocationEpochs = _getStorage_MaxAllocationEpochs(); - assertEq(readMaxAllocationEpochs, maxAllocationEpochs); - } - - function _getStorage_MaxAllocationEpochs() internal view returns (uint256) { - uint256 slot = 13; - - // Read the current value of the storage slot - uint256 currentSlotValue = uint256(vm.load(address(staking), bytes32(slot))); - - // Mask to isolate bits 128-159 - uint256 mask = uint256(0xFFFFFFFF) << 128; - - // Extract the maxAllocationEpochs by masking and shifting - uint256 maxAllocationEpochs = (currentSlotValue & mask) >> 128; - - return maxAllocationEpochs; - } - function _setStorage_DelegationPool( address serviceProvider, uint256 tokens, @@ -2216,144 +1682,6 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { vm.store(address(staking), tokensSlot, bytes32(tokens)); } - function _setStorage_RebateParameters( - uint32 alphaNumerator_, - uint32 alphaDenominator_, - uint32 lambdaNumerator_, - uint32 lambdaDenominator_ - ) internal { - // Store alpha numerator and denominator in slot 13 - uint256 alphaSlot = 13; - - uint256 newAlphaSlotValue; - { - uint256 alphaNumeratorOffset = 160; // Offset for __DEPRECATED_alphaNumerator (20th byte) - uint256 alphaDenominatorOffset = 192; // Offset for __DEPRECATED_alphaDenominator (24th byte) - - // Read current value of the slot - uint256 currentAlphaSlotValue = uint256(vm.load(address(staking), bytes32(alphaSlot))); - - // Create a mask to clear the bits for alphaNumerator and alphaDenominator - uint256 alphaMask = ~(uint256(0xFFFFFFFF) << alphaNumeratorOffset) & - ~(uint256(0xFFFFFFFF) << alphaDenominatorOffset); - - // Clear and set new values - newAlphaSlotValue = - (currentAlphaSlotValue & alphaMask) | - (uint256(alphaNumerator_) << alphaNumeratorOffset) | - (uint256(alphaDenominator_) << alphaDenominatorOffset); - } - - // Store the updated value back into the slot - vm.store(address(staking), bytes32(alphaSlot), bytes32(newAlphaSlotValue)); - - // Store lambda numerator and denominator in slot 25 - uint256 lambdaSlot = 25; - - uint256 newLambdaSlotValue; - { - uint256 lambdaNumeratorOffset = 160; // Offset for lambdaNumerator (20th byte) - uint256 lambdaDenominatorOffset = 192; // Offset for lambdaDenominator (24th byte) - - // Read current value of the slot - uint256 currentLambdaSlotValue = uint256(vm.load(address(staking), bytes32(lambdaSlot))); - - // Create a mask to clear the bits for lambdaNumerator and lambdaDenominator - uint256 lambdaMask = ~(uint256(0xFFFFFFFF) << lambdaNumeratorOffset) & - ~(uint256(0xFFFFFFFF) << lambdaDenominatorOffset); - - // Clear and set new values - newLambdaSlotValue = - (currentLambdaSlotValue & lambdaMask) | - (uint256(lambdaNumerator_) << lambdaNumeratorOffset) | - (uint256(lambdaDenominator_) << lambdaDenominatorOffset); - } - - // Store the updated value back into the slot - vm.store(address(staking), bytes32(lambdaSlot), bytes32(newLambdaSlotValue)); - - // Verify the storage - ( - uint32 readAlphaNumerator, - uint32 readAlphaDenominator, - uint32 readLambdaNumerator, - uint32 readLambdaDenominator - ) = _getStorage_RebateParameters(); - assertEq(readAlphaNumerator, alphaNumerator_); - assertEq(readAlphaDenominator, alphaDenominator_); - assertEq(readLambdaNumerator, lambdaNumerator_); - assertEq(readLambdaDenominator, lambdaDenominator_); - } - - function _getStorage_RebateParameters() internal view returns (uint32, uint32, uint32, uint32) { - // Read alpha numerator and denominator - uint256 alphaSlot = 13; - uint256 alphaValues = uint256(vm.load(address(staking), bytes32(alphaSlot))); - uint32 alphaNumerator_ = uint32(alphaValues >> 160); - uint32 alphaDenominator_ = uint32(alphaValues >> 192); - - // Read lambda numerator and denominator - uint256 lambdaSlot = 25; - uint256 lambdaValues = uint256(vm.load(address(staking), bytes32(lambdaSlot))); - uint32 lambdaNumerator_ = uint32(lambdaValues >> 160); - uint32 lambdaDenominator_ = uint32(lambdaValues >> 192); - - return (alphaNumerator_, alphaDenominator_, lambdaNumerator_, lambdaDenominator_); - } - - // function _setStorage_ProtocolTaxAndCuration(uint32 curationPercentage, uint32 taxPercentage) private { - // bytes32 slot = bytes32(uint256(13)); - // uint256 curationOffset = 4; - // uint256 protocolTaxOffset = 8; - // bytes32 originalValue = vm.load(address(staking), slot); - - // bytes32 newProtocolTaxValue = bytes32( - // ((uint256(originalValue) & - // ~((0xFFFFFFFF << (8 * curationOffset)) | (0xFFFFFFFF << (8 * protocolTaxOffset)))) | - // (uint256(curationPercentage) << (8 * curationOffset))) | - // (uint256(taxPercentage) << (8 * protocolTaxOffset)) - // ); - // vm.store(address(staking), slot, newProtocolTaxValue); - - // (uint32 readCurationPercentage, uint32 readTaxPercentage) = _getStorage_ProtocolTaxAndCuration(); - // assertEq(readCurationPercentage, curationPercentage); - // } - - function _setStorage_ProtocolTaxAndCuration(uint32 curationPercentage, uint32 taxPercentage) internal { - bytes32 slot = bytes32(uint256(13)); - - // Offsets for the percentages - uint256 curationOffset = 32; // __DEPRECATED_curationPercentage (2nd uint32, bits 32-63) - uint256 protocolTaxOffset = 64; // __DEPRECATED_protocolPercentage (3rd uint32, bits 64-95) - - // Read the current slot value - uint256 originalValue = uint256(vm.load(address(staking), slot)); - - // Create masks to clear the specific bits for the two percentages - uint256 mask = ~(uint256(0xFFFFFFFF) << curationOffset) & ~(uint256(0xFFFFFFFF) << protocolTaxOffset); // Mask for curationPercentage // Mask for protocolTax - - // Clear the existing bits and set the new values - uint256 newSlotValue = (originalValue & mask) | - (uint256(curationPercentage) << curationOffset) | - (uint256(taxPercentage) << protocolTaxOffset); - - // Store the updated slot value - vm.store(address(staking), slot, bytes32(newSlotValue)); - - // Verify the values were set correctly - (uint32 readCurationPercentage, uint32 readTaxPercentage) = _getStorage_ProtocolTaxAndCuration(); - assertEq(readCurationPercentage, curationPercentage); - assertEq(readTaxPercentage, taxPercentage); - } - - function _getStorage_ProtocolTaxAndCuration() internal view returns (uint32, uint32) { - bytes32 slot = bytes32(uint256(13)); - bytes32 value = vm.load(address(staking), slot); - uint32 curationPercentage = uint32(uint256(value) >> 32); - uint32 taxPercentage = uint32(uint256(value) >> 64); - return (curationPercentage, taxPercentage); - } - /* * MISC: private functions to help with testing */ diff --git a/packages/horizon/test/unit/staking/allocation/allocation.t.sol b/packages/horizon/test/unit/staking/allocation/allocation.t.sol deleted file mode 100644 index 5c9bb179d..000000000 --- a/packages/horizon/test/unit/staking/allocation/allocation.t.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "forge-std/Test.sol"; - -import { HorizonStakingTest } from "../HorizonStaking.t.sol"; -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; - -contract HorizonStakingAllocationTest is HorizonStakingTest { - /* - * TESTS - */ - - function testAllocation_GetAllocationState_Active(uint256 tokens) public useIndexer useAllocation(tokens) { - IHorizonStakingExtension.AllocationState state = staking.getAllocationState(_allocationId); - assertEq(uint16(state), uint16(IHorizonStakingExtension.AllocationState.Active)); - } - - function testAllocation_GetAllocationState_Null() public view { - IHorizonStakingExtension.AllocationState state = staking.getAllocationState(_allocationId); - assertEq(uint16(state), uint16(IHorizonStakingExtension.AllocationState.Null)); - } - - function testAllocation_IsAllocation(uint256 tokens) public useIndexer useAllocation(tokens) { - bool isAllocation = staking.isAllocation(_allocationId); - assertTrue(isAllocation); - } - - function testAllocation_IsNotAllocation() public view { - bool isAllocation = staking.isAllocation(_allocationId); - assertFalse(isAllocation); - } -} diff --git a/packages/horizon/test/unit/staking/allocation/close.t.sol b/packages/horizon/test/unit/staking/allocation/close.t.sol deleted file mode 100644 index cac390099..000000000 --- a/packages/horizon/test/unit/staking/allocation/close.t.sol +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "forge-std/Test.sol"; - -import { HorizonStakingTest } from "../HorizonStaking.t.sol"; -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; -import { PPMMath } from "../../../../contracts/libraries/PPMMath.sol"; - -contract HorizonStakingCloseAllocationTest is HorizonStakingTest { - using PPMMath for uint256; - - bytes32 internal constant _poi = keccak256("poi"); - - /* - * MODIFIERS - */ - - modifier useLegacyOperator() { - resetPrank(users.indexer); - _setOperator(subgraphDataServiceLegacyAddress, users.operator, true); - vm.startPrank(users.operator); - _; - vm.stopPrank(); - } - - /* - * TESTS - */ - - function testCloseAllocation(uint256 tokens) public useIndexer 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_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); - - address beneficiary = makeAddr("beneficiary"); - _setStorage_RewardsDestination(users.indexer, beneficiary); - - // Skip 15 epochs - vm.roll(15); - - _closeAllocation(_allocationId, _poi); - } - - function testCloseAllocation_RevertWhen_NotActive() public { - vm.expectRevert("!active"); - staking.closeAllocation(_allocationId, _poi); - } - - function testCloseAllocation_RevertWhen_NotIndexer() public useIndexer useAllocation(1 ether) { - resetPrank(users.delegator); - vm.expectRevert("!auth"); - staking.closeAllocation(_allocationId, _poi); - } - - function testCloseAllocation_AfterMaxEpochs_AnyoneCanClose( - uint256 tokens - ) public useIndexer useAllocation(1 ether) { - tokens = bound(tokens, 1, MAX_STAKING_TOKENS); - _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, 0); - - // Skip to over the max allocation epochs - vm.roll((MAX_ALLOCATION_EPOCHS + 1) * EPOCH_LENGTH + 1); - - resetPrank(users.delegator); - _closeAllocation(_allocationId, 0x0); - } - - function testCloseAllocation_RevertWhen_ZeroTokensNotAuthorized() public useIndexer useAllocation(1 ether) { - _createProvision(users.indexer, subgraphDataServiceLegacyAddress, 100 ether, 0, 0); - - resetPrank(users.delegator); - vm.expectRevert("!auth"); - staking.closeAllocation(_allocationId, 0x0); - } - - function testCloseAllocation_WithDelegation( - uint256 tokens, - uint256 delegationTokens, - uint32 indexingRewardCut - ) public useIndexer useAllocation(1 ether) { - tokens = bound(tokens, 2, MAX_STAKING_TOKENS); - delegationTokens = bound(delegationTokens, 0, MAX_STAKING_TOKENS); - vm.assume(indexingRewardCut <= MAX_PPM); - - uint256 legacyAllocationTokens = tokens / 2; - uint256 provisionTokens = tokens - legacyAllocationTokens; - - _createProvision(users.indexer, subgraphDataServiceLegacyAddress, provisionTokens, 0, 0); - _setStorage_DelegationPool(users.indexer, delegationTokens, indexingRewardCut, 0); - - // Skip 15 epochs - vm.roll(15); - - _closeAllocation(_allocationId, _poi); - } -} diff --git a/packages/horizon/test/unit/staking/allocation/collect.t.sol b/packages/horizon/test/unit/staking/allocation/collect.t.sol deleted file mode 100644 index 31a5138b2..000000000 --- a/packages/horizon/test/unit/staking/allocation/collect.t.sol +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "forge-std/Test.sol"; - -import { HorizonStakingTest } from "../HorizonStaking.t.sol"; -import { ExponentialRebates } from "../../../../contracts/staking/libraries/ExponentialRebates.sol"; -import { PPMMath } from "../../../../contracts/libraries/PPMMath.sol"; - -contract HorizonStakingCollectAllocationTest is HorizonStakingTest { - using PPMMath for uint256; - - /* - * TESTS - */ - - function testCollectAllocation_RevertWhen_InvalidAllocationId( - uint256 tokens - ) public useIndexer useAllocation(1 ether) { - vm.expectRevert("!alloc"); - staking.collect(tokens, address(0)); - } - - function testCollectAllocation_RevertWhen_Null(uint256 tokens) public { - vm.expectRevert("!collect"); - staking.collect(tokens, _allocationId); - } - - function testCollect_Tokens( - uint256 allocationTokens, - uint256 collectTokens, - uint256 curationTokens, - uint32 curationPercentage, - uint32 protocolTaxPercentage, - uint256 delegationTokens, - uint32 queryFeeCut - ) public useIndexer useRebateParameters useAllocation(allocationTokens) { - collectTokens = bound(collectTokens, 0, MAX_STAKING_TOKENS); - curationTokens = bound(curationTokens, 0, MAX_STAKING_TOKENS); - delegationTokens = bound(delegationTokens, 0, MAX_STAKING_TOKENS); - vm.assume(curationPercentage <= MAX_PPM); - vm.assume(protocolTaxPercentage <= MAX_PPM); - vm.assume(queryFeeCut <= MAX_PPM); - - resetPrank(users.indexer); - _setStorage_ProtocolTaxAndCuration(curationPercentage, protocolTaxPercentage); - console.log("queryFeeCut", queryFeeCut); - _setStorage_DelegationPool(users.indexer, delegationTokens, 0, queryFeeCut); - curation.signal(_subgraphDeploymentID, curationTokens); - - resetPrank(users.gateway); - approve(address(staking), collectTokens); - _collect(collectTokens, _allocationId); - } - - function testCollect_WithBeneficiaryAddress( - uint256 allocationTokens, - uint256 collectTokens - ) public useIndexer useRebateParameters useAllocation(allocationTokens) { - collectTokens = bound(collectTokens, 0, MAX_STAKING_TOKENS); - - address beneficiary = makeAddr("beneficiary"); - _setStorage_RewardsDestination(users.indexer, beneficiary); - - resetPrank(users.gateway); - approve(address(staking), collectTokens); - _collect(collectTokens, _allocationId); - - uint256 newRebates = ExponentialRebates.exponentialRebates( - collectTokens, - allocationTokens, - alphaNumerator, - alphaDenominator, - lambdaNumerator, - lambdaDenominator - ); - uint256 payment = newRebates > collectTokens ? collectTokens : newRebates; - - assertEq(token.balanceOf(beneficiary), payment); - } -} diff --git a/packages/horizon/test/unit/staking/delegation/forceWithdrawDelegated.t.sol b/packages/horizon/test/unit/staking/delegation/forceWithdrawDelegated.t.sol new file mode 100644 index 000000000..d60afe029 --- /dev/null +++ b/packages/horizon/test/unit/staking/delegation/forceWithdrawDelegated.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol"; +import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingForceWithdrawDelegatedTest 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 _forceWithdrawDelegated(address _indexer, address _delegator) internal { + IHorizonStakingTypes.DelegationPool memory pool = staking.getDelegationPool( + _indexer, + subgraphDataServiceLegacyAddress + ); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + uint256 beforeDelegatorBalance = token.balanceOf(_delegator); + + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.StakeDelegatedWithdrawn(_indexer, _delegator, pool.tokens); + staking.forceWithdrawDelegated(_indexer, _delegator); + + uint256 afterStakingBalance = token.balanceOf(address(staking)); + uint256 afterDelegatorBalance = token.balanceOf(_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 testForceWithdrawDelegated_Tokens(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); + + // switch to a third party (not the delegator) + resetPrank(users.operator); + + _forceWithdrawDelegated(users.indexer, users.delegator); + } + + function testForceWithdrawDelegated_CalledByDelegator(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); + + // delegator can also call forceWithdrawDelegated on themselves + _forceWithdrawDelegated(users.indexer, users.delegator); + } + + function testForceWithdrawDelegated_RevertWhen_NoTokens() public useDelegator { + _setStorage_DelegationPool(users.indexer, 0, 0, 0); + _setLegacyDelegation(users.indexer, users.delegator, 0, 0, 0); + + // switch to a third party + resetPrank(users.operator); + + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNothingToWithdraw()"); + vm.expectRevert(expectedError); + staking.forceWithdrawDelegated(users.indexer, users.delegator); + } + + function testForceWithdrawDelegated_RevertWhen_StillLocked(uint256 tokensLocked) public useDelegator { + vm.assume(tokensLocked > 0); + + // Set a future epoch for tokensLockedUntil + uint256 futureEpoch = 1000; + _setStorage_DelegationPool(users.indexer, tokensLocked, 0, 0); + _setLegacyDelegation(users.indexer, users.delegator, 0, tokensLocked, futureEpoch); + token.transfer(address(staking), tokensLocked); + + // switch to a third party + resetPrank(users.operator); + + // Should revert because tokens are still locked (current epoch < futureEpoch) + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNothingToWithdraw()"); + vm.expectRevert(expectedError); + staking.forceWithdrawDelegated(users.indexer, users.delegator); + } +} diff --git a/packages/horizon/test/unit/staking/delegation/legacyWithdraw.t.sol b/packages/horizon/test/unit/staking/delegation/legacyWithdraw.t.sol index e5ba447e4..400d5a4f1 100644 --- a/packages/horizon/test/unit/staking/delegation/legacyWithdraw.t.sol +++ b/packages/horizon/test/unit/staking/delegation/legacyWithdraw.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import { IHorizonStakingMain } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; import { LinkedList } from "../../../../contracts/libraries/LinkedList.sol"; import { HorizonStakingTest } from "../HorizonStaking.t.sol"; diff --git a/packages/horizon/test/unit/staking/governance/governance.t.sol b/packages/horizon/test/unit/staking/governance/governance.t.sol index 2fe4a46da..040629b05 100644 --- a/packages/horizon/test/unit/staking/governance/governance.t.sol +++ b/packages/horizon/test/unit/staking/governance/governance.t.sol @@ -39,19 +39,6 @@ contract HorizonStakingGovernanceTest is HorizonStakingTest { staking.setDelegationSlashingEnabled(); } - function testGovernance_ClearThawingPeriod(uint32 thawingPeriod) public useGovernor { - // simulate previous thawing period - _setStorage_DeprecatedThawingPeriod(thawingPeriod); - - _clearThawingPeriod(); - } - - function testGovernance_ClearThawingPeriod_NotGovernor() public useIndexer { - bytes memory expectedError = abi.encodeWithSignature("ManagedOnlyGovernor()"); - vm.expectRevert(expectedError); - staking.clearThawingPeriod(); - } - function testGovernance__SetMaxThawingPeriod(uint64 maxThawingPeriod) public useGovernor { _setMaxThawingPeriod(maxThawingPeriod); } diff --git a/packages/horizon/test/unit/staking/provision/provision.t.sol b/packages/horizon/test/unit/staking/provision/provision.t.sol index c87e13a45..8cfafff5b 100644 --- a/packages/horizon/test/unit/staking/provision/provision.t.sol +++ b/packages/horizon/test/unit/staking/provision/provision.t.sol @@ -96,22 +96,6 @@ contract HorizonStakingProvisionTest is HorizonStakingTest { staking.provision(users.indexer, subgraphDataServiceAddress, amount, maxVerifierCut, thawingPeriod); } - function testProvision_RevertWhen_VerifierIsNotSubgraphDataServiceDuringTransitionPeriod( - uint256 amount - ) public useIndexer useStake(amount) { - // simulate the transition period - _setStorage_DeprecatedThawingPeriod(THAWING_PERIOD_IN_BLOCKS); - - // oddly we use subgraphDataServiceLegacyAddress as the subgraph service address - // so subgraphDataServiceAddress is not the subgraph service ¯\_(ツ)_/¯ - bytes memory expectedError = abi.encodeWithSignature( - "HorizonStakingInvalidVerifier(address)", - subgraphDataServiceAddress - ); - vm.expectRevert(expectedError); - staking.provision(users.indexer, subgraphDataServiceAddress, amount, 0, 0); - } - function testProvision_AddTokensToProvision( uint256 amount, uint32 maxVerifierCut, diff --git a/packages/horizon/test/unit/staking/serviceProvider/serviceProvider.t.sol b/packages/horizon/test/unit/staking/serviceProvider/serviceProvider.t.sol index 9d6a87fc0..af4449211 100644 --- a/packages/horizon/test/unit/staking/serviceProvider/serviceProvider.t.sol +++ b/packages/horizon/test/unit/staking/serviceProvider/serviceProvider.t.sol @@ -101,37 +101,6 @@ contract HorizonStakingServiceProviderTest is HorizonStakingTest { assertEq(providerTokensAvailable, amount); } - function testServiceProvider_HasStake( - uint256 amount - ) public useIndexer useProvision(amount, MAX_PPM, MAX_THAWING_PERIOD) { - assertTrue(staking.hasStake(users.indexer)); - - _thaw(users.indexer, subgraphDataServiceAddress, amount); - skip(MAX_THAWING_PERIOD + 1); - _deprovision(users.indexer, subgraphDataServiceAddress, 0); - staking.unstake(amount); - - assertFalse(staking.hasStake(users.indexer)); - } - - function testServiceProvider_GetIndexerStakedTokens( - uint256 amount - ) public useIndexer useProvision(amount, MAX_PPM, MAX_THAWING_PERIOD) { - assertEq(staking.getIndexerStakedTokens(users.indexer), amount); - - _thaw(users.indexer, subgraphDataServiceAddress, amount); - // Does not discount thawing tokens - assertEq(staking.getIndexerStakedTokens(users.indexer), amount); - - skip(MAX_THAWING_PERIOD + 1); - _deprovision(users.indexer, subgraphDataServiceAddress, 0); - // Does not discount thawing tokens - assertEq(staking.getIndexerStakedTokens(users.indexer), amount); - - staking.unstake(amount); - assertEq(staking.getIndexerStakedTokens(users.indexer), 0); - } - function testServiceProvider_RevertIf_InvalidDelegationFeeCut( uint256 cut, uint8 paymentTypeInput diff --git a/packages/horizon/test/unit/staking/slash/legacySlash.t.sol b/packages/horizon/test/unit/staking/slash/legacySlash.t.sol deleted file mode 100644 index 1af4670db..000000000 --- a/packages/horizon/test/unit/staking/slash/legacySlash.t.sol +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "forge-std/Test.sol"; - -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/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); - ServiceProviderInternal memory beforeIndexer = _getStorage_ServiceProviderInternal(_indexer); - - // calculate slashable stake - uint256 slashableStake = beforeIndexer.tokensStaked - beforeIndexer.tokensProvisioned; - uint256 actualTokens = _tokens; - uint256 actualRewards = _rewards; - if (slashableStake == 0) { - actualTokens = 0; - actualRewards = 0; - } else if (_tokens > slashableStake) { - actualRewards = (_rewards * slashableStake) / _tokens; - actualTokens = slashableStake; - } - - // slash - vm.expectEmit(address(staking)); - emit IHorizonStakingExtension.StakeSlashed(_indexer, actualTokens, actualRewards, _beneficiary); - staking.slash(_indexer, _tokens, _rewards, _beneficiary); - - // after - uint256 afterStakingBalance = token.balanceOf(address(staking)); - uint256 afterRewardsDestinationBalance = token.balanceOf(_beneficiary); - ServiceProviderInternal memory afterIndexer = _getStorage_ServiceProviderInternal(_indexer); - - assertEq(beforeStakingBalance - actualTokens, afterStakingBalance); - assertEq(beforeRewardsDestinationBalance, afterRewardsDestinationBalance - actualRewards); - assertEq(afterIndexer.tokensStaked, beforeIndexer.tokensStaked - actualTokens); - } - - /* - * TESTS - */ - function testSlash_Legacy( - uint256 tokensStaked, - uint256 tokensProvisioned, - uint256 slashTokens, - uint256 reward - ) public useIndexer useLegacySlasher(users.legacySlasher) { - vm.assume(tokensStaked > 0); - vm.assume(tokensStaked <= MAX_STAKING_TOKENS); - vm.assume(tokensProvisioned > 0); - vm.assume(tokensProvisioned <= tokensStaked); - slashTokens = bound(slashTokens, 1, tokensStaked); - reward = bound(reward, 0, slashTokens); - - _stake(tokensStaked); - _provision(users.indexer, subgraphDataServiceLegacyAddress, tokensProvisioned, 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")); - } - - function test_LegacySlash_WhenDelegateCallFails() public useIndexer useLegacySlasher(users.legacySlasher) { - // Setup indexer with: - // - tokensStaked = 1000 GRT - // - tokensAllocated = 800 GRT - // - tokensLocked = 300 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); - - // Change staking extension code to an invalid opcode so the delegatecall reverts - address stakingExtension = staking.getStakingExtension(); - vm.etch(stakingExtension, hex"fe"); - - resetPrank(users.legacySlasher); - bytes memory expectedError = abi.encodeWithSignature("HorizonStakingLegacySlashFailed()"); - vm.expectRevert(expectedError); - staking.slash(users.indexer, 1000 ether, 500 ether, makeAddr("fisherman")); - } -} diff --git a/packages/horizon/test/unit/staking/stake/forceWithdraw.t.sol b/packages/horizon/test/unit/staking/stake/forceWithdraw.t.sol new file mode 100644 index 000000000..5e75b7d25 --- /dev/null +++ b/packages/horizon/test/unit/staking/stake/forceWithdraw.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import "forge-std/Test.sol"; + +import { IHorizonStakingMain } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingForceWithdrawTest is HorizonStakingTest { + /* + * HELPERS + */ + + function _forceWithdraw(address _serviceProvider) internal { + (, address msgSender, ) = vm.readCallers(); + + // before + ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(_serviceProvider); + uint256 beforeServiceProviderBalance = token.balanceOf(_serviceProvider); + uint256 beforeCallerBalance = token.balanceOf(msgSender); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + + // forceWithdraw + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.HorizonStakeWithdrawn( + _serviceProvider, + beforeServiceProvider.__DEPRECATED_tokensLocked + ); + staking.forceWithdraw(_serviceProvider); + + // after + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(_serviceProvider); + uint256 afterServiceProviderBalance = token.balanceOf(_serviceProvider); + uint256 afterCallerBalance = token.balanceOf(msgSender); + uint256 afterStakingBalance = token.balanceOf(address(staking)); + + // assert - tokens go to service provider, not caller + assertEq( + afterServiceProviderBalance - beforeServiceProviderBalance, + beforeServiceProvider.__DEPRECATED_tokensLocked + ); + assertEq(afterCallerBalance, beforeCallerBalance); // caller balance unchanged + assertEq(beforeStakingBalance - afterStakingBalance, beforeServiceProvider.__DEPRECATED_tokensLocked); + + // assert - service provider state updated + assertEq( + afterServiceProvider.tokensStaked, + beforeServiceProvider.tokensStaked - beforeServiceProvider.__DEPRECATED_tokensLocked + ); + assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned); + assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, 0); + assertEq(afterServiceProvider.__DEPRECATED_tokensLockedUntil, 0); + } + + /* + * TESTS + */ + + function testForceWithdraw_Tokens(uint256 tokens, uint256 tokensLocked) public useIndexer { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + tokensLocked = bound(tokensLocked, 1, tokens); + + // simulate locked tokens ready to withdraw + token.transfer(address(staking), tokens); + _setStorage_ServiceProvider(users.indexer, tokens, 0, tokensLocked, block.number, 0); + + _createProvision(users.indexer, subgraphDataServiceAddress, tokens, 0, MAX_THAWING_PERIOD); + + // switch to a different user (not the service provider) + resetPrank(users.delegator); + + _forceWithdraw(users.indexer); + } + + function testForceWithdraw_CalledByServiceProvider(uint256 tokens, uint256 tokensLocked) public useIndexer { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + tokensLocked = bound(tokensLocked, 1, tokens); + + // simulate locked tokens ready to withdraw + token.transfer(address(staking), tokens); + _setStorage_ServiceProvider(users.indexer, tokens, 0, tokensLocked, block.number, 0); + + _createProvision(users.indexer, subgraphDataServiceAddress, tokens, 0, MAX_THAWING_PERIOD); + + // before + ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(users.indexer); + uint256 beforeServiceProviderBalance = token.balanceOf(users.indexer); + uint256 beforeStakingBalance = token.balanceOf(address(staking)); + + // service provider can also call forceWithdraw on themselves + vm.expectEmit(address(staking)); + emit IHorizonStakingMain.HorizonStakeWithdrawn(users.indexer, beforeServiceProvider.__DEPRECATED_tokensLocked); + staking.forceWithdraw(users.indexer); + + // after + ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(users.indexer); + uint256 afterServiceProviderBalance = token.balanceOf(users.indexer); + uint256 afterStakingBalance = token.balanceOf(address(staking)); + + // assert + assertEq( + afterServiceProviderBalance - beforeServiceProviderBalance, + beforeServiceProvider.__DEPRECATED_tokensLocked + ); + assertEq(beforeStakingBalance - afterStakingBalance, beforeServiceProvider.__DEPRECATED_tokensLocked); + assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, 0); + assertEq(afterServiceProvider.__DEPRECATED_tokensLockedUntil, 0); + } + + function testForceWithdraw_RevertWhen_ZeroTokens(uint256 tokens) public useIndexer { + tokens = bound(tokens, 1, MAX_STAKING_TOKENS); + + // simulate zero locked tokens + token.transfer(address(staking), tokens); + _setStorage_ServiceProvider(users.indexer, tokens, 0, 0, 0, 0); + + _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, MAX_THAWING_PERIOD); + + // switch to a different user + resetPrank(users.delegator); + + vm.expectRevert(abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidZeroTokens.selector)); + staking.forceWithdraw(users.indexer); + } +} diff --git a/packages/horizon/test/unit/staking/stake/unstake.t.sol b/packages/horizon/test/unit/staking/stake/unstake.t.sol index 83c6a0a81..147edf233 100644 --- a/packages/horizon/test/unit/staking/stake/unstake.t.sol +++ b/packages/horizon/test/unit/staking/stake/unstake.t.sol @@ -26,79 +26,6 @@ contract HorizonStakingUnstakeTest is HorizonStakingTest { _unstake(tokensToUnstake); } - function testUnstake_LockingPeriodGreaterThanZero_NoThawing( - uint256 tokens, - uint256 tokensToUnstake, - uint32 maxVerifierCut, - uint64 thawingPeriod - ) public useIndexer useProvision(tokens, maxVerifierCut, thawingPeriod) { - tokensToUnstake = bound(tokensToUnstake, 1, tokens); - - // simulate transition period - _setStorage_DeprecatedThawingPeriod(THAWING_PERIOD_IN_BLOCKS); - - // thaw, wait and deprovision - _thaw(users.indexer, subgraphDataServiceAddress, tokens); - skip(thawingPeriod + 1); - _deprovision(users.indexer, subgraphDataServiceAddress, 0); - - // unstake - _unstake(tokensToUnstake); - } - - function testUnstake_LockingPeriodGreaterThanZero_TokensDoneThawing( - uint256 tokens, - uint256 tokensToUnstake, - uint256 tokensLocked - ) public useIndexer { - // bounds - tokens = bound(tokens, 1, MAX_STAKING_TOKENS); - tokensToUnstake = bound(tokensToUnstake, 1, tokens); - tokensLocked = bound(tokensLocked, 1, MAX_STAKING_TOKENS); - - // simulate locked tokens with past locking period - _setStorage_DeprecatedThawingPeriod(THAWING_PERIOD_IN_BLOCKS); - token.transfer(address(staking), tokensLocked); - _setStorage_ServiceProvider(users.indexer, tokensLocked, 0, tokensLocked, block.number, 0); - - // create provision, thaw and deprovision - _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, MAX_THAWING_PERIOD); - _thaw(users.indexer, subgraphDataServiceLegacyAddress, tokens); - skip(MAX_THAWING_PERIOD + 1); - _deprovision(users.indexer, subgraphDataServiceLegacyAddress, 0); - - // unstake - _unstake(tokensToUnstake); - } - - function testUnstake_LockingPeriodGreaterThanZero_TokensStillThawing( - uint256 tokens, - uint256 tokensToUnstake, - uint256 tokensThawing, - uint32 tokensThawingUntilBlock - ) public useIndexer { - // bounds - tokens = bound(tokens, 1, MAX_STAKING_TOKENS); - tokensToUnstake = bound(tokensToUnstake, 1, tokens); - tokensThawing = bound(tokensThawing, 1, MAX_STAKING_TOKENS); - vm.assume(tokensThawingUntilBlock > block.number); - vm.assume(tokensThawingUntilBlock < block.number + THAWING_PERIOD_IN_BLOCKS); - - // simulate locked tokens still thawing - _setStorage_DeprecatedThawingPeriod(THAWING_PERIOD_IN_BLOCKS); - token.transfer(address(staking), tokensThawing); - _setStorage_ServiceProvider(users.indexer, tokensThawing, 0, tokensThawing, tokensThawingUntilBlock, 0); - - // create provision, thaw and deprovision - _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, MAX_THAWING_PERIOD); - _thaw(users.indexer, subgraphDataServiceLegacyAddress, tokens); - skip(MAX_THAWING_PERIOD + 1); - _deprovision(users.indexer, subgraphDataServiceLegacyAddress, 0); - - // unstake - _unstake(tokensToUnstake); - } - function testUnstake_RevertWhen_ZeroTokens( uint256 amount, uint32 maxVerifierCut, diff --git a/packages/horizon/test/unit/staking/stake/withdraw.t.sol b/packages/horizon/test/unit/staking/stake/withdraw.t.sol index eac19e416..188a3dcee 100644 --- a/packages/horizon/test/unit/staking/stake/withdraw.t.sol +++ b/packages/horizon/test/unit/staking/stake/withdraw.t.sol @@ -37,19 +37,4 @@ contract HorizonStakingWithdrawTest is HorizonStakingTest { vm.expectRevert(abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingInvalidZeroTokens.selector)); staking.withdraw(); } - - function testWithdraw_RevertWhen_StillThawing(uint256 tokens, uint256 tokensLocked) public useIndexer { - tokens = bound(tokens, 1, MAX_STAKING_TOKENS); - tokensLocked = bound(tokensLocked, 1, tokens); - - // simulate locked tokens still thawing - uint256 thawUntil = block.timestamp + 1; - token.transfer(address(staking), tokens); - _setStorage_ServiceProvider(users.indexer, tokens, 0, tokensLocked, thawUntil, 0); - - _createProvision(users.indexer, subgraphDataServiceLegacyAddress, tokens, 0, MAX_THAWING_PERIOD); - - vm.expectRevert(abi.encodeWithSelector(IHorizonStakingMain.HorizonStakingStillThawing.selector, thawUntil)); - staking.withdraw(); - } } diff --git a/packages/horizon/test/unit/utilities/GraphDirectory.t.sol b/packages/horizon/test/unit/utilities/GraphDirectory.t.sol index 180590a1e..6399be255 100644 --- a/packages/horizon/test/unit/utilities/GraphDirectory.t.sol +++ b/packages/horizon/test/unit/utilities/GraphDirectory.t.sol @@ -19,8 +19,7 @@ contract GraphDirectoryTest is GraphBaseTest { _getContractFromController("EpochManager"), _getContractFromController("RewardsManager"), _getContractFromController("GraphTokenGateway"), - _getContractFromController("GraphProxyAdmin"), - _getContractFromController("Curation") + _getContractFromController("GraphProxyAdmin") ); _deployImplementation(address(controller)); } @@ -49,7 +48,6 @@ contract GraphDirectoryTest is GraphBaseTest { assertEq(_getContractFromController("RewardsManager"), address(directory.graphRewardsManager())); assertEq(_getContractFromController("GraphTokenGateway"), address(directory.graphTokenGateway())); assertEq(_getContractFromController("GraphProxyAdmin"), address(directory.graphProxyAdmin())); - assertEq(_getContractFromController("Curation"), address(directory.graphCuration())); } function test_RevertWhen_AnInvalidContractGetterIsCalled() external { diff --git a/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol b/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol index 4a88bf0cd..f10741296 100644 --- a/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol +++ b/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol @@ -12,7 +12,6 @@ import { IEpochManager } from "@graphprotocol/interfaces/contracts/contracts/epo import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; import { IGraphProxyAdmin } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol"; -import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; import { GraphDirectory } from "./../../../contracts/utilities/GraphDirectory.sol"; @@ -57,8 +56,4 @@ contract GraphDirectoryImplementation is GraphDirectory { function graphProxyAdmin() external view returns (IGraphProxyAdmin) { return _graphProxyAdmin(); } - - function graphCuration() external view returns (ICuration) { - return _graphCuration(); - } } diff --git a/packages/horizon/test/unit/utils/Users.sol b/packages/horizon/test/unit/utils/Users.sol index 6213e4e82..c47395a14 100644 --- a/packages/horizon/test/unit/utils/Users.sol +++ b/packages/horizon/test/unit/utils/Users.sol @@ -9,5 +9,4 @@ struct Users { address gateway; address verifier; address delegator; - address legacySlasher; } diff --git a/packages/interfaces/contracts/horizon/IHorizonStaking.sol b/packages/interfaces/contracts/horizon/IHorizonStaking.sol index 4e680a1e5..9b16ad368 100644 --- a/packages/interfaces/contracts/horizon/IHorizonStaking.sol +++ b/packages/interfaces/contracts/horizon/IHorizonStaking.sol @@ -5,15 +5,14 @@ pragma solidity ^0.8.22; import { IHorizonStakingTypes } from "./internal/IHorizonStakingTypes.sol"; import { IHorizonStakingMain } from "./internal/IHorizonStakingMain.sol"; import { IHorizonStakingBase } from "./internal/IHorizonStakingBase.sol"; -import { IHorizonStakingExtension } from "./internal/IHorizonStakingExtension.sol"; /** * @title Complete interface for the Horizon Staking contract * @author Edge & Node - * @notice This interface exposes all functions implemented by the {HorizonStaking} contract and its extension - * {HorizonStakingExtension} as well as the custom data types used by the contract. + * @notice This interface exposes all functions implemented by the {HorizonStaking} contract + * as well as the custom data types used by the contract. * @dev Use this interface to interact with the Horizon Staking contract. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ -interface IHorizonStaking is IHorizonStakingTypes, IHorizonStakingBase, IHorizonStakingMain, IHorizonStakingExtension {} +interface IHorizonStaking is IHorizonStakingTypes, IHorizonStakingBase, IHorizonStakingMain {} diff --git a/packages/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol b/packages/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol index c48f20099..4bc81d44f 100644 --- a/packages/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol +++ b/packages/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol @@ -13,7 +13,7 @@ import { ILinkedList } from "./ILinkedList.sol"; /** * @title Interface for the {HorizonStakingBase} contract. * @author Edge & Node - * @notice Provides getters for {HorizonStaking} and {HorizonStakingExtension} storage variables. + * @notice Provides getters for {HorizonStaking} storage variables. * @dev Most functions operate over {HorizonStaking} provisions. To uniquely identify a provision * functions take `serviceProvider` and `verifier` addresses. * @custom:security-contact Please email security+contracts@thegraph.com if you find any @@ -21,19 +21,15 @@ import { ILinkedList } from "./ILinkedList.sol"; */ interface IHorizonStakingBase { /** - * @notice Emitted when a service provider stakes tokens. - * @dev TRANSITION PERIOD: After transition period move to IHorizonStakingMain. Temporarily it - * needs to be here since it's emitted by {_stake} which is used by both {HorizonStaking} - * and {HorizonStakingExtension}. - * @param serviceProvider The address of the service provider. - * @param tokens The amount of tokens staked. + * @notice Thrown when using an invalid thaw request type. */ - event HorizonStakeDeposited(address indexed serviceProvider, uint256 tokens); + error HorizonStakingInvalidThawRequestType(); /** - * @notice Thrown when using an invalid thaw request type. + * @notice Gets the address of the subgraph data service. + * @return The address of the subgraph data service. */ - error HorizonStakingInvalidThawRequestType(); + function getSubgraphService() external view returns (address); /** * @notice Gets the details of a service provider. diff --git a/packages/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol b/packages/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol deleted file mode 100644 index d487b2eca..000000000 --- a/packages/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.8.22; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - -import { IRewardsIssuer } from "../../contracts/rewards/IRewardsIssuer.sol"; - -/** - * @title Interface for {HorizonStakingExtension} contract. - * @author Edge & Node - * @notice Provides functions for managing legacy allocations. - * @custom:security-contact Please email security+contracts@thegraph.com if you find any - * bugs. We may have an active bug bounty program. - */ -interface IHorizonStakingExtension is IRewardsIssuer { - /** - * @dev Allocate GRT tokens for the purpose of serving queries of a subgraph deployment - * An allocation is created in the allocate() function and closed in closeAllocation() - * @param indexer The indexer address - * @param subgraphDeploymentID The subgraph deployment ID - * @param tokens The amount of tokens allocated to the subgraph deployment - * @param createdAtEpoch The epoch when the allocation was created - * @param closedAtEpoch The epoch when the allocation was closed - * @param collectedFees The amount of collected fees for the allocation - * @param __DEPRECATED_effectiveAllocation Deprecated field. - * @param accRewardsPerAllocatedToken Snapshot used for reward calculation - * @param distributedRebates The amount of collected rebates that have been rebated - */ - struct Allocation { - address indexer; - bytes32 subgraphDeploymentID; - uint256 tokens; - uint256 createdAtEpoch; - uint256 closedAtEpoch; - uint256 collectedFees; - uint256 __DEPRECATED_effectiveAllocation; - uint256 accRewardsPerAllocatedToken; - uint256 distributedRebates; - } - - /** - * @dev Possible states an allocation can be. - * States: - * - Null = indexer == address(0) - * - Active = not Null && tokens > 0 - * - Closed = Active && closedAtEpoch != 0 - */ - enum AllocationState { - Null, - Active, - Closed - } - - /** - * @notice Emitted when `indexer` close an allocation in `epoch` for `allocationID`. - * An amount of `tokens` get unallocated from `subgraphDeploymentID`. - * This event also emits the POI (proof of indexing) submitted by the indexer. - * `isPublic` is true if the sender was someone other than the indexer. - * @param indexer The indexer address - * @param subgraphDeploymentID The subgraph deployment ID - * @param epoch The protocol epoch the allocation was closed on - * @param tokens The amount of tokens unallocated from the allocation - * @param allocationID The allocation identifier - * @param sender The address closing the allocation - * @param poi The proof of indexing submitted by the sender - * @param isPublic True if the allocation was force closed by someone other than the indexer/operator - */ - event AllocationClosed( - address indexed indexer, - bytes32 indexed subgraphDeploymentID, - uint256 epoch, - uint256 tokens, - address indexed allocationID, - address sender, - bytes32 poi, - bool isPublic - ); - - /** - * @notice Emitted when `indexer` collects a rebate on `subgraphDeploymentID` for `allocationID`. - * `epoch` is the protocol epoch the rebate was collected on - * The rebate is for `tokens` amount which are being provided by `assetHolder`; `queryFees` - * is the amount up for rebate after `curationFees` are distributed and `protocolTax` is burnt. - * `queryRebates` is the amount distributed to the `indexer` with `delegationFees` collected - * and sent to the delegation pool. - * @param assetHolder The address of the asset holder, the entity paying the query fees - * @param indexer The indexer address - * @param subgraphDeploymentID The subgraph deployment ID - * @param allocationID The allocation identifier - * @param epoch The protocol epoch the rebate was collected on - * @param tokens The amount of tokens collected - * @param protocolTax The amount of tokens burnt as protocol tax - * @param curationFees The amount of tokens distributed to the curation pool - * @param queryFees The amount of tokens collected as query fees - * @param queryRebates The amount of tokens distributed to the indexer - * @param delegationRewards The amount of tokens collected from the delegation pool - */ - event RebateCollected( - address assetHolder, - address indexed indexer, - bytes32 indexed subgraphDeploymentID, - address indexed allocationID, - uint256 epoch, - uint256 tokens, - uint256 protocolTax, - uint256 curationFees, - uint256 queryFees, - uint256 queryRebates, - uint256 delegationRewards - ); - - /** - * @notice Emitted when `indexer` was slashed for a total of `tokens` amount. - * Tracks `reward` amount of tokens given to `beneficiary`. - * @param indexer The indexer address - * @param tokens The amount of tokens slashed - * @param reward The amount of reward tokens to send to a beneficiary - * @param beneficiary The address of a beneficiary to receive a reward for the slashing - */ - 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. - * Presenting a bad proof is subject to slashable condition. - * To opt out of rewards set _poi to 0x0 - * @param allocationID The allocation identifier - * @param poi Proof of indexing submitted for the allocated period - */ - function closeAllocation(address allocationID, bytes32 poi) external; - - /** - * @notice Collect and rebate query fees to the indexer - * This function will accept calls with zero tokens. - * We use an exponential rebate formula to calculate the amount of tokens to rebate to the indexer. - * This implementation allows collecting multiple times on the same allocation, keeping track of the - * total amount rebated, the total amount collected and compensating the indexer for the difference. - * @param tokens Amount of tokens to collect - * @param allocationID Allocation where the tokens will be assigned - */ - function collect(uint256 tokens, address allocationID) external; - - /** - * @notice Slash the indexer stake. Delegated tokens are not subject to slashing. - * Note that depending on the state of the indexer's stake, the slashed amount might be smaller than the - * requested slash amount. This can happen if the indexer has moved a significant part of their stake to - * a provision. Any outstanding slashing amount should be settled using Horizon's slash function - * {IHorizonStaking.slash}. - * @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; - - /** - * @notice (Legacy) Return true if operator is allowed for the service provider on the subgraph data service. - * @param operator Address of the operator - * @param indexer Address of the service provider - * @return True if operator is allowed for indexer, false otherwise - */ - function isOperator(address operator, address indexer) external view returns (bool); - - /** - * @notice Getter that returns if an indexer has any stake. - * @param indexer Address of the indexer - * @return True if indexer has staked tokens - */ - function hasStake(address indexer) external view returns (bool); - - /** - * @notice Get the total amount of tokens staked by the indexer. - * @param indexer Address of the indexer - * @return Amount of tokens staked by the indexer - */ - function getIndexerStakedTokens(address indexer) external view returns (uint256); - - /** - * @notice Return the allocation by ID. - * @param allocationID Address used as allocation identifier - * @return Allocation data - */ - function getAllocation(address allocationID) external view returns (Allocation memory); - - /** - * @notice Return the current state of an allocation - * @param allocationID Allocation identifier - * @return AllocationState enum with the state of the allocation - */ - function getAllocationState(address allocationID) external view returns (AllocationState); - - /** - * @notice Return if allocationID is used. - * @param allocationID Address used as signer by the indexer for an allocation - * @return True if allocationID already used - */ - function isAllocation(address allocationID) external view returns (bool); - - /** - * @notice Return the time in blocks to unstake - * Deprecated, now enforced by each data service (verifier) - * @return Thawing period in blocks - */ - function __DEPRECATED_getThawingPeriod() external view returns (uint64); - - /** - * @notice Return the address of the subgraph data service. - * @dev TRANSITION PERIOD: After transition period move to main HorizonStaking contract - * @return Address of the subgraph data service - */ - function getSubgraphService() external view returns (address); -} diff --git a/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol b/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol index 19c1e1cf8..b5763a8f2 100644 --- a/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol +++ b/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol @@ -12,13 +12,8 @@ import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; * @title Inferface for the {HorizonStaking} contract. * @author Edge & Node * @notice Provides functions for managing stake, provisions, delegations, and slashing. - * @dev Note that this interface only includes the functions implemented by {HorizonStaking} contract, - * and not those implemented by {HorizonStakingExtension}. - * Do not use this interface to interface with the {HorizonStaking} contract, use {IHorizonStaking} for - * the complete interface. * @dev Most functions operate over {HorizonStaking} provisions. To uniquely identify a provision * functions take `serviceProvider` and `verifier` addresses. - * @dev TRANSITION PERIOD: After transition period rename to IHorizonStaking. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ @@ -26,15 +21,14 @@ interface IHorizonStakingMain { // -- Events: stake -- /** - * @notice Emitted when a service provider unstakes tokens during the transition period. - * @param serviceProvider The address of the service provider - * @param tokens The amount of tokens now locked (including previously locked tokens) - * @param until The block number until the stake is locked + * @notice Emitted when a service provider stakes tokens. + * @param serviceProvider The address of the service provider. + * @param tokens The amount of tokens staked. */ - event HorizonStakeLocked(address indexed serviceProvider, uint256 tokens, uint256 until); + event HorizonStakeDeposited(address indexed serviceProvider, uint256 tokens); /** - * @notice Emitted when a service provider withdraws tokens during the transition period. + * @notice Emitted when a service provider unstakes tokens. * @param serviceProvider The address of the service provider * @param tokens The amount of tokens withdrawn */ @@ -219,7 +213,7 @@ interface IHorizonStakingMain { /** * @notice Emitted when `delegator` withdrew delegated `tokens` from `indexer` using `withdrawDelegated`. - * @dev This event is for the legacy `withdrawDelegated` function. + * @dev This event is for the legacy `withdrawDelegated` function, only emitted for pre-horizon undelegations. * @param indexer The address of the indexer * @param delegator The address of the delegator * @param tokens The amount of tokens withdrawn @@ -324,12 +318,6 @@ interface IHorizonStakingMain { */ event AllowedLockedVerifierSet(address indexed verifier, bool allowed); - /** - * @notice Emitted when the legacy global thawing period is set to zero. - * @dev This marks the end of the transition period. - */ - event ThawingPeriodCleared(); - /** * @notice Emitted when the delegation slashing global flag is set. */ @@ -373,13 +361,6 @@ interface IHorizonStakingMain { */ error HorizonStakingNotAuthorized(address serviceProvider, address verifier, address caller); - /** - * @notice Thrown when attempting to create a provision with a verifier other than the - * subgraph data service. This restriction only applies during the transition period. - * @param verifier The verifier address - */ - error HorizonStakingInvalidVerifier(address verifier); - /** * @notice Thrown when attempting to create a provision with an invalid maximum verifier cut. * @param maxVerifierCut The maximum verifier cut @@ -407,14 +388,6 @@ interface IHorizonStakingMain { */ error HorizonStakingInsufficientIdleStake(uint256 tokens, uint256 minTokens); - /** - * @notice Thrown during the transition period when the service provider has insufficient stake to - * cover their existing legacy allocations. - * @param tokens The actual token amount - * @param minTokens The minimum required token amount - */ - error HorizonStakingInsufficientStakeForLegacyAllocations(uint256 tokens, uint256 minTokens); - // -- Errors: delegation -- /** @@ -480,18 +453,12 @@ interface IHorizonStakingMain { error HorizonStakingTooManyThawRequests(); /** - * @notice Thrown when attempting to withdraw tokens that have not thawed (legacy undelegate). + * @notice Thrown when attempting to withdraw tokens that have not thawed. + * @dev This error is only thrown for pre-horizon undelegations. */ error HorizonStakingNothingToWithdraw(); // -- Errors: misc -- - /** - * @notice Thrown during the transition period when attempting to withdraw tokens that are still thawing. - * @dev Note this thawing refers to the global thawing period applied to legacy allocated tokens, - * it does not refer to thaw requests. - * @param until The block number until the stake is locked - */ - error HorizonStakingStillThawing(uint256 until); /** * @notice Thrown when a service provider attempts to operate on verifiers that are not allowed. @@ -511,11 +478,6 @@ interface IHorizonStakingMain { */ error HorizonStakingInvalidDelegationFeeCut(uint256 feeCut); - /** - * @notice Thrown when a legacy slash fails. - */ - error HorizonStakingLegacySlashFailed(); - /** * @notice Thrown when there attempting to slash a provision with no tokens to slash. */ @@ -571,19 +533,12 @@ interface IHorizonStakingMain { /** * @notice Move idle stake back to the owner's account. - * Stake is removed from the protocol: - * - 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}. + * Stake is immediately removed from the protocol. * @dev Requirements: * - `_tokens` cannot be zero. - * - `_serviceProvider` must have enough idle stake to cover the staking amount and any - * legacy allocation. + * - `_serviceProvider` must have enough idle stake to cover the staking amount. * - * Emits a {HorizonStakeLocked} event during the transition period. - * Emits a {HorizonStakeWithdrawn} event after the transition period. + * Emits a {HorizonStakeWithdrawn} event. * * @param tokens Amount of tokens to unstake */ @@ -592,8 +547,12 @@ interface IHorizonStakingMain { /** * @notice Withdraw service provider tokens once the thawing period (initiated by {unstake}) has passed. * All thawed tokens are withdrawn. - * @dev This is only needed during the transition period while we still have - * a global lock. After that, unstake() will automatically withdraw. + * This function is for backwards compatibility with the legacy staking contract. + * It only allows withdrawing tokens unstaked before horizon upgrade. + * @dev This function can't be removed in case there are still pre-horizon unstakes. + * + * Emits a {HorizonStakeWithdrawn} event. + * */ function withdraw() external; @@ -603,8 +562,6 @@ interface IHorizonStakingMain { * service, where the data service is the verifier. * This function can be called by the service provider or by an operator authorized by the provider * for this specific verifier. - * @dev During the transition period, only the subgraph data service can be used as a verifier. This - * prevents an escape hatch for legacy allocation stake. * @dev Requirements: * - `tokens` cannot be zero. * - The `serviceProvider` must have enough idle stake to cover the tokens to provision. @@ -883,6 +840,7 @@ 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 tokens undelegated before horizon upgrade. + * @dev This function can't be removed in case there are still pre-horizon undelegations. * @dev See {delegate}. * @param serviceProvider The service provider address * @param deprecated Deprecated parameter kept for backwards compatibility @@ -971,14 +929,6 @@ interface IHorizonStakingMain { */ function setDelegationSlashingEnabled() external; - /** - * @notice Clear the legacy global thawing period. - * This signifies the end of the transition period, after which no legacy allocations should be left. - * @dev This function can only be called by the contract governor. - * @dev Emits a {ThawingPeriodCleared} event. - */ - function clearThawingPeriod() external; - /** * @notice Sets the global maximum thawing period allowed for provisions. * @param maxThawingPeriod The new maximum thawing period, in seconds @@ -1004,8 +954,28 @@ interface IHorizonStakingMain { function isAuthorized(address serviceProvider, address verifier, address operator) external view returns (bool); /** - * @notice Get the address of the staking extension. - * @return The address of the staking extension + * @notice Withdraw service provider legacy locked tokens. + * This is a permissionless function that allows anyone to withdraw on behalf of a service provider. + * It only allows withdrawing tokens that were unstaked before the Horizon upgrade. + * @dev Tokens are always sent to the service provider, not the caller. + * + * Emits a {HorizonStakeWithdrawn} event. + * + * @param serviceProvider Address of service provider to withdraw funds from + */ + function forceWithdraw(address serviceProvider) external; + + /** + * @notice Withdraw delegator legacy undelegated tokens. + * This is a permissionless function that allows anyone to withdraw on behalf of a delegator. + * It only allows withdrawing tokens that were undelegated before the Horizon upgrade. + * @dev Tokens are always sent to the delegator, not the caller. + * + * Emits a {StakeDelegatedWithdrawn} event. + * + * @param serviceProvider The service provider address + * @param delegator The delegator address to withdraw funds for + * @return The amount of tokens withdrawn */ - function getStakingExtension() external view returns (address); + function forceWithdrawDelegated(address serviceProvider, address delegator) external returns (uint256); } diff --git a/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol b/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol index da1324cc9..c7a7dc2dd 100644 --- a/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol +++ b/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol @@ -20,7 +20,7 @@ interface IDisputeManager { Null, IndexingDispute, QueryDispute, - LegacyDispute + __DEPRECATED_LegacyDispute } /// @notice Status of a dispute @@ -143,25 +143,6 @@ interface IDisputeManager { uint256 cancellableAt ); - /** - * @notice Emitted when a legacy dispute is created for `allocationId` and `fisherman`. - * The event emits the amount of `tokensSlash` to slash and `tokensRewards` to reward the fisherman. - * @param disputeId The dispute id - * @param indexer The indexer address - * @param fisherman The fisherman address to be credited with the rewards - * @param allocationId The allocation id - * @param tokensSlash The amount of tokens to slash - * @param tokensRewards The amount of tokens to reward the fisherman - */ - event LegacyDisputeCreated( - bytes32 indexed disputeId, - address indexed indexer, - address indexed fisherman, - address allocationId, - uint256 tokensSlash, - uint256 tokensRewards - ); - /** * @notice Emitted when arbitrator accepts a `disputeId` to `indexer` created by `fisherman`. * The event emits the amount `tokens` transferred to the fisherman, the deposit plus reward. @@ -469,39 +450,6 @@ interface IDisputeManager { */ function createIndexingDispute(address allocationId, bytes32 poi, uint256 blockNumber) external returns (bytes32); - /** - * @notice Creates and auto-accepts a legacy dispute. - * This disputes can be created to settle outstanding slashing amounts with an indexer that has been - * "legacy slashed" during or shortly after the transition period. See {HorizonStakingExtension.legacySlash} - * for more details. - * - * Note that this type of dispute: - * - can only be created by the arbitrator - * - does not require a bond - * - is automatically accepted when created - * - * Additionally, note that this type of disputes allow the arbitrator to directly set the slash and rewards - * amounts, bypassing the usual mechanisms that impose restrictions on those. This is done to give arbitrators - * maximum flexibility to ensure outstanding slashing amounts are settled fairly. This function needs to be removed - * after the transition period. - * - * Requirements: - * - Indexer must have been legacy slashed during or shortly after the transition period - * - Indexer must have provisioned funds to the Subgraph Service - * - * @param allocationId The allocation to dispute - * @param fisherman The fisherman address to be credited with the rewards - * @param tokensSlash The amount of tokens to slash - * @param tokensRewards The amount of tokens to reward the fisherman - * @return The dispute id - */ - function createAndAcceptLegacyDispute( - address allocationId, - address fisherman, - uint256 tokensSlash, - uint256 tokensRewards - ) external returns (bytes32); - // -- Arbitrator -- /** diff --git a/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol b/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol index 5b084c7a7..18829a024 100644 --- a/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol +++ b/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol @@ -198,16 +198,6 @@ interface ISubgraphService is IDataServiceFees { */ function resizeAllocation(address indexer, address allocationId, uint256 tokens) external; - /** - * @notice Imports a legacy allocation id into the subgraph service - * This is a governor only action that is required to prevent indexers from re-using allocation ids from the - * legacy staking contract. - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - */ - function migrateLegacyAllocation(address indexer, address allocationId, bytes32 subgraphDeploymentId) external; - /** * @notice Sets a pause guardian * @param pauseGuardian The address of the pause guardian diff --git a/packages/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol b/packages/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol index c5bf7f8c7..b6422fad8 100644 --- a/packages/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol +++ b/packages/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol @@ -23,14 +23,8 @@ interface ILegacyAllocation { } /** - * @notice Thrown when attempting to migrate an allocation with an existing id + * @notice Thrown when attempting to create an allocation with an existing legacy id * @param allocationId The allocation id */ error LegacyAllocationAlreadyExists(address allocationId); - - /** - * @notice Thrown when trying to get a non-existent allocation - * @param allocationId The allocation id - */ - error LegacyAllocationDoesNotExist(address allocationId); } diff --git a/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol b/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol index 9e6e8b704..3b6809a63 100644 --- a/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol +++ b/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol @@ -44,12 +44,6 @@ interface IAllocationManager { bool forceClosed ); - event LegacyAllocationMigrated( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId - ); - event MaxPOIStalenessSet(uint256 maxPOIStaleness); // Errors diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 6f73b2c5d..660f19251 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -204,46 +204,6 @@ contract DisputeManager is return (dId1, dId2); } - /// @inheritdoc IDisputeManager - function createAndAcceptLegacyDispute( - address allocationId, - address fisherman, - uint256 tokensSlash, - uint256 tokensRewards - ) external override onlyArbitrator returns (bytes32) { - // Create a disputeId - bytes32 disputeId = keccak256(abi.encodePacked(allocationId, "legacy")); - - // Get the indexer for the legacy allocation - address indexer = _graphStaking().getAllocation(allocationId).indexer; - require(indexer != address(0), DisputeManagerIndexerNotFound(allocationId)); - - // Store dispute - disputes[disputeId] = Dispute( - indexer, - fisherman, - 0, - 0, - DisputeType.LegacyDispute, - IDisputeManager.DisputeStatus.Accepted, - block.timestamp, - block.timestamp + disputePeriod, - 0 - ); - - // Slash the indexer - ISubgraphService subgraphService_ = _getSubgraphService(); - subgraphService_.slash(indexer, abi.encode(tokensSlash, tokensRewards)); - - // Reward the fisherman - _graphToken().pushTokens(fisherman, tokensRewards); - - emit LegacyDisputeCreated(disputeId, indexer, fisherman, allocationId, tokensSlash, tokensRewards); - emit DisputeAccepted(disputeId, indexer, fisherman, tokensRewards); - - return disputeId; - } - /// @inheritdoc IDisputeManager function acceptDispute( bytes32 disputeId, diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 0ba0b3035..23f1d5a61 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -327,15 +327,6 @@ contract SubgraphService is _resizeAllocation(allocationId, tokens, _delegationRatio); } - /// @inheritdoc ISubgraphService - function migrateLegacyAllocation( - address indexer, - address allocationId, - bytes32 subgraphDeploymentID - ) external override onlyOwner { - _migrateLegacyAllocation(indexer, allocationId, subgraphDeploymentID); - } - /// @inheritdoc ISubgraphService function setPauseGuardian(address pauseGuardian, bool allowed) external override onlyOwner { _setPauseGuardian(pauseGuardian, allowed); diff --git a/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol b/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol index 4717cefed..ed9003433 100644 --- a/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol +++ b/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; -import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol"; /** @@ -14,59 +13,17 @@ import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph- library LegacyAllocation { using LegacyAllocation for ILegacyAllocation.State; - /** - * @notice Migrate a legacy allocation - * @dev Requirements: - * - The allocation must not have been previously migrated - * @param self The legacy allocation list mapping - * @param indexer The indexer that owns the allocation - * @param allocationId The allocation id - * @param subgraphDeploymentId The subgraph deployment id the allocation is for - * @custom:error LegacyAllocationAlreadyMigrated if the allocation has already been migrated - */ - function migrate( - mapping(address => ILegacyAllocation.State) storage self, - address indexer, - address allocationId, - bytes32 subgraphDeploymentId - ) internal { - require(!self[allocationId].exists(), ILegacyAllocation.LegacyAllocationAlreadyExists(allocationId)); - - self[allocationId] = ILegacyAllocation.State({ indexer: indexer, subgraphDeploymentId: subgraphDeploymentId }); - } - - /** - * @notice Get a legacy allocation - * @param self The legacy allocation list mapping - * @param allocationId The allocation id - * @return The legacy allocation details - */ - function get( - mapping(address => ILegacyAllocation.State) storage self, - address allocationId - ) internal view returns (ILegacyAllocation.State memory) { - return _get(self, allocationId); - } - /** * @notice Revert if a legacy allocation exists - * @dev We first check the migrated mapping then the old staking contract. - * @dev TRANSITION PERIOD: after the transition period when all the allocations are migrated we can - * remove the call to the staking contract. + * @dev We check the migrated allocations mapping. * @param self The legacy allocation list mapping - * @param graphStaking The Horizon Staking contract * @param allocationId The allocation id */ function revertIfExists( mapping(address => ILegacyAllocation.State) storage self, - IHorizonStaking graphStaking, address allocationId ) internal view { require(!self[allocationId].exists(), ILegacyAllocation.LegacyAllocationAlreadyExists(allocationId)); - require( - !graphStaking.isAllocation(allocationId), - ILegacyAllocation.LegacyAllocationAlreadyExists(allocationId) - ); } /** @@ -77,19 +34,4 @@ library LegacyAllocation { function exists(ILegacyAllocation.State memory self) internal pure returns (bool) { return self.indexer != address(0); } - - /** - * @notice Get a legacy allocation - * @param self The legacy allocation list mapping - * @param allocationId The allocation id - * @return The legacy allocation details - */ - function _get( - mapping(address => ILegacyAllocation.State) storage self, - address allocationId - ) private view returns (ILegacyAllocation.State storage) { - ILegacyAllocation.State storage allocation = self[allocationId]; - require(allocation.exists(), ILegacyAllocation.LegacyAllocationDoesNotExist(allocationId)); - return allocation; - } } diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 08608d8b4..4c642561c 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -116,18 +116,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca bool forceClosed ); - /** - * @notice Emitted when a legacy allocation is migrated into the subgraph service - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - */ - event LegacyAllocationMigrated( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId - ); - /** * @notice Emitted when the maximum POI staleness is updated * @param maxPOIStaleness The max POI staleness in seconds @@ -175,19 +163,6 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca */ function __AllocationManager_init_unchained() internal onlyInitializing {} - /** - * @notice Imports a legacy allocation id into the subgraph service - * This is a governor only action that is required to prevent indexers from re-using allocation ids from the - * legacy staking contract. It will revert with LegacyAllocationAlreadyMigrated if the allocation has already been migrated. - * @param _indexer The address of the indexer - * @param _allocationId The id of the allocation - * @param _subgraphDeploymentId The id of the subgraph deployment - */ - function _migrateLegacyAllocation(address _indexer, address _allocationId, bytes32 _subgraphDeploymentId) internal { - _legacyAllocations.migrate(_indexer, _allocationId, _subgraphDeploymentId); - emit LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentId); - } - /** * @notice Create an allocation * @dev The `_allocationProof` is a 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationId)` @@ -218,7 +193,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca // Ensure allocation id is not reused // need to check both subgraph service (on allocations.create()) and legacy allocations - _legacyAllocations.revertIfExists(_graphStaking(), _allocationId); + _legacyAllocations.revertIfExists(_allocationId); uint256 currentEpoch = _graphEpochManager().currentEpoch(); IAllocation.State memory allocation = _allocations.create( diff --git a/packages/subgraph-service/scripts/integration b/packages/subgraph-service/scripts/integration index d5d7f1c0d..58a7ba4fe 100755 --- a/packages/subgraph-service/scripts/integration +++ b/packages/subgraph-service/scripts/integration @@ -124,13 +124,6 @@ npx hardhat deploy:migrate --network localhost --horizon-config integration --st cd ../subgraph-service npx hardhat test:seed --network localhost -# Run integration tests - During transition period -npx hardhat test:integration --phase during-transition-period --network localhost - -# Clear thawing period -cd ../horizon -npx hardhat transition:clear-thawing --network localhost --governor-index 1 - # Run integration tests - After transition period cd ../subgraph-service npx hardhat test:integration --phase after-transition-period --network localhost diff --git a/packages/subgraph-service/tasks/test/integration.ts b/packages/subgraph-service/tasks/test/integration.ts index 130058e90..ef63c42f4 100644 --- a/packages/subgraph-service/tasks/test/integration.ts +++ b/packages/subgraph-service/tasks/test/integration.ts @@ -4,13 +4,9 @@ import { TASK_TEST } from 'hardhat/builtin-tasks/task-names' import { task } from 'hardhat/config' task('test:integration', 'Runs all integration tests') - .addParam( - 'phase', - 'Test phase to run: "during-transition-period", "after-transition-period", "after-delegation-slashing-enabled"', - ) + .addParam('phase', 'Test phase to run: "after-transition-period", "after-delegation-slashing-enabled"') .setAction(async (taskArgs, hre) => { // Get test files for each phase - const duringTransitionPeriodFiles = await glob('test/integration/during-transition-period/**/*.{js,ts}') const afterTransitionPeriodFiles = await glob('test/integration/after-transition-period/**/*.{js,ts}') // Display banner for the current test phase @@ -18,15 +14,12 @@ task('test:integration', 'Runs all integration tests') // Run tests for the current phase switch (taskArgs.phase) { - case 'during-transition-period': - await hre.run(TASK_TEST, { testFiles: duringTransitionPeriodFiles }) - break case 'after-transition-period': await hre.run(TASK_TEST, { testFiles: afterTransitionPeriodFiles }) break default: throw new Error( - 'Invalid phase. Must be "during-transition-period", "after-transition-period", "after-delegation-slashing-enabled", or "all"', + 'Invalid phase. Must be "after-transition-period", "after-delegation-slashing-enabled", or "all"', ) } }) diff --git a/packages/subgraph-service/test/integration/during-transition-period/dispute-manager.test.ts b/packages/subgraph-service/test/integration/during-transition-period/dispute-manager.test.ts deleted file mode 100644 index a24f9703a..000000000 --- a/packages/subgraph-service/test/integration/during-transition-period/dispute-manager.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { - DisputeManager, - HorizonStaking, - L2GraphToken, - LegacyDisputeManager, - SubgraphService, -} from '@graphprotocol/interfaces' -import { generateLegacyIndexingDisputeId, generateLegacyTypeDisputeId } from '@graphprotocol/toolshed' -import { indexersData as indexers } from '@graphprotocol/toolshed/fixtures' -import { setGRTBalance } from '@graphprotocol/toolshed/hardhat' -import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import { ethers } from 'hardhat' -import hre from 'hardhat' - -describe('Dispute Manager', () => { - let disputeManager: DisputeManager - let legacyDisputeManager: LegacyDisputeManager - let graphToken: L2GraphToken - let staking: HorizonStaking - let subgraphService: SubgraphService - - let snapshotId: string - - // Test addresses - let governor: HardhatEthersSigner - let fisherman: HardhatEthersSigner - let arbitrator: HardhatEthersSigner - let indexer: HardhatEthersSigner - - let disputeDeposit: bigint - - // Allocation variables - let allocationId: string - - before(async () => { - // Get contracts - const graph = hre.graph() - disputeManager = graph.subgraphService.contracts.DisputeManager - legacyDisputeManager = graph.subgraphService.contracts.LegacyDisputeManager - graphToken = graph.horizon.contracts.GraphToken - staking = graph.horizon.contracts.HorizonStaking - subgraphService = graph.subgraphService.contracts.SubgraphService - - // Get signers - governor = await graph.accounts.getGovernor() - arbitrator = await graph.accounts.getArbitrator() - ;[fisherman] = await graph.accounts.getTestAccounts() - - // Get indexer - const indexerFixture = indexers[0] - indexer = await ethers.getSigner(indexerFixture.address) - - // Get allocation - const allocation = indexerFixture.legacyAllocations[0] - allocationId = allocation.allocationID - - // Get dispute deposit - disputeDeposit = ethers.parseEther('10000') - - // Set GRT balance for fisherman - await setGRTBalance(graph.provider, graphToken.target, fisherman.address, ethers.parseEther('1000000')) - - // Set arbitrator - await legacyDisputeManager.connect(governor).setArbitrator(arbitrator.address) - }) - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('Legacy dispute type', () => { - describe('Arbitrator', () => { - it('should allow arbitrator to create and accept a legacy dispute on the new dispute manager after slashing on the legacy dispute manager', async () => { - // Create an indexing dispute on legacy dispute manager - await graphToken.connect(fisherman).approve(legacyDisputeManager.target, disputeDeposit) - await legacyDisputeManager.connect(fisherman).createIndexingDispute(allocationId, disputeDeposit) - const legacyDisputeId = generateLegacyIndexingDisputeId(allocationId) - - // Accept the dispute on the legacy dispute manager - await legacyDisputeManager.connect(arbitrator).acceptDispute(legacyDisputeId) - - // Get fisherman's balance before creating dispute - const fishermanBalanceBefore = await graphToken.balanceOf(fisherman.address) - - // Get indexer's provision before creating dispute - const provision = await staking.getProviderTokensAvailable(indexer.address, await subgraphService.getAddress()) - - // Create and accept legacy dispute using the same allocation ID - const tokensToSlash = ethers.parseEther('100000') - const tokensToReward = tokensToSlash / 2n - await disputeManager - .connect(arbitrator) - .createAndAcceptLegacyDispute(allocationId, fisherman.address, tokensToSlash, tokensToReward) - - // Get dispute ID from event - const disputeId = generateLegacyTypeDisputeId(allocationId) - - // Verify dispute was created and accepted - const dispute = await disputeManager.disputes(disputeId) - expect(dispute.indexer).to.equal(indexer.address, 'Indexer address mismatch') - expect(dispute.fisherman).to.equal(fisherman.address, 'Fisherman address mismatch') - expect(dispute.disputeType).to.equal(3, 'Dispute type should be legacy') - expect(dispute.status).to.equal(1, 'Dispute status should be accepted') - - // Verify indexer's stake was slashed - const updatedProvision = await staking.getProviderTokensAvailable( - indexer.address, - await subgraphService.getAddress(), - ) - expect(updatedProvision).to.equal(provision - tokensToSlash, 'Indexer stake should be slashed') - - // Verify fisherman got the reward - const fishermanBalance = await graphToken.balanceOf(fisherman.address) - expect(fishermanBalance).to.equal( - fishermanBalanceBefore + tokensToReward, - 'Fisherman balance should be increased by the reward', - ) - }) - - it('should not allow creating a legacy dispute for non-existent allocation', async () => { - const tokensToSlash = ethers.parseEther('1000') - const tokensToReward = tokensToSlash / 2n - - // Attempt to create legacy dispute with non-existent allocation - await expect( - disputeManager - .connect(arbitrator) - .createAndAcceptLegacyDispute( - ethers.Wallet.createRandom().address, - fisherman.address, - tokensToSlash, - tokensToReward, - ), - ).to.be.revertedWithCustomError(disputeManager, 'DisputeManagerIndexerNotFound') - }) - }) - - it('should not allow non-arbitrator to create a legacy dispute', async () => { - const tokensToSlash = ethers.parseEther('1000') - const tokensToReward = tokensToSlash / 2n - - // Attempt to create legacy dispute as fisherman - await expect( - disputeManager - .connect(fisherman) - .createAndAcceptLegacyDispute(allocationId, fisherman.address, tokensToSlash, tokensToReward), - ).to.be.revertedWithCustomError(disputeManager, 'DisputeManagerNotArbitrator') - }) - }) -}) diff --git a/packages/subgraph-service/test/integration/during-transition-period/governance.test.ts b/packages/subgraph-service/test/integration/during-transition-period/governance.test.ts deleted file mode 100644 index ad638b306..000000000 --- a/packages/subgraph-service/test/integration/during-transition-period/governance.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { SubgraphService } from '@graphprotocol/interfaces' -import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import { ethers } from 'hardhat' -import hre from 'hardhat' - -describe('Governance', () => { - let subgraphService: SubgraphService - let snapshotId: string - - // Test addresses - let governor: HardhatEthersSigner - let indexer: HardhatEthersSigner - let nonOwner: HardhatEthersSigner - let allocationId: string - let subgraphDeploymentId: string - - const graph = hre.graph() - - before(() => { - subgraphService = graph.subgraphService.contracts.SubgraphService - }) - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - - // Get signers - governor = await graph.accounts.getGovernor() - ;[indexer, nonOwner] = await graph.accounts.getTestAccounts() - - // Generate test addresses - allocationId = ethers.Wallet.createRandom().address - subgraphDeploymentId = ethers.keccak256(ethers.toUtf8Bytes('test-subgraph-deployment')) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('Legacy Allocation Migration', () => { - it('should migrate legacy allocation', async () => { - // Migrate legacy allocation - await subgraphService - .connect(governor) - .migrateLegacyAllocation(indexer.address, allocationId, subgraphDeploymentId) - - // Verify the legacy allocation was migrated - const legacyAllocation = await subgraphService.getLegacyAllocation(allocationId) - expect(legacyAllocation.indexer).to.equal(indexer.address) - expect(legacyAllocation.subgraphDeploymentId).to.equal(subgraphDeploymentId) - }) - - it('should not allow non-owner to migrate legacy allocation', async () => { - // Attempt to migrate legacy allocation as non-owner - await expect( - subgraphService.connect(nonOwner).migrateLegacyAllocation(indexer.address, allocationId, subgraphDeploymentId), - ).to.be.revertedWithCustomError(subgraphService, 'OwnableUnauthorizedAccount') - }) - - it('should not allow migrating a legacy allocation that was already migrated', async () => { - // First migration - await subgraphService - .connect(governor) - .migrateLegacyAllocation(indexer.address, allocationId, subgraphDeploymentId) - - // Attempt to migrate the same allocation again - await expect( - subgraphService.connect(governor).migrateLegacyAllocation(indexer.address, allocationId, subgraphDeploymentId), - ) - .to.be.revertedWithCustomError(subgraphService, 'LegacyAllocationAlreadyExists') - .withArgs(allocationId) - }) - }) -}) diff --git a/packages/subgraph-service/test/integration/during-transition-period/indexer.test.ts b/packages/subgraph-service/test/integration/during-transition-period/indexer.test.ts deleted file mode 100644 index 7fd508c40..000000000 --- a/packages/subgraph-service/test/integration/during-transition-period/indexer.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { SubgraphService } from '@graphprotocol/interfaces' -import { encodeStartServiceData, generateAllocationProof } from '@graphprotocol/toolshed' -import { indexersData as indexers } from '@graphprotocol/toolshed/fixtures' -import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import { ethers } from 'hardhat' -import hre from 'hardhat' - -describe('Indexer', () => { - let subgraphService: SubgraphService - let snapshotId: string - let chainId: number - - // Test addresses - let governor: HardhatEthersSigner - let indexer: HardhatEthersSigner - let allocationId: string - let subgraphDeploymentId: string - let allocationPrivateKey: string - let subgraphServiceAddress: string - - const graph = hre.graph() - - before(async () => { - // Get contracts - subgraphService = graph.subgraphService.contracts.SubgraphService - - // Get governor and non-owner - governor = await graph.accounts.getGovernor() - - // Get chain id - chainId = Number((await hre.ethers.provider.getNetwork()).chainId) - - // Get subgraph service address - subgraphServiceAddress = await subgraphService.getAddress() - }) - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('Allocation', () => { - beforeEach(async () => { - // Get indexer - const indexerFixture = indexers[0] - indexer = await ethers.getSigner(indexerFixture.address) - - // Generate test addresses - const allocation = indexerFixture.legacyAllocations[0] - allocationId = allocation.allocationID - subgraphDeploymentId = allocation.subgraphDeploymentID - allocationPrivateKey = allocation.allocationPrivateKey - }) - - it('should not be able to create an allocation with an AllocationID that already exists in HorizonStaking contract', async () => { - // Build allocation proof - const signature = await generateAllocationProof( - indexer.address, - allocationPrivateKey, - subgraphServiceAddress, - chainId, - ) - - // Attempt to create an allocation with the same ID - const data = encodeStartServiceData(subgraphDeploymentId, 1000n, allocationId, signature) - - await expect(subgraphService.connect(indexer).startService(indexer.address, data)) - .to.be.revertedWithCustomError(subgraphService, 'LegacyAllocationAlreadyExists') - .withArgs(allocationId) - }) - - it('should not be able to create an allocation that was already migrated by the owner', async () => { - // Migrate legacy allocation - await subgraphService - .connect(governor) - .migrateLegacyAllocation(indexer.address, allocationId, subgraphDeploymentId) - - // Build allocation proof - const signature = await generateAllocationProof( - indexer.address, - allocationPrivateKey, - subgraphServiceAddress, - chainId, - ) - - // Attempt to create the same allocation - const data = encodeStartServiceData(subgraphDeploymentId, 1000n, allocationId, signature) - - await expect(subgraphService.connect(indexer).startService(indexer.address, data)) - .to.be.revertedWithCustomError(subgraphService, 'LegacyAllocationAlreadyExists') - .withArgs(allocationId) - }) - }) -}) diff --git a/packages/subgraph-service/test/integration/during-transition-period/legacy-dispute-manager.test.ts b/packages/subgraph-service/test/integration/during-transition-period/legacy-dispute-manager.test.ts deleted file mode 100644 index 51cfc557c..000000000 --- a/packages/subgraph-service/test/integration/during-transition-period/legacy-dispute-manager.test.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { HorizonStaking, L2GraphToken, LegacyDisputeManager } from '@graphprotocol/interfaces' -import { - generateAttestationData, - generateLegacyIndexingDisputeId, - generateLegacyQueryDisputeId, -} from '@graphprotocol/toolshed' -import { indexersData as indexers } from '@graphprotocol/toolshed/fixtures' -import { setGRTBalance } from '@graphprotocol/toolshed/hardhat' -import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -import { expect } from 'chai' -import { ethers } from 'hardhat' -import hre from 'hardhat' - -describe('Legacy Dispute Manager', () => { - let legacyDisputeManager: LegacyDisputeManager - let graphToken: L2GraphToken - let staking: HorizonStaking - - let snapshotId: string - - let governor: HardhatEthersSigner - let arbitrator: HardhatEthersSigner - let indexer: HardhatEthersSigner - let fisherman: HardhatEthersSigner - - let disputeDeposit: bigint - - const graph = hre.graph() - - // We have to use Aribtrm Sepolia since we're testing an already deployed contract but running on a hardhat fork - const chainId = 421614 - - before(async () => { - governor = await graph.accounts.getGovernor() - ;[arbitrator, fisherman] = await graph.accounts.getTestAccounts() - - // Get contract instances with correct types - legacyDisputeManager = graph.subgraphService.contracts.LegacyDisputeManager - graphToken = graph.horizon.contracts.GraphToken - staking = graph.horizon.contracts.HorizonStaking - - // Set GRT balances - await setGRTBalance(graph.provider, graphToken.target, fisherman.address, ethers.parseEther('100000')) - }) - - beforeEach(async () => { - // Take a snapshot before each test - snapshotId = await ethers.provider.send('evm_snapshot', []) - - // Legacy dispute manager - disputeDeposit = ethers.parseEther('10000') - - // Set arbitrator - await legacyDisputeManager.connect(governor).setArbitrator(arbitrator.address) - }) - - afterEach(async () => { - // Revert to the snapshot after each test - await ethers.provider.send('evm_revert', [snapshotId]) - }) - - describe('Indexing Disputes', () => { - let allocationId: string - - beforeEach(async () => { - // Get Indexer - const indexerFixture = indexers[0] - indexer = await ethers.getSigner(indexerFixture.address) - - // Get allocation - allocationId = indexerFixture.legacyAllocations[0].allocationID - }) - - it('should allow creating and accepting indexing disputes', async () => { - // Create an indexing dispute - await graphToken.connect(fisherman).approve(legacyDisputeManager.target, disputeDeposit) - await legacyDisputeManager.connect(fisherman).createIndexingDispute(allocationId, disputeDeposit) - const disputeId = generateLegacyIndexingDisputeId(allocationId) - - // Verify dispute was created - const disputeExists = await legacyDisputeManager.isDisputeCreated(disputeId) - expect(disputeExists).to.be.true - - // Get state before slashing - const idxSlashingPercentage = 25000n - const indexerStakeBefore = (await staking.getServiceProvider(indexer.address)).tokensStaked - const slashedAmount = (indexerStakeBefore * idxSlashingPercentage) / 1_000_000n - const fishermanBalanceBefore = await graphToken.balanceOf(fisherman.address) - - // Accept the dispute - await legacyDisputeManager.connect(arbitrator).acceptDispute(disputeId) - - // Verify indexer was slashed for 2.5% of their stake - const indexerStake = (await staking.getServiceProvider(indexer.address)).tokensStaked - expect(indexerStake).to.equal(indexerStakeBefore - slashedAmount, 'Indexer stake was not slashed correctly') - - // Verify fisherman received their deposit and 50% of the slashed amount - const fishermanBalance = await graphToken.balanceOf(fisherman.address) - expect(fishermanBalance).to.equal( - fishermanBalanceBefore + slashedAmount / 2n + disputeDeposit, - 'Fisherman balance was not updated correctly', - ) - }) - }) - - describe('Query Disputes', () => { - let allocationPrivateKey: string - let subgraphDeploymentId: string - - beforeEach(async () => { - // Get Indexer - const indexerFixture = indexers[0] - indexer = await ethers.getSigner(indexerFixture.address) - - // Get allocation - const allocation = indexerFixture.legacyAllocations[0] - allocationPrivateKey = allocation.allocationPrivateKey - subgraphDeploymentId = allocation.subgraphDeploymentID - }) - - it('should allow creating and accepting query disputes', async () => { - // Create attestation data - const queryHash = ethers.keccak256(ethers.toUtf8Bytes('test-query')) - const responseHash = ethers.keccak256(ethers.toUtf8Bytes('test-response')) - const attestationData = await generateAttestationData( - queryHash, - responseHash, - subgraphDeploymentId, - allocationPrivateKey, - await legacyDisputeManager.getAddress(), - chainId, - ) - - // Create a query dispute - await graphToken.connect(fisherman).approve(legacyDisputeManager.target, disputeDeposit) - await legacyDisputeManager.connect(fisherman).createQueryDispute(attestationData, disputeDeposit) - const disputeId = generateLegacyQueryDisputeId( - queryHash, - responseHash, - subgraphDeploymentId, - indexer.address, - fisherman.address, - ) - - // Verify dispute was created - const disputeExists = await legacyDisputeManager.isDisputeCreated(disputeId) - expect(disputeExists).to.be.true - - // Get state before slashing - const qrySlashingPercentage = 25000n - const indexerStakeBefore = (await staking.getServiceProvider(indexer.address)).tokensStaked - const slashedAmount = (indexerStakeBefore * qrySlashingPercentage) / 1_000_000n - const fishermanBalanceBefore = await graphToken.balanceOf(fisherman.address) - - // Accept the dispute - await legacyDisputeManager.connect(arbitrator).acceptDispute(disputeId) - - // Verify indexer was slashed for 2.5% of their stake - const indexerStake = (await staking.getServiceProvider(indexer.address)).tokensStaked - expect(indexerStake).to.equal(indexerStakeBefore - slashedAmount, 'Indexer stake was not slashed correctly') - - // Verify fisherman received their deposit and 50% of the slashed amount - const fishermanBalance = await graphToken.balanceOf(fisherman.address) - expect(fishermanBalance).to.equal( - fishermanBalanceBefore + slashedAmount / 2n + disputeDeposit, - 'Fisherman balance was not updated correctly', - ) - }) - }) - - describe('Query Dispute Conflict', () => { - let allocationPrivateKey: string - let subgraphDeploymentId: string - - beforeEach(async () => { - // Get Indexer - const indexerFixture = indexers[0] - indexer = await ethers.getSigner(indexerFixture.address) - - // Get allocation - const allocation = indexerFixture.legacyAllocations[0] - allocationPrivateKey = allocation.allocationPrivateKey - subgraphDeploymentId = allocation.subgraphDeploymentID - }) - - it('should allow creating conflicting query disputes', async () => { - // Create first attestation data - const queryHash = ethers.keccak256(ethers.toUtf8Bytes('test-query')) - const responseHash1 = ethers.keccak256(ethers.toUtf8Bytes('test-response-1')) - const attestationData1 = await generateAttestationData( - queryHash, - responseHash1, - subgraphDeploymentId, - allocationPrivateKey, - await legacyDisputeManager.getAddress(), - chainId, - ) - - // Create second attestation data with different query/response - const responseHash2 = ethers.keccak256(ethers.toUtf8Bytes('test-response-2')) - const attestationData2 = await generateAttestationData( - queryHash, - responseHash2, - subgraphDeploymentId, - allocationPrivateKey, - await legacyDisputeManager.getAddress(), - chainId, - ) - - // Create query dispute - await legacyDisputeManager.connect(fisherman).createQueryDisputeConflict(attestationData1, attestationData2) - - // Create dispute IDs - const disputeId1 = generateLegacyQueryDisputeId( - queryHash, - responseHash1, - subgraphDeploymentId, - indexer.address, - fisherman.address, - ) - const disputeId2 = generateLegacyQueryDisputeId( - queryHash, - responseHash2, - subgraphDeploymentId, - indexer.address, - fisherman.address, - ) - - // Verify both disputes were created - const disputeExists1 = await legacyDisputeManager.isDisputeCreated(disputeId1) - const disputeExists2 = await legacyDisputeManager.isDisputeCreated(disputeId2) - expect(disputeExists1).to.be.true - expect(disputeExists2).to.be.true - - // Get state before slashing - const qrySlashingPercentage = 25000n - const indexerStakeBefore = (await staking.getServiceProvider(indexer.address)).tokensStaked - const slashedAmount = (indexerStakeBefore * qrySlashingPercentage) / 1_000_000n - const fishermanBalanceBefore = await graphToken.balanceOf(fisherman.address) - - // Accept one dispute - await legacyDisputeManager.connect(arbitrator).acceptDispute(disputeId1) - - // Verify indexer was slashed for 2.5% of their stake - const indexerStake = (await staking.getServiceProvider(indexer.address)).tokensStaked - expect(indexerStake).to.equal(indexerStakeBefore - slashedAmount, 'Indexer stake was not slashed correctly') - - // Verify fisherman received 50% of the slashed amount - const fishermanBalance = await graphToken.balanceOf(fisherman.address) - expect(fishermanBalance).to.equal( - fishermanBalanceBefore + slashedAmount / 2n, - 'Fisherman balance was not updated correctly', - ) - }) - }) -}) diff --git a/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol b/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol index 0997d1aeb..574f120cb 100644 --- a/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol +++ b/packages/subgraph-service/test/unit/SubgraphBaseTest.t.sol @@ -8,7 +8,6 @@ import { GraphPayments } from "@graphprotocol/horizon/contracts/payments/GraphPa import { GraphProxy } from "@graphprotocol/contracts/contracts/upgrades/GraphProxy.sol"; import { GraphProxyAdmin } from "@graphprotocol/contracts/contracts/upgrades/GraphProxyAdmin.sol"; import { HorizonStaking } from "@graphprotocol/horizon/contracts/staking/HorizonStaking.sol"; -import { HorizonStakingExtension } from "@graphprotocol/horizon/contracts/staking/HorizonStakingExtension.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { IPaymentsEscrow } from "@graphprotocol/interfaces/contracts/horizon/IPaymentsEscrow.sol"; @@ -45,7 +44,6 @@ abstract contract SubgraphBaseTest is Utils, Constants { GraphTallyCollector graphTallyCollector; HorizonStaking private stakingBase; - HorizonStakingExtension private stakingExtension; MockCuration curation; MockGRTToken token; @@ -174,8 +172,7 @@ abstract contract SubgraphBaseTest is Utils, Constants { ); subgraphService = SubgraphService(subgraphServiceProxy); - stakingExtension = new HorizonStakingExtension(address(controller), address(subgraphService)); - stakingBase = new HorizonStaking(address(controller), address(stakingExtension), address(subgraphService)); + stakingBase = new HorizonStaking(address(controller), address(subgraphService)); graphPayments = new GraphPayments{ salt: saltGraphPayments }(address(controller), protocolPaymentCut); escrow = new PaymentsEscrow{ salt: saltEscrow }(address(controller), withdrawEscrowThawingPeriod); diff --git a/packages/subgraph-service/test/unit/disputeManager/DisputeManager.t.sol b/packages/subgraph-service/test/unit/disputeManager/DisputeManager.t.sol index e6115057e..d49d2b6a7 100644 --- a/packages/subgraph-service/test/unit/disputeManager/DisputeManager.t.sol +++ b/packages/subgraph-service/test/unit/disputeManager/DisputeManager.t.sol @@ -205,81 +205,6 @@ contract DisputeManagerTest is SubgraphServiceSharedTest { return _disputeID; } - struct Balances { - uint256 indexer; - uint256 fisherman; - uint256 arbitrator; - uint256 disputeManager; - uint256 staking; - } - - function _createAndAcceptLegacyDispute( - address _allocationId, - address _fisherman, - uint256 _tokensSlash, - uint256 _tokensRewards - ) internal returns (bytes32) { - (, address arbitrator, ) = vm.readCallers(); - address indexer = staking.getAllocation(_allocationId).indexer; - - Balances memory beforeBalances = Balances({ - indexer: token.balanceOf(indexer), - fisherman: token.balanceOf(_fisherman), - arbitrator: token.balanceOf(arbitrator), - disputeManager: token.balanceOf(address(disputeManager)), - staking: token.balanceOf(address(staking)) - }); - - vm.expectEmit(address(disputeManager)); - emit IDisputeManager.LegacyDisputeCreated( - keccak256(abi.encodePacked(_allocationId, "legacy")), - indexer, - _fisherman, - _allocationId, - _tokensSlash, - _tokensRewards - ); - vm.expectEmit(address(disputeManager)); - emit IDisputeManager.DisputeAccepted( - keccak256(abi.encodePacked(_allocationId, "legacy")), - indexer, - _fisherman, - _tokensRewards - ); - bytes32 _disputeId = disputeManager.createAndAcceptLegacyDispute( - _allocationId, - _fisherman, - _tokensSlash, - _tokensRewards - ); - - Balances memory afterBalances = Balances({ - indexer: token.balanceOf(indexer), - fisherman: token.balanceOf(_fisherman), - arbitrator: token.balanceOf(arbitrator), - disputeManager: token.balanceOf(address(disputeManager)), - staking: token.balanceOf(address(staking)) - }); - - assertEq(afterBalances.indexer, beforeBalances.indexer); - assertEq(afterBalances.fisherman, beforeBalances.fisherman + _tokensRewards); - assertEq(afterBalances.arbitrator, beforeBalances.arbitrator); - assertEq(afterBalances.disputeManager, beforeBalances.disputeManager); - assertEq(afterBalances.staking, beforeBalances.staking - _tokensSlash); - - IDisputeManager.Dispute memory dispute = _getDispute(_disputeId); - assertEq(dispute.indexer, indexer); - assertEq(dispute.fisherman, _fisherman); - assertEq(dispute.deposit, 0); - assertEq(dispute.relatedDisputeId, bytes32(0)); - assertEq(uint8(dispute.disputeType), uint8(IDisputeManager.DisputeType.LegacyDispute)); - assertEq(uint8(dispute.status), uint8(IDisputeManager.DisputeStatus.Accepted)); - assertEq(dispute.createdAt, block.timestamp); - assertEq(dispute.stakeSnapshot, 0); - - return _disputeId; - } - struct BeforeValues_CreateQueryDisputeConflict { IAttestation.State attestation1; IAttestation.State attestation2; diff --git a/packages/subgraph-service/test/unit/disputeManager/disputes/legacy.t.sol b/packages/subgraph-service/test/unit/disputeManager/disputes/legacy.t.sol deleted file mode 100644 index 3bab2eaa6..000000000 --- a/packages/subgraph-service/test/unit/disputeManager/disputes/legacy.t.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "forge-std/Test.sol"; - -import { Attestation } from "../../../../contracts/libraries/Attestation.sol"; -import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol"; -import { IDisputeManager } from "@graphprotocol/interfaces/contracts/subgraph-service/IDisputeManager.sol"; -import { DisputeManagerTest } from "../DisputeManager.t.sol"; - -contract DisputeManagerLegacyDisputeTest is DisputeManagerTest { - using PPMMath for uint256; - - bytes32 private requestCID = keccak256(abi.encodePacked("Request CID")); - bytes32 private responseCID = keccak256(abi.encodePacked("Response CID")); - bytes32 private subgraphDeploymentId = keccak256(abi.encodePacked("Subgraph Deployment ID")); - - /* - * TESTS - */ - - function test_LegacyDispute( - uint256 tokensStaked, - uint256 tokensProvisioned, - uint256 tokensSlash, - uint256 tokensRewards - ) public { - vm.assume(tokensStaked <= MAX_TOKENS); - vm.assume(tokensStaked >= minimumProvisionTokens); - tokensProvisioned = bound(tokensProvisioned, minimumProvisionTokens, tokensStaked); - tokensSlash = bound(tokensSlash, 2, tokensProvisioned); - tokensRewards = bound(tokensRewards, 1, tokensSlash.mulPPM(fishermanRewardPercentage)); - - // setup indexer state - resetPrank(users.indexer); - _stake(tokensStaked); - _setStorage_allocation_hardcoded(users.indexer, allocationID, tokensStaked - tokensProvisioned); - _provision(users.indexer, tokensProvisioned, fishermanRewardPercentage, disputePeriod); - - resetPrank(users.arbitrator); - _createAndAcceptLegacyDispute(allocationID, users.fisherman, tokensSlash, tokensRewards); - } - - function test_LegacyDispute_RevertIf_NotArbitrator() public useIndexer { - vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerNotArbitrator.selector)); - disputeManager.createAndAcceptLegacyDispute(allocationID, users.fisherman, 0, 0); - } - - function test_LegacyDispute_RevertIf_AllocationNotFound() public useIndexer { - resetPrank(users.arbitrator); - vm.expectRevert(abi.encodeWithSelector(IDisputeManager.DisputeManagerIndexerNotFound.selector, address(0))); - disputeManager.createAndAcceptLegacyDispute(address(0), users.fisherman, 0, 0); - } -} diff --git a/packages/subgraph-service/test/unit/shared/HorizonStakingShared.t.sol b/packages/subgraph-service/test/unit/shared/HorizonStakingShared.t.sol index 290644bea..66b67a408 100644 --- a/packages/subgraph-service/test/unit/shared/HorizonStakingShared.t.sol +++ b/packages/subgraph-service/test/unit/shared/HorizonStakingShared.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; -import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; import { SubgraphBaseTest } from "../SubgraphBaseTest.t.sol"; @@ -77,67 +76,6 @@ abstract contract HorizonStakingSharedTest is SubgraphBaseTest { staking.setProvisionParameters(_indexer, _verifier, _maxVerifierCut, _thawingPeriod); } - function _setStorage_allocation_hardcoded(address indexer, address allocationId, uint256 tokens) internal { - IHorizonStakingExtension.Allocation memory allocation = IHorizonStakingExtension.Allocation({ - indexer: indexer, - subgraphDeploymentID: bytes32("0x12344321"), - tokens: tokens, - createdAtEpoch: 1234, - closedAtEpoch: 1235, - collectedFees: 1234, - __DEPRECATED_effectiveAllocation: 1222234, - accRewardsPerAllocatedToken: 1233334, - distributedRebates: 1244434 - }); - - // __DEPRECATED_allocations - uint256 allocationsSlot = 15; - bytes32 allocationBaseSlot = keccak256(abi.encode(allocationId, allocationsSlot)); - vm.store(address(staking), allocationBaseSlot, bytes32(uint256(uint160(allocation.indexer)))); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 1), allocation.subgraphDeploymentID); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 2), bytes32(tokens)); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 3), bytes32(allocation.createdAtEpoch)); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 4), bytes32(allocation.closedAtEpoch)); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 5), bytes32(allocation.collectedFees)); - vm.store( - address(staking), - bytes32(uint256(allocationBaseSlot) + 6), - bytes32(allocation.__DEPRECATED_effectiveAllocation) - ); - vm.store( - address(staking), - bytes32(uint256(allocationBaseSlot) + 7), - bytes32(allocation.accRewardsPerAllocatedToken) - ); - vm.store(address(staking), bytes32(uint256(allocationBaseSlot) + 8), bytes32(allocation.distributedRebates)); - - // _serviceProviders - uint256 serviceProviderSlot = 14; - bytes32 serviceProviderBaseSlot = keccak256(abi.encode(allocation.indexer, serviceProviderSlot)); - uint256 currentTokensStaked = uint256(vm.load(address(staking), serviceProviderBaseSlot)); - uint256 currentTokensProvisioned = uint256( - vm.load(address(staking), bytes32(uint256(serviceProviderBaseSlot) + 1)) - ); - vm.store( - address(staking), - bytes32(uint256(serviceProviderBaseSlot) + 0), - bytes32(currentTokensStaked + tokens) - ); - vm.store( - address(staking), - bytes32(uint256(serviceProviderBaseSlot) + 1), - bytes32(currentTokensProvisioned + tokens) - ); - - // __DEPRECATED_subgraphAllocations - uint256 subgraphsAllocationsSlot = 16; - bytes32 subgraphAllocationsBaseSlot = keccak256( - abi.encode(allocation.subgraphDeploymentID, subgraphsAllocationsSlot) - ); - uint256 currentAllocatedTokens = uint256(vm.load(address(staking), subgraphAllocationsBaseSlot)); - vm.store(address(staking), subgraphAllocationsBaseSlot, bytes32(currentAllocatedTokens + tokens)); - } - function _stakeTo(address _indexer, uint256 _tokens) internal { token.approve(address(staking), _tokens); staking.stakeTo(_indexer, _tokens); diff --git a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol index 74c677504..2fac2595a 100644 --- a/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol @@ -383,7 +383,7 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { CollectPaymentData memory collectPaymentDataBefore, CollectPaymentData memory collectPaymentDataAfter ) private view { - (IGraphTallyCollector.SignedRAV memory signedRav, uint256 tokensToCollect) = abi.decode( + (IGraphTallyCollector.SignedRAV memory signedRav, ) = abi.decode( _data, (IGraphTallyCollector.SignedRAV, uint256) ); @@ -487,10 +487,16 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest { } function _migrateLegacyAllocation(address _indexer, address _allocationId, bytes32 _subgraphDeploymentID) internal { - vm.expectEmit(address(subgraphService)); - emit AllocationManager.LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentID); + // migrate fn was removed, we simulate history by manually setting the storage state + uint256 legacyAllocationsSlot = 208; + bytes32 legacyAllocationBaseSlot = keccak256(abi.encode(_allocationId, legacyAllocationsSlot)); - subgraphService.migrateLegacyAllocation(_indexer, _allocationId, _subgraphDeploymentID); + vm.store(address(subgraphService), legacyAllocationBaseSlot, bytes32(uint256(uint160(_indexer)))); + vm.store( + address(subgraphService), + bytes32(uint256(legacyAllocationBaseSlot) + 1), + bytes32(_subgraphDeploymentID) + ); ILegacyAllocation.State memory afterLegacyAllocation = subgraphService.getLegacyAllocation(_allocationId); assertEq(afterLegacyAllocation.indexer, _indexer); diff --git a/packages/subgraph-service/test/unit/subgraphService/allocation/start.t.sol b/packages/subgraph-service/test/unit/subgraphService/allocation/start.t.sol index 4a251f506..c44f5364a 100644 --- a/packages/subgraph-service/test/unit/subgraphService/allocation/start.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/allocation/start.t.sol @@ -170,8 +170,8 @@ contract SubgraphServiceAllocationStartTest is SubgraphServiceTest { _createProvision(users.indexer, tokens, fishermanRewardPercentage, disputePeriod); _register(users.indexer, abi.encode("url", "geoHash", address(0))); - // create dummy allo in staking contract - _setStorage_allocation_hardcoded(users.indexer, allocationID, tokens); + // simulate legacy allocation migration + _migrateLegacyAllocation(users.indexer, allocationID, subgraphDeployment); bytes memory data = _generateData(tokens); vm.expectRevert(abi.encodeWithSelector(ILegacyAllocation.LegacyAllocationAlreadyExists.selector, allocationID)); diff --git a/packages/subgraph-service/test/unit/subgraphService/governance/legacy.t.sol b/packages/subgraph-service/test/unit/subgraphService/governance/legacy.t.sol deleted file mode 100644 index d1b5dd124..000000000 --- a/packages/subgraph-service/test/unit/subgraphService/governance/legacy.t.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import "forge-std/Test.sol"; - -import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -import { SubgraphServiceTest } from "../SubgraphService.t.sol"; - -contract SubgraphServiceLegacyAllocation is SubgraphServiceTest { - /* - * TESTS - */ - - function test_MigrateAllocation() public useGovernor { - _migrateLegacyAllocation(users.indexer, allocationID, subgraphDeployment); - } - - function test_MigrateAllocation_WhenNotGovernor() public useIndexer { - vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.indexer)); - subgraphService.migrateLegacyAllocation(users.indexer, allocationID, subgraphDeployment); - } -} diff --git a/packages/toolshed/src/deployments/horizon/actions.ts b/packages/toolshed/src/deployments/horizon/actions.ts index 8fc9bd4df..144342a82 100644 --- a/packages/toolshed/src/deployments/horizon/actions.ts +++ b/packages/toolshed/src/deployments/horizon/actions.ts @@ -62,15 +62,6 @@ export function loadActions(contracts: GraphHorizonContracts) { */ provision: (signer: HardhatEthersSigner, args: Parameters) => provision(contracts, signer, args), - /** - * [Legacy] Collects query fees from the Horizon staking contract - * Note that it will approve HorizonStaking to spend the tokens - * @param signer - The signer that will execute the collect transaction - * @param args Parameters: - * - `[tokens, allocationID]` - The collect parameters - */ - collect: (signer: HardhatEthersSigner, args: Parameters) => - collect(contracts, signer, args), /** * Delegates tokens in the Horizon staking contract * Note that it will approve HorizonStaking to spend the tokens @@ -157,18 +148,6 @@ async function provision( await HorizonStaking.connect(signer).provision(serviceProvider, verifier, tokens, maxVerifierCut, thawingPeriod) } -async function collect( - contracts: GraphHorizonContracts, - signer: HardhatEthersSigner, - args: Parameters, -) { - const { GraphToken, HorizonStaking } = contracts - const [tokens, allocationID] = args - - await GraphToken.connect(signer).approve(HorizonStaking.target, tokens) - await HorizonStaking.connect(signer).collect(tokens, allocationID) -} - async function delegate( contracts: GraphHorizonContracts, signer: HardhatEthersSigner,