From 0f71288102508fe2eec830791acf9889aad42759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 19 Dec 2025 11:51:01 -0300 Subject: [PATCH 1/5] fix: re-validate thawingPeriod when accepting provision parameters (OZ L-01) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .../contracts/staking/HorizonStaking.sol | 5 +++ .../unit/staking/provision/parameters.t.sol | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index 57ef2453f..9b8f5b817 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -216,6 +216,11 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { require(prov.createdAt != 0, HorizonStakingInvalidProvision(serviceProvider, verifier)); if ((prov.maxVerifierCutPending != prov.maxVerifierCut) || (prov.thawingPeriodPending != prov.thawingPeriod)) { + // Re-validate thawing period in case governor reduced _maxThawingPeriod after staging + require( + prov.thawingPeriodPending <= _maxThawingPeriod, + HorizonStakingInvalidThawingPeriod(prov.thawingPeriodPending, _maxThawingPeriod) + ); prov.maxVerifierCut = prov.maxVerifierCutPending; prov.thawingPeriod = prov.thawingPeriodPending; emit ProvisionParametersSet(serviceProvider, verifier, prov.maxVerifierCut, prov.thawingPeriod); diff --git a/packages/horizon/test/unit/staking/provision/parameters.t.sol b/packages/horizon/test/unit/staking/provision/parameters.t.sol index f7c74f508..f9ab3202a 100644 --- a/packages/horizon/test/unit/staking/provision/parameters.t.sol +++ b/packages/horizon/test/unit/staking/provision/parameters.t.sol @@ -177,4 +177,36 @@ contract HorizonStakingProvisionParametersTest is HorizonStakingTest { ); staking.acceptProvisionParameters(users.indexer); } + + function test_ProvisionParametersAccept_RevertWhen_MaxThawingPeriodReduced( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useValidParameters(maxVerifierCut, thawingPeriod) { + vm.assume(amount > 0); + vm.assume(amount <= MAX_STAKING_TOKENS); + vm.assume(thawingPeriod > 0); + + // Create provision with initial parameters (thawingPeriod = 0) + _createProvision(users.indexer, subgraphDataServiceAddress, amount, 0, 0); + + // Stage new parameters with valid thawing period + _setProvisionParameters(users.indexer, subgraphDataServiceAddress, maxVerifierCut, thawingPeriod); + + // Governor reduces max thawing period to below the staged value + uint64 newMaxThawingPeriod = thawingPeriod - 1; + resetPrank(users.governor); + _setMaxThawingPeriod(newMaxThawingPeriod); + + // Verifier tries to accept the parameters - should revert + resetPrank(subgraphDataServiceAddress); + vm.expectRevert( + abi.encodeWithSelector( + IHorizonStakingMain.HorizonStakingInvalidThawingPeriod.selector, + thawingPeriod, + newMaxThawingPeriod + ) + ); + staking.acceptProvisionParameters(users.indexer); + } } From a91f59ad479aa470b6f892ec1dab7012464087a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 19 Dec 2025 11:59:35 -0300 Subject: [PATCH 2/5] fix: return correct result for getThawedTokens when called for delegations (OZ L-02) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .../contracts/staking/HorizonStakingBase.sol | 20 +++++-- .../unit/staking/delegation/withdraw.t.sol | 52 +++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/horizon/contracts/staking/HorizonStakingBase.sol b/packages/horizon/contracts/staking/HorizonStakingBase.sol index 75e2cd37c..ab637d5da 100644 --- a/packages/horizon/contracts/staking/HorizonStakingBase.sol +++ b/packages/horizon/contracts/staking/HorizonStakingBase.sol @@ -182,14 +182,26 @@ abstract contract HorizonStakingBase is } uint256 thawedTokens = 0; - Provision storage prov = _provisions[serviceProvider][verifier]; - uint256 tokensThawing = prov.tokensThawing; - uint256 sharesThawing = prov.sharesThawing; + uint256 tokensThawing; + uint256 sharesThawing; + uint256 thawingNonce; + + if (requestType == ThawRequestType.Provision) { + Provision storage prov = _provisions[serviceProvider][verifier]; + tokensThawing = prov.tokensThawing; + sharesThawing = prov.sharesThawing; + thawingNonce = prov.thawingNonce; + } else { + DelegationPoolInternal storage pool = _getDelegationPool(serviceProvider, verifier); + tokensThawing = pool.tokensThawing; + sharesThawing = pool.sharesThawing; + thawingNonce = pool.thawingNonce; + } bytes32 thawRequestId = thawRequestList.head; while (thawRequestId != bytes32(0)) { ThawRequest storage thawRequest = _getThawRequest(requestType, thawRequestId); - if (thawRequest.thawingNonce == prov.thawingNonce) { + if (thawRequest.thawingNonce == thawingNonce) { if (thawRequest.thawingUntil <= block.timestamp) { // sharesThawing cannot be zero if there is a valid thaw request so the next division is safe uint256 tokens = (thawRequest.shares * tokensThawing) / sharesThawing; diff --git a/packages/horizon/test/unit/staking/delegation/withdraw.t.sol b/packages/horizon/test/unit/staking/delegation/withdraw.t.sol index 948961591..faeb4d78b 100644 --- a/packages/horizon/test/unit/staking/delegation/withdraw.t.sol +++ b/packages/horizon/test/unit/staking/delegation/withdraw.t.sol @@ -162,4 +162,56 @@ contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { resetPrank(users.delegator); _withdrawDelegated(users.indexer, subgraphDataServiceAddress, 0); } + + function testWithdrawDelegation_GetThawedTokens( + uint256 delegationAmount, + uint256 withdrawShares + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(withdrawShares) + { + ILinkedList.List memory thawingRequests = staking.getThawRequestList( + IHorizonStakingTypes.ThawRequestType.Delegation, + users.indexer, + subgraphDataServiceAddress, + users.delegator + ); + ThawRequest memory thawRequest = staking.getThawRequest( + IHorizonStakingTypes.ThawRequestType.Delegation, + thawingRequests.tail + ); + + // Before thawing period passes, thawed tokens should be 0 + uint256 thawedTokensBefore = staking.getThawedTokens( + IHorizonStakingTypes.ThawRequestType.Delegation, + users.indexer, + subgraphDataServiceAddress, + users.delegator + ); + assertEq(thawedTokensBefore, 0); + + // Skip past thawing period + skip(thawRequest.thawingUntil + 1); + + // After thawing period, thawed tokens should match expected amount + uint256 thawedTokensAfter = staking.getThawedTokens( + IHorizonStakingTypes.ThawRequestType.Delegation, + users.indexer, + subgraphDataServiceAddress, + users.delegator + ); + + // Thawed tokens should be greater than 0 and should match what we can withdraw + assertGt(thawedTokensAfter, 0); + + // Withdraw and verify the amount matches + uint256 balanceBefore = token.balanceOf(users.delegator); + _withdrawDelegated(users.indexer, subgraphDataServiceAddress, 0); + uint256 balanceAfter = token.balanceOf(users.delegator); + + assertEq(balanceAfter - balanceBefore, thawedTokensAfter); + } } From ed356fe40546c2a9fcab1072d815767d657148bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 19 Dec 2025 12:10:15 -0300 Subject: [PATCH 3/5] fix: remove more deprecated code (OZ N-01) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .../contracts/l2/curation/L2Curation.sol | 7 ++----- .../test/tests/unit/l2/l2Curation.test.ts | 2 +- .../contracts/staking/HorizonStaking.sol | 5 +---- .../delegation/forceWithdrawDelegated.t.sol | 18 ------------------ 4 files changed, 4 insertions(+), 28 deletions(-) diff --git a/packages/contracts/contracts/l2/curation/L2Curation.sol b/packages/contracts/contracts/l2/curation/L2Curation.sol index 56e83c13a..fd26bd2ac 100644 --- a/packages/contracts/contracts/l2/curation/L2Curation.sol +++ b/packages/contracts/contracts/l2/curation/L2Curation.sol @@ -171,11 +171,8 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { * @param _tokens Amount of Graph Tokens to add to reserves */ function collect(bytes32 _subgraphDeploymentID, uint256 _tokens) external override { - // Only SubgraphService and Staking contract are authorized as callers - require( - msg.sender == subgraphService || msg.sender == address(staking()), - "Caller must be the subgraph service or staking contract" - ); + // Only SubgraphService is authorized as caller + require(msg.sender == subgraphService, "Caller must be the subgraph service"); // Must be curated to accept tokens require(isCurated(_subgraphDeploymentID), "Subgraph deployment must be curated to collect fees"); diff --git a/packages/contracts/test/tests/unit/l2/l2Curation.test.ts b/packages/contracts/test/tests/unit/l2/l2Curation.test.ts index 6ee8a5cd3..77024b4b0 100644 --- a/packages/contracts/test/tests/unit/l2/l2Curation.test.ts +++ b/packages/contracts/test/tests/unit/l2/l2Curation.test.ts @@ -529,7 +529,7 @@ describe('L2Curation', () => { it('reject collect tokens distributed from invalid address', async function () { const tx = curation.connect(me).collect(subgraphDeploymentID, tokensToCollect) - await expect(tx).revertedWith('Caller must be the subgraph service or staking contract') + await expect(tx).revertedWith('Caller must be the subgraph service') }) it('should collect tokens distributed to the curation pool', async function () { diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index 9b8f5b817..6d1de6226 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -1128,10 +1128,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { // Validation uint256 tokensToWithdraw = 0; - uint256 currentEpoch = _graphEpochManager().currentEpoch(); - if ( - delegation.__DEPRECATED_tokensLockedUntil > 0 && currentEpoch >= delegation.__DEPRECATED_tokensLockedUntil - ) { + if (delegation.__DEPRECATED_tokensLockedUntil > 0) { tokensToWithdraw = delegation.__DEPRECATED_tokensLocked; } require(tokensToWithdraw > 0, HorizonStakingNothingToWithdraw()); diff --git a/packages/horizon/test/unit/staking/delegation/forceWithdrawDelegated.t.sol b/packages/horizon/test/unit/staking/delegation/forceWithdrawDelegated.t.sol index d60afe029..af823a8d3 100644 --- a/packages/horizon/test/unit/staking/delegation/forceWithdrawDelegated.t.sol +++ b/packages/horizon/test/unit/staking/delegation/forceWithdrawDelegated.t.sol @@ -113,22 +113,4 @@ contract HorizonStakingForceWithdrawDelegatedTest is HorizonStakingTest { 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); - } } From 1c3e30607f1775886fcfa1f618a2c9b74786f231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 19 Dec 2025 12:14:42 -0300 Subject: [PATCH 4/5] fix: outdated documentation (OZ N-02) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- packages/horizon/contracts/staking/HorizonStaking.sol | 4 ++-- .../contracts/contracts/rewards/IRewardsManager.sol | 2 +- .../contracts/horizon/internal/IHorizonStakingMain.sol | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index 6d1de6226..8998728e5 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -842,8 +842,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * @dev The parameter `nThawRequests` can be set to a non zero value to fulfill a specific number of thaw * requests in the event that fulfilling all of them results in a gas limit error. Otherwise, the function * will attempt to fulfill all thaw requests until the first one that is not yet expired is found. - * @dev If the delegation pool was completely slashed before withdrawing, calling this function will fulfill - * the thaw requests with an amount equal to zero. + * @dev If the delegation pool was completely slashed before withdrawing, calling this function will revert + * until the pool state is repaired with {IHorizonStakingMain-addToDelegationPool}. * @param _serviceProvider The service provider address * @param _verifier The verifier address * @param _newServiceProvider The new service provider address diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index 72a73e19b..33daf71fe 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -122,7 +122,7 @@ interface IRewardsManager { /** * @notice Pull rewards from the contract for a particular allocation - * @dev This function can only be called by the Staking contract. + * @dev This function can only be called by the Subgraph Service contract. * This function will mint the necessary tokens to reward based on the inflation calculation. * @param allocationID Allocation * @return Assigned rewards amount diff --git a/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol b/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol index b5763a8f2..ddc595409 100644 --- a/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol +++ b/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol @@ -783,7 +783,7 @@ interface IHorizonStakingMain { * - `newServiceProvider` and `newVerifier` must not be the zero address. * - `newServiceProvider` must have previously provisioned stake to `newVerifier`. * - * Emits {ThawRequestFulfilled}, {ThawRequestsFulfilled} and {DelegatedTokensWithdrawn} events. + * Emits {ThawRequestFulfilled} and {ThawRequestsFulfilled} events. * * @param oldServiceProvider The old service provider address * @param oldVerifier The old verifier address From d9f053a7d96a8a4d81415303ae1d537f836f887c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 19 Dec 2025 12:39:26 -0300 Subject: [PATCH 5/5] test: fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .../test/tests/unit/l2/l2Curation.test.ts | 25 +++-- .../test/tests/unit/l2/l2GNS.test.ts | 106 +----------------- 2 files changed, 15 insertions(+), 116 deletions(-) diff --git a/packages/contracts/test/tests/unit/l2/l2Curation.test.ts b/packages/contracts/test/tests/unit/l2/l2Curation.test.ts index 77024b4b0..a680ec28c 100644 --- a/packages/contracts/test/tests/unit/l2/l2Curation.test.ts +++ b/packages/contracts/test/tests/unit/l2/l2Curation.test.ts @@ -154,7 +154,7 @@ describe('L2Curation', () => { let me: SignerWithAddress let governor: SignerWithAddress let curator: SignerWithAddress - let stakingMock: SignerWithAddress + let subgraphServiceMock: SignerWithAddress let gnsImpersonator: Signer let fixture: NetworkFixture @@ -310,8 +310,8 @@ describe('L2Curation', () => { const beforeTotalBalance = await grt.balanceOf(curation.address) // Source of tokens must be the staking for this to work - await grt.connect(stakingMock).transfer(curation.address, tokensToCollect) - const tx = curation.connect(stakingMock).collect(subgraphDeploymentID, tokensToCollect) + await grt.connect(subgraphServiceMock).transfer(curation.address, tokensToCollect) + const tx = curation.connect(subgraphServiceMock).collect(subgraphDeploymentID, tokensToCollect) await expect(tx).emit(curation, 'Collected').withArgs(subgraphDeploymentID, tokensToCollect) // After state @@ -325,7 +325,7 @@ describe('L2Curation', () => { before(async function () { // Use stakingMock so we can call collect - ;[me, curator, stakingMock] = await graph.getTestAccounts() + ;[me, curator, subgraphServiceMock] = await graph.getTestAccounts() ;({ governor } = await graph.getNamedAccounts()) fixture = new NetworkFixture(graph.provider) contracts = await fixture.load(governor, true) @@ -343,8 +343,11 @@ describe('L2Curation', () => { await grt.connect(gnsImpersonator).approve(curation.address, curatorTokens) // Give some funds to the staking contract and approve the curation contract - await grt.connect(governor).mint(stakingMock.address, tokensToCollect) - await grt.connect(stakingMock).approve(curation.address, tokensToCollect) + await grt.connect(governor).mint(subgraphServiceMock.address, tokensToCollect) + await grt.connect(subgraphServiceMock).approve(curation.address, tokensToCollect) + + // Set the subgraph service + await curation.connect(governor).setSubgraphService(subgraphServiceMock.address) }) beforeEach(async function () { @@ -514,10 +517,10 @@ describe('L2Curation', () => { context('> not curated', function () { it('reject collect tokens distributed to the curation pool', async function () { // Source of tokens must be the staking for this to work - await controller.connect(governor).setContractProxy(utils.id('Staking'), stakingMock.address) + await controller.connect(governor).setContractProxy(utils.id('Staking'), subgraphServiceMock.address) await curation.connect(governor).syncAllContracts() // call sync because we change the proxy for staking - const tx = curation.connect(stakingMock).collect(subgraphDeploymentID, tokensToCollect) + const tx = curation.connect(subgraphServiceMock).collect(subgraphDeploymentID, tokensToCollect) await expect(tx).revertedWith('Subgraph deployment must be curated to collect fees') }) }) @@ -533,7 +536,7 @@ describe('L2Curation', () => { }) it('should collect tokens distributed to the curation pool', async function () { - await controller.connect(governor).setContractProxy(utils.id('Staking'), stakingMock.address) + await controller.connect(governor).setContractProxy(utils.id('Staking'), subgraphServiceMock.address) await curation.connect(governor).syncAllContracts() // call sync because we change the proxy for staking await shouldCollect(toGRT('1')) @@ -544,7 +547,7 @@ describe('L2Curation', () => { }) it('should collect tokens and then unsignal all', async function () { - await controller.connect(governor).setContractProxy(utils.id('Staking'), stakingMock.address) + await controller.connect(governor).setContractProxy(utils.id('Staking'), subgraphServiceMock.address) await curation.connect(governor).syncAllContracts() // call sync because we change the proxy for staking // Collect increase the pool reserves @@ -556,7 +559,7 @@ describe('L2Curation', () => { }) it('should collect tokens and then unsignal multiple times', async function () { - await controller.connect(governor).setContractProxy(utils.id('Staking'), stakingMock.address) + await controller.connect(governor).setContractProxy(utils.id('Staking'), subgraphServiceMock.address) await curation.connect(governor).syncAllContracts() // call sync because we change the proxy for staking // Collect increase the pool reserves diff --git a/packages/contracts/test/tests/unit/l2/l2GNS.test.ts b/packages/contracts/test/tests/unit/l2/l2GNS.test.ts index 5b8f1d028..0fd691939 100644 --- a/packages/contracts/test/tests/unit/l2/l2GNS.test.ts +++ b/packages/contracts/test/tests/unit/l2/l2GNS.test.ts @@ -2,12 +2,10 @@ import { L2GNS } from '@graphprotocol/contracts' import { L2GraphTokenGateway } from '@graphprotocol/contracts' import { L2Curation } from '@graphprotocol/contracts' import { GraphToken } from '@graphprotocol/contracts' -import { IL2Staking } from '@graphprotocol/contracts' import { L1GNS, L1GraphTokenGateway } from '@graphprotocol/contracts' import { buildSubgraph, buildSubgraphId, - deriveChannelKey, GraphNetworkContracts, helpers, PublishSubgraph, @@ -44,7 +42,6 @@ interface L1SubgraphParams { describe('L2GNS', () => { const graph = hre.graph() let me: SignerWithAddress - let attacker: SignerWithAddress let other: SignerWithAddress let governor: SignerWithAddress let fixture: NetworkFixture @@ -58,7 +55,6 @@ describe('L2GNS', () => { let gns: L2GNS let curation: L2Curation let grt: GraphToken - let staking: IL2Staking let newSubgraph0: PublishSubgraph let newSubgraph1: PublishSubgraph @@ -109,7 +105,7 @@ describe('L2GNS', () => { before(async function () { newSubgraph0 = buildSubgraph() - ;[me, attacker, other] = await graph.getTestAccounts() + ;[me, other] = await graph.getTestAccounts() ;({ governor } = await graph.getNamedAccounts()) fixture = new NetworkFixture(graph.provider) @@ -118,7 +114,6 @@ describe('L2GNS', () => { fixtureContracts = await fixture.load(governor, true) l2GraphTokenGateway = fixtureContracts.L2GraphTokenGateway as L2GraphTokenGateway gns = fixtureContracts.L2GNS as L2GNS - staking = fixtureContracts.L2Staking as unknown as IL2Staking curation = fixtureContracts.L2Curation as L2Curation grt = fixtureContracts.GraphToken as GraphToken @@ -354,61 +349,6 @@ describe('L2GNS', () => { .emit(gns, 'SignalMinted') .withArgs(l2SubgraphId, me.address, expectedNSignal, expectedSignal, curatedTokens) }) - it('protects the owner against a rounding attack', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - const collectTokens = curatedTokens.mul(20) - - await staking.connect(governor).setCurationPercentage(100000) - - // Set up an indexer account with some stake - await grt.connect(governor).mint(attacker.address, toGRT('1000000')) - // Curate 1 wei GRT by minting 1 GRT and burning most of it - await grt.connect(attacker).approve(curation.address, toBN(1)) - await curation.connect(attacker).mint(newSubgraph0.subgraphDeploymentID, toBN(1), 0) - - // Check this actually gave us 1 wei signal - expect(await curation.getCurationPoolTokens(newSubgraph0.subgraphDeploymentID)).eq(1) - await grt.connect(attacker).approve(staking.address, toGRT('1000000')) - await staking.connect(attacker).stake(toGRT('100000')) - const channelKey = deriveChannelKey() - // Allocate to the same deployment ID - await staking - .connect(attacker) - .allocateFrom( - attacker.address, - newSubgraph0.subgraphDeploymentID, - toGRT('100000'), - channelKey.address, - randomHexBytes(32), - await channelKey.generateProof(attacker.address), - ) - // Spoof some query fees, 10% of which will go to the Curation pool - await staking.connect(attacker).collect(collectTokens, channelKey.address) - // The curation pool now has 1 wei shares and a lot of tokens, so the rounding attack is prepared - // But L2GNS will protect the owner by sending the tokens - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) - await gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatedTokens, callhookData) - - const l2SubgraphId = await gns.getAliasedL2SubgraphID(l1SubgraphId) - const tx = gns - .connect(me) - .finishSubgraphTransferFromL1( - l2SubgraphId, - newSubgraph0.subgraphDeploymentID, - subgraphMetadata, - versionMetadata, - ) - await expect(tx) - .emit(gns, 'SubgraphPublished') - .withArgs(l2SubgraphId, newSubgraph0.subgraphDeploymentID, DEFAULT_RESERVE_RATIO) - await expect(tx).emit(gns, 'SubgraphMetadataUpdated').withArgs(l2SubgraphId, subgraphMetadata) - await expect(tx).emit(gns, 'CuratorBalanceReturnedToBeneficiary') - await expect(tx).emit(gns, 'SubgraphUpgraded').withArgs(l2SubgraphId, 0, 0, newSubgraph0.subgraphDeploymentID) - await expect(tx) - .emit(gns, 'SubgraphVersionUpdated') - .withArgs(l2SubgraphId, newSubgraph0.subgraphDeploymentID, versionMetadata) - await expect(tx).emit(gns, 'SubgraphL2TransferFinalized').withArgs(l2SubgraphId) - }) it('cannot be called by someone other than the subgraph owner', async function () { const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(0), l1SubgraphId, me.address]) @@ -654,50 +594,6 @@ describe('L2GNS', () => { expect(gnsBalanceAfter).eq(gnsBalanceBefore) }) - it('protects the curator against a rounding attack', async function () { - // Transfer a subgraph from L1 with only 1 wei GRT of curated signal - const { l1SubgraphId, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() - const curatedTokens = toBN('1') - await transferMockSubgraphFromL1(l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata) - // Prepare the rounding attack by setting up an indexer and collecting a lot of query fees - const curatorTokens = toGRT('10000') - const collectTokens = curatorTokens.mul(20) - await staking.connect(governor).setCurationPercentage(100000) - // Set up an indexer account with some stake - await grt.connect(governor).mint(attacker.address, toGRT('1000000')) - - await grt.connect(attacker).approve(staking.address, toGRT('1000000')) - await staking.connect(attacker).stake(toGRT('100000')) - const channelKey = deriveChannelKey() - // Allocate to the same deployment ID - await staking - .connect(attacker) - .allocateFrom( - attacker.address, - newSubgraph0.subgraphDeploymentID, - toGRT('100000'), - channelKey.address, - randomHexBytes(32), - await channelKey.generateProof(attacker.address), - ) - // Spoof some query fees, 10% of which will go to the Curation pool - await staking.connect(attacker).collect(collectTokens, channelKey.address) - - const callhookData = defaultAbiCoder.encode(['uint8', 'uint256', 'address'], [toBN(1), l1SubgraphId, me.address]) - const curatorTokensBefore = await grt.balanceOf(me.address) - const gnsBalanceBefore = await grt.balanceOf(gns.address) - const tx = gatewayFinalizeTransfer(l1GNSMock.address, gns.address, curatorTokens, callhookData) - await expect(tx) - .emit(gns, 'CuratorBalanceReturnedToBeneficiary') - .withArgs(l1SubgraphId, me.address, curatorTokens) - const curatorTokensAfter = await grt.balanceOf(me.address) - expect(curatorTokensAfter).eq(curatorTokensBefore.add(curatorTokens)) - const gnsBalanceAfter = await grt.balanceOf(gns.address) - // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, - // so the GNS balance should be the same - expect(gnsBalanceAfter).eq(gnsBalanceBefore) - }) - it('if a subgraph was deprecated after transfer, it returns the tokens to the beneficiary', async function () { const l1GNSMockL2Alias = await helpers.getL2SignerFromL1(l1GNSMock.address) // Eth for gas: