From 1651716f094c2e3195f0afa03af260131f682be8 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 16 Jun 2025 15:06:43 -0300 Subject: [PATCH] test: Add extra tests for Indexing Agreements --- packages/horizon/package.json | 4 +- .../utilities/ProvisionManager.t.sol | 53 +++++++++ .../utilities/ProvisionManagerImpl.t.sol | 15 +++ .../test/unit/libraries/StakeClaims.t.sol | 18 +++ .../test/unit/mocks/HorizonStakingMock.t.sol | 32 ++++++ .../unit/mocks/InvalidControllerMock.t.sol | 8 ++ .../unit/mocks/PartialControllerMock.t.sol | 33 ++++++ .../RecurringCollectorAuthorizableTest.t.sol | 10 +- .../RecurringCollectorControllerMock.t.sol | 25 ---- .../payments/recurring-collector/base.t.sol | 44 +++++++ .../payments/recurring-collector/shared.t.sol | 6 +- packages/subgraph-service/package.json | 4 +- .../test/unit/libraries/IndexingAgreement.sol | 18 --- .../unit/libraries/IndexingAgreement.t.sol | 107 ++++++++++++++++++ .../indexing-agreement/accept.t.sol | 19 ++++ .../indexing-agreement/base.t.sol | 21 ++++ .../indexing-agreement/collect.t.sol | 37 ++++++ .../indexing-agreement/shared.t.sol | 59 ++++++++-- 18 files changed, 446 insertions(+), 67 deletions(-) create mode 100644 packages/horizon/test/unit/data-service/utilities/ProvisionManager.t.sol create mode 100644 packages/horizon/test/unit/data-service/utilities/ProvisionManagerImpl.t.sol create mode 100644 packages/horizon/test/unit/libraries/StakeClaims.t.sol create mode 100644 packages/horizon/test/unit/mocks/HorizonStakingMock.t.sol create mode 100644 packages/horizon/test/unit/mocks/InvalidControllerMock.t.sol create mode 100644 packages/horizon/test/unit/mocks/PartialControllerMock.t.sol delete mode 100644 packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorControllerMock.t.sol create mode 100644 packages/horizon/test/unit/payments/recurring-collector/base.t.sol delete mode 100644 packages/subgraph-service/test/unit/libraries/IndexingAgreement.sol create mode 100644 packages/subgraph-service/test/unit/libraries/IndexingAgreement.t.sol diff --git a/packages/horizon/package.json b/packages/horizon/package.json index 641fc61dd..4e2d86374 100644 --- a/packages/horizon/package.json +++ b/packages/horizon/package.json @@ -26,7 +26,9 @@ "build": "hardhat compile", "test": "forge test", "test:deployment": "SECURE_ACCOUNTS_DISABLE_PROVIDER=true hardhat test test/deployment/*.ts", - "test:integration": "./scripts/integration" + "test:integration": "./scripts/integration", + "test:coverage": "forge coverage --no-match-coverage \"test/*|contracts/mocks/*\"", + "test:coverage:lcov": "forge coverage --no-match-coverage \"test/*|contracts/mocks/*\" --report lcov" }, "devDependencies": { "@defi-wonderland/natspec-smells": "^1.1.6", diff --git a/packages/horizon/test/unit/data-service/utilities/ProvisionManager.t.sol b/packages/horizon/test/unit/data-service/utilities/ProvisionManager.t.sol new file mode 100644 index 000000000..3617e95a5 --- /dev/null +++ b/packages/horizon/test/unit/data-service/utilities/ProvisionManager.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +import { ProvisionManager } from "../../../../contracts/data-service/utilities/ProvisionManager.sol"; +import { IHorizonStakingTypes } from "../../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; +import { PartialControllerMock } from "../../mocks/PartialControllerMock.t.sol"; +import { HorizonStakingMock } from "../../mocks/HorizonStakingMock.t.sol"; +import { ProvisionManagerImpl } from "./ProvisionManagerImpl.t.sol"; + +contract ProvisionManagerTest is Test { + ProvisionManagerImpl internal _provisionManager; + HorizonStakingMock internal _horizonStakingMock; + + function setUp() public { + _horizonStakingMock = new HorizonStakingMock(); + + PartialControllerMock.Entry[] memory entries = new PartialControllerMock.Entry[](1); + entries[0] = PartialControllerMock.Entry({ name: "Staking", addr: address(_horizonStakingMock) }); + _provisionManager = new ProvisionManagerImpl(address(new PartialControllerMock(entries))); + } + + /* solhint-disable graph/func-name-mixedcase */ + + function test_OnlyValidProvision(address serviceProvider) public { + vm.expectRevert( + abi.encodeWithSelector(ProvisionManager.ProvisionManagerProvisionNotFound.selector, serviceProvider) + ); + _provisionManager.onlyValidProvision_(serviceProvider); + + IHorizonStakingTypes.Provision memory provision; + provision.createdAt = 1; + + _horizonStakingMock.setProvision(serviceProvider, address(_provisionManager), provision); + + _provisionManager.onlyValidProvision_(serviceProvider); + } + + function test_OnlyAuthorizedForProvision(address serviceProvider, address sender) public { + vm.expectRevert( + abi.encodeWithSelector(ProvisionManager.ProvisionManagerNotAuthorized.selector, serviceProvider, sender) + ); + vm.prank(sender); + _provisionManager.onlyAuthorizedForProvision_(serviceProvider); + + _horizonStakingMock.setIsAuthorized(serviceProvider, address(_provisionManager), sender, true); + vm.prank(sender); + _provisionManager.onlyAuthorizedForProvision_(serviceProvider); + } + + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/horizon/test/unit/data-service/utilities/ProvisionManagerImpl.t.sol b/packages/horizon/test/unit/data-service/utilities/ProvisionManagerImpl.t.sol new file mode 100644 index 000000000..4170d17da --- /dev/null +++ b/packages/horizon/test/unit/data-service/utilities/ProvisionManagerImpl.t.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { ProvisionManager } from "../../../../contracts/data-service/utilities/ProvisionManager.sol"; +import { GraphDirectory } from "../../../../contracts/utilities/GraphDirectory.sol"; + +contract ProvisionManagerImpl is GraphDirectory, ProvisionManager { + constructor(address controller) GraphDirectory(controller) {} + + function onlyValidProvision_(address serviceProvider) public view onlyValidProvision(serviceProvider) {} + + function onlyAuthorizedForProvision_( + address serviceProvider + ) public view onlyAuthorizedForProvision(serviceProvider) {} +} diff --git a/packages/horizon/test/unit/libraries/StakeClaims.t.sol b/packages/horizon/test/unit/libraries/StakeClaims.t.sol new file mode 100644 index 000000000..d98bdf78e --- /dev/null +++ b/packages/horizon/test/unit/libraries/StakeClaims.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +import { StakeClaims } from "../../../contracts/data-service/libraries/StakeClaims.sol"; + +contract StakeClaimsTest is Test { + /* solhint-disable graph/func-name-mixedcase */ + + function test_BuildStakeClaimId(address dataService, address serviceProvider, uint256 nonce) public pure { + bytes32 id = StakeClaims.buildStakeClaimId(dataService, serviceProvider, nonce); + bytes32 expectedId = keccak256(abi.encodePacked(dataService, serviceProvider, nonce)); + assertEq(id, expectedId, "StakeClaim ID does not match expected value"); + } + + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/horizon/test/unit/mocks/HorizonStakingMock.t.sol b/packages/horizon/test/unit/mocks/HorizonStakingMock.t.sol new file mode 100644 index 000000000..647df06f7 --- /dev/null +++ b/packages/horizon/test/unit/mocks/HorizonStakingMock.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IHorizonStakingTypes } from "../../../contracts/interfaces/internal/IHorizonStakingTypes.sol"; + +contract HorizonStakingMock { + mapping(address => mapping(address => IHorizonStakingTypes.Provision)) public provisions; + mapping(address => mapping(address => mapping(address => bool))) public authorizations; + + function setProvision( + address serviceProvider, + address verifier, + IHorizonStakingTypes.Provision memory provision + ) external { + provisions[serviceProvider][verifier] = provision; + } + + function getProvision( + address serviceProvider, + address verifier + ) external view returns (IHorizonStakingTypes.Provision memory) { + return provisions[serviceProvider][verifier]; + } + + function isAuthorized(address serviceProvider, address verifier, address operator) external view returns (bool) { + return authorizations[serviceProvider][verifier][operator]; + } + + function setIsAuthorized(address serviceProvider, address verifier, address operator, bool authorized) external { + authorizations[serviceProvider][verifier][operator] = authorized; + } +} diff --git a/packages/horizon/test/unit/mocks/InvalidControllerMock.t.sol b/packages/horizon/test/unit/mocks/InvalidControllerMock.t.sol new file mode 100644 index 000000000..f4d31da12 --- /dev/null +++ b/packages/horizon/test/unit/mocks/InvalidControllerMock.t.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { PartialControllerMock } from "./PartialControllerMock.t.sol"; + +contract InvalidControllerMock is PartialControllerMock { + constructor() PartialControllerMock(new PartialControllerMock.Entry[](0)) {} +} diff --git a/packages/horizon/test/unit/mocks/PartialControllerMock.t.sol b/packages/horizon/test/unit/mocks/PartialControllerMock.t.sol new file mode 100644 index 000000000..f315ff5ea --- /dev/null +++ b/packages/horizon/test/unit/mocks/PartialControllerMock.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +import { ControllerMock } from "../../../contracts/mocks/ControllerMock.sol"; + +contract PartialControllerMock is ControllerMock, Test { + struct Entry { + string name; + address addr; + } + + address private _invalidContractAddress; + + Entry[] private _contracts; + + constructor(Entry[] memory contracts) ControllerMock(address(0)) { + for (uint256 i = 0; i < contracts.length; i++) { + _contracts.push(Entry({ name: contracts[i].name, addr: contracts[i].addr })); + } + _invalidContractAddress = makeAddr("invalidContractAddress"); + } + + function getContractProxy(bytes32 data) external view override returns (address) { + for (uint256 i = 0; i < _contracts.length; i++) { + if (keccak256(abi.encodePacked(_contracts[i].name)) == data) { + return _contracts[i].addr; + } + } + return _invalidContractAddress; + } +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorAuthorizableTest.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorAuthorizableTest.t.sol index ff5e39848..91244fea1 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorAuthorizableTest.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorAuthorizableTest.t.sol @@ -5,16 +5,10 @@ import { IAuthorizable } from "../../../../contracts/interfaces/IAuthorizable.so import { RecurringCollector } from "../../../../contracts/payments/collectors/RecurringCollector.sol"; import { AuthorizableTest } from "../../../unit/utilities/Authorizable.t.sol"; -import { RecurringCollectorControllerMock } from "./RecurringCollectorControllerMock.t.sol"; +import { InvalidControllerMock } from "../../mocks/InvalidControllerMock.t.sol"; contract RecurringCollectorAuthorizableTest is AuthorizableTest { function newAuthorizable(uint256 thawPeriod) public override returns (IAuthorizable) { - return - new RecurringCollector( - "RecurringCollector", - "1", - address(new RecurringCollectorControllerMock(address(1))), - thawPeriod - ); + return new RecurringCollector("RecurringCollector", "1", address(new InvalidControllerMock()), thawPeriod); } } diff --git a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorControllerMock.t.sol b/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorControllerMock.t.sol deleted file mode 100644 index 3425e8b01..000000000 --- a/packages/horizon/test/unit/payments/recurring-collector/RecurringCollectorControllerMock.t.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.27; - -import { Test } from "forge-std/Test.sol"; - -import { IPaymentsEscrow } from "../../../../contracts/interfaces/IPaymentsEscrow.sol"; -import { ControllerMock } from "../../../../contracts/mocks/ControllerMock.sol"; - -contract RecurringCollectorControllerMock is ControllerMock, Test { - address private _invalidContractAddress; - IPaymentsEscrow private _paymentsEscrow; - - constructor(address paymentsEscrow) ControllerMock(address(0)) { - _invalidContractAddress = makeAddr("invalidContractAddress"); - _paymentsEscrow = IPaymentsEscrow(paymentsEscrow); - } - - function getContractProxy(bytes32 data) external view override returns (address) { - return data == keccak256("PaymentsEscrow") ? address(_paymentsEscrow) : _invalidContractAddress; - } - - function getPaymentsEscrow() external view returns (address) { - return address(_paymentsEscrow); - } -} diff --git a/packages/horizon/test/unit/payments/recurring-collector/base.t.sol b/packages/horizon/test/unit/payments/recurring-collector/base.t.sol new file mode 100644 index 000000000..9512fbf87 --- /dev/null +++ b/packages/horizon/test/unit/payments/recurring-collector/base.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.27; + +import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurringCollector.sol"; + +import { RecurringCollectorSharedTest } from "./shared.t.sol"; + +contract RecurringCollectorBaseTest is RecurringCollectorSharedTest { + /* + * TESTS + */ + + /* solhint-disable graph/func-name-mixedcase */ + + function test_RecoverRCASigner(FuzzyTestAccept memory fuzzyTestAccept) public view { + uint256 signerKey = boundKey(fuzzyTestAccept.unboundedSignerKey); + IRecurringCollector.SignedRCA memory signedRCA = _recurringCollectorHelper.generateSignedRCA( + fuzzyTestAccept.rca, + signerKey + ); + + assertEq( + _recurringCollector.recoverRCASigner(signedRCA), + vm.addr(signerKey), + "Recovered RCA signer does not match" + ); + } + + function test_RecoverRCAUSigner(FuzzyTestUpdate memory fuzzyTestUpdate) public view { + uint256 signerKey = boundKey(fuzzyTestUpdate.fuzzyTestAccept.unboundedSignerKey); + IRecurringCollector.SignedRCAU memory signedRCAU = _recurringCollectorHelper.generateSignedRCAU( + fuzzyTestUpdate.rcau, + signerKey + ); + + assertEq( + _recurringCollector.recoverRCAUSigner(signedRCAU), + vm.addr(signerKey), + "Recovered RCAU signer does not match" + ); + } + + /* solhint-enable graph/func-name-mixedcase */ +} diff --git a/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol b/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol index 397925600..8dd270b2f 100644 --- a/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol +++ b/packages/horizon/test/unit/payments/recurring-collector/shared.t.sol @@ -9,7 +9,7 @@ import { IRecurringCollector } from "../../../../contracts/interfaces/IRecurring import { RecurringCollector } from "../../../../contracts/payments/collectors/RecurringCollector.sol"; import { Bounder } from "../../../unit/utils/Bounder.t.sol"; -import { RecurringCollectorControllerMock } from "./RecurringCollectorControllerMock.t.sol"; +import { PartialControllerMock } from "../../mocks/PartialControllerMock.t.sol"; import { PaymentsEscrowMock } from "./PaymentsEscrowMock.t.sol"; import { RecurringCollectorHelper } from "./RecurringCollectorHelper.t.sol"; @@ -35,10 +35,12 @@ contract RecurringCollectorSharedTest is Test, Bounder { function setUp() public { _paymentsEscrow = new PaymentsEscrowMock(); + PartialControllerMock.Entry[] memory entries = new PartialControllerMock.Entry[](1); + entries[0] = PartialControllerMock.Entry({ name: "PaymentsEscrow", addr: address(_paymentsEscrow) }); _recurringCollector = new RecurringCollector( "RecurringCollector", "1", - address(new RecurringCollectorControllerMock(address(_paymentsEscrow))), + address(new PartialControllerMock(entries)), 1 ); _recurringCollectorHelper = new RecurringCollectorHelper(_recurringCollector); diff --git a/packages/subgraph-service/package.json b/packages/subgraph-service/package.json index 0b000778c..6138c0c61 100644 --- a/packages/subgraph-service/package.json +++ b/packages/subgraph-service/package.json @@ -26,7 +26,9 @@ "build": "hardhat compile", "test": "forge test", "test:deployment": "SECURE_ACCOUNTS_DISABLE_PROVIDER=true hardhat test test/deployment/*.ts", - "test:integration": "./scripts/integration" + "test:integration": "./scripts/integration", + "test:coverage": "forge coverage --no-match-coverage \"test/*|contracts/mocks/*\"", + "test:coverage:lcov": "forge coverage --no-match-coverage \"test/*|contracts/mocks/*\" --report lcov" }, "devDependencies": { "@defi-wonderland/natspec-smells": "^1.1.6", diff --git a/packages/subgraph-service/test/unit/libraries/IndexingAgreement.sol b/packages/subgraph-service/test/unit/libraries/IndexingAgreement.sol deleted file mode 100644 index 4afc6707e..000000000 --- a/packages/subgraph-service/test/unit/libraries/IndexingAgreement.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.27; - -import { Test } from "forge-std/Test.sol"; -import { IndexingAgreement } from "../../../contracts/libraries/IndexingAgreement.sol"; - -contract IndexingAgreementTest is Test { - function test_StorageManagerLocation() public pure { - assertEq( - IndexingAgreement.INDEXING_AGREEMENT_STORAGE_MANAGER_LOCATION, - keccak256( - abi.encode( - uint256(keccak256("graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement")) - 1 - ) - ) & ~bytes32(uint256(0xff)) - ); - } -} diff --git a/packages/subgraph-service/test/unit/libraries/IndexingAgreement.t.sol b/packages/subgraph-service/test/unit/libraries/IndexingAgreement.t.sol new file mode 100644 index 000000000..a545c8571 --- /dev/null +++ b/packages/subgraph-service/test/unit/libraries/IndexingAgreement.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.27; + +import { Test } from "forge-std/Test.sol"; + +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { IndexingAgreement } from "../../../contracts/libraries/IndexingAgreement.sol"; +import { Directory } from "../../../contracts/utilities/Directory.sol"; + +contract IndexingAgreementTest is Test { + IndexingAgreement.StorageManager private _storageManager; + address private _mockCollector; + + function setUp() public { + _mockCollector = makeAddr("mockCollector"); + } + + function test_IndexingAgreement_Get(bytes16 agreementId) public { + vm.assume(agreementId != bytes16(0)); + + vm.mockCall( + address(this), + abi.encodeWithSelector(Directory.recurringCollector.selector), + abi.encode(IRecurringCollector(_mockCollector)) + ); + + IRecurringCollector.AgreementData memory collectorAgreement; + vm.mockCall( + _mockCollector, + abi.encodeWithSelector(IRecurringCollector.getAgreement.selector, agreementId), + abi.encode(collectorAgreement) + ); + + vm.expectRevert(abi.encodeWithSelector(IndexingAgreement.IndexingAgreementNotActive.selector, agreementId)); + IndexingAgreement.get(_storageManager, agreementId); + + collectorAgreement.dataService = address(this); + vm.mockCall( + _mockCollector, + abi.encodeWithSelector(IRecurringCollector.getAgreement.selector, agreementId), + abi.encode(collectorAgreement) + ); + + IndexingAgreement.AgreementWrapper memory wrapper = IndexingAgreement.get(_storageManager, agreementId); + assertEq(wrapper.collectorAgreement.dataService, address(this)); + } + + function test_IndexingAgreement_OnCloseAllocation(bytes16 agreementId, address allocationId, bool stale) public { + vm.assume(agreementId != bytes16(0)); + vm.assume(allocationId != address(0)); + + delete _storageManager; + vm.clearMockedCalls(); + + // No active agreement for allocation ID, returns early, no assertions needed + IndexingAgreement.onCloseAllocation(_storageManager, allocationId, stale); + + // Active agreement for allocation ID, but collector agreement is not set, returns early, no assertions needed + _storageManager.allocationToActiveAgreementId[allocationId] = agreementId; + + IRecurringCollector.AgreementData memory collectorAgreement; + + vm.mockCall( + address(this), + abi.encodeWithSelector(Directory.recurringCollector.selector), + abi.encode(IRecurringCollector(_mockCollector)) + ); + + vm.mockCall( + _mockCollector, + abi.encodeWithSelector(IRecurringCollector.getAgreement.selector, agreementId), + abi.encode(collectorAgreement) + ); + + IndexingAgreement.onCloseAllocation(_storageManager, allocationId, stale); + + // Active agreement for allocation ID, collector agreement is set, should cancel the agreement + collectorAgreement.dataService = address(this); + collectorAgreement.state = IRecurringCollector.AgreementState.Accepted; + + _storageManager.agreements[agreementId] = IndexingAgreement.State({ + allocationId: allocationId, + version: IndexingAgreement.IndexingAgreementVersion.V1 + }); + + vm.mockCall( + _mockCollector, + abi.encodeWithSelector(IRecurringCollector.getAgreement.selector, agreementId), + abi.encode(collectorAgreement) + ); + + vm.expectCall(_mockCollector, abi.encodeWithSelector(IRecurringCollector.cancel.selector, agreementId)); + + IndexingAgreement.onCloseAllocation(_storageManager, allocationId, stale); + } + + function test_IndexingAgreement_StorageManagerLocation() public pure { + assertEq( + IndexingAgreement.INDEXING_AGREEMENT_STORAGE_MANAGER_LOCATION, + keccak256( + abi.encode( + uint256(keccak256("graphprotocol.subgraph-service.storage.StorageManager.IndexingAgreement")) - 1 + ) + ) & ~bytes32(uint256(0xff)) + ); + } +} diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol index 29f83126c..77b18308c 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/accept.t.sol @@ -225,6 +225,25 @@ contract SubgraphServiceIndexingAgreementAcceptTest is SubgraphServiceIndexingAg function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenAgreementAlreadyAllocated() public {} + function test_SubgraphService_AcceptIndexingAgreement_Revert_WhenInvalidTermsData(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory acceptable = _generateAcceptableSignedRCA(ctx, indexerState.addr); + bytes memory invalidTermsData = bytes("invalid terms data"); + acceptable.rca.metadata = abi.encode( + _newAcceptIndexingAgreementMetadataV1Terms(indexerState.subgraphDeploymentId, invalidTermsData) + ); + + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreementDecoder.IndexingAgreementDecoderInvalidData.selector, + "decodeCollectIndexingFeeData", + invalidTermsData + ); + vm.expectRevert(expectedErr); + resetPrank(indexerState.addr); + subgraphService.acceptIndexingAgreement(indexerState.allocationId, acceptable); + } + function test_SubgraphService_AcceptIndexingAgreement(Seed memory seed) public { Context storage ctx = _newCtx(seed); IndexerState memory indexerState = _withIndexer(ctx); diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol index 822cc21d7..2eda9dfc0 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/base.t.sol @@ -2,7 +2,9 @@ pragma solidity 0.8.27; import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { IRecurringCollector } from "@graphprotocol/horizon/contracts/interfaces/IRecurringCollector.sol"; +import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; contract SubgraphServiceIndexingAgreementBaseTest is SubgraphServiceIndexingAgreementSharedTest { @@ -11,6 +13,25 @@ contract SubgraphServiceIndexingAgreementBaseTest is SubgraphServiceIndexingAgre */ /* solhint-disable graph/func-name-mixedcase */ + function test_SubgraphService_GetIndexingAgreement(Seed memory seed, address operator, bytes16 agreementId) public { + vm.assume(_isSafeSubgraphServiceCaller(operator)); + + resetPrank(address(operator)); + + // Get unkown indexing agreement + vm.expectRevert(abi.encodeWithSelector(IndexingAgreement.IndexingAgreementNotActive.selector, agreementId)); + subgraphService.getIndexingAgreement(agreementId); + + // Accept an indexing agreement + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + IndexingAgreement.AgreementWrapper memory agreement = subgraphService.getIndexingAgreement( + accepted.rca.agreementId + ); + _assertEqualAgreement(accepted.rca, agreement); + } + function test_SubgraphService_Revert_WhenUnsafeAddress_WhenProxyAdmin(address indexer, bytes16 agreementId) public { address operator = _transparentUpgradeableProxyAdmin(); assertFalse(_isSafeSubgraphServiceCaller(operator)); diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol index 85c203b6e..57a7a907f 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/collect.t.sol @@ -11,6 +11,7 @@ import { ISubgraphService } from "../../../../contracts/interfaces/ISubgraphServ import { Allocation } from "../../../../contracts/libraries/Allocation.sol"; import { AllocationHandler } from "../../../../contracts/libraries/AllocationHandler.sol"; import { IndexingAgreement } from "../../../../contracts/libraries/IndexingAgreement.sol"; +import { IndexingAgreementDecoder } from "../../../../contracts/libraries/IndexingAgreementDecoder.sol"; import { SubgraphServiceIndexingAgreementSharedTest } from "./shared.t.sol"; @@ -175,6 +176,21 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA ); } + function test_SubgraphService_CollectIndexingFees_Revert_WhenInvalidData(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + + bytes memory invalidData = bytes("invalid data"); + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreementDecoder.IndexingAgreementDecoderInvalidData.selector, + "decodeCollectData", + invalidData + ); + vm.expectRevert(expectedErr); + resetPrank(indexerState.addr); + subgraphService.collect(indexerState.addr, IGraphPayments.PaymentTypes.IndexingFee, invalidData); + } + function test_SubgraphService_CollectIndexingFees_Revert_WhenInvalidAgreement( Seed memory seed, bytes16 agreementId, @@ -195,6 +211,27 @@ contract SubgraphServiceIndexingAgreementCollectTest is SubgraphServiceIndexingA ); } + function test_SubgraphService_CollectIndexingFees_Reverts_WhenInvalidNestedData(Seed memory seed) public { + Context storage ctx = _newCtx(seed); + IndexerState memory indexerState = _withIndexer(ctx); + IRecurringCollector.SignedRCA memory accepted = _withAcceptedIndexingAgreement(ctx, indexerState); + + resetPrank(indexerState.addr); + + bytes memory invalidNestedData = bytes("invalid nested data"); + bytes memory expectedErr = abi.encodeWithSelector( + IndexingAgreementDecoder.IndexingAgreementDecoderInvalidData.selector, + "decodeCollectIndexingFeeDataV1", + invalidNestedData + ); + vm.expectRevert(expectedErr); + subgraphService.collect( + indexerState.addr, + IGraphPayments.PaymentTypes.IndexingFee, + _encodeCollectData(accepted.rca.agreementId, invalidNestedData) + ); + } + function test_SubgraphService_CollectIndexingFees_Reverts_WhenStopService( Seed memory seed, uint256 entities, diff --git a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol index 8574e60e7..2a5b2385a 100644 --- a/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol +++ b/packages/subgraph-service/test/unit/subgraphService/indexing-agreement/shared.t.sol @@ -310,14 +310,25 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun function _newAcceptIndexingAgreementMetadataV1( bytes32 _subgraphDeploymentId + ) internal pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { + return + _newAcceptIndexingAgreementMetadataV1Terms( + _subgraphDeploymentId, + abi.encode( + IndexingAgreement.IndexingAgreementTermsV1({ tokensPerSecond: 0, tokensPerEntityPerSecond: 0 }) + ) + ); + } + + function _newAcceptIndexingAgreementMetadataV1Terms( + bytes32 _subgraphDeploymentId, + bytes memory _terms ) internal pure returns (IndexingAgreement.AcceptIndexingAgreementMetadata memory) { return IndexingAgreement.AcceptIndexingAgreementMetadata({ subgraphDeploymentId: _subgraphDeploymentId, version: IndexingAgreement.IndexingAgreementVersion.V1, - terms: abi.encode( - IndexingAgreement.IndexingAgreementTermsV1({ tokensPerSecond: 0, tokensPerEntityPerSecond: 0 }) - ) + terms: _terms }); } @@ -343,18 +354,28 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun bytes32 _poi, uint256 _poiBlock, bytes memory _metadata + ) internal pure returns (bytes memory) { + return _encodeCollectData(_agreementId, _encodeV1Data(_entities, _poi, _poiBlock, _metadata)); + } + + function _encodeCollectData(bytes16 _agreementId, bytes memory _nestedData) internal pure returns (bytes memory) { + return abi.encode(_agreementId, _nestedData); + } + + function _encodeV1Data( + uint256 _entities, + bytes32 _poi, + uint256 _poiBlock, + bytes memory _metadata ) internal pure returns (bytes memory) { return abi.encode( - _agreementId, - abi.encode( - IndexingAgreement.CollectIndexingFeeDataV1({ - entities: _entities, - poi: _poi, - poiBlockNumber: _poiBlock, - metadata: _metadata - }) - ) + IndexingAgreement.CollectIndexingFeeDataV1({ + entities: _entities, + poi: _poi, + poiBlockNumber: _poiBlock, + metadata: _metadata + }) ); } @@ -377,4 +398,18 @@ contract SubgraphServiceIndexingAgreementSharedTest is SubgraphServiceTest, Boun ) internal pure returns (bytes memory) { return abi.encode(_t); } + + function _assertEqualAgreement( + IRecurringCollector.RecurringCollectionAgreement memory _expected, + IndexingAgreement.AgreementWrapper memory _actual + ) internal pure { + assertEq(_expected.dataService, _actual.collectorAgreement.dataService); + assertEq(_expected.payer, _actual.collectorAgreement.payer); + assertEq(_expected.serviceProvider, _actual.collectorAgreement.serviceProvider); + assertEq(_expected.endsAt, _actual.collectorAgreement.endsAt); + assertEq(_expected.maxInitialTokens, _actual.collectorAgreement.maxInitialTokens); + assertEq(_expected.maxOngoingTokensPerSecond, _actual.collectorAgreement.maxOngoingTokensPerSecond); + assertEq(_expected.minSecondsPerCollection, _actual.collectorAgreement.minSecondsPerCollection); + assertEq(_expected.maxSecondsPerCollection, _actual.collectorAgreement.maxSecondsPerCollection); + } }