From cdf2c29595a3920cf8d566537cd2f814ab2fa6cc Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 10 Jul 2025 14:49:37 -0300 Subject: [PATCH 01/14] fix: remove protocol access for revocable vesting contracts --- .../contracts/GraphTokenLockWallet.sol | 30 ++------ .../test/l1TokenLockTransferTool.test.ts | 2 +- .../test/tokenLockWallet.test.ts | 69 +------------------ 3 files changed, 9 insertions(+), 92 deletions(-) diff --git a/packages/token-distribution/contracts/GraphTokenLockWallet.sol b/packages/token-distribution/contracts/GraphTokenLockWallet.sol index 3ac24cb13..d3f9c5ce8 100644 --- a/packages/token-distribution/contracts/GraphTokenLockWallet.sol +++ b/packages/token-distribution/contracts/GraphTokenLockWallet.sol @@ -35,7 +35,7 @@ contract GraphTokenLockWallet is GraphTokenLock { // -- State -- IGraphTokenLockManager public manager; - uint256 public usedAmount; + uint256 public __DEPRECATED_usedAmount; // -- Events -- @@ -151,9 +151,7 @@ contract GraphTokenLockWallet is GraphTokenLock { } // A beneficiary can never have more releasable tokens than the contract balance - // We consider the `usedAmount` in the protocol as part of the calculations - // the beneficiary should not release funds that are used. - uint256 releasable = availableAmount().sub(releasedAmount).sub(usedAmount); + uint256 releasable = availableAmount().sub(releasedAmount); return MathUtils.min(currentBalance(), releasable); } @@ -167,33 +165,15 @@ contract GraphTokenLockWallet is GraphTokenLock { require(msg.sender == beneficiary, "Unauthorized caller"); require(msg.value == 0, "ETH transfers not supported"); + // Only non-revocable contracts can forward calls + require(revocable == Revocability.Disabled, "Revocable contracts cannot forward calls"); + // Function call validation address _target = manager.getAuthFunctionCallTarget(msg.sig); require(_target != address(0), "Unauthorized function"); - uint256 oldBalance = currentBalance(); - // Call function with data Address.functionCall(_target, msg.data); - - // Tracked used tokens in the protocol - // We do this check after balances were updated by the forwarded call - // Check is only enforced for revocable contracts to save some gas - if (revocable == Revocability.Enabled) { - // Track contract balance change - uint256 newBalance = currentBalance(); - if (newBalance < oldBalance) { - // Outflow - uint256 diff = oldBalance.sub(newBalance); - usedAmount = usedAmount.add(diff); - } else { - // Inflow: We can receive profits from the protocol, that could make usedAmount to - // underflow. We set it to zero in that case. - uint256 diff = newBalance.sub(oldBalance); - usedAmount = (diff >= usedAmount) ? 0 : usedAmount.sub(diff); - } - require(usedAmount <= vestedAmount(), "Cannot use more tokens than vested amount"); - } } /** diff --git a/packages/token-distribution/test/l1TokenLockTransferTool.test.ts b/packages/token-distribution/test/l1TokenLockTransferTool.test.ts index b40af5356..470102793 100644 --- a/packages/token-distribution/test/l1TokenLockTransferTool.test.ts +++ b/packages/token-distribution/test/l1TokenLockTransferTool.test.ts @@ -327,7 +327,7 @@ describe('L1GraphTokenLockTransferTool', () => { const tx = lockAsTransferTool .connect(beneficiary.signer) .depositToL2Locked(toGRT('10000000'), l2Beneficiary.address, maxGas, gasPrice, maxSubmissionCost) - await expect(tx).revertedWith('REVOCABLE') + await expect(tx).revertedWith('Revocable contracts cannot forward calls') }) it('rejects calls if the wallet does not have enough tokens', async function () { await tokenLock.connect(beneficiary.signer).acceptLock() diff --git a/packages/token-distribution/test/tokenLockWallet.test.ts b/packages/token-distribution/test/tokenLockWallet.test.ts index 2eed5b269..362aabfe5 100644 --- a/packages/token-distribution/test/tokenLockWallet.test.ts +++ b/packages/token-distribution/test/tokenLockWallet.test.ts @@ -263,7 +263,7 @@ describe('GraphTokenLockWallet', () => { }) }) - describe('Revokability, Call Forwarding and Used Tokens', function () { + describe('Revokability and Call Forwarding', function () { let lockAsStaking beforeEach(async () => { @@ -281,77 +281,14 @@ describe('GraphTokenLockWallet', () => { await tokenLock.connect(beneficiary.signer).approveProtocol() }) - it('reject using more than vested amount in the protocol', async function () { + it('reject call forwarding for revocable contracts', async function () { await advanceToStart(tokenLock) // At this point no period has passed so we haven't vested any token // Try to stake funds into the protocol should fail const stakeAmount = toGRT('100') const tx = lockAsStaking.connect(beneficiary.signer).stake(stakeAmount) - await expect(tx).revertedWith('Cannot use more tokens than vested amount') - }) - - it('should release considering what is used in the protocol', async function () { - // Move to have some vested periods - await advanceToStart(tokenLock) - await advancePeriods(tokenLock, 2) - - // Get amount that can be released with no used tokens yet - const releasableAmount = await tokenLock.releasableAmount() - - // Use tokens in the protocol - const stakeAmount = toGRT('100') - await lockAsStaking.connect(beneficiary.signer).stake(stakeAmount) - - // Release - should take into account used tokens - const tx = await tokenLock.connect(beneficiary.signer).release() - await expect(tx) - .emit(tokenLock, 'TokensReleased') - .withArgs(beneficiary.address, releasableAmount.sub(stakeAmount)) - - // Revoke should work - await tokenLock.connect(deployer.signer).revoke() - }) - - it('should release considering what is used in the protocol (even if most is used)', async function () { - // Move to have some vested periods - await advanceToStart(tokenLock) - await advancePeriods(tokenLock, 2) - - // Get amount that can be released with no used tokens yet - const releasableAmount = await tokenLock.releasableAmount() - - // Use tokens in the protocol - const stakeAmount = (await tokenLock.availableAmount()).sub(toGRT('1')) - await lockAsStaking.connect(beneficiary.signer).stake(stakeAmount) - - // Release - should take into account used tokens - const tx = await tokenLock.connect(beneficiary.signer).release() - await expect(tx) - .emit(tokenLock, 'TokensReleased') - .withArgs(beneficiary.address, releasableAmount.sub(stakeAmount)) - - // Revoke should work - await tokenLock.connect(deployer.signer).revoke() - }) - - it('should allow to get profit from the protocol', async function () { - await advanceToStart(tokenLock) - await advancePeriods(tokenLock, 1) - - // At this point we vested one period, we have tokens - // Stake funds into the protocol - const stakeAmount = toGRT('100') - await lockAsStaking.connect(beneficiary.signer).stake(stakeAmount) - - // Simulate having a profit - await grt.approve(staking.address, toGRT('1000000')) - await staking.stakeTo(lockAsStaking.address, toGRT('1000000')) - - // Unstake more than we used in the protocol, this should work! - await lockAsStaking.connect(beneficiary.signer).unstake(toGRT('1000000')) - await advanceBlocks(20) - await lockAsStaking.connect(beneficiary.signer).withdraw() + await expect(tx).revertedWith('Revocable contracts cannot forward calls') }) }) }) From 4e59b43671da46b350953141eebabd0f58425683 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Fri, 3 Oct 2025 10:54:25 -0300 Subject: [PATCH 02/14] fix: remove deprecated usedAmount variable --- packages/token-distribution/contracts/GraphTokenLockWallet.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/token-distribution/contracts/GraphTokenLockWallet.sol b/packages/token-distribution/contracts/GraphTokenLockWallet.sol index d3f9c5ce8..52c01a90c 100644 --- a/packages/token-distribution/contracts/GraphTokenLockWallet.sol +++ b/packages/token-distribution/contracts/GraphTokenLockWallet.sol @@ -35,7 +35,6 @@ contract GraphTokenLockWallet is GraphTokenLock { // -- State -- IGraphTokenLockManager public manager; - uint256 public __DEPRECATED_usedAmount; // -- Events -- From 506790a7bfa4668475b3c2a15d7e32db8b07043a Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 9 Oct 2025 16:31:09 -0300 Subject: [PATCH 03/14] fix: remove redundant override (OZ N-01) --- .../contracts/GraphTokenLockWallet.sol | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/token-distribution/contracts/GraphTokenLockWallet.sol b/packages/token-distribution/contracts/GraphTokenLockWallet.sol index 52c01a90c..307b9ef9c 100644 --- a/packages/token-distribution/contracts/GraphTokenLockWallet.sol +++ b/packages/token-distribution/contracts/GraphTokenLockWallet.sol @@ -123,37 +123,6 @@ contract GraphTokenLockWallet is GraphTokenLock { emit TokenDestinationsRevoked(); } - /** - * @notice Gets tokens currently available for release - * @dev Considers the schedule, takes into account already released tokens and used amount - * @return Amount of tokens ready to be released - */ - function releasableAmount() public view override returns (uint256) { - if (revocable == Revocability.Disabled) { - return super.releasableAmount(); - } - - // -- Revocability enabled logic - // This needs to deal with additional considerations for when tokens are used in the protocol - - // If a release start time is set no tokens are available for release before this date - // If not set it follows the default schedule and tokens are available on - // the first period passed - if (releaseStartTime > 0 && currentTime() < releaseStartTime) { - return 0; - } - - // Vesting cliff is activated and it has not passed means nothing is vested yet - // so funds cannot be released - if (revocable == Revocability.Enabled && vestingCliffTime > 0 && currentTime() < vestingCliffTime) { - return 0; - } - - // A beneficiary can never have more releasable tokens than the contract balance - uint256 releasable = availableAmount().sub(releasedAmount); - return MathUtils.min(currentBalance(), releasable); - } - /** * @notice Forward authorized contract calls to protocol contracts * @dev Fallback function can be called by the beneficiary only if function call is allowed From b4f8b5122a585eac5acb035e509ecbe98779e317 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Thu, 9 Oct 2025 16:50:31 -0300 Subject: [PATCH 04/14] fix: remove redundant code protection (OZ N-02) --- .../contracts/GraphTokenLockWallet.sol | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/token-distribution/contracts/GraphTokenLockWallet.sol b/packages/token-distribution/contracts/GraphTokenLockWallet.sol index 307b9ef9c..bf47f2f88 100644 --- a/packages/token-distribution/contracts/GraphTokenLockWallet.sol +++ b/packages/token-distribution/contracts/GraphTokenLockWallet.sol @@ -128,10 +128,9 @@ contract GraphTokenLockWallet is GraphTokenLock { * @dev Fallback function can be called by the beneficiary only if function call is allowed */ // solhint-disable-next-line no-complex-fallback - fallback() external payable { + fallback() external { // Only beneficiary can forward calls require(msg.sender == beneficiary, "Unauthorized caller"); - require(msg.value == 0, "ETH transfers not supported"); // Only non-revocable contracts can forward calls require(revocable == Revocability.Disabled, "Revocable contracts cannot forward calls"); @@ -143,12 +142,4 @@ contract GraphTokenLockWallet is GraphTokenLock { // Call function with data Address.functionCall(_target, msg.data); } - - /** - * @notice Receive function that always reverts. - * @dev Only included to supress warnings, see https://github.com/ethereum/solidity/issues/10159 - */ - receive() external payable { - revert("Bad call"); - } } From a6273c19b6cfdae2eadbcf0456deaf08a70ec450 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Wed, 15 Oct 2025 08:20:39 +0000 Subject: [PATCH 05/14] chore(contracts): fix solidity linting issues across all packages - Add NatSpec documentation (@title, @author, @notice, @param) - Add solhint-disable directives with TODO comments for future fixes - Minor code formatting changes to address lint rules All packages now pass pnpm lint with no reported issues. Verified with scripts/compare-repo-contract-bytecode-excluding-metadata.mjs: only mock contracts have functional bytecode changes. --- .../contracts/arbitrum/AddressAliasHelper.sol | 9 +- .../contracts/arbitrum/IArbToken.sol | 10 + .../contracts/contracts/arbitrum/IBridge.sol | 83 +++++++- .../contracts/contracts/arbitrum/IInbox.sol | 83 +++++++- .../contracts/arbitrum/IMessageProvider.sol | 14 ++ .../contracts/contracts/arbitrum/IOutbox.sol | 57 +++++ .../contracts/arbitrum/ITokenGateway.sol | 29 ++- .../arbitrum/L1ArbitrumMessenger.sol | 62 +++++- .../arbitrum/L2ArbitrumMessenger.sol | 26 ++- .../contracts/bancor/BancorFormula.sol | 76 +++++-- .../contracts/contracts/base/IMulticall.sol | 1 + .../contracts/contracts/base/Multicall.sol | 9 +- .../contracts/contracts/curation/Curation.sol | 42 ++-- .../contracts/curation/CurationStorage.sol | 29 ++- .../contracts/curation/GraphCurationToken.sol | 13 +- .../contracts/curation/ICuration.sol | 3 +- .../curation/IGraphCurationToken.sol | 21 +- .../contracts/contracts/discovery/GNS.sol | 183 ++++++++-------- .../contracts/discovery/GNSStorage.sol | 21 +- .../contracts/contracts/discovery/IGNS.sol | 16 +- .../contracts/discovery/IServiceRegistry.sol | 33 +++ .../contracts/discovery/ISubgraphNFT.sol | 44 +++- .../discovery/ISubgraphNFTDescriptor.sol | 6 +- .../contracts/contracts/discovery/L1GNS.sol | 25 ++- .../contracts/discovery/L1GNSStorage.sol | 6 +- .../contracts/discovery/ServiceRegistry.sol | 50 +++-- .../discovery/ServiceRegistryStorage.sol | 13 +- .../contracts/discovery/SubgraphNFT.sol | 77 ++++--- .../discovery/SubgraphNFTDescriptor.sol | 10 +- .../discovery/erc1056/EthereumDIDRegistry.sol | 174 +++++++++++++++ .../erc1056/IEthereumDIDRegistry.sol | 17 ++ .../contracts/disputes/DisputeManager.sol | 199 ++++++++++-------- .../disputes/DisputeManagerStorage.sol | 32 ++- .../contracts/disputes/IDisputeManager.sol | 120 ++++++++++- .../contracts/epochs/EpochManager.sol | 62 +++--- .../contracts/epochs/EpochManagerStorage.sol | 14 +- .../contracts/epochs/IEpochManager.sol | 47 +++++ .../contracts/gateway/BridgeEscrow.sol | 3 +- .../contracts/gateway/GraphTokenGateway.sol | 7 +- .../contracts/gateway/ICallhookReceiver.sol | 8 +- .../contracts/gateway/L1GraphTokenGateway.sol | 133 +++++++----- .../contracts/governance/Controller.sol | 46 ++-- .../contracts/governance/Governed.sol | 28 ++- .../contracts/governance/IController.sol | 50 +++++ .../contracts/governance/IManaged.sol | 12 +- .../contracts/governance/Managed.sol | 75 ++++--- .../contracts/governance/Pausable.sol | 38 +++- .../contracts/l2/curation/IL2Curation.sol | 2 + .../contracts/l2/curation/L2Curation.sol | 67 +++--- .../contracts/l2/discovery/IL2GNS.sol | 11 + .../contracts/l2/discovery/L2GNS.sol | 56 ++--- .../contracts/l2/discovery/L2GNSStorage.sol | 7 +- .../l2/gateway/L2GraphTokenGateway.sol | 79 ++++--- .../contracts/l2/staking/IL2Staking.sol | 1 + .../contracts/l2/staking/IL2StakingBase.sol | 10 + .../contracts/l2/staking/IL2StakingTypes.sol | 5 + .../contracts/l2/staking/L2Staking.sol | 21 +- .../l2/token/GraphTokenUpgradeable.sol | 28 ++- .../contracts/l2/token/L2GraphToken.sol | 45 ++-- .../contracts/libraries/Base58Encoder.sol | 39 +++- .../contracts/libraries/HexStrings.sol | 21 +- .../contracts/payments/AllocationExchange.sol | 64 ++++-- .../contracts/rewards/IRewardsIssuer.sol | 7 +- .../contracts/rewards/IRewardsManager.sol | 57 ----- .../contracts/rewards/RewardsManager.sol | 105 ++++----- .../rewards/RewardsManagerStorage.sol | 50 ++++- .../rewards/SubgraphAvailabilityManager.sol | 46 ++-- .../staking/IL1GraphTokenLockTransferTool.sol | 3 +- .../contracts/staking/IL1Staking.sol | 1 + .../contracts/staking/IL1StakingBase.sol | 33 ++- .../contracts/contracts/staking/IStaking.sol | 1 + .../contracts/staking/IStakingBase.sol | 85 ++++++-- .../contracts/staking/IStakingData.sol | 26 ++- .../contracts/staking/IStakingExtension.sol | 42 +++- .../contracts/contracts/staking/L1Staking.sol | 90 ++------ .../contracts/staking/L1StakingStorage.sol | 6 +- .../contracts/contracts/staking/Staking.sol | 192 ++++++----------- .../contracts/staking/StakingExtension.sol | 157 ++++---------- .../contracts/staking/StakingStorage.sol | 15 +- .../contracts/staking/libs/Exponential.sol | 3 +- .../contracts/staking/libs/IStakes.sol | 5 + .../contracts/staking/libs/LibFixedMath.sol | 111 +++++++--- .../contracts/staking/libs/MathUtils.sol | 19 +- .../contracts/staking/libs/Stakes.sol | 32 +-- .../contracts/tests/CallhookReceiverMock.sol | 22 +- .../contracts/tests/GovernedMock.sol | 9 +- .../L1GraphTokenLockTransferToolBadMock.sol | 32 ++- .../L1GraphTokenLockTransferToolMock.sol | 31 ++- .../contracts/tests/LegacyGNSMock.sol | 3 + .../contracts/tests/arbitrum/ArbSysMock.sol | 10 + .../contracts/tests/arbitrum/BridgeMock.sol | 57 +++-- .../contracts/tests/arbitrum/InboxMock.sol | 49 +++-- .../contracts/tests/arbitrum/OutboxMock.sol | 52 ++--- .../contracts/contracts/tests/ens/IENS.sol | 24 ++- .../contracts/tests/ens/IPublicResolver.sol | 19 ++ .../contracts/tests/ens/ITestRegistrar.sol | 12 ++ .../contracts/contracts/token/GraphToken.sol | 65 ++++-- .../contracts/contracts/token/IGraphToken.sol | 72 ++++++- .../contracts/upgrades/GraphProxy.sol | 52 +++-- .../contracts/upgrades/GraphProxyAdmin.sol | 5 +- .../contracts/upgrades/GraphProxyStorage.sol | 28 ++- .../contracts/upgrades/GraphUpgradeable.sol | 8 +- .../contracts/upgrades/IGraphProxy.sol | 58 +++++ .../contracts/contracts/utils/TokenUtils.sol | 9 +- packages/data-edge/contracts/DataEdge.sol | 4 +- .../data-edge/contracts/EventfulDataEdge.sol | 5 + .../contracts/data-service/DataService.sol | 2 + .../data-service/DataServiceStorage.sol | 3 +- .../extensions/DataServiceFees.sol | 1 + .../extensions/DataServiceFeesStorage.sol | 2 + .../extensions/DataServicePausable.sol | 1 + .../DataServicePausableUpgradeable.sol | 3 +- .../extensions/DataServiceRescuable.sol | 7 +- .../libraries/ProvisionTracker.sol | 4 + .../utilities/ProvisionManager.sol | 5 + .../utilities/ProvisionManagerStorage.sol | 2 + .../contracts/libraries/Denominations.sol | 3 +- .../contracts/libraries/LibFixedMath.sol | 81 +++++-- .../contracts/libraries/LinkedList.sol | 4 + .../horizon/contracts/libraries/MathUtils.sol | 14 +- .../horizon/contracts/libraries/PPMMath.sol | 4 + .../horizon/contracts/libraries/UintRange.sol | 4 + .../contracts/mocks/ControllerMock.sol | 14 +- .../horizon/contracts/mocks/CurationMock.sol | 21 ++ packages/horizon/contracts/mocks/Dummy.sol | 5 + .../contracts/mocks/EpochManagerMock.sol | 50 +++++ .../horizon/contracts/mocks/MockGRTToken.sol | 60 ++++++ .../contracts/mocks/RewardsManagerMock.sol | 32 ++- .../contracts/payments/GraphPayments.sol | 4 + .../contracts/payments/PaymentsEscrow.sol | 4 + .../collectors/GraphTallyCollector.sol | 18 +- .../contracts/staking/HorizonStaking.sol | 19 +- .../contracts/staking/HorizonStakingBase.sol | 11 +- .../staking/HorizonStakingExtension.sol | 35 +-- .../staking/HorizonStakingStorage.sol | 3 +- .../staking/libraries/ExponentialRebates.sol | 32 +-- .../contracts/staking/utilities/Managed.sol | 9 +- .../contracts/utilities/Authorizable.sol | 4 + .../contracts/utilities/GraphDirectory.sol | 1 + .../contracts/arbitrum/IArbToken.sol | 10 + .../contracts/contracts/arbitrum/IBridge.sol | 83 +++++++- .../contracts/contracts/arbitrum/IInbox.sol | 79 +++++++ .../contracts/arbitrum/IMessageProvider.sol | 14 ++ .../contracts/contracts/arbitrum/IOutbox.sol | 57 +++++ .../contracts/arbitrum/ITokenGateway.sol | 29 ++- .../contracts/contracts/base/IMulticall.sol | 1 + .../contracts/curation/ICuration.sol | 3 +- .../curation/IGraphCurationToken.sol | 19 ++ .../contracts/contracts/discovery/IGNS.sol | 23 +- .../contracts/discovery/IServiceRegistry.sol | 33 +++ .../contracts/discovery/ISubgraphNFT.sol | 42 ++++ .../discovery/ISubgraphNFTDescriptor.sol | 6 +- .../erc1056/IEthereumDIDRegistry.sol | 17 ++ .../contracts/disputes/IDisputeManager.sol | 118 ++++++++++- .../contracts/epochs/IEpochManager.sol | 47 +++++ .../contracts/gateway/ICallhookReceiver.sol | 8 +- .../contracts/governance/IController.sol | 50 +++++ .../contracts/governance/IGoverned.sol | 21 +- .../contracts/governance/IManaged.sol | 3 +- .../contracts/l2/curation/IL2Curation.sol | 2 + .../contracts/l2/discovery/IL2GNS.sol | 11 + .../l2/gateway/IL2GraphTokenGateway.sol | 96 +++++++++ .../contracts/l2/staking/IL2Staking.sol | 1 + .../contracts/l2/staking/IL2StakingBase.sol | 10 + .../contracts/l2/staking/IL2StakingTypes.sol | 5 + .../contracts/l2/token/IL2GraphToken.sol | 43 ++++ .../rewards/ILegacyRewardsManager.sol | 10 + .../contracts/rewards/IRewardsIssuer.sol | 7 +- .../contracts/rewards/IRewardsManager.sol | 98 ++++++++- .../staking/IL1GraphTokenLockTransferTool.sol | 3 +- .../contracts/staking/IL1Staking.sol | 1 + .../contracts/staking/IL1StakingBase.sol | 33 ++- .../contracts/contracts/staking/IStaking.sol | 1 + .../contracts/staking/IStakingBase.sol | 87 ++++++-- .../contracts/staking/IStakingData.sol | 26 ++- .../contracts/staking/IStakingExtension.sol | 42 +++- .../contracts/staking/libs/IStakes.sol | 5 + .../contracts/contracts/token/IGraphToken.sol | 54 +++++ .../contracts/upgrades/IGraphProxy.sol | 58 +++++ .../contracts/upgrades/IGraphProxyAdmin.sol | 53 ++++- .../contracts/data-service/IDataService.sol | 4 + .../data-service/IDataServiceFees.sol | 4 + .../data-service/IDataServicePausable.sol | 4 + .../data-service/IDataServiceRescuable.sol | 4 + .../contracts/horizon/IAuthorizable.sol | 5 + .../contracts/horizon/IGraphPayments.sol | 1 + .../horizon/IGraphTallyCollector.sol | 7 +- .../contracts/horizon/IHorizonStaking.sol | 1 + .../contracts/horizon/IPaymentsCollector.sol | 1 + .../contracts/horizon/IPaymentsEscrow.sol | 1 + .../horizon/internal/IHorizonStakingBase.sol | 4 + .../internal/IHorizonStakingExtension.sol | 12 +- .../horizon/internal/IHorizonStakingMain.sol | 8 +- .../horizon/internal/IHorizonStakingTypes.sol | 2 + .../horizon/internal/ILinkedList.sol | 2 + .../subgraph-service/IDisputeManager.sol | 22 +- .../subgraph-service/ISubgraphService.sol | 4 + .../subgraph-service/internal/IAllocation.sol | 2 + .../internal/IAttestation.sol | 2 + .../internal/ILegacyAllocation.sol | 2 + .../IGraphTokenLockWallet.sol | 111 ++++++++++ .../toolshed/IControllerToolshed.sol | 2 + .../toolshed/IDisputeManagerToolshed.sol | 2 + .../toolshed/IEpochManagerToolshed.sol | 2 + .../toolshed/IGraphTallyCollectorToolshed.sol | 2 + .../IGraphTokenLockWalletToolshed.sol | 3 + .../toolshed/IHorizonStakingToolshed.sol | 2 + .../toolshed/IL2CurationToolshed.sol | 2 + .../contracts/toolshed/IL2GNSToolshed.sol | 2 + .../toolshed/IPaymentsEscrowToolshed.sol | 2 + .../toolshed/IRewardsManagerToolshed.sol | 13 +- .../toolshed/IServiceRegistryToolshed.sol | 2 + .../toolshed/ISubgraphServiceToolshed.sol | 2 + .../toolshed/internal/IAllocationManager.sol | 5 + .../contracts/toolshed/internal/IOwnable.sol | 2 + .../contracts/toolshed/internal/IPausable.sol | 2 + .../toolshed/internal/IProvisionManager.sol | 5 + .../toolshed/internal/IProvisionTracker.sol | 2 + .../contracts/DisputeManager.sol | 6 +- .../contracts/DisputeManagerStorage.sol | 3 +- .../contracts/SubgraphService.sol | 6 + .../contracts/SubgraphServiceStorage.sol | 3 +- .../contracts/libraries/Allocation.sol | 3 +- .../contracts/libraries/Attestation.sol | 16 +- .../contracts/libraries/LegacyAllocation.sol | 3 +- .../contracts/utilities/AllocationManager.sol | 12 +- .../utilities/AllocationManagerStorage.sol | 3 +- .../utilities/AttestationManager.sol | 16 +- .../utilities/AttestationManagerStorage.sol | 3 +- .../contracts/utilities/Directory.sol | 6 +- .../test/unit/mocks/MockRewardsManager.sol | 6 + .../contracts/GraphTokenDistributor.sol | 12 +- .../contracts/GraphTokenLock.sol | 29 ++- .../contracts/GraphTokenLockManager.sol | 17 +- .../contracts/GraphTokenLockSimple.sol | 9 +- .../contracts/GraphTokenLockWallet.sol | 15 +- .../contracts/ICallhookReceiver.sol | 3 + .../contracts/IGraphTokenLock.sol | 3 +- .../contracts/IGraphTokenLockManager.sol | 7 +- .../L1GraphTokenLockTransferTool.sol | 8 + .../contracts/L2GraphTokenLockManager.sol | 6 + .../L2GraphTokenLockTransferTool.sol | 6 + .../contracts/L2GraphTokenLockWallet.sol | 3 + .../contracts/MathUtils.sol | 3 + .../contracts/MinimalProxyFactory.sol | 3 + .../token-distribution/contracts/Ownable.sol | 3 + .../contracts/arbitrum/ITokenGateway.sol | 3 + .../contracts/tests/BridgeMock.sol | 5 +- .../contracts/tests/GraphTokenMock.sol | 7 +- .../contracts/tests/InboxMock.sol | 8 +- .../contracts/tests/L1TokenGatewayMock.sol | 2 + .../contracts/tests/L2TokenGatewayMock.sol | 4 + .../contracts/tests/Stakes.sol | 5 +- .../contracts/tests/StakingMock.sol | 58 +++-- .../contracts/tests/WalletMock.sol | 8 + .../tests/arbitrum/AddressAliasHelper.sol | 6 +- .../contracts/tests/arbitrum/IBridge.sol | 3 + .../contracts/tests/arbitrum/IInbox.sol | 7 +- .../tests/arbitrum/IMessageProvider.sol | 3 + 259 files changed, 5076 insertions(+), 1566 deletions(-) delete mode 100644 packages/contracts/contracts/rewards/IRewardsManager.sol diff --git a/packages/contracts/contracts/arbitrum/AddressAliasHelper.sol b/packages/contracts/contracts/arbitrum/AddressAliasHelper.sol index 740b70361..005df41c0 100644 --- a/packages/contracts/contracts/arbitrum/AddressAliasHelper.sol +++ b/packages/contracts/contracts/arbitrum/AddressAliasHelper.sol @@ -25,8 +25,15 @@ pragma solidity ^0.7.6; +/** + * @title Address Alias Helper Library + * @author Edge & Node + * @notice Utility library for converting addresses between L1 and L2 in Arbitrum + */ library AddressAliasHelper { - uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); + /// @dev Offset used for L1 to L2 address aliasing + // solhint-disable-next-line const-name-snakecase + uint160 internal constant offset = uint160(0x1111000000000000000000000000000000001111); /// @notice Utility function that converts the address in the L1 that submitted a tx to /// the inbox to the msg.sender viewed in the L2 diff --git a/packages/contracts/contracts/arbitrum/IArbToken.sol b/packages/contracts/contracts/arbitrum/IArbToken.sol index d7d5a2d8c..6517f0c57 100644 --- a/packages/contracts/contracts/arbitrum/IArbToken.sol +++ b/packages/contracts/contracts/arbitrum/IArbToken.sol @@ -29,18 +29,28 @@ */ pragma solidity ^0.7.6; +/** + * @title Arbitrum Token Interface + * @author Edge & Node + * @notice Interface for tokens that can be minted and burned on Arbitrum L2 + */ interface IArbToken { /** * @notice should increase token supply by amount, and should (probably) only be callable by the L1 bridge. + * @param account Account to mint tokens to + * @param amount Amount of tokens to mint */ function bridgeMint(address account, uint256 amount) external; /** * @notice should decrease token supply by amount, and should (probably) only be callable by the L1 bridge. + * @param account Account to burn tokens from + * @param amount Amount of tokens to burn */ function bridgeBurn(address account, uint256 amount) external; /** + * @notice Get the L1 token address * @return address of layer 1 token */ function l1Address() external view returns (address); diff --git a/packages/contracts/contracts/arbitrum/IBridge.sol b/packages/contracts/contracts/arbitrum/IBridge.sol index 536ee075b..92deee878 100644 --- a/packages/contracts/contracts/arbitrum/IBridge.sol +++ b/packages/contracts/contracts/arbitrum/IBridge.sol @@ -25,7 +25,24 @@ pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + +/** + * @title Bridge Interface + * @author Edge & Node + * @notice Interface for the Arbitrum Bridge contract + */ interface IBridge { + /** + * @notice Emitted when a message is delivered to the inbox + * @param messageIndex Index of the message + * @param beforeInboxAcc Inbox accumulator before this message + * @param inbox Address of the inbox + * @param kind Type of the message + * @param sender Address that sent the message + * @param messageDataHash Hash of the message data + */ event MessageDelivered( uint256 indexed messageIndex, bytes32 indexed beforeInboxAcc, @@ -35,38 +52,102 @@ interface IBridge { bytes32 messageDataHash ); + /** + * @notice Emitted when a bridge call is triggered + * @param outbox Address of the outbox + * @param destAddr Destination address for the call + * @param amount ETH amount sent with the call + * @param data Calldata for the function call + */ event BridgeCallTriggered(address indexed outbox, address indexed destAddr, uint256 amount, bytes data); + /** + * @notice Emitted when an inbox is enabled or disabled + * @param inbox Address of the inbox + * @param enabled Whether the inbox is enabled + */ event InboxToggle(address indexed inbox, bool enabled); + /** + * @notice Emitted when an outbox is enabled or disabled + * @param outbox Address of the outbox + * @param enabled Whether the outbox is enabled + */ event OutboxToggle(address indexed outbox, bool enabled); + /** + * @notice Deliver a message to the inbox + * @param kind Type of the message + * @param sender Address that is sending the message + * @param messageDataHash keccak256 hash of the message data + * @return The message index + */ function deliverMessageToInbox( uint8 kind, address sender, bytes32 messageDataHash ) external payable returns (uint256); + /** + * @notice Execute a call from L2 to L1 + * @param destAddr Contract to call + * @param amount ETH value to send + * @param data Calldata for the function call + * @return success True if the call was successful, false otherwise + * @return returnData Return data from the call + */ function executeCall( address destAddr, uint256 amount, bytes calldata data ) external returns (bool success, bytes memory returnData); - // These are only callable by the admin + /** + * @notice Set the address of an inbox + * @param inbox Address of the inbox + * @param enabled Whether to enable the inbox + */ function setInbox(address inbox, bool enabled) external; + /** + * @notice Set the address of an outbox + * @param inbox Address of the outbox + * @param enabled Whether to enable the outbox + */ function setOutbox(address inbox, bool enabled) external; // View functions + /** + * @notice Get the active outbox address + * @return The active outbox address + */ function activeOutbox() external view returns (address); + /** + * @notice Check if an address is an allowed inbox + * @param inbox Address to check + * @return True if the address is an allowed inbox, false otherwise + */ function allowedInboxes(address inbox) external view returns (bool); + /** + * @notice Check if an address is an allowed outbox + * @param outbox Address to check + * @return True if the address is an allowed outbox, false otherwise + */ function allowedOutboxes(address outbox) external view returns (bool); + /** + * @notice Get the inbox accumulator at a specific index + * @param index Index to query + * @return The inbox accumulator at the given index + */ function inboxAccs(uint256 index) external view returns (bytes32); + /** + * @notice Get the count of messages in the inbox + * @return Number of messages in the inbox + */ function messageCount() external view returns (uint256); } diff --git a/packages/contracts/contracts/arbitrum/IInbox.sol b/packages/contracts/contracts/arbitrum/IInbox.sol index a9315bbf8..8ded1c1bb 100644 --- a/packages/contracts/contracts/arbitrum/IInbox.sol +++ b/packages/contracts/contracts/arbitrum/IInbox.sol @@ -25,12 +25,32 @@ pragma solidity ^0.7.6; -import "./IBridge.sol"; -import "./IMessageProvider.sol"; +import { IBridge } from "./IBridge.sol"; +import { IMessageProvider } from "./IMessageProvider.sol"; +/** + * @title Inbox Interface + * @author Edge & Node + * @notice Interface for the Arbitrum Inbox contract + */ interface IInbox is IMessageProvider { + /** + * @notice Send a message to L2 + * @param messageData Encoded data to send in the message + * @return Message number returned by the inbox + */ function sendL2Message(bytes calldata messageData) external returns (uint256); + /** + * @notice Send an unsigned transaction to L2 + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param nonce Nonce for the transaction + * @param destAddr Destination address on L2 + * @param amount Amount of ETH to send + * @param data Transaction data + * @return Message number returned by the inbox + */ function sendUnsignedTransaction( uint256 maxGas, uint256 gasPriceBid, @@ -40,6 +60,15 @@ interface IInbox is IMessageProvider { bytes calldata data ) external returns (uint256); + /** + * @notice Send a contract transaction to L2 + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param destAddr Destination address on L2 + * @param amount Amount of ETH to send + * @param data Transaction data + * @return Message number returned by the inbox + */ function sendContractTransaction( uint256 maxGas, uint256 gasPriceBid, @@ -48,6 +77,15 @@ interface IInbox is IMessageProvider { bytes calldata data ) external returns (uint256); + /** + * @notice Send an L1-funded unsigned transaction to L2 + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param nonce Nonce for the transaction + * @param destAddr Destination address on L2 + * @param data Transaction data + * @return Message number returned by the inbox + */ function sendL1FundedUnsignedTransaction( uint256 maxGas, uint256 gasPriceBid, @@ -56,6 +94,14 @@ interface IInbox is IMessageProvider { bytes calldata data ) external payable returns (uint256); + /** + * @notice Send an L1-funded contract transaction to L2 + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param destAddr Destination address on L2 + * @param data Transaction data + * @return Message number returned by the inbox + */ function sendL1FundedContractTransaction( uint256 maxGas, uint256 gasPriceBid, @@ -63,6 +109,18 @@ interface IInbox is IMessageProvider { bytes calldata data ) external payable returns (uint256); + /** + * @notice Create a retryable ticket for an L2 transaction + * @param destAddr Destination address on L2 + * @param arbTxCallValue Call value for the L2 transaction + * @param maxSubmissionCost Maximum cost for submitting the ticket + * @param submissionRefundAddress Address to refund submission cost to + * @param valueRefundAddress Address to refund excess value to + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param data Transaction data + * @return Message number returned by the inbox + */ function createRetryableTicket( address destAddr, uint256 arbTxCallValue, @@ -74,15 +132,36 @@ interface IInbox is IMessageProvider { bytes calldata data ) external payable returns (uint256); + /** + * @notice Deposit ETH to L2 + * @param maxSubmissionCost Maximum cost for submitting the deposit + * @return Message number returned by the inbox + */ function depositEth(uint256 maxSubmissionCost) external payable returns (uint256); + /** + * @notice Get the bridge contract + * @return The bridge contract address + */ function bridge() external view returns (IBridge); + /** + * @notice Pause the creation of retryable tickets + */ function pauseCreateRetryables() external; + /** + * @notice Unpause the creation of retryable tickets + */ function unpauseCreateRetryables() external; + /** + * @notice Start rewriting addresses + */ function startRewriteAddress() external; + /** + * @notice Stop rewriting addresses + */ function stopRewriteAddress() external; } diff --git a/packages/contracts/contracts/arbitrum/IMessageProvider.sol b/packages/contracts/contracts/arbitrum/IMessageProvider.sol index 8fbfdb171..ce5822358 100644 --- a/packages/contracts/contracts/arbitrum/IMessageProvider.sol +++ b/packages/contracts/contracts/arbitrum/IMessageProvider.sol @@ -25,8 +25,22 @@ pragma solidity ^0.7.6; +/** + * @title Message Provider Interface + * @author Edge & Node + * @notice Interface for Arbitrum message providers + */ interface IMessageProvider { + /** + * @notice Emitted when a message is delivered to the inbox + * @param messageNum Message number + * @param data Message data + */ event InboxMessageDelivered(uint256 indexed messageNum, bytes data); + /** + * @notice Emitted when a message is delivered from origin + * @param messageNum Message number + */ event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); } diff --git a/packages/contracts/contracts/arbitrum/IOutbox.sol b/packages/contracts/contracts/arbitrum/IOutbox.sol index 2e4f05bd5..658a6c233 100644 --- a/packages/contracts/contracts/arbitrum/IOutbox.sol +++ b/packages/contracts/contracts/arbitrum/IOutbox.sol @@ -25,13 +25,36 @@ pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + +/** + * @title Arbitrum Outbox Interface + * @author Edge & Node + * @notice Interface for the Arbitrum outbox contract + */ interface IOutbox { + /** + * @notice Emitted when an outbox entry is created + * @param batchNum Batch number + * @param outboxEntryIndex Index of the outbox entry + * @param outputRoot Output root hash + * @param numInBatch Number of messages in the batch + */ event OutboxEntryCreated( uint256 indexed batchNum, uint256 outboxEntryIndex, bytes32 outputRoot, uint256 numInBatch ); + + /** + * @notice Emitted when an outbox transaction is executed + * @param destAddr Destination address + * @param l2Sender L2 sender address + * @param outboxEntryIndex Index of the outbox entry + * @param transactionIndex Index of the transaction + */ event OutBoxTransactionExecuted( address indexed destAddr, address indexed l2Sender, @@ -39,19 +62,53 @@ interface IOutbox { uint256 transactionIndex ); + /** + * @notice Get the L2 to L1 sender address + * @return The sender address + */ function l2ToL1Sender() external view returns (address); + /** + * @notice Get the L2 to L1 block number + * @return The block number + */ function l2ToL1Block() external view returns (uint256); + /** + * @notice Get the L2 to L1 Ethereum block number + * @return The Ethereum block number + */ function l2ToL1EthBlock() external view returns (uint256); + /** + * @notice Get the L2 to L1 timestamp + * @return The timestamp + */ function l2ToL1Timestamp() external view returns (uint256); + /** + * @notice Get the L2 to L1 batch number + * @return The batch number + */ function l2ToL1BatchNum() external view returns (uint256); + /** + * @notice Get the L2 to L1 output ID + * @return The output ID + */ function l2ToL1OutputId() external view returns (bytes32); + /** + * @notice Process outgoing messages + * @param sendsData Encoded message data + * @param sendLengths Array of message lengths + */ function processOutgoingMessages(bytes calldata sendsData, uint256[] calldata sendLengths) external; + /** + * @notice Check if an outbox entry exists + * @param batchNum Batch number to check + * @return True if the entry exists + */ function outboxEntryExists(uint256 batchNum) external view returns (bool); } diff --git a/packages/contracts/contracts/arbitrum/ITokenGateway.sol b/packages/contracts/contracts/arbitrum/ITokenGateway.sol index 3b12e578e..2a0d1a2f3 100644 --- a/packages/contracts/contracts/arbitrum/ITokenGateway.sol +++ b/packages/contracts/contracts/arbitrum/ITokenGateway.sol @@ -25,6 +25,11 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Token Gateway Interface + * @author Edge & Node + * @notice Interface for token gateways that handle cross-chain token transfers + */ interface ITokenGateway { /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated // event OutboundTransferInitiated( @@ -46,15 +51,33 @@ interface ITokenGateway { // bytes _data // ); + /** + * @notice Transfer tokens from L1 to L2 or L2 to L1 + * @param token Address of the token being transferred + * @param to Recipient address on the destination chain + * @param amount Amount of tokens to transfer + * @param maxGas Maximum gas for the transaction + * @param gasPriceBid Gas price bid for the transaction + * @param data Additional data for the transfer + * @return Transaction data + */ function outboundTransfer( address token, address to, - uint256 amunt, - uint256 maxas, - uint256 gasPiceBid, + uint256 amount, + uint256 maxGas, + uint256 gasPriceBid, bytes calldata data ) external payable returns (bytes memory); + /** + * @notice Finalize an inbound token transfer + * @param token Address of the token being transferred + * @param from Sender address on the source chain + * @param to Recipient address on the destination chain + * @param amount Amount of tokens being transferred + * @param data Additional data for the transfer + */ function finalizeInboundTransfer( address token, address from, diff --git a/packages/contracts/contracts/arbitrum/L1ArbitrumMessenger.sol b/packages/contracts/contracts/arbitrum/L1ArbitrumMessenger.sol index 839e1930b..9613294ad 100644 --- a/packages/contracts/contracts/arbitrum/L1ArbitrumMessenger.sol +++ b/packages/contracts/contracts/arbitrum/L1ArbitrumMessenger.sol @@ -25,20 +25,49 @@ pragma solidity ^0.7.6; -import "./IInbox.sol"; -import "./IOutbox.sol"; +import { IInbox } from "./IInbox.sol"; +import { IOutbox } from "./IOutbox.sol"; +import { IBridge } from "./IBridge.sol"; -/// @notice L1 utility contract to assist with L1 <=> L2 interactions -/// @dev this is an abstract contract instead of library so the functions can be easily overridden when testing +/** + * @title L1 Arbitrum Messenger + * @author Edge & Node + * @notice L1 utility contract to assist with L1 <=> L2 interactions + * @dev this is an abstract contract instead of library so the functions can be easily overridden when testing + */ abstract contract L1ArbitrumMessenger { + /** + * @notice Emitted when a transaction is sent to L2 + * @param _from Address sending the transaction + * @param _to Address receiving the transaction on L2 + * @param _seqNum Sequence number of the retryable ticket + * @param _data Transaction data + */ event TxToL2(address indexed _from, address indexed _to, uint256 indexed _seqNum, bytes _data); + /** + * @dev Parameters for L2 gas configuration + * @param _maxSubmissionCost Maximum cost for submitting the transaction + * @param _maxGas Maximum gas for the L2 transaction + * @param _gasPriceBid Gas price bid for the L2 transaction + */ struct L2GasParams { uint256 _maxSubmissionCost; uint256 _maxGas; uint256 _gasPriceBid; } + /** + * @notice Send a transaction to L2 using gas parameters struct + * @param _inbox Address of the inbox contract + * @param _to Destination address on L2 + * @param _user Address that will be credited as the sender + * @param _l1CallValue ETH value to send with the L1 transaction + * @param _l2CallValue ETH value to send with the L2 transaction + * @param _l2GasParams Gas parameters for the L2 transaction + * @param _data Calldata for the L2 transaction + * @return Sequence number of the retryable ticket + */ function sendTxToL2( address _inbox, address _to, @@ -63,6 +92,19 @@ abstract contract L1ArbitrumMessenger { ); } + /** + * @notice Send a transaction to L2 with individual gas parameters + * @param _inbox Address of the inbox contract + * @param _to Destination address on L2 + * @param _user Address that will be credited as the sender + * @param _l1CallValue ETH value to send with the L1 transaction + * @param _l2CallValue ETH value to send with the L2 transaction + * @param _maxSubmissionCost Maximum cost for submitting the transaction + * @param _maxGas Maximum gas for the L2 transaction + * @param _gasPriceBid Gas price bid for the L2 transaction + * @param _data Calldata for the L2 transaction + * @return Sequence number of the retryable ticket + */ function sendTxToL2( address _inbox, address _to, @@ -88,11 +130,21 @@ abstract contract L1ArbitrumMessenger { return seqNum; } + /** + * @notice Get the bridge contract from an inbox + * @param _inbox Address of the inbox contract + * @return Bridge contract interface + */ function getBridge(address _inbox) internal view virtual returns (IBridge) { return IInbox(_inbox).bridge(); } - /// @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies + /** + * @notice Get the L2 to L1 sender address from the outbox + * @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies + * @param _inbox Address of the inbox contract + * @return Address of the L2 to L1 sender + */ function getL2ToL1Sender(address _inbox) internal view virtual returns (address) { IOutbox outbox = IOutbox(getBridge(_inbox).activeOutbox()); address l2ToL1Sender = outbox.l2ToL1Sender(); diff --git a/packages/contracts/contracts/arbitrum/L2ArbitrumMessenger.sol b/packages/contracts/contracts/arbitrum/L2ArbitrumMessenger.sol index e34a29262..ac6774748 100644 --- a/packages/contracts/contracts/arbitrum/L2ArbitrumMessenger.sol +++ b/packages/contracts/contracts/arbitrum/L2ArbitrumMessenger.sol @@ -25,15 +25,35 @@ pragma solidity ^0.7.6; -import "arbos-precompiles/arbos/builtin/ArbSys.sol"; +import { ArbSys } from "arbos-precompiles/arbos/builtin/ArbSys.sol"; -/// @notice L2 utility contract to assist with L1 <=> L2 interactions -/// @dev this is an abstract contract instead of library so the functions can be easily overridden when testing +/** + * @title L2 Arbitrum Messenger + * @author Edge & Node + * @notice L2 utility contract to assist with L1 <=> L2 interactions + * @dev this is an abstract contract instead of library so the functions can be easily overridden when testing + */ abstract contract L2ArbitrumMessenger { + /// @dev Address of the ArbSys precompile address internal constant ARB_SYS_ADDRESS = address(100); + /** + * @notice Emitted when a transaction is sent to L1 + * @param _from Address sending the transaction + * @param _to Address receiving the transaction on L1 + * @param _id ID of the L2 to L1 message + * @param _data Transaction data + */ event TxToL1(address indexed _from, address indexed _to, uint256 indexed _id, bytes _data); + /** + * @notice Send a transaction from L2 to L1 + * @param _l1CallValue ETH value to send with the L1 transaction + * @param _from Address that is sending the transaction + * @param _to Destination address on L1 + * @param _data Calldata for the L1 transaction + * @return ID of the L2 to L1 message + */ function sendTxToL1( uint256 _l1CallValue, address _from, diff --git a/packages/contracts/contracts/bancor/BancorFormula.sol b/packages/contracts/contracts/bancor/BancorFormula.sol index 689eebaba..0d221be56 100644 --- a/packages/contracts/contracts/bancor/BancorFormula.sol +++ b/packages/contracts/contracts/bancor/BancorFormula.sol @@ -2,35 +2,55 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts/math/SafeMath.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable function-max-lines, gas-increment-by-one, gas-strict-inequalities +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +/** + * @title Bancor Formula Contract + * @author Edge & Node + * @notice Contract implementing Bancor's bonding curve formula for token conversion + */ contract BancorFormula { using SafeMath for uint256; - uint16 public constant version = 6; + /// @notice Version of the Bancor formula implementation + uint16 public constant version = 6; // solhint-disable-line const-name-snakecase + /// @dev Constant representing the value 1 uint256 private constant ONE = 1; + /// @dev Maximum ratio value (100% in parts per million) uint32 private constant MAX_RATIO = 1000000; + /// @dev Minimum precision for calculations uint8 private constant MIN_PRECISION = 32; + /// @dev Maximum precision for calculations uint8 private constant MAX_PRECISION = 127; /** * @dev Auto-generated via 'PrintIntScalingFactors.py' */ + /// @dev Fixed point representation of 1 (2^127) uint256 private constant FIXED_1 = 0x080000000000000000000000000000000; + /// @dev Fixed point representation of 2 (2^128) uint256 private constant FIXED_2 = 0x100000000000000000000000000000000; + /// @dev Maximum number for calculations (2^129) uint256 private constant MAX_NUM = 0x200000000000000000000000000000000; /** * @dev Auto-generated via 'PrintLn2ScalingFactors.py' */ + /// @dev Natural logarithm of 2 numerator for fixed point calculations uint256 private constant LN2_NUMERATOR = 0x3f80fe03f80fe03f80fe03f80fe03f8; + /// @dev Natural logarithm of 2 denominator for fixed point calculations uint256 private constant LN2_DENOMINATOR = 0x5b9de1d10bf4103d647b0955897ba80; /** * @dev Auto-generated via 'PrintFunctionOptimalLog.py' and 'PrintFunctionOptimalExp.py' */ + /// @dev Maximum value for optimal logarithm calculation uint256 private constant OPT_LOG_MAX_VAL = 0x15bf0a8b1457695355fb8ac404e7a79e3; + /// @dev Maximum value for optimal exponentiation calculation uint256 private constant OPT_EXP_MAX_VAL = 0x800000000000000000000000000000000; /** @@ -38,6 +58,7 @@ contract BancorFormula { */ uint256[128] private maxExpArray; + /// @notice Initialize the Bancor formula with maximum exponent array values constructor() { // maxExpArray[ 0] = 0x6bffffffffffffffffffffffffffffffff; // maxExpArray[ 1] = 0x67ffffffffffffffffffffffffffffffff; @@ -170,7 +191,7 @@ contract BancorFormula { } /** - * @dev given a token supply, reserve balance, ratio and a deposit amount (in the reserve token), + * @notice Given a token supply, reserve balance, ratio and a deposit amount (in the reserve token), * calculates the return for a given conversion (in the main token) * * Formula: @@ -210,7 +231,7 @@ contract BancorFormula { } /** - * @dev given a token supply, reserve balance, ratio and a sell amount (in the main token), + * @notice Given a token supply, reserve balance, ratio and a sell amount (in the main token), * calculates the return for a given conversion (in the reserve token) * * Formula: @@ -258,7 +279,7 @@ contract BancorFormula { } /** - * @dev given two reserve balances/ratios and a sell amount (in the first reserve token), + * @notice Given two reserve balances/ratios and a sell amount (in the first reserve token), * calculates the return for a conversion from the first reserve token to the second reserve token (in the second reserve token) * note that prior to version 4, you should use 'calculateCrossConnectorReturn' instead * @@ -304,7 +325,7 @@ contract BancorFormula { } /** - * @dev given a smart token supply, reserve balance, total ratio and an amount of requested smart tokens, + * @notice Given a smart token supply, reserve balance, total ratio and an amount of requested smart tokens, * calculates the amount of reserve tokens required for purchasing the given amount of smart tokens * * Formula: @@ -341,7 +362,7 @@ contract BancorFormula { } /** - * @dev given a smart token supply, reserve balance, total ratio and an amount of smart tokens to liquidate, + * @notice Given a smart token supply, reserve balance, total ratio and an amount of smart tokens to liquidate, * calculates the amount of reserve tokens received for selling the given amount of smart tokens * * Formula: @@ -384,7 +405,7 @@ contract BancorFormula { } /** - * @dev General Description: + * @notice General Description: * Determine a value of precision. * Calculate an integer approximation of (_baseN / _baseD) ^ (_expN / _expD) * 2 ^ precision. * Return the result along with the precision used. @@ -400,6 +421,12 @@ contract BancorFormula { * This allows us to compute "base ^ exp" with maximum accuracy and without exceeding 256 bits in any of the intermediate computations. * This functions assumes that "_expN < 2 ^ 256 / log(MAX_NUM - 1)", otherwise the multiplication should be replaced with a "safeMul". * Since we rely on unsigned-integer arithmetic and "base < 1" ==> "log(base) < 0", this function does not support "_baseN < _baseD". + * @param _baseN Base numerator + * @param _baseD Base denominator + * @param _expN Exponent numerator + * @param _expD Exponent denominator + * @return result The computed power result + * @return precision The precision used in the calculation */ function power(uint256 _baseN, uint256 _baseD, uint32 _expN, uint32 _expD) internal view returns (uint256, uint8) { require(_baseN < MAX_NUM); @@ -422,8 +449,10 @@ contract BancorFormula { } /** - * @dev computes log(x / FIXED_1) * FIXED_1. + * @notice Computes log(x / FIXED_1) * FIXED_1. * This functions assumes that "x >= FIXED_1", because the output would be negative otherwise. + * @param x The input value (must be >= FIXED_1) + * @return The computed logarithm */ function generalLog(uint256 x) internal pure returns (uint256) { uint256 res = 0; @@ -450,7 +479,9 @@ contract BancorFormula { } /** - * @dev computes the largest integer smaller than or equal to the binary logarithm of the input. + * @notice Computes the largest integer smaller than or equal to the binary logarithm of the input. + * @param _n The input value + * @return The floor of the binary logarithm */ function floorLog2(uint256 _n) internal pure returns (uint8) { uint8 res = 0; @@ -475,9 +506,11 @@ contract BancorFormula { } /** - * @dev the global "maxExpArray" is sorted in descending order, and therefore the following statements are equivalent: + * @notice The global "maxExpArray" is sorted in descending order, and therefore the following statements are equivalent: * - This function finds the position of [the smallest value in "maxExpArray" larger than or equal to "x"] * - This function finds the highest position of [a value in "maxExpArray" larger than or equal to "x"] + * @param _x The value to find position for + * @return The position in the maxExpArray */ function findPositionInMaxExpArray(uint256 _x) internal view returns (uint8) { uint8 lo = MIN_PRECISION; @@ -497,11 +530,14 @@ contract BancorFormula { } /** - * @dev this function can be auto-generated by the script 'PrintFunctionGeneralExp.py'. + * @notice This function can be auto-generated by the script 'PrintFunctionGeneralExp.py'. * it approximates "e ^ x" via maclaurin summation: "(x^0)/0! + (x^1)/1! + ... + (x^n)/n!". * it returns "e ^ (x / 2 ^ precision) * 2 ^ precision", that is, the result is upshifted for accuracy. * the global "maxExpArray" maps each "precision" to "((maximumExponent + 1) << (MAX_PRECISION - precision)) - 1". * the maximum permitted value for "x" is therefore given by "maxExpArray[precision] >> (MAX_PRECISION - precision)". + * @param _x The exponent value + * @param _precision The precision to use + * @return The computed exponential result */ function generalExp(uint256 _x, uint8 _precision) internal pure returns (uint256) { uint256 xi = _x; @@ -576,7 +612,7 @@ contract BancorFormula { } /** - * @dev computes log(x / FIXED_1) * FIXED_1 + * @notice Computes log(x / FIXED_1) * FIXED_1 * Input range: FIXED_1 <= x <= LOG_EXP_MAX_VAL - 1 * Auto-generated via 'PrintFunctionOptimalLog.py' * Detailed description: @@ -585,6 +621,8 @@ contract BancorFormula { * - The natural logarithm of r is calculated via Taylor series for log(1 + x), where x = r - 1 * - The natural logarithm of the input is calculated by summing up the intermediate results above * - For example: log(250) = log(e^4 * e^1 * e^0.5 * 1.021692859) = 4 + 1 + 0.5 + log(1 + 0.021692859) + * @param x The input value + * @return The computed logarithm */ function optimalLog(uint256 x) internal pure returns (uint256) { uint256 res = 0; @@ -648,7 +686,7 @@ contract BancorFormula { } /** - * @dev computes e ^ (x / FIXED_1) * FIXED_1 + * @notice Computes e ^ (x / FIXED_1) * FIXED_1 * input range: 0 <= x <= OPT_EXP_MAX_VAL - 1 * auto-generated via 'PrintFunctionOptimalExp.py' * Detailed description: @@ -657,6 +695,8 @@ contract BancorFormula { * - The exponentiation of r is calculated via Taylor series for e^x, where x = r * - The exponentiation of the input is calculated by multiplying the intermediate results above * - For example: e^5.521692859 = e^(4 + 1 + 0.5 + 0.021692859) = e^4 * e^1 * e^0.5 * e^0.021692859 + * @param x The input value + * @return The computed exponential result */ function optimalExp(uint256 x) internal pure returns (uint256) { uint256 res = 0; @@ -724,7 +764,13 @@ contract BancorFormula { } /** - * @dev deprecated, backward compatibility + * @notice Deprecated function for backward compatibility + * @param _fromConnectorBalance input connector balance + * @param _fromConnectorWeight input connector weight + * @param _toConnectorBalance output connector balance + * @param _toConnectorWeight output connector weight + * @param _amount input connector amount + * @return output connector amount */ function calculateCrossConnectorReturn( uint256 _fromConnectorBalance, diff --git a/packages/contracts/contracts/base/IMulticall.sol b/packages/contracts/contracts/base/IMulticall.sol index 10f7fa469..07f40ea36 100644 --- a/packages/contracts/contracts/base/IMulticall.sol +++ b/packages/contracts/contracts/base/IMulticall.sol @@ -5,6 +5,7 @@ pragma abicoder v2; /** * @title Multicall interface + * @author Edge & Node * @notice Enables calling multiple methods in a single call to the contract */ interface IMulticall { diff --git a/packages/contracts/contracts/base/Multicall.sol b/packages/contracts/contracts/base/Multicall.sol index 49111840d..9d9d48d34 100644 --- a/packages/contracts/contracts/base/Multicall.sol +++ b/packages/contracts/contracts/base/Multicall.sol @@ -3,13 +3,17 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "./IMulticall.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one + +import { IMulticall } from "./IMulticall.sol"; // Inspired by https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/base/Multicall.sol // Note: Removed payable from the multicall /** * @title Multicall + * @author Edge & Node * @notice Enables calling multiple methods in a single call to the contract */ abstract contract Multicall is IMulticall { @@ -17,11 +21,12 @@ abstract contract Multicall is IMulticall { function multicall(bytes[] calldata data) external override returns (bytes[] memory results) { results = new bytes[](data.length); for (uint256 i = 0; i < data.length; i++) { - (bool success, bytes memory result) = address(this).delegatecall(data[i]); + (bool success, bytes memory result) = address(this).delegatecall(data[i]); // solhint-disable-line avoid-low-level-calls if (!success) { // Next 5 lines from https://ethereum.stackexchange.com/a/83577 if (result.length < 68) revert(); + // solhint-disable-next-line no-inline-assembly assembly { result := add(result, 0x04) } diff --git a/packages/contracts/contracts/curation/Curation.sol b/packages/contracts/contracts/curation/Curation.sol index 827c230b7..acbbc5df9 100644 --- a/packages/contracts/contracts/curation/Curation.sol +++ b/packages/contracts/contracts/curation/Curation.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events, gas-small-strings, gas-strict-inequalities + import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; import { ClonesUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol"; @@ -10,7 +13,7 @@ import { ClonesUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/Clo import { BancorFormula } from "../bancor/BancorFormula.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; import { TokenUtils } from "../utils/TokenUtils.sol"; -import { IRewardsManager } from "../rewards/IRewardsManager.sol"; +import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { Managed } from "../governance/Managed.sol"; import { IGraphToken } from "../token/IGraphToken.sol"; import { CurationV2Storage } from "./CurationStorage.sol"; @@ -18,7 +21,8 @@ import { IGraphCurationToken } from "./IGraphCurationToken.sol"; /** * @title Curation contract - * @dev Allows curators to signal on subgraph deployments that might be relevant to indexers by + * @author Edge & Node + * @notice Allows curators to signal on subgraph deployments that might be relevant to indexers by * staking Graph Tokens (GRT). Additionally, curators earn fees from the Query Market related to the * subgraph deployment they curate. * A curators deposit goes to a curation pool along with the deposits of other curators, @@ -40,9 +44,14 @@ contract Curation is CurationV2Storage, GraphUpgradeable { // -- Events -- /** - * @dev Emitted when `curator` deposited `tokens` on `subgraphDeploymentID` as curation signal. + * @notice Emitted when `curator` deposited `tokens` on `subgraphDeploymentID` as curation signal. * The `curator` receives `signal` amount according to the curation pool bonding curve. * An amount of `curationTax` will be collected and burned. + * @param curator Address of the curator + * @param subgraphDeploymentID Subgraph deployment being signaled on + * @param tokens Amount of tokens deposited + * @param signal Amount of signal minted + * @param curationTax Amount of tokens burned as curation tax */ event Signalled( address indexed curator, @@ -53,14 +62,20 @@ contract Curation is CurationV2Storage, GraphUpgradeable { ); /** - * @dev Emitted when `curator` burned `signal` for a `subgraphDeploymentID`. + * @notice Emitted when `curator` burned `signal` for a `subgraphDeploymentID`. * The curator will receive `tokens` according to the value of the bonding curve. + * @param curator Address of the curator + * @param subgraphDeploymentID Subgraph deployment being signaled on + * @param tokens Amount of tokens received + * @param signal Amount of signal burned */ event Burned(address indexed curator, bytes32 indexed subgraphDeploymentID, uint256 tokens, uint256 signal); /** - * @dev Emitted when `tokens` amount were collected for `subgraphDeploymentID` as part of fees + * @notice Emitted when `tokens` amount were collected for `subgraphDeploymentID` as part of fees * distributed by an indexer from query fees received from state channels. + * @param subgraphDeploymentID Subgraph deployment that collected fees + * @param tokens Amount of tokens collected as fees */ event Collected(bytes32 indexed subgraphDeploymentID, uint256 tokens); @@ -94,8 +109,7 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Set the default reserve ratio percentage for a curation pool. - * @notice Update the default reserve ratio to `_defaultReserveRatio` + * @notice Set the default reserve ratio percentage for a curation pool. * @param _defaultReserveRatio Reserve ratio (in PPM) */ function setDefaultReserveRatio(uint32 _defaultReserveRatio) external override onlyGovernor { @@ -103,8 +117,7 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Set the minimum deposit amount for curators. - * @notice Update the minimum deposit amount to `_minimumCurationDeposit` + * @notice Set the minimum deposit amount for curators. * @param _minimumCurationDeposit Minimum amount of tokens required deposit */ function setMinimumCurationDeposit(uint256 _minimumCurationDeposit) external override onlyGovernor { @@ -207,7 +220,6 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Return an amount of signal to get tokens back. * @notice Burn _signal from the SubgraphDeployment curation pool * @param _subgraphDeploymentID SubgraphDeployment the curator is returning signal * @param _signalIn Amount of signal to return @@ -313,7 +325,7 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Calculate amount of signal that can be bought with tokens in a curation pool. + * @notice Calculate amount of signal that can be bought with tokens in a curation pool. * @param _subgraphDeploymentID Subgraph deployment to mint signal * @param _tokensIn Amount of tokens used to mint signal * @return Amount of signal that can be bought with tokens @@ -367,7 +379,6 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Internal: Set the default reserve ratio percentage for a curation pool. * @notice Update the default reserver ratio to `_defaultReserveRatio` * @param _defaultReserveRatio Reserve ratio (in PPM) */ @@ -381,7 +392,6 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Internal: Set the minimum deposit amount for curators. * @notice Update the minimum deposit amount to `_minimumCurationDeposit` * @param _minimumCurationDeposit Minimum amount of tokens required deposit */ @@ -393,7 +403,7 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Internal: Set the curation tax percentage (in PPM) to charge when a curator deposits GRT tokens. + * @notice Internal: Set the curation tax percentage (in PPM) to charge when a curator deposits GRT tokens. * @param _percentage Curation tax charged when depositing GRT tokens in PPM */ function _setCurationTaxPercentage(uint32 _percentage) private { @@ -404,7 +414,7 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Internal: Set the master copy to use as clones for the curation token. + * @notice Internal: Set the master copy to use as clones for the curation token. * @param _curationTokenMaster Address of implementation contract to use for curation tokens */ function _setCurationTokenMaster(address _curationTokenMaster) private { @@ -416,7 +426,7 @@ contract Curation is CurationV2Storage, GraphUpgradeable { } /** - * @dev Triggers an update of rewards due to a change in signal. + * @notice Triggers an update of rewards due to a change in signal. * @param _subgraphDeploymentID Subgraph deployment updated */ function _updateRewards(bytes32 _subgraphDeploymentID) private { diff --git a/packages/contracts/contracts/curation/CurationStorage.sol b/packages/contracts/contracts/curation/CurationStorage.sol index 12f5b255b..92e0f7843 100644 --- a/packages/contracts/contracts/curation/CurationStorage.sol +++ b/packages/contracts/contracts/curation/CurationStorage.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// solhint-disable one-contract-per-file pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable named-parameters-mapping + import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; import { ICuration } from "./ICuration.sol"; @@ -10,7 +14,8 @@ import { Managed } from "../governance/Managed.sol"; /** * @title Curation Storage version 1 - * @dev This contract holds the first version of the storage variables + * @author Edge & Node + * @notice This contract holds the first version of the storage variables * for the Curation and L2Curation contracts. * When adding new variables, create a new version that inherits this and update * the contracts to use the new version instead. @@ -21,6 +26,9 @@ abstract contract CurationV1Storage is Managed, ICuration { /** * @dev CurationPool structure that holds the pool's state * for a particular subgraph deployment. + * @param tokens GRT Tokens stored as reserves for the subgraph deployment + * @param reserveRatio Ratio for the bonding curve, unused and deprecated in L2 where it will always be 100% but appear as 0 + * @param gcs Curation token contract for this curation pool */ struct CurationPool { uint256 tokens; // GRT Tokens stored as reserves for the subgraph deployment @@ -30,35 +38,36 @@ abstract contract CurationV1Storage is Managed, ICuration { // -- State -- - /// Tax charged when curators deposit funds. + /// @notice Tax charged when curators deposit funds. /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) uint32 public override curationTaxPercentage; - /// Default reserve ratio to configure curator shares bonding curve + /// @notice Default reserve ratio to configure curator shares bonding curve /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%). /// Unused in L2. uint32 public defaultReserveRatio; - /// Master copy address that holds implementation of curation token. + /// @notice Master copy address that holds implementation of curation token. /// @dev This is used as the target for GraphCurationToken clones. address public curationTokenMaster; - /// Minimum amount allowed to be deposited by curators to initialize a pool + /// @notice Minimum amount allowed to be deposited by curators to initialize a pool /// @dev This is the `startPoolBalance` for the bonding curve uint256 public minimumCurationDeposit; - /// Bonding curve library + /// @notice Bonding curve library /// Unused in L2. address public bondingCurve; - /// @dev Mapping of subgraphDeploymentID => CurationPool + /// @notice Mapping of subgraphDeploymentID => CurationPool /// There is only one CurationPool per SubgraphDeploymentID mapping(bytes32 => CurationPool) public pools; } /** * @title Curation Storage version 2 - * @dev This contract holds the second version of the storage variables + * @author Edge & Node + * @notice This contract holds the second version of the storage variables * for the Curation and L2Curation contracts. * It doesn't add new variables at this contract's level, but adds the Initializable * contract to the inheritance chain, which includes storage variables. @@ -71,6 +80,8 @@ abstract contract CurationV2Storage is CurationV1Storage, Initializable { /** * @title Curation Storage version 3 + * @author Edge & Node + * @notice This contract holds the third version of the storage variables for the Curation and L2Curation contracts * @dev This contract holds the third version of the storage variables * for the Curation and L2Curation contracts. * It adds a new variable subgraphService to the storage. @@ -78,6 +89,6 @@ abstract contract CurationV2Storage is CurationV1Storage, Initializable { * the contracts to use the new version instead. */ abstract contract CurationV3Storage is CurationV2Storage { - // Address of the subgraph service + /// @notice Address of the subgraph service address public subgraphService; } diff --git a/packages/contracts/contracts/curation/GraphCurationToken.sol b/packages/contracts/contracts/curation/GraphCurationToken.sol index 78b721e1b..108cc0680 100644 --- a/packages/contracts/contracts/curation/GraphCurationToken.sol +++ b/packages/contracts/contracts/curation/GraphCurationToken.sol @@ -2,13 +2,14 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../governance/Governed.sol"; +import { Governed } from "../governance/Governed.sol"; /** * @title GraphCurationToken contract - * @dev This is the implementation of the Curation ERC20 token (GCS). + * @author Edge & Node + * @notice This is the implementation of the Curation ERC20 token (GCS). * * GCS are created for each subgraph deployment curated in the Curation contract. * The Curation contract is the owner of GCS tokens and the only one allowed to mint or @@ -20,7 +21,7 @@ import "../governance/Governed.sol"; */ contract GraphCurationToken is ERC20Upgradeable, Governed { /** - * @dev Graph Curation Token Contract initializer. + * @notice Graph Curation Token Contract initializer. * @param _owner Address of the contract issuing this token */ function initialize(address _owner) external initializer { @@ -29,7 +30,7 @@ contract GraphCurationToken is ERC20Upgradeable, Governed { } /** - * @dev Mint new tokens. + * @notice Mint new tokens. * @param _to Address to send the newly minted tokens * @param _amount Amount of tokens to mint */ @@ -38,7 +39,7 @@ contract GraphCurationToken is ERC20Upgradeable, Governed { } /** - * @dev Burn tokens from an address. + * @notice Burn tokens from an address. * @param _account Address from where tokens will be burned * @param _amount Amount of tokens to burn */ diff --git a/packages/contracts/contracts/curation/ICuration.sol b/packages/contracts/contracts/curation/ICuration.sol index 4f2c2bac5..b71273c25 100644 --- a/packages/contracts/contracts/curation/ICuration.sol +++ b/packages/contracts/contracts/curation/ICuration.sol @@ -4,7 +4,8 @@ pragma solidity ^0.7.6 || 0.8.27; /** * @title Curation Interface - * @dev Interface for the Curation contract (and L2Curation too) + * @author Edge & Node + * @notice Interface for the Curation contract (and L2Curation too) */ interface ICuration { // -- Configuration -- diff --git a/packages/contracts/contracts/curation/IGraphCurationToken.sol b/packages/contracts/contracts/curation/IGraphCurationToken.sol index 43679aba6..10dda6dcf 100644 --- a/packages/contracts/contracts/curation/IGraphCurationToken.sol +++ b/packages/contracts/contracts/curation/IGraphCurationToken.sol @@ -2,12 +2,31 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +/** + * @title Graph Curation Token Interface + * @author Edge & Node + * @notice Interface for curation tokens that represent shares in subgraph curation pools + */ interface IGraphCurationToken is IERC20Upgradeable { + /** + * @notice Graph Curation Token Contract initializer. + * @param _owner Address of the contract issuing this token + */ function initialize(address _owner) external; + /** + * @notice Burn tokens from an address. + * @param _account Address from where tokens will be burned + * @param _amount Amount of tokens to burn + */ function burnFrom(address _account, uint256 _amount) external; + /** + * @notice Mint new tokens. + * @param _to Address to send the newly minted tokens + * @param _amount Amount of tokens to mint + */ function mint(address _to, uint256 _amount) external; } diff --git a/packages/contracts/contracts/discovery/GNS.sol b/packages/contracts/contracts/discovery/GNS.sol index 3cbb9ca8a..27310242c 100644 --- a/packages/contracts/contracts/discovery/GNS.sol +++ b/packages/contracts/contracts/discovery/GNS.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable function-max-lines, gas-indexed-events, gas-small-strings, gas-strict-inequalities + import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; @@ -18,7 +21,8 @@ import { GNSV3Storage } from "./GNSStorage.sol"; /** * @title GNS - * @dev The Graph Name System contract provides a decentralized naming system for subgraphs + * @author Edge & Node + * @notice The Graph Name System contract provides a decentralized naming system for subgraphs * used in the scope of the Graph Network. It translates Subgraphs into Subgraph Versions. * Each version is associated with a Subgraph Deployment. The contract has no knowledge of * human-readable names. All human readable names emitted in events. @@ -34,15 +38,21 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { uint32 private constant MAX_PPM = 1000000; /// @dev Equates to Connector weight on bancor formula to be CW = 1 + // solhint-disable-next-line immutable-vars-naming uint32 internal immutable fixedReserveRatio = MAX_PPM; // -- Events -- - /// @dev Emitted when the subgraph NFT contract is updated + /// @notice Emitted when the subgraph NFT contract is updated + /// @param subgraphNFT Address of the new subgraph NFT contract event SubgraphNFTUpdated(address subgraphNFT); /** - * @dev Emitted when graph account sets its default name + * @notice Emitted when graph account sets its default name + * @param graphAccount Address of the graph account + * @param nameSystem Name system identifier (only ENS for now) + * @param nameIdentifier Name identifier in the name system + * @param name Human-readable name */ event SetDefaultName( address indexed graphAccount, @@ -52,12 +62,17 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { ); /** - * @dev Emitted when the subgraph metadata is updated. + * @notice Emitted when the subgraph metadata is updated. + * @param subgraphID ID of the subgraph + * @param subgraphMetadata IPFS hash of the subgraph metadata */ event SubgraphMetadataUpdated(uint256 indexed subgraphID, bytes32 subgraphMetadata); /** - * @dev Emitted when a subgraph version is updated. + * @notice Emitted when a subgraph version is updated. + * @param subgraphID ID of the subgraph + * @param subgraphDeploymentID Subgraph deployment ID for the new version + * @param versionMetadata IPFS hash of the version metadata */ event SubgraphVersionUpdated( uint256 indexed subgraphID, @@ -66,7 +81,12 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { ); /** - * @dev Emitted when a curator mints signal. + * @notice Emitted when a curator mints signal. + * @param subgraphID ID of the subgraph + * @param curator Address of the curator + * @param nSignalCreated Amount of name signal created + * @param vSignalCreated Amount of version signal created + * @param tokensDeposited Amount of tokens deposited */ event SignalMinted( uint256 indexed subgraphID, @@ -77,7 +97,12 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { ); /** - * @dev Emitted when a curator burns signal. + * @notice Emitted when a curator burns signal. + * @param subgraphID ID of the subgraph + * @param curator Address of the curator + * @param nSignalBurnt Amount of name signal burned + * @param vSignalBurnt Amount of version signal burned + * @param tokensReceived Amount of tokens received */ event SignalBurned( uint256 indexed subgraphID, @@ -88,7 +113,11 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { ); /** - * @dev Emitted when a curator transfers signal. + * @notice Emitted when a curator transfers signal. + * @param subgraphID ID of the subgraph + * @param from Address transferring the signal + * @param to Address receiving the signal + * @param nSignalTransferred Amount of name signal transferred */ event SignalTransferred( uint256 indexed subgraphID, @@ -98,14 +127,21 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { ); /** - * @dev Emitted when a subgraph is created. + * @notice Emitted when a subgraph is created. + * @param subgraphID ID of the subgraph + * @param subgraphDeploymentID Subgraph deployment ID + * @param reserveRatio Reserve ratio for the bonding curve */ event SubgraphPublished(uint256 indexed subgraphID, bytes32 indexed subgraphDeploymentID, uint32 reserveRatio); /** - * @dev Emitted when a subgraph is upgraded to point to a new + * @notice Emitted when a subgraph is upgraded to point to a new * subgraph deployment, burning all the old vSignal and depositing the GRT into the * new vSignal curve. + * @param subgraphID ID of the subgraph + * @param vSignalCreated Amount of version signal created in the new deployment + * @param tokensSignalled Amount of tokens signalled in the new deployment + * @param subgraphDeploymentID New subgraph deployment ID */ event SubgraphUpgraded( uint256 indexed subgraphID, @@ -115,29 +151,39 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { ); /** - * @dev Emitted when a subgraph is deprecated. + * @notice Emitted when a subgraph is deprecated. + * @param subgraphID ID of the subgraph + * @param withdrawableGRT Amount of GRT available for withdrawal */ event SubgraphDeprecated(uint256 indexed subgraphID, uint256 withdrawableGRT); /** - * @dev Emitted when a curator withdraws GRT from a deprecated subgraph + * @notice Emitted when a curator withdraws GRT from a deprecated subgraph + * @param subgraphID ID of the subgraph + * @param curator Address of the curator + * @param nSignalBurnt Amount of name signal burned + * @param withdrawnGRT Amount of GRT withdrawn */ event GRTWithdrawn(uint256 indexed subgraphID, address indexed curator, uint256 nSignalBurnt, uint256 withdrawnGRT); /** - * @dev Emitted when the counterpart (L1/L2) GNS address is updated + * @notice Emitted when the counterpart (L1/L2) GNS address is updated + * @param _counterpart Address of the counterpart GNS contract */ event CounterpartGNSAddressUpdated(address _counterpart); // -- Modifiers -- /** - * @dev Emitted when a legacy subgraph is claimed + * @notice Emitted when a legacy subgraph is claimed + * @param graphAccount Address of the graph account that created the subgraph + * @param subgraphNumber Sequence number of the subgraph */ event LegacySubgraphClaimed(address indexed graphAccount, uint256 subgraphNumber); /** - * @dev Modifier that allows only a subgraph operator to be the caller + * @notice Modifier that allows only a subgraph operator to be the caller + * @param _subgraphID ID of the subgraph to check authorization for */ modifier onlySubgraphAuth(uint256 _subgraphID) { require(ownerOf(_subgraphID) == msg.sender, "GNS: Must be authorized"); @@ -160,7 +206,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Approve curation contract to pull funds. + * @inheritdoc IGNS */ function approveAll() external override { graphToken().approve(address(curation()), type(uint256).max); @@ -169,9 +215,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { // -- Config -- /** - * @notice Set the owner fee percentage. This is used to prevent a subgraph owner to drain all - * the name curators tokens while upgrading or deprecating and is configurable in parts per million. - * @param _ownerTaxPercentage Owner tax percentage + * @inheritdoc IGNS */ function setOwnerTaxPercentage(uint32 _ownerTaxPercentage) external override onlyGovernor { _setOwnerTaxPercentage(_ownerTaxPercentage); @@ -200,11 +244,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { // -- Actions -- /** - * @notice Allows a graph account to set a default name - * @param _graphAccount Account that is setting its name - * @param _nameSystem Name system account already has ownership of a name in - * @param _nameIdentifier The unique identifier that is used to identify the name in the system - * @param _name The name being set as default + * @inheritdoc IGNS */ function setDefaultName( address _graphAccount, @@ -217,9 +257,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Allows a subgraph owner to update the metadata of a subgraph they have published - * @param _subgraphID Subgraph ID - * @param _subgraphMetadata IPFS hash for the subgraph metadata + * @inheritdoc IGNS */ function updateSubgraphMetadata( uint256 _subgraphID, @@ -229,10 +267,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Publish a new subgraph. - * @param _subgraphDeploymentID Subgraph deployment for the subgraph - * @param _versionMetadata IPFS hash for the subgraph version metadata - * @param _subgraphMetadata IPFS hash for the subgraph metadata + * @inheritdoc IGNS */ function publishNewSubgraph( bytes32 _subgraphDeploymentID, @@ -261,10 +296,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Publish a new version of an existing subgraph. - * @param _subgraphID Subgraph ID - * @param _subgraphDeploymentID Subgraph deployment ID of the new version - * @param _versionMetadata IPFS hash for the subgraph version metadata + * @inheritdoc IGNS */ function publishNewVersion( uint256 _subgraphID, @@ -322,10 +354,10 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Deprecate a subgraph. The bonding curve is destroyed, the vSignal is burned, and the GNS + * @inheritdoc IGNS + * @notice The bonding curve is destroyed, the vSignal is burned, and the GNS * contract holds the GRT from burning the vSignal, which all curators can withdraw manually. * Can only be done by the subgraph owner. - * @param _subgraphID Subgraph ID */ function deprecateSubgraph(uint256 _subgraphID) external override notPaused onlySubgraphAuth(_subgraphID) { // Subgraph check @@ -350,10 +382,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Deposit GRT into a subgraph and mint signal. - * @param _subgraphID Subgraph ID - * @param _tokensIn The amount of tokens the nameCurator wants to deposit - * @param _nSignalOutMin Expected minimum amount of name signal to receive + * @inheritdoc IGNS */ function mintSignal( uint256 _subgraphID, @@ -383,10 +412,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Burn signal for a subgraph and return the GRT. - * @param _subgraphID Subgraph ID - * @param _nSignal The amount of nSignal the nameCurator wants to burn - * @param _tokensOutMin Expected minimum amount of tokens to receive + * @inheritdoc IGNS */ function burnSignal( uint256 _subgraphID, @@ -543,7 +569,9 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { * @notice Calculate subgraph signal to be returned for an amount of tokens. * @param _subgraphID Subgraph ID * @param _tokensIn Tokens being exchanged for subgraph signal - * @return Amount of subgraph signal and curation tax + * @return nSignalOut Amount of name signal minted + * @return curationTax Amount of curation tax charged + * @return vSignalOut Amount of version signal minted */ function tokensToNSignal( uint256 _subgraphID, @@ -562,7 +590,8 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { * @notice Calculate tokens returned for an amount of subgraph signal. * @param _subgraphID Subgraph ID * @param _nSignalIn Subgraph signal being exchanged for tokens - * @return Amount of tokens returned for an amount of subgraph signal + * @return vSignalOut Amount of version signal burned + * @return tokensOut Amount of tokens returned */ function nSignalToTokens(uint256 _subgraphID, uint256 _nSignalIn) public view override returns (uint256, uint256) { // Get subgraph or revert if not published @@ -574,10 +603,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Calculate subgraph signal to be returned for an amount of subgraph deployment signal. - * @param _subgraphID Subgraph ID - * @param _vSignalIn Amount of subgraph deployment signal to exchange for subgraph signal - * @return Amount of subgraph signal that can be bought + * @inheritdoc IGNS */ function vSignalToNSignal(uint256 _subgraphID, uint256 _vSignalIn) public view override returns (uint256) { SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); @@ -591,10 +617,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Calculate subgraph deployment signal to be returned for an amount of subgraph signal. - * @param _subgraphID Subgraph ID - * @param _nSignalIn Subgraph signal being exchanged for subgraph deployment signal - * @return Amount of subgraph deployment signal that can be returned + * @inheritdoc IGNS */ function nSignalToVSignal(uint256 _subgraphID, uint256 _nSignalIn) public view override returns (uint256) { SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); @@ -602,29 +625,21 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Get the amount of subgraph signal a curator has. - * @param _subgraphID Subgraph ID - * @param _curator Curator address - * @return Amount of subgraph signal owned by a curator + * @inheritdoc IGNS */ function getCuratorSignal(uint256 _subgraphID, address _curator) public view override returns (uint256) { return _getSubgraphData(_subgraphID).curatorNSignal[_curator]; } /** - * @notice Return whether a subgraph is published. - * @param _subgraphID Subgraph ID - * @return Return true if subgraph is currently published + * @inheritdoc IGNS */ function isPublished(uint256 _subgraphID) public view override returns (bool) { return _isPublished(_getSubgraphData(_subgraphID)); } /** - * @notice Returns account and sequence ID for a legacy subgraph (created before subgraph NFTs). - * @param _subgraphID Subgraph ID - * @return account Account that created the subgraph (or 0 if it's not a legacy subgraph) - * @return seqID Sequence number for the subgraph + * @inheritdoc IGNS */ function getLegacySubgraphKey(uint256 _subgraphID) public view override returns (address account, uint256 seqID) { LegacySubgraphKey storage legacySubgraphKey = legacySubgraphKeys[_subgraphID]; @@ -633,16 +648,14 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @notice Return the owner of a subgraph. - * @param _tokenID Subgraph ID - * @return Owner address + * @inheritdoc IGNS */ function ownerOf(uint256 _tokenID) public view override returns (address) { return subgraphNFT.ownerOf(_tokenID); } /** - * @dev Calculate tax that owner will have to cover for upgrading or deprecating. + * @notice Calculate tax that owner will have to cover for upgrading or deprecating. * @param _tokens Tokens that were received from deprecating the old subgraph * @param _owner Subgraph owner * @param _curationTaxPercentage Tax percentage on curation deposits from Curation contract @@ -689,8 +702,9 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Return the next subgraphID given the account that is creating the subgraph. + * @notice Return the next subgraphID given the account that is creating the subgraph. * NOTE: This function updates the sequence ID for the account + * @param _account The account creating the subgraph * @return Sequence ID for the account */ function _nextSubgraphID(address _account) internal returns (uint256) { @@ -698,8 +712,9 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Return a new consecutive sequence ID for an account and update to the next value. + * @notice Return a new consecutive sequence ID for an account and update to the next value. * NOTE: This function updates the sequence ID for the account + * @param _account The account to get the next sequence ID for * @return Sequence ID for the account */ function _nextAccountSeqID(address _account) internal returns (uint256) { @@ -709,7 +724,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Mint the NFT for the subgraph. + * @notice Mint the NFT for the subgraph. * @param _owner Owner address * @param _tokenID Subgraph ID */ @@ -718,7 +733,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Burn the NFT for the subgraph. + * @notice Burn the NFT for the subgraph. * @param _tokenID Subgraph ID */ function _burnNFT(uint256 _tokenID) internal { @@ -726,7 +741,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Set the subgraph metadata. + * @notice Set the subgraph metadata. * @param _tokenID Subgraph ID * @param _subgraphMetadata IPFS hash of the subgraph metadata */ @@ -739,7 +754,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Get subgraph data. + * @notice Get subgraph data. * This function will first look for a v1 subgraph and return it if found. * @param _subgraphID Subgraph ID * @return Subgraph Data @@ -755,7 +770,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Return whether a subgraph is published. + * @notice Return whether a subgraph is published. * @param _subgraphData Subgraph Data * @return Return true if subgraph is currently published */ @@ -764,7 +779,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Return the subgraph data or revert if not published or deprecated. + * @notice Return the subgraph data or revert if not published or deprecated. * @param _subgraphID Subgraph ID * @return Subgraph Data */ @@ -775,9 +790,11 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Build a subgraph ID based on the account creating it and a sequence number for that account. + * @notice Build a subgraph ID based on the account creating it and a sequence number for that account. * Only used for legacy subgraphs being migrated, as new ones will also use the chainid. * Subgraph ID is the keccak hash of account+seqID + * @param _account The account creating the subgraph + * @param _seqID The sequence ID for the account * @return Subgraph ID */ function _buildLegacySubgraphID(address _account, uint256 _seqID) internal pure returns (uint256) { @@ -785,8 +802,10 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Build a subgraph ID based on the account creating it and a sequence number for that account. + * @notice Build a subgraph ID based on the account creating it and a sequence number for that account. * Subgraph ID is the keccak hash of account+seqID + * @param _account The account creating the subgraph + * @param _seqID The sequence ID for the account * @return Subgraph ID */ function _buildSubgraphID(address _account, uint256 _seqID) internal pure returns (uint256) { @@ -800,7 +819,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Internal: Set the owner tax percentage. This is used to prevent a subgraph owner to drain all + * @notice Internal: Set the owner tax percentage. This is used to prevent a subgraph owner to drain all * the name curators tokens while upgrading or deprecating and is configurable in parts per million. * @param _ownerTaxPercentage Owner tax percentage */ @@ -811,7 +830,7 @@ abstract contract GNS is GNSV3Storage, GraphUpgradeable, IGNS, Multicall { } /** - * @dev Internal: Set the NFT registry contract + * @notice Internal: Set the NFT registry contract * @param _subgraphNFT Address of the ERC721 contract */ function _setSubgraphNFT(address _subgraphNFT) private { diff --git a/packages/contracts/contracts/discovery/GNSStorage.sol b/packages/contracts/contracts/discovery/GNSStorage.sol index 80122c9ba..696546cab 100644 --- a/packages/contracts/contracts/discovery/GNSStorage.sol +++ b/packages/contracts/contracts/discovery/GNSStorage.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// solhint-disable one-contract-per-file pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable named-parameters-mapping + import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; import { Managed } from "../governance/Managed.sol"; @@ -12,12 +16,13 @@ import { ISubgraphNFT } from "./ISubgraphNFT.sol"; /** * @title GNSV1Storage + * @author Edge & Node * @notice This contract holds all the storage variables for the GNS contract, version 1 */ abstract contract GNSV1Storage is Managed { // -- State -- - /// Percentage of curation tax that must be paid by the owner, in parts per million. + /// @notice Percentage of curation tax that must be paid by the owner, in parts per million. uint32 public ownerTaxPercentage; /// @dev [DEPRECATED] Bonding curve formula. @@ -29,11 +34,11 @@ abstract contract GNSV1Storage is Managed { /// (graphAccountID, subgraphNumber) => subgraphDeploymentID mapping(address => mapping(uint256 => bytes32)) internal legacySubgraphs; - /// Every time an account creates a subgraph it increases a per-account sequence ID. + /// @notice Every time an account creates a subgraph it increases a per-account sequence ID. /// account => seqID mapping(address => uint256) public nextAccountSeqID; - /// Stores all the signal deposited on a legacy subgraph. + /// @notice Stores all the signal deposited on a legacy subgraph. /// (graphAccountID, subgraphNumber) => SubgraphData mapping(address => mapping(uint256 => IGNS.SubgraphData)) public legacySubgraphData; @@ -44,31 +49,33 @@ abstract contract GNSV1Storage is Managed { /** * @title GNSV2Storage + * @author Edge & Node * @notice This contract holds all the storage variables for the GNS contract, version 2 */ abstract contract GNSV2Storage is GNSV1Storage { - /// Stores the account and seqID for a legacy subgraph that has been migrated. + /// @notice Stores the account and seqID for a legacy subgraph that has been migrated. /// Use it whenever a legacy (v1) subgraph NFT was claimed to maintain compatibility. /// Keep a reference from subgraphID => (graphAccount, subgraphNumber) mapping(uint256 => IGNS.LegacySubgraphKey) public legacySubgraphKeys; - /// Store data for all NFT-based (v2) subgraphs. + /// @notice Store data for all NFT-based (v2) subgraphs. /// subgraphID => SubgraphData mapping(uint256 => IGNS.SubgraphData) public subgraphs; - /// Contract that represents subgraph ownership through an NFT + /// @notice Contract that represents subgraph ownership through an NFT ISubgraphNFT public subgraphNFT; } /** * @title GNSV3Storage + * @author Edge & Node * @notice This contract holds all the storage variables for the base GNS contract, version 3. * @dev Note that this is the first version that includes a storage gap - if adding * future versions, make sure to move the gap to the new version and * reduce the size of the gap accordingly. */ abstract contract GNSV3Storage is GNSV2Storage, Initializable { - /// Address of the counterpart GNS contract (L1GNS/L2GNS) + /// @notice Address of the counterpart GNS contract (L1GNS/L2GNS) address public counterpartGNSAddress; /// @dev Gap to allow adding variables in future upgrades (since L1GNS and L2GNS have their own storage as well) uint256[50] private __gap; diff --git a/packages/contracts/contracts/discovery/IGNS.sol b/packages/contracts/contracts/discovery/IGNS.sol index 70b366d9b..98f4b339e 100644 --- a/packages/contracts/contracts/discovery/IGNS.sol +++ b/packages/contracts/contracts/discovery/IGNS.sol @@ -4,6 +4,8 @@ pragma solidity ^0.7.6 || 0.8.27; /** * @title Interface for GNS + * @author Edge & Node + * @notice Interface for the Graph Name System (GNS) contract */ interface IGNS { // -- Pool -- @@ -12,6 +14,13 @@ interface IGNS { * @dev The SubgraphData struct holds information about subgraphs * and their signal; both nSignal (i.e. name signal at the GNS level) * and vSignal (i.e. version signal at the Curation contract level) + * @param vSignal The token of the subgraph-deployment bonding curve + * @param nSignal The token of the subgraph bonding curve + * @param curatorNSignal Mapping of curator addresses to their name signal amounts + * @param subgraphDeploymentID The deployment ID this subgraph points to + * @param __DEPRECATED_reserveRatio Deprecated reserve ratio field + * @param disabled Whether the subgraph is disabled/deprecated + * @param withdrawableGRT Amount of GRT available for withdrawal after deprecation */ struct SubgraphData { uint256 vSignal; // The token of the subgraph-deployment bonding curve @@ -26,6 +35,8 @@ interface IGNS { /** * @dev The LegacySubgraphKey struct holds the account and sequence ID * used to generate subgraph IDs in legacy subgraphs. + * @param account The account that created the legacy subgraph + * @param accountSeqID The sequence ID for the account's subgraphs */ struct LegacySubgraphKey { address account; @@ -158,7 +169,9 @@ interface IGNS { * @notice Calculate subgraph signal to be returned for an amount of tokens. * @param _subgraphID Subgraph ID * @param _tokensIn Tokens being exchanged for subgraph signal - * @return Amount of subgraph signal and curation tax + * @return Amount of subgraph signal that can be bought + * @return Amount of version signal that can be bought + * @return Amount of curation tax */ function tokensToNSignal(uint256 _subgraphID, uint256 _tokensIn) external view returns (uint256, uint256, uint256); @@ -167,6 +180,7 @@ interface IGNS { * @param _subgraphID Subgraph ID * @param _nSignalIn Subgraph signal being exchanged for tokens * @return Amount of tokens returned for an amount of subgraph signal + * @return Amount of version signal returned */ function nSignalToTokens(uint256 _subgraphID, uint256 _nSignalIn) external view returns (uint256, uint256); diff --git a/packages/contracts/contracts/discovery/IServiceRegistry.sol b/packages/contracts/contracts/discovery/IServiceRegistry.sol index 724f7bebe..89dafafd3 100644 --- a/packages/contracts/contracts/discovery/IServiceRegistry.sol +++ b/packages/contracts/contracts/discovery/IServiceRegistry.sol @@ -2,19 +2,52 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Service Registry Interface + * @author Edge & Node + * @notice Interface for the Service Registry contract that manages indexer service information + */ interface IServiceRegistry { + /** + * @dev Indexer service information + * @param url URL of the indexer service + * @param geohash Geohash of the indexer service location + */ struct IndexerService { string url; string geohash; } + /** + * @notice Register an indexer service + * @param _url URL of the indexer service + * @param _geohash Geohash of the indexer service location + */ function register(string calldata _url, string calldata _geohash) external; + /** + * @notice Register an indexer service + * @param _indexer Address of the indexer + * @param _url URL of the indexer service + * @param _geohash Geohash of the indexer service location + */ function registerFor(address _indexer, string calldata _url, string calldata _geohash) external; + /** + * @notice Unregister an indexer service + */ function unregister() external; + /** + * @notice Unregister an indexer service + * @param _indexer Address of the indexer + */ function unregisterFor(address _indexer) external; + /** + * @notice Return the registration status of an indexer service + * @param _indexer Address of the indexer + * @return True if the indexer service is registered + */ function isRegistered(address _indexer) external view returns (bool); } diff --git a/packages/contracts/contracts/discovery/ISubgraphNFT.sol b/packages/contracts/contracts/discovery/ISubgraphNFT.sol index 6cef69297..f735c00f6 100644 --- a/packages/contracts/contracts/discovery/ISubgraphNFT.sol +++ b/packages/contracts/contracts/discovery/ISubgraphNFT.sol @@ -2,24 +2,66 @@ pragma solidity ^0.7.6 || 0.8.27; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +/** + * @title Subgraph NFT Interface + * @author Edge & Node + * @notice Interface for the Subgraph NFT contract that represents subgraph ownership + */ interface ISubgraphNFT is IERC721 { // -- Config -- + /** + * @notice Set the minter allowed to perform actions on the NFT + * @dev Minter can mint, burn and update the metadata + * @param _minter Address of the allowed minter + */ function setMinter(address _minter) external; + /** + * @notice Set the token descriptor contract + * @dev Token descriptor can be zero. If set, it must be a contract + * @param _tokenDescriptor Address of the contract that creates the NFT token URI + */ function setTokenDescriptor(address _tokenDescriptor) external; + /** + * @notice Set the base URI + * @dev Can be set to empty + * @param _baseURI Base URI to use to build the token URI + */ function setBaseURI(string memory _baseURI) external; // -- Actions -- + /** + * @notice Mint `_tokenId` and transfers it to `_to` + * @dev `tokenId` must not exist and `to` cannot be the zero address + * @param _to Address receiving the minted NFT + * @param _tokenId ID of the NFT + */ function mint(address _to, uint256 _tokenId) external; + /** + * @notice Burn `_tokenId` + * @dev The approval is cleared when the token is burned + * @param _tokenId ID of the NFT + */ function burn(uint256 _tokenId) external; + /** + * @notice Set the metadata for a subgraph represented by `_tokenId` + * @dev `_tokenId` must exist + * @param _tokenId ID of the NFT + * @param _subgraphMetadata IPFS hash for the metadata + */ function setSubgraphMetadata(uint256 _tokenId, bytes32 _subgraphMetadata) external; + /** + * @notice Returns the Uniform Resource Identifier (URI) for `_tokenId` token + * @param _tokenId ID of the NFT + * @return The URI for the token + */ function tokenURI(uint256 _tokenId) external view returns (string memory); } diff --git a/packages/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol b/packages/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol index cd0785dcb..5a6893234 100644 --- a/packages/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol +++ b/packages/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol @@ -2,7 +2,11 @@ pragma solidity ^0.7.6; -/// @title Describes subgraph NFT tokens via URI +/** + * @title Describes subgraph NFT tokens via URI + * @author Edge & Node + * @notice Interface for describing subgraph NFT tokens via URI + */ interface ISubgraphNFTDescriptor { /// @notice Produces the URI describing a particular token ID for a Subgraph /// @dev Note this URI may be data: URI with the JSON contents directly inlined diff --git a/packages/contracts/contracts/discovery/L1GNS.sol b/packages/contracts/contracts/discovery/L1GNS.sol index 31e9b0fb3..03167dcd9 100644 --- a/packages/contracts/contracts/discovery/L1GNS.sol +++ b/packages/contracts/contracts/discovery/L1GNS.sol @@ -14,7 +14,8 @@ import { L1GNSV1Storage } from "./L1GNSStorage.sol"; /** * @title L1GNS - * @dev The Graph Name System contract provides a decentralized naming system for subgraphs + * @author Edge & Node + * @notice The Graph Name System contract provides a decentralized naming system for subgraphs * used in the scope of the Graph Network. It translates Subgraphs into Subgraph Versions. * Each version is associated with a Subgraph Deployment. The contract has no knowledge of * human-readable names. All human readable names emitted in events. @@ -25,7 +26,13 @@ import { L1GNSV1Storage } from "./L1GNSStorage.sol"; contract L1GNS is GNS, L1GNSV1Storage { using SafeMathUpgradeable for uint256; - /// @dev Emitted when a subgraph was sent to L2 through the bridge + /** + * @notice Emitted when a subgraph was sent to L2 through the bridge + * @param _subgraphID ID of the subgraph being transferred + * @param _l1Owner Address of the subgraph owner on L1 + * @param _l2Owner Address that will own the subgraph on L2 + * @param _tokens Amount of tokens transferred with the subgraph + */ event SubgraphSentToL2( uint256 indexed _subgraphID, address indexed _l1Owner, @@ -33,7 +40,13 @@ contract L1GNS is GNS, L1GNSV1Storage { uint256 _tokens ); - /// @dev Emitted when a curator's balance for a subgraph was sent to L2 + /** + * @notice Emitted when a curator's balance for a subgraph was sent to L2 + * @param _subgraphID ID of the subgraph + * @param _l1Curator Address of the curator on L1 + * @param _l2Beneficiary Address that will receive the tokens on L2 + * @param _tokens Amount of tokens transferred + */ event CuratorBalanceSentToL2( uint256 indexed _subgraphID, address indexed _l1Curator, @@ -42,10 +55,10 @@ contract L1GNS is GNS, L1GNSV1Storage { ); /** - * @notice Send a subgraph's data and tokens to L2. - * Use the Arbitrum SDK to estimate the L2 retryable ticket parameters. + * @notice Send a subgraph's data and tokens to L2 + * @dev Use the Arbitrum SDK to estimate the L2 retryable ticket parameters. * Note that any L2 gas/fee refunds will be lost, so the function only accepts - * the exact amount of ETH to cover _maxSubmissionCost + _maxGas * _gasPriceBid. + * the exact amount of ETH to cover _maxSubmissionCost + _maxGas * _gasPriceBid * @param _subgraphID Subgraph ID * @param _l2Owner Address that will own the subgraph in L2 (could be the L1 owner, but could be different if the L1 owner is an L1 contract) * @param _maxGas Max gas to use for the L2 retryable ticket diff --git a/packages/contracts/contracts/discovery/L1GNSStorage.sol b/packages/contracts/contracts/discovery/L1GNSStorage.sol index 557814513..72af676f2 100644 --- a/packages/contracts/contracts/discovery/L1GNSStorage.sol +++ b/packages/contracts/contracts/discovery/L1GNSStorage.sol @@ -1,16 +1,20 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable named-parameters-mapping + pragma solidity ^0.7.6; pragma abicoder v2; /** * @title L1GNSV1Storage + * @author Edge & Node * @notice This contract holds all the L1-specific storage variables for the L1GNS contract, version 1 * @dev When adding new versions, make sure to move the gap to the new version and * reduce the size of the gap accordingly. */ abstract contract L1GNSV1Storage { - /// True for subgraph IDs that have been transferred to L2 + /// @notice True for subgraph IDs that have been transferred to L2 mapping(uint256 => bool) public subgraphTransferredToL2; /// @dev Storage gap to keep storage slots fixed in future versions uint256[50] private __gap; diff --git a/packages/contracts/contracts/discovery/ServiceRegistry.sol b/packages/contracts/contracts/discovery/ServiceRegistry.sol index 1eb1393d3..3fa27f452 100644 --- a/packages/contracts/contracts/discovery/ServiceRegistry.sol +++ b/packages/contracts/contracts/discovery/ServiceRegistry.sol @@ -3,58 +3,68 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "../governance/Managed.sol"; -import "../upgrades/GraphUpgradeable.sol"; +import { Managed } from "../governance/Managed.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; -import "./ServiceRegistryStorage.sol"; -import "./IServiceRegistry.sol"; +import { ServiceRegistryV1Storage } from "./ServiceRegistryStorage.sol"; +import { IServiceRegistry } from "./IServiceRegistry.sol"; /** * @title ServiceRegistry contract - * @dev This contract supports the service discovery process by allowing indexers to + * @author Edge & Node + * @notice This contract supports the service discovery process by allowing indexers to * register their service url and any other relevant information. */ contract ServiceRegistry is ServiceRegistryV1Storage, GraphUpgradeable, IServiceRegistry { // -- Events -- + /** + * @notice Emitted when an indexer registers their service + * @param indexer Address of the indexer + * @param url URL of the indexer service + * @param geohash Geohash of the indexer service location + */ event ServiceRegistered(address indexed indexer, string url, string geohash); + + /** + * @notice Emitted when an indexer unregisters their service + * @param indexer Address of the indexer + */ event ServiceUnregistered(address indexed indexer); /** - * @dev Check if the caller is authorized (indexer or operator) + * @notice Check if the caller is authorized (indexer or operator) + * @param _indexer Address of the indexer to check authorization for + * @return True if the caller is authorized, false otherwise */ function _isAuth(address _indexer) internal view returns (bool) { return msg.sender == _indexer || staking().isOperator(msg.sender, _indexer) == true; } /** - * @dev Initialize this contract. + * @notice Initialize this contract. + * @param _controller Address of the controller contract */ function initialize(address _controller) external onlyImpl { Managed._initialize(_controller); } /** - * @dev Register an indexer service - * @param _url URL of the indexer service - * @param _geohash Geohash of the indexer service location + * @inheritdoc IServiceRegistry */ function register(string calldata _url, string calldata _geohash) external override { _register(msg.sender, _url, _geohash); } /** - * @dev Register an indexer service - * @param _indexer Address of the indexer - * @param _url URL of the indexer service - * @param _geohash Geohash of the indexer service location + * @inheritdoc IServiceRegistry */ function registerFor(address _indexer, string calldata _url, string calldata _geohash) external override { _register(_indexer, _url, _geohash); } /** - * @dev Internal: Register an indexer service + * @notice Internal: Register an indexer service * @param _indexer Address of the indexer * @param _url URL of the indexer service * @param _geohash Geohash of the indexer service location @@ -69,22 +79,21 @@ contract ServiceRegistry is ServiceRegistryV1Storage, GraphUpgradeable, IService } /** - * @dev Unregister an indexer service + * @inheritdoc IServiceRegistry */ function unregister() external override { _unregister(msg.sender); } /** - * @dev Unregister an indexer service - * @param _indexer Address of the indexer + * @inheritdoc IServiceRegistry */ function unregisterFor(address _indexer) external override { _unregister(_indexer); } /** - * @dev Unregister an indexer service + * @notice Unregister an indexer service * @param _indexer Address of the indexer */ function _unregister(address _indexer) private { @@ -96,8 +105,7 @@ contract ServiceRegistry is ServiceRegistryV1Storage, GraphUpgradeable, IService } /** - * @dev Return the registration status of an indexer service - * @return True if the indexer service is registered + * @inheritdoc IServiceRegistry */ function isRegistered(address _indexer) public view override returns (bool) { return bytes(services[_indexer].url).length > 0; diff --git a/packages/contracts/contracts/discovery/ServiceRegistryStorage.sol b/packages/contracts/contracts/discovery/ServiceRegistryStorage.sol index 1cd484970..23b0cf63d 100644 --- a/packages/contracts/contracts/discovery/ServiceRegistryStorage.sol +++ b/packages/contracts/contracts/discovery/ServiceRegistryStorage.sol @@ -1,13 +1,22 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable named-parameters-mapping + pragma solidity ^0.7.6; -import "../governance/Managed.sol"; +import { Managed } from "../governance/Managed.sol"; -import "./IServiceRegistry.sol"; +import { IServiceRegistry } from "./IServiceRegistry.sol"; +/** + * @title Service Registry Storage V1 + * @author Edge & Node + * @notice Storage contract for the Service Registry + */ contract ServiceRegistryV1Storage is Managed { // -- State -- + /// @notice Mapping of indexer addresses to their service information mapping(address => IServiceRegistry.IndexerService) public services; } diff --git a/packages/contracts/contracts/discovery/SubgraphNFT.sol b/packages/contracts/contracts/discovery/SubgraphNFT.sol index 3c514718c..f33766d02 100644 --- a/packages/contracts/contracts/discovery/SubgraphNFT.sol +++ b/packages/contracts/contracts/discovery/SubgraphNFT.sol @@ -2,35 +2,66 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; - -import "../governance/Governed.sol"; -import "../libraries/HexStrings.sol"; -import "./ISubgraphNFT.sol"; -import "./ISubgraphNFTDescriptor.sol"; - -/// @title NFT that represents ownership of a Subgraph +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-calldata-parameters, gas-indexed-events, gas-small-strings +// solhint-disable named-parameters-mapping + +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +import { Governed } from "../governance/Governed.sol"; +import { HexStrings } from "../libraries/HexStrings.sol"; +import { ISubgraphNFT } from "./ISubgraphNFT.sol"; +import { ISubgraphNFTDescriptor } from "./ISubgraphNFTDescriptor.sol"; + +/** + * @title NFT that represents ownership of a Subgraph + * @author Edge & Node + * @notice NFT that represents ownership of a Subgraph + */ contract SubgraphNFT is Governed, ERC721, ISubgraphNFT { // -- State -- + /// @notice Address of the minter contract address public minter; + /// @notice Address of the token descriptor contract ISubgraphNFTDescriptor public tokenDescriptor; + /// @dev Mapping from token ID to subgraph metadata hash mapping(uint256 => bytes32) private _subgraphMetadataHashes; // -- Events -- + /** + * @notice Emitted when the minter address is updated + * @param minter Address of the new minter + */ event MinterUpdated(address minter); + + /** + * @notice Emitted when the token descriptor is updated + * @param tokenDescriptor Address of the new token descriptor + */ event TokenDescriptorUpdated(address tokenDescriptor); + + /** + * @notice Emitted when subgraph metadata is updated + * @param tokenID ID of the token + * @param subgraphURI IPFS hash of the subgraph metadata + */ event SubgraphMetadataUpdated(uint256 indexed tokenID, bytes32 subgraphURI); // -- Modifiers -- + /// @dev Modifier to restrict access to minter only modifier onlyMinter() { require(msg.sender == minter, "Must be a minter"); _; } + /** + * @notice Constructor for the SubgraphNFT contract + * @param _governor Address that will have governance privileges + */ constructor(address _governor) ERC721("Subgraph", "SG") { _initialize(_governor); } @@ -38,9 +69,7 @@ contract SubgraphNFT is Governed, ERC721, ISubgraphNFT { // -- Config -- /** - * @notice Set the minter allowed to perform actions on the NFT. - * @dev Minter can mint, burn and update the metadata - * @param _minter Address of the allowed minter + * @inheritdoc ISubgraphNFT */ function setMinter(address _minter) external override onlyGovernor { _setMinter(_minter); @@ -57,16 +86,14 @@ contract SubgraphNFT is Governed, ERC721, ISubgraphNFT { } /** - * @notice Set the token descriptor contract. - * @dev Token descriptor can be zero. If set, it must be a contract. - * @param _tokenDescriptor Address of the contract that creates the NFT token URI + * @inheritdoc ISubgraphNFT */ function setTokenDescriptor(address _tokenDescriptor) external override onlyGovernor { _setTokenDescriptor(_tokenDescriptor); } /** - * @dev Internal: Set the token descriptor contract used to create the ERC-721 metadata URI. + * @notice Internal: Set the token descriptor contract used to create the ERC-721 metadata URI. * @param _tokenDescriptor Address of the contract that creates the NFT token URI */ function _setTokenDescriptor(address _tokenDescriptor) internal { @@ -79,9 +106,7 @@ contract SubgraphNFT is Governed, ERC721, ISubgraphNFT { } /** - * @notice Set the base URI. - * @dev Can be set to empty. - * @param _baseURI Base URI to use to build the token URI + * @inheritdoc ISubgraphNFT */ function setBaseURI(string memory _baseURI) external override onlyGovernor { _setBaseURI(_baseURI); @@ -90,29 +115,21 @@ contract SubgraphNFT is Governed, ERC721, ISubgraphNFT { // -- Minter actions -- /** - * @notice Mint `_tokenId` and transfers it to `_to`. - * @dev `tokenId` must not exist and `to` cannot be the zero address. - * @param _to Address receiving the minted NFT - * @param _tokenId ID of the NFT + * @inheritdoc ISubgraphNFT */ function mint(address _to, uint256 _tokenId) external override onlyMinter { _mint(_to, _tokenId); } /** - * @notice Burn `_tokenId`. - * @dev The approval is cleared when the token is burned. - * @param _tokenId ID of the NFT + * @inheritdoc ISubgraphNFT */ function burn(uint256 _tokenId) external override onlyMinter { _burn(_tokenId); } /** - * @notice Set the metadata for a subgraph represented by `_tokenId`. - * @dev `_tokenId` must exist. - * @param _tokenId ID of the NFT - * @param _subgraphMetadata IPFS hash for the metadata + * @inheritdoc ISubgraphNFT */ function setSubgraphMetadata(uint256 _tokenId, bytes32 _subgraphMetadata) external override onlyMinter { require(_exists(_tokenId), "ERC721Metadata: URI set of nonexistent token"); diff --git a/packages/contracts/contracts/discovery/SubgraphNFTDescriptor.sol b/packages/contracts/contracts/discovery/SubgraphNFTDescriptor.sol index 81f6da696..a21cadfb7 100644 --- a/packages/contracts/contracts/discovery/SubgraphNFTDescriptor.sol +++ b/packages/contracts/contracts/discovery/SubgraphNFTDescriptor.sol @@ -2,10 +2,14 @@ pragma solidity ^0.7.6; -import "../libraries/Base58Encoder.sol"; -import "./ISubgraphNFTDescriptor.sol"; +import { Base58Encoder } from "../libraries/Base58Encoder.sol"; +import { ISubgraphNFTDescriptor } from "./ISubgraphNFTDescriptor.sol"; -/// @title Describes subgraph NFT tokens via URI +/** + * @title Describes subgraph NFT tokens via URI + * @author Edge & Node + * @notice Describes subgraph NFT tokens via URI + */ contract SubgraphNFTDescriptor is ISubgraphNFTDescriptor { /// @inheritdoc ISubgraphNFTDescriptor function tokenURI( diff --git a/packages/contracts/contracts/discovery/erc1056/EthereumDIDRegistry.sol b/packages/contracts/contracts/discovery/erc1056/EthereumDIDRegistry.sol index e8545dd4a..76c1a41f9 100644 --- a/packages/contracts/contracts/discovery/erc1056/EthereumDIDRegistry.sol +++ b/packages/contracts/contracts/discovery/erc1056/EthereumDIDRegistry.sol @@ -12,19 +12,51 @@ As well as all testnets pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one, gas-indexed-events, gas-small-strings +// solhint-disable named-parameters-mapping + +/** + * @title Ethereum DID Registry + * @author Edge & Node + * @notice Registry for Ethereum Decentralized Identifiers (DIDs) + */ contract EthereumDIDRegistry { + /// @notice Mapping of identity addresses to their owners mapping(address => address) public owners; + /// @notice Mapping of identity addresses to delegate types to delegate addresses to validity periods mapping(address => mapping(bytes32 => mapping(address => uint256))) public delegates; + /// @notice Mapping of identity addresses to their last change block numbers mapping(address => uint256) public changed; + /// @notice Mapping of identity addresses to their nonce values mapping(address => uint256) public nonce; + /** + * @notice Modifier to restrict access to identity owners only + * @param identity The identity address + * @param actor The address performing the action + */ modifier onlyOwner(address identity, address actor) { require(actor == identityOwner(identity), "Caller must be the identity owner"); _; } + /** + * @notice Emitted when a DID owner is changed + * @param identity The identity address + * @param owner The new owner address + * @param previousChange Block number of the previous change + */ event DIDOwnerChanged(address indexed identity, address owner, uint256 previousChange); + /** + * @notice Emitted when a DID delegate is changed + * @param identity The identity address + * @param delegateType The type of delegate + * @param delegate The delegate address + * @param validTo Timestamp until which the delegate is valid + * @param previousChange Block number of the previous change + */ event DIDDelegateChanged( address indexed identity, bytes32 delegateType, @@ -33,6 +65,14 @@ contract EthereumDIDRegistry { uint256 previousChange ); + /** + * @notice Emitted when a DID attribute is changed + * @param identity The identity address + * @param name The attribute name + * @param value The attribute value + * @param validTo Timestamp until which the attribute is valid + * @param previousChange Block number of the previous change + */ event DIDAttributeChanged( address indexed identity, bytes32 name, @@ -41,6 +81,11 @@ contract EthereumDIDRegistry { uint256 previousChange ); + /** + * @notice Get the owner of an identity + * @param identity The identity address + * @return The address of the identity owner + */ function identityOwner(address identity) public view returns (address) { address owner = owners[identity]; if (owner != address(0)) { @@ -49,6 +94,15 @@ contract EthereumDIDRegistry { return identity; } + /** + * @notice Verify signature and return signer address + * @param identity The identity address + * @param sigV Recovery ID of the signature + * @param sigR R component of the signature + * @param sigS S component of the signature + * @param hash Hash that was signed + * @return The address of the signer + */ function checkSignature( address identity, uint8 sigV, @@ -62,22 +116,48 @@ contract EthereumDIDRegistry { return signer; } + /** + * @notice Check if a delegate is valid for an identity + * @param identity The identity address + * @param delegateType The type of delegate + * @param delegate The delegate address + * @return True if the delegate is valid, false otherwise + */ function validDelegate(address identity, bytes32 delegateType, address delegate) public view returns (bool) { uint256 validity = delegates[identity][keccak256(abi.encode(delegateType))][delegate]; /* solium-disable-next-line security/no-block-members*/ return (validity > block.timestamp); } + /** + * @notice Internal function to change the owner of an identity + * @param identity The identity address + * @param actor The address performing the action + * @param newOwner The new owner address + */ function changeOwner(address identity, address actor, address newOwner) internal onlyOwner(identity, actor) { owners[identity] = newOwner; emit DIDOwnerChanged(identity, newOwner, changed[identity]); changed[identity] = block.number; } + /** + * @notice Change the owner of an identity + * @param identity The identity address + * @param newOwner The new owner address + */ function changeOwner(address identity, address newOwner) public { changeOwner(identity, msg.sender, newOwner); } + /** + * @notice Change the owner of an identity using a signed message + * @param identity The identity address + * @param sigV Recovery ID of the signature + * @param sigR R component of the signature + * @param sigS S component of the signature + * @param newOwner The new owner address + */ function changeOwnerSigned(address identity, uint8 sigV, bytes32 sigR, bytes32 sigS, address newOwner) public { bytes32 hash = keccak256( abi.encodePacked( @@ -93,6 +173,14 @@ contract EthereumDIDRegistry { changeOwner(identity, checkSignature(identity, sigV, sigR, sigS, hash), newOwner); } + /** + * @notice Internal function to add a delegate for an identity + * @param identity The identity address + * @param actor The address performing the action + * @param delegateType The type of delegate + * @param delegate The delegate address + * @param validity The validity period in seconds + */ function addDelegate( address identity, address actor, @@ -113,10 +201,27 @@ contract EthereumDIDRegistry { changed[identity] = block.number; } + /** + * @notice Add a delegate for an identity + * @param identity The identity to add a delegate for + * @param delegateType The type of delegate + * @param delegate The address of the delegate + * @param validity The validity period in seconds + */ function addDelegate(address identity, bytes32 delegateType, address delegate, uint256 validity) public { addDelegate(identity, msg.sender, delegateType, delegate, validity); } + /** + * @notice Add a delegate for an identity using a signed message + * @param identity The identity to add a delegate for + * @param sigV The recovery id of the signature + * @param sigR The r component of the signature + * @param sigS The s component of the signature + * @param delegateType The type of delegate + * @param delegate The address of the delegate + * @param validity The validity period in seconds + */ function addDelegateSigned( address identity, uint8 sigV, @@ -142,6 +247,13 @@ contract EthereumDIDRegistry { addDelegate(identity, checkSignature(identity, sigV, sigR, sigS, hash), delegateType, delegate, validity); } + /** + * @notice Internal function to revoke a delegate for an identity + * @param identity The identity address + * @param actor The address performing the action + * @param delegateType The type of delegate + * @param delegate The delegate address + */ function revokeDelegate( address identity, address actor, @@ -155,10 +267,25 @@ contract EthereumDIDRegistry { changed[identity] = block.number; } + /** + * @notice Revoke a delegate for an identity + * @param identity The identity to revoke a delegate for + * @param delegateType The type of delegate + * @param delegate The address of the delegate + */ function revokeDelegate(address identity, bytes32 delegateType, address delegate) public { revokeDelegate(identity, msg.sender, delegateType, delegate); } + /** + * @notice Revoke a delegate for an identity using a signed message + * @param identity The identity to revoke a delegate for + * @param sigV The recovery id of the signature + * @param sigR The r component of the signature + * @param sigS The s component of the signature + * @param delegateType The type of delegate + * @param delegate The address of the delegate + */ function revokeDelegateSigned( address identity, uint8 sigV, @@ -182,6 +309,14 @@ contract EthereumDIDRegistry { revokeDelegate(identity, checkSignature(identity, sigV, sigR, sigS, hash), delegateType, delegate); } + /** + * @notice Internal function to set an attribute for an identity + * @param identity The identity address + * @param actor The address performing the action + * @param name The attribute name + * @param value The attribute value + * @param validity The validity period in seconds + */ function setAttribute( address identity, address actor, @@ -194,10 +329,27 @@ contract EthereumDIDRegistry { changed[identity] = block.number; } + /** + * @notice Set an attribute for an identity + * @param identity The identity to set an attribute for + * @param name The name of the attribute + * @param value The value of the attribute + * @param validity The validity period in seconds + */ function setAttribute(address identity, bytes32 name, bytes memory value, uint256 validity) public { setAttribute(identity, msg.sender, name, value, validity); } + /** + * @notice Set an attribute for an identity using a signed message + * @param identity The identity to set an attribute for + * @param sigV The recovery id of the signature + * @param sigR The r component of the signature + * @param sigS The s component of the signature + * @param name The name of the attribute + * @param value The value of the attribute + * @param validity The validity period in seconds + */ function setAttributeSigned( address identity, uint8 sigV, @@ -223,6 +375,13 @@ contract EthereumDIDRegistry { setAttribute(identity, checkSignature(identity, sigV, sigR, sigS, hash), name, value, validity); } + /** + * @notice Internal function to revoke an attribute for an identity + * @param identity The identity address + * @param actor The address performing the action + * @param name The attribute name + * @param value The attribute value + */ function revokeAttribute( address identity, address actor, @@ -233,10 +392,25 @@ contract EthereumDIDRegistry { changed[identity] = block.number; } + /** + * @notice Revoke an attribute for an identity + * @param identity The identity to revoke an attribute for + * @param name The name of the attribute + * @param value The value of the attribute + */ function revokeAttribute(address identity, bytes32 name, bytes memory value) public { revokeAttribute(identity, msg.sender, name, value); } + /** + * @notice Revoke an attribute for an identity using a signed message + * @param identity The identity to revoke an attribute for + * @param sigV The recovery id of the signature + * @param sigR The r component of the signature + * @param sigS The s component of the signature + * @param name The name of the attribute + * @param value The value of the attribute + */ function revokeAttributeSigned( address identity, uint8 sigV, diff --git a/packages/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol b/packages/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol index 8de69f304..0be104968 100644 --- a/packages/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol +++ b/packages/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol @@ -2,8 +2,25 @@ pragma solidity ^0.7.6; +/** + * @title Ethereum DID Registry Interface + * @author Edge & Node + * @notice Interface for the Ethereum DID Registry contract + */ interface IEthereumDIDRegistry { + /** + * @notice Get the owner of an identity + * @param identity The identity address + * @return The address of the identity owner + */ function identityOwner(address identity) external view returns (address); + /** + * @notice Set an attribute for an identity + * @param identity The identity address + * @param name The attribute name + * @param value The attribute value + * @param validity The validity period in seconds + */ function setAttribute(address identity, bytes32 name, bytes calldata value, uint256 validity) external; } diff --git a/packages/contracts/contracts/disputes/DisputeManager.sol b/packages/contracts/contracts/disputes/DisputeManager.sol index 013a21b03..51c28e5db 100644 --- a/packages/contracts/contracts/disputes/DisputeManager.sol +++ b/packages/contracts/contracts/disputes/DisputeManager.sol @@ -3,18 +3,23 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-small-strings, gas-strict-inequalities -import "../governance/Managed.sol"; -import "../upgrades/GraphUpgradeable.sol"; -import "../utils/TokenUtils.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { ECDSA } from "@openzeppelin/contracts/cryptography/ECDSA.sol"; -import "./DisputeManagerStorage.sol"; -import "./IDisputeManager.sol"; +import { Managed } from "../governance/Managed.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; +import { TokenUtils } from "../utils/TokenUtils.sol"; +import { IStaking } from "../staking/IStaking.sol"; -/* +import { DisputeManagerV1Storage } from "./DisputeManagerStorage.sol"; +import { IDisputeManager } from "./IDisputeManager.sol"; + +/** * @title DisputeManager + * @author Edge & Node * @notice Provides a way to align the incentives of participants by having slashing as deterrent * for incorrect behaviour. * @@ -41,39 +46,61 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa // -- EIP-712 -- + /// @dev EIP-712 domain type hash for signature verification bytes32 private constant DOMAIN_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)"); + /// @dev EIP-712 domain name hash bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Protocol"); + /// @dev EIP-712 domain version hash bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); + /// @dev EIP-712 domain salt for uniqueness bytes32 private constant DOMAIN_SALT = 0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2; + /// @dev EIP-712 receipt type hash for attestation verification bytes32 private constant RECEIPT_TYPE_HASH = keccak256("Receipt(bytes32 requestCID,bytes32 responseCID,bytes32 subgraphDeploymentID)"); // -- Constants -- - // Attestation size is the sum of the receipt (96) + signature (65) + /// @dev Total size of attestation in bytes (receipt + signature) uint256 private constant ATTESTATION_SIZE_BYTES = RECEIPT_SIZE_BYTES + SIG_SIZE_BYTES; + /// @dev Size of receipt in bytes uint256 private constant RECEIPT_SIZE_BYTES = 96; + /// @dev Length of signature R component in bytes uint256 private constant SIG_R_LENGTH = 32; + /// @dev Length of signature S component in bytes uint256 private constant SIG_S_LENGTH = 32; + /// @dev Length of signature V component in bytes uint256 private constant SIG_V_LENGTH = 1; + /// @dev Offset of signature R component in attestation data uint256 private constant SIG_R_OFFSET = RECEIPT_SIZE_BYTES; + /// @dev Offset of signature S component in attestation data uint256 private constant SIG_S_OFFSET = RECEIPT_SIZE_BYTES + SIG_R_LENGTH; + /// @dev Offset of signature V component in attestation data uint256 private constant SIG_V_OFFSET = RECEIPT_SIZE_BYTES + SIG_R_LENGTH + SIG_S_LENGTH; + /// @dev Total size of signature in bytes uint256 private constant SIG_SIZE_BYTES = SIG_R_LENGTH + SIG_S_LENGTH + SIG_V_LENGTH; + /// @dev Length of uint8 type in bytes uint256 private constant UINT8_BYTE_LENGTH = 1; + /// @dev Length of bytes32 type in bytes uint256 private constant BYTES32_BYTE_LENGTH = 32; + /// @dev Maximum percentage in parts per million (100%) uint256 private constant MAX_PPM = 1000000; // 100% in parts per million // -- Events -- /** - * @dev Emitted when a query dispute is created for `subgraphDeploymentID` and `indexer` + * @notice Emitted when a query dispute is created for `subgraphDeploymentID` and `indexer` * by `fisherman`. * The event emits the amount of `tokens` deposited by the fisherman and `attestation` submitted. + * @param disputeID ID of the dispute + * @param indexer Address of the indexer being disputed + * @param fisherman Address of the fisherman creating the dispute + * @param tokens Amount of tokens deposited by the fisherman + * @param subgraphDeploymentID Subgraph deployment ID being disputed + * @param attestation Attestation data submitted */ event QueryDisputeCreated( bytes32 indexed disputeID, @@ -85,9 +112,14 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa ); /** - * @dev Emitted when an indexing dispute is created for `allocationID` and `indexer` + * @notice Emitted when an indexing dispute is created for `allocationID` and `indexer` * by `fisherman`. * The event emits the amount of `tokens` deposited by the fisherman. + * @param disputeID ID of the dispute + * @param indexer Address of the indexer being disputed + * @param fisherman Address of the fisherman creating the dispute + * @param tokens Amount of tokens deposited by the fisherman + * @param allocationID Allocation ID being disputed */ event IndexingDisputeCreated( bytes32 indexed disputeID, @@ -98,8 +130,12 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa ); /** - * @dev Emitted when arbitrator accepts a `disputeID` to `indexer` created by `fisherman`. + * @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. + * @param disputeID ID of the dispute + * @param indexer Address of the indexer being disputed + * @param fisherman Address of the fisherman who created the dispute + * @param tokens Amount of tokens transferred to the fisherman (deposit plus reward) */ event DisputeAccepted( bytes32 indexed disputeID, @@ -109,8 +145,12 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa ); /** - * @dev Emitted when arbitrator rejects a `disputeID` for `indexer` created by `fisherman`. + * @notice Emitted when arbitrator rejects a `disputeID` for `indexer` created by `fisherman`. * The event emits the amount `tokens` burned from the fisherman deposit. + * @param disputeID ID of the dispute + * @param indexer Address of the indexer being disputed + * @param fisherman Address of the fisherman who created the dispute + * @param tokens Amount of tokens burned from the fisherman deposit */ event DisputeRejected( bytes32 indexed disputeID, @@ -120,20 +160,29 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa ); /** - * @dev Emitted when arbitrator draw a `disputeID` for `indexer` created by `fisherman`. + * @notice Emitted when arbitrator draw a `disputeID` for `indexer` created by `fisherman`. * The event emits the amount `tokens` used as deposit and returned to the fisherman. + * @param disputeID ID of the dispute + * @param indexer Address of the indexer being disputed + * @param fisherman Address of the fisherman who created the dispute + * @param tokens Amount of tokens used as deposit and returned to the fisherman */ event DisputeDrawn(bytes32 indexed disputeID, address indexed indexer, address indexed fisherman, uint256 tokens); /** - * @dev Emitted when two disputes are in conflict to link them. + * @notice Emitted when two disputes are in conflict to link them. * This event will be emitted after each DisputeCreated event is emitted * for each of the individual disputes. + * @param disputeID1 ID of the first dispute + * @param disputeID2 ID of the second dispute */ event DisputeLinked(bytes32 indexed disputeID1, bytes32 indexed disputeID2); // -- Modifiers -- + /** + * @notice Internal function to check if the caller is the arbitrator + */ function _onlyArbitrator() internal view { require(msg.sender == arbitrator, "Caller is not the Arbitrator"); } @@ -146,6 +195,10 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa _; } + /** + * @dev Check if the dispute exists and is pending + * @param _disputeID ID of the dispute to check + */ modifier onlyPendingDispute(bytes32 _disputeID) { require(isDisputeCreated(_disputeID), "Dispute does not exist"); require(disputes[_disputeID].status == IDisputeManager.DisputeStatus.Pending, "Dispute must be pending"); @@ -155,7 +208,8 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa // -- Functions -- /** - * @dev Initialize this contract. + * @notice Initialize this contract. + * @param _controller Controller address * @param _arbitrator Arbitrator role * @param _minimumDeposit Minimum deposit required to create a Dispute * @param _fishermanRewardPercentage Percent of slashed funds for fisherman (ppm) @@ -192,16 +246,13 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Set the arbitrator address. - * @notice Update the arbitrator to `_arbitrator` - * @param _arbitrator The address of the arbitration contract or party + * @inheritdoc IDisputeManager */ function setArbitrator(address _arbitrator) external override onlyGovernor { _setArbitrator(_arbitrator); } /** - * @dev Internal: Set the arbitrator address. * @notice Update the arbitrator to `_arbitrator` * @param _arbitrator The address of the arbitration contract or party */ @@ -212,16 +263,13 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Set the minimum deposit required to create a dispute. - * @notice Update the minimum deposit to `_minimumDeposit` Graph Tokens - * @param _minimumDeposit The minimum deposit in Graph Tokens + * @inheritdoc IDisputeManager */ function setMinimumDeposit(uint256 _minimumDeposit) external override onlyGovernor { _setMinimumDeposit(_minimumDeposit); } /** - * @dev Internal: Set the minimum deposit required to create a dispute. * @notice Update the minimum deposit to `_minimumDeposit` Graph Tokens * @param _minimumDeposit The minimum deposit in Graph Tokens */ @@ -232,17 +280,14 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Set the percent reward that the fisherman gets when slashing occurs. - * @notice Update the reward percentage to `_percentage` - * @param _percentage Reward as a percentage of indexer stake + * @inheritdoc IDisputeManager */ function setFishermanRewardPercentage(uint32 _percentage) external override onlyGovernor { _setFishermanRewardPercentage(_percentage); } /** - * @dev Internal: Set the percent reward that the fisherman gets when slashing occurs. - * @notice Update the reward percentage to `_percentage` + * @notice Set the percent reward that the fisherman gets when slashing occurs. * @param _percentage Reward as a percentage of indexer stake */ function _setFishermanRewardPercentage(uint32 _percentage) private { @@ -253,16 +298,14 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Set the percentage used for slashing indexers. - * @param _qryPercentage Percentage slashing for query disputes - * @param _idxPercentage Percentage slashing for indexing disputes + * @inheritdoc IDisputeManager */ function setSlashingPercentage(uint32 _qryPercentage, uint32 _idxPercentage) external override onlyGovernor { _setSlashingPercentage(_qryPercentage, _idxPercentage); } /** - * @dev Internal: Set the percentage used for slashing indexers. + * @notice Internal: Set the percentage used for slashing indexers. * @param _qryPercentage Percentage slashing for query disputes * @param _idxPercentage Percentage slashing for indexing disputes */ @@ -279,21 +322,16 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Return whether a dispute exists or not. - * @notice Return if dispute with ID `_disputeID` exists - * @param _disputeID True if dispute already exists + * @inheritdoc IDisputeManager */ function isDisputeCreated(bytes32 _disputeID) public view override returns (bool) { return disputes[_disputeID].status != DisputeStatus.Null; } /** - * @dev Get the message hash that an indexer used to sign the receipt. - * Encodes a receipt using a domain separator, as described on + * @inheritdoc IDisputeManager + * @dev Encodes a receipt using a domain separator, as described on * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#specification. - * @notice Return the message hash used to sign the receipt - * @param _receipt Receipt returned by indexer and submitted by fisherman - * @return Message hash used to sign the receipt */ function encodeHashReceipt(Receipt memory _receipt) public view override returns (bytes32) { return @@ -314,11 +352,8 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Returns if two attestations are conflicting. - * Everything must match except for the responseID. - * @param _attestation1 Attestation - * @param _attestation2 Attestation - * @return True if the two attestations are conflicting + * @inheritdoc IDisputeManager + * @dev Everything must match except for the responseID. */ function areConflictingAttestations( Attestation memory _attestation1, @@ -330,9 +365,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Returns the indexer that signed an attestation. - * @param _attestation Attestation - * @return Indexer address + * @inheritdoc IDisputeManager */ function getAttestationIndexer(Attestation memory _attestation) public view override returns (address) { // Get attestation signer. Indexers signs with the allocationID @@ -348,11 +381,9 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Create a query dispute for the arbitrator to resolve. - * This function is called by a fisherman that will need to `_deposit` at + * @inheritdoc IDisputeManager + * @dev This function is called by a fisherman that will need to `_deposit` at * least `minimumDeposit` GRT tokens. - * @param _attestationData Attestation bytes submitted by the fisherman - * @param _deposit Amount of tokens staked as deposit */ function createQueryDispute(bytes calldata _attestationData, uint256 _deposit) external override returns (bytes32) { // Get funds from submitter @@ -369,16 +400,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Create query disputes for two conflicting attestations. - * A conflicting attestation is a proof presented by two different indexers - * where for the same request on a subgraph the response is different. - * For this type of dispute the submitter is not required to present a deposit - * as one of the attestation is considered to be right. - * Two linked disputes will be created and if the arbitrator resolve one, the other - * one will be automatically resolved. - * @param _attestationData1 First attestation data submitted - * @param _attestationData2 Second attestation data submitted - * @return DisputeID1, DisputeID2 + * @inheritdoc IDisputeManager */ function createQueryDisputeConflict( bytes calldata _attestationData1, @@ -409,7 +431,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Create a query dispute passing the parsed attestation. + * @notice Create a query dispute passing the parsed attestation. * To be used in createQueryDispute() and createQueryDisputeConflict() * to avoid calling parseAttestation() multiple times * `_attestationData` is only passed to be emitted @@ -472,8 +494,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa * The disputes are created in reference to an allocationID * This function is called by a challenger that will need to `_deposit` at * least `minimumDeposit` GRT tokens. - * @param _allocationID The allocation to dispute - * @param _deposit Amount of tokens staked as deposit + * @inheritdoc IDisputeManager */ function createIndexingDispute(address _allocationID, uint256 _deposit) external override returns (bytes32) { // Get funds from submitter @@ -484,12 +505,12 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Create indexing dispute internal function. + * @notice Create indexing dispute internal function. * @param _fisherman The challenger creating the dispute * @param _deposit Amount of tokens staked as deposit * @param _allocationID Allocation disputed + * @return disputeID The ID of the created dispute */ - function _createIndexingDisputeWithAllocation( address _fisherman, uint256 _deposit, @@ -525,12 +546,10 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev The arbitrator accepts a dispute as being valid. - * This function will revert if the indexer is not slashable, whether because it does not have + * @dev This function will revert if the indexer is not slashable, whether because it does not have * any stake available or the slashing percentage is configured to be zero. In those cases * a dispute must be resolved using drawDispute or rejectDispute. - * @notice Accept a dispute with ID `_disputeID` - * @param _disputeID ID of the dispute to be accepted + * @inheritdoc IDisputeManager */ function acceptDispute(bytes32 _disputeID) external override onlyArbitrator onlyPendingDispute(_disputeID) { Dispute storage dispute = disputes[_disputeID]; @@ -552,9 +571,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev The arbitrator rejects a dispute as being invalid. - * @notice Reject a dispute with ID `_disputeID` - * @param _disputeID ID of the dispute to be rejected + * @inheritdoc IDisputeManager */ function rejectDispute(bytes32 _disputeID) public override onlyArbitrator onlyPendingDispute(_disputeID) { Dispute storage dispute = disputes[_disputeID]; @@ -575,9 +592,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev The arbitrator draws dispute. - * @notice Ignore a dispute with ID `_disputeID` - * @param _disputeID ID of the dispute to be disregarded + * @inheritdoc IDisputeManager */ function drawDispute(bytes32 _disputeID) external override onlyArbitrator onlyPendingDispute(_disputeID) { Dispute storage dispute = disputes[_disputeID]; @@ -595,7 +610,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Returns whether the dispute is for a conflicting attestation or not. + * @notice Returns whether the dispute is for a conflicting attestation or not. * @param _dispute Dispute * @return True conflicting attestation dispute */ @@ -606,7 +621,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Resolve the conflicting dispute if there is any for the one passed to this function. + * @notice Resolve the conflicting dispute if there is any for the one passed to this function. * @param _dispute Dispute * @return True if resolved */ @@ -621,7 +636,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Pull deposit from submitter account. + * @notice Pull deposit from submitter account. * @param _deposit Amount of tokens to deposit */ function _pullSubmitterDeposit(uint256 _deposit) private { @@ -633,7 +648,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Make the staking contract slash the indexer and reward the challenger. + * @notice Make the staking contract slash the indexer and reward the challenger. * Give the challenger a reward equal to the fishermanRewardPercentage of slashed amount * @param _indexer Address of the indexer * @param _challenger Address of the challenger @@ -664,7 +679,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Return the slashing percentage for the dispute type. + * @notice Return the slashing percentage for the dispute type. * @param _disputeType Dispute type * @return Slashing percentage to use for the dispute type */ @@ -675,7 +690,7 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Recover the signer address of the `_attestation`. + * @notice Recover the signer address of the `_attestation`. * @param _attestation The attestation struct * @return Signer address */ @@ -694,11 +709,12 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Get the running network chain ID + * @notice Get the running network chain ID * @return The chain ID */ function _getChainID() private pure returns (uint256) { uint256 id; + // solhint-disable-next-line no-inline-assembly assembly { id := chainid() } @@ -706,7 +722,8 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Parse the bytes attestation into a struct from `_data`. + * @notice Parse the bytes attestation into a struct from `_data`. + * @param _data The bytes data to parse into an attestation * @return Attestation struct */ function _parseAttestation(bytes memory _data) private pure returns (Attestation memory) { @@ -729,13 +746,16 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Parse a uint8 from `_bytes` starting at offset `_start`. + * @notice Parse a uint8 from `_bytes` starting at offset `_start`. + * @param _bytes The bytes array to parse from + * @param _start The starting offset in the bytes array * @return uint8 value */ function _toUint8(bytes memory _bytes, uint256 _start) private pure returns (uint8) { require(_bytes.length >= (_start + UINT8_BYTE_LENGTH), "Bytes: out of bounds"); uint8 tempUint; + // solhint-disable-next-line no-inline-assembly assembly { tempUint := mload(add(add(_bytes, 0x1), _start)) } @@ -744,13 +764,16 @@ contract DisputeManager is DisputeManagerV1Storage, GraphUpgradeable, IDisputeMa } /** - * @dev Parse a bytes32 from `_bytes` starting at offset `_start`. + * @notice Parse a bytes32 from `_bytes` starting at offset `_start`. + * @param _bytes The bytes array to parse from + * @param _start The starting offset in the bytes array * @return bytes32 value */ function _toBytes32(bytes memory _bytes, uint256 _start) private pure returns (bytes32) { require(_bytes.length >= (_start + BYTES32_BYTE_LENGTH), "Bytes: out of bounds"); bytes32 tempBytes32; + // solhint-disable-next-line no-inline-assembly assembly { tempBytes32 := mload(add(add(_bytes, 0x20), _start)) } diff --git a/packages/contracts/contracts/disputes/DisputeManagerStorage.sol b/packages/contracts/contracts/disputes/DisputeManagerStorage.sol index 4df6e0ae6..f82cff337 100644 --- a/packages/contracts/contracts/disputes/DisputeManagerStorage.sol +++ b/packages/contracts/contracts/disputes/DisputeManagerStorage.sol @@ -1,34 +1,44 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable named-parameters-mapping + pragma solidity ^0.7.6; -import "../governance/Managed.sol"; +import { Managed } from "../governance/Managed.sol"; -import "./IDisputeManager.sol"; +import { IDisputeManager } from "./IDisputeManager.sol"; +/** + * @title Dispute Manager Storage V1 + * @author Edge & Node + * @notice Storage contract for the Dispute Manager + */ contract DisputeManagerV1Storage is Managed { // -- State -- - bytes32 internal DOMAIN_SEPARATOR; + /// @dev Domain separator for EIP-712 signature verification + bytes32 internal DOMAIN_SEPARATOR; // solhint-disable-line var-name-mixedcase - // The arbitrator is solely in control of arbitrating disputes + /// @notice The arbitrator is solely in control of arbitrating disputes address public arbitrator; - // Minimum deposit required to create a Dispute + /// @notice Minimum deposit required to create a Dispute uint256 public minimumDeposit; // -- Slot 0xf - // Percentage of indexer slashed funds to assign as a reward to fisherman in successful dispute - // Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + /// @notice Percentage of indexer slashed funds to assign as a reward to fisherman in successful dispute + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) uint32 public fishermanRewardPercentage; - // Percentage of indexer stake to slash on disputes - // Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) + /// @notice Percentage of indexer stake to slash on disputes + /// Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) uint32 public qrySlashingPercentage; + /// @notice Percentage of indexer stake to slash on disputes uint32 public idxSlashingPercentage; // -- Slot 0x10 - // Disputes created : disputeID => Dispute - // disputeID - check creation functions to see how disputeID is built + /// @notice Disputes created : disputeID => Dispute + /// @dev disputeID - check creation functions to see how disputeID is built mapping(bytes32 => IDisputeManager.Dispute) public disputes; } diff --git a/packages/contracts/contracts/disputes/IDisputeManager.sol b/packages/contracts/contracts/disputes/IDisputeManager.sol index e42386941..0db94e290 100644 --- a/packages/contracts/contracts/disputes/IDisputeManager.sol +++ b/packages/contracts/contracts/disputes/IDisputeManager.sol @@ -1,17 +1,28 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.6.12 <0.8.0 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27; pragma abicoder v2; +/** + * @title Dispute Manager Interface + * @author Edge & Node + * @notice Interface for the Dispute Manager contract that handles indexing and query disputes + */ interface IDisputeManager { // -- Dispute -- + /** + * @dev Types of disputes that can be created + */ enum DisputeType { Null, IndexingDispute, QueryDispute } + /** + * @dev Status of a dispute + */ enum DisputeStatus { Null, Accepted, @@ -20,7 +31,15 @@ interface IDisputeManager { Pending } - // Disputes contain info necessary for the Arbitrator to verify and resolve + /** + * @dev Disputes contain info necessary for the Arbitrator to verify and resolve + * @param indexer Address of the indexer being disputed + * @param fisherman Address of the challenger creating the dispute + * @param deposit Amount of tokens staked as deposit + * @param relatedDisputeID ID of related dispute (for conflicting attestations) + * @param disputeType Type of dispute (Query or Indexing) + * @param status Current status of the dispute + */ struct Dispute { address indexer; address fisherman; @@ -32,14 +51,27 @@ interface IDisputeManager { // -- Attestation -- - // Receipt content sent from indexer in response to request + /** + * @dev Receipt content sent from indexer in response to request + * @param requestCID Content ID of the request + * @param responseCID Content ID of the response + * @param subgraphDeploymentID ID of the subgraph deployment + */ struct Receipt { bytes32 requestCID; bytes32 responseCID; bytes32 subgraphDeploymentID; } - // Attestation sent from indexer in response to a request + /** + * @dev Attestation sent from indexer in response to a request + * @param requestCID Content ID of the request + * @param responseCID Content ID of the response + * @param subgraphDeploymentID ID of the subgraph deployment + * @param r R component of the signature + * @param s S component of the signature + * @param v Recovery ID of the signature + */ struct Attestation { bytes32 requestCID; bytes32 responseCID; @@ -51,41 +83,121 @@ interface IDisputeManager { // -- Configuration -- + /** + * @dev Set the arbitrator address. + * @notice Update the arbitrator to `_arbitrator` + * @param _arbitrator The address of the arbitration contract or party + */ function setArbitrator(address _arbitrator) external; + /** + * @dev Set the minimum deposit required to create a dispute. + * @notice Update the minimum deposit to `_minimumDeposit` Graph Tokens + * @param _minimumDeposit The minimum deposit in Graph Tokens + */ function setMinimumDeposit(uint256 _minimumDeposit) external; + /** + * @dev Set the percent reward that the fisherman gets when slashing occurs. + * @notice Update the reward percentage to `_percentage` + * @param _percentage Reward as a percentage of indexer stake + */ function setFishermanRewardPercentage(uint32 _percentage) external; + /** + * @notice Set the percentage used for slashing indexers. + * @param _qryPercentage Percentage slashing for query disputes + * @param _idxPercentage Percentage slashing for indexing disputes + */ function setSlashingPercentage(uint32 _qryPercentage, uint32 _idxPercentage) external; // -- Getters -- + /** + * @notice Check if a dispute has been created + * @param _disputeID Dispute identifier + * @return True if the dispute exists + */ function isDisputeCreated(bytes32 _disputeID) external view returns (bool); + /** + * @notice Encode a receipt into a hash for EIP-712 signature verification + * @param _receipt The receipt to encode + * @return The encoded hash + */ function encodeHashReceipt(Receipt memory _receipt) external view returns (bytes32); + /** + * @notice Check if two attestations are conflicting + * @param _attestation1 First attestation + * @param _attestation2 Second attestation + * @return True if attestations are conflicting + */ function areConflictingAttestations( Attestation memory _attestation1, Attestation memory _attestation2 ) external pure returns (bool); + /** + * @notice Get the indexer address from an attestation + * @param _attestation The attestation to extract indexer from + * @return The indexer address + */ function getAttestationIndexer(Attestation memory _attestation) external view returns (address); // -- Dispute -- + /** + * @notice Create a query dispute for the arbitrator to resolve. + * This function is called by a fisherman that will need to `_deposit` at + * least `minimumDeposit` GRT tokens. + * @param _attestationData Attestation bytes submitted by the fisherman + * @param _deposit Amount of tokens staked as deposit + * @return The dispute ID + */ function createQueryDispute(bytes calldata _attestationData, uint256 _deposit) external returns (bytes32); + /** + * @notice Create query disputes for two conflicting attestations. + * A conflicting attestation is a proof presented by two different indexers + * where for the same request on a subgraph the response is different. + * For this type of dispute the submitter is not required to present a deposit + * as one of the attestation is considered to be right. + * Two linked disputes will be created and if the arbitrator resolve one, the other + * one will be automatically resolved. + * @param _attestationData1 First attestation data submitted + * @param _attestationData2 Second attestation data submitted + * @return First dispute ID + * @return Second dispute ID + */ function createQueryDisputeConflict( bytes calldata _attestationData1, bytes calldata _attestationData2 ) external returns (bytes32, bytes32); + /** + * @notice Create an indexing dispute + * @param _allocationID Allocation ID being disputed + * @param _deposit Deposit amount for the dispute + * @return The dispute ID + */ function createIndexingDispute(address _allocationID, uint256 _deposit) external returns (bytes32); + /** + * @notice Accept a dispute (arbitrator only) + * @param _disputeID ID of the dispute to accept + */ function acceptDispute(bytes32 _disputeID) external; + /** + * @notice Reject a dispute (arbitrator only) + * @param _disputeID ID of the dispute to reject + */ function rejectDispute(bytes32 _disputeID) external; + /** + * @notice Draw a dispute (arbitrator only) + * @param _disputeID ID of the dispute to draw + */ function drawDispute(bytes32 _disputeID) external; } diff --git a/packages/contracts/contracts/epochs/EpochManager.sol b/packages/contracts/contracts/epochs/EpochManager.sol index 281b63896..440f3d1cb 100644 --- a/packages/contracts/contracts/epochs/EpochManager.sol +++ b/packages/contracts/contracts/epochs/EpochManager.sol @@ -2,27 +2,45 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts/math/SafeMath.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events, gas-small-strings, gas-strict-inequalities -import "../upgrades/GraphUpgradeable.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import "./EpochManagerStorage.sol"; -import "./IEpochManager.sol"; +import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; +import { Managed } from "../governance/Managed.sol"; + +import { EpochManagerV1Storage } from "./EpochManagerStorage.sol"; +import { IEpochManager } from "./IEpochManager.sol"; /** * @title EpochManager contract - * @dev Produce epochs based on a number of blocks to coordinate contracts in the protocol. + * @author Edge & Node + * @notice Produce epochs based on a number of blocks to coordinate contracts in the protocol. */ contract EpochManager is EpochManagerV1Storage, GraphUpgradeable, IEpochManager { using SafeMath for uint256; // -- Events -- + /** + * @notice Emitted when an epoch is run + * @param epoch The epoch number that was run + * @param caller Address that called runEpoch() + */ event EpochRun(uint256 indexed epoch, address caller); + + /** + * @notice Emitted when the epoch length is updated + * @param epoch The epoch when the length was updated + * @param epochLength The new epoch length in blocks + */ event EpochLengthUpdate(uint256 indexed epoch, uint256 epochLength); /** - * @dev Initialize this contract. + * @notice Initialize this contract. + * @param _controller Address of the Controller contract + * @param _epochLength Length of each epoch in blocks */ function initialize(address _controller, uint256 _epochLength) external onlyImpl { require(_epochLength > 0, "Epoch length cannot be 0"); @@ -39,9 +57,7 @@ contract EpochManager is EpochManagerV1Storage, GraphUpgradeable, IEpochManager } /** - * @dev Set the epoch length. - * @notice Set epoch length to `_epochLength` blocks - * @param _epochLength Epoch length in blocks + * @inheritdoc IEpochManager */ function setEpochLength(uint256 _epochLength) external override onlyGovernor { require(_epochLength > 0, "Epoch length cannot be 0"); @@ -55,8 +71,7 @@ contract EpochManager is EpochManagerV1Storage, GraphUpgradeable, IEpochManager } /** - * @dev Run a new epoch, should be called once at the start of any epoch. - * @notice Perform state changes for the current epoch + * @inheritdoc IEpochManager */ function runEpoch() external override { // Check if already called for the current epoch @@ -70,24 +85,21 @@ contract EpochManager is EpochManagerV1Storage, GraphUpgradeable, IEpochManager } /** - * @dev Return true if the current epoch has already run. - * @return Return true if current epoch is the last epoch that has run + * @inheritdoc IEpochManager */ function isCurrentEpochRun() public view override returns (bool) { return lastRunEpoch == currentEpoch(); } /** - * @dev Return current block number. - * @return Block number + * @inheritdoc IEpochManager */ function blockNum() public view override returns (uint256) { return block.number; } /** - * @dev Return blockhash for a block. - * @return BlockHash for `_block` number + * @inheritdoc IEpochManager */ function blockHash(uint256 _block) external view override returns (bytes32) { uint256 currentBlock = blockNum(); @@ -99,33 +111,28 @@ contract EpochManager is EpochManagerV1Storage, GraphUpgradeable, IEpochManager } /** - * @dev Return the current epoch, it may have not been run yet. - * @return The current epoch based on epoch length + * @inheritdoc IEpochManager */ function currentEpoch() public view override returns (uint256) { return lastLengthUpdateEpoch.add(epochsSinceUpdate()); } /** - * @dev Return block where the current epoch started. - * @return The block number when the current epoch started + * @inheritdoc IEpochManager */ function currentEpochBlock() public view override returns (uint256) { return lastLengthUpdateBlock.add(epochsSinceUpdate().mul(epochLength)); } /** - * @dev Return the number of blocks that passed since current epoch started. - * @return Blocks that passed since start of epoch + * @inheritdoc IEpochManager */ function currentEpochBlockSinceStart() external view override returns (uint256) { return blockNum() - currentEpochBlock(); } /** - * @dev Return the number of epoch that passed since another epoch. - * @param _epoch Epoch to use as since epoch value - * @return Number of epochs and current epoch + * @inheritdoc IEpochManager */ function epochsSince(uint256 _epoch) external view override returns (uint256) { uint256 epoch = currentEpoch(); @@ -133,8 +140,7 @@ contract EpochManager is EpochManagerV1Storage, GraphUpgradeable, IEpochManager } /** - * @dev Return number of epochs passed since last epoch length update. - * @return The number of epoch that passed since last epoch length update + * @inheritdoc IEpochManager */ function epochsSinceUpdate() public view override returns (uint256) { return blockNum().sub(lastLengthUpdateBlock).div(epochLength); diff --git a/packages/contracts/contracts/epochs/EpochManagerStorage.sol b/packages/contracts/contracts/epochs/EpochManagerStorage.sol index 5f8599434..894f34a46 100644 --- a/packages/contracts/contracts/epochs/EpochManagerStorage.sol +++ b/packages/contracts/contracts/epochs/EpochManagerStorage.sol @@ -2,18 +2,24 @@ pragma solidity ^0.7.6; -import "../governance/Managed.sol"; +import { Managed } from "../governance/Managed.sol"; +/** + * @title Epoch Manager Storage V1 + * @author Edge & Node + * @notice Storage contract for the Epoch Manager + */ contract EpochManagerV1Storage is Managed { // -- State -- - // Epoch length in blocks + /// @notice Epoch length in blocks uint256 public epochLength; - // Epoch that was last run + /// @notice Epoch that was last run uint256 public lastRunEpoch; - // Block and epoch when epoch length was last updated + /// @notice Epoch when epoch length was last updated uint256 public lastLengthUpdateEpoch; + /// @notice Block when epoch length was last updated uint256 public lastLengthUpdateBlock; } diff --git a/packages/contracts/contracts/epochs/IEpochManager.sol b/packages/contracts/contracts/epochs/IEpochManager.sol index c65280d59..24759f603 100644 --- a/packages/contracts/contracts/epochs/IEpochManager.sol +++ b/packages/contracts/contracts/epochs/IEpochManager.sol @@ -2,30 +2,77 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Epoch Manager Interface + * @author Edge & Node + * @notice Interface for the Epoch Manager contract that handles protocol epochs + */ interface IEpochManager { // -- Configuration -- + /** + * @notice Set epoch length to `_epochLength` blocks + * @param _epochLength Epoch length in blocks + */ function setEpochLength(uint256 _epochLength) external; // -- Epochs + /** + * @dev Run a new epoch, should be called once at the start of any epoch. + * @notice Perform state changes for the current epoch + */ function runEpoch() external; // -- Getters -- + /** + * @notice Check if the current epoch has been run + * @return True if current epoch has been run, false otherwise + */ function isCurrentEpochRun() external view returns (bool); + /** + * @notice Get the current block number + * @return Current block number + */ function blockNum() external view returns (uint256); + /** + * @notice Get the hash of a specific block + * @param _block Block number to get hash for + * @return Block hash + */ function blockHash(uint256 _block) external view returns (bytes32); + /** + * @notice Get the current epoch number + * @return Current epoch number + */ function currentEpoch() external view returns (uint256); + /** + * @notice Get the block number when the current epoch started + * @return Block number of current epoch start + */ function currentEpochBlock() external view returns (uint256); + /** + * @notice Get the number of blocks since the current epoch started + * @return Number of blocks since current epoch start + */ function currentEpochBlockSinceStart() external view returns (uint256); + /** + * @notice Get the number of epochs since a given epoch + * @param _epoch Epoch to calculate from + * @return Number of epochs since the given epoch + */ function epochsSince(uint256 _epoch) external view returns (uint256); + /** + * @notice Get the number of epochs since the last epoch length update + * @return Number of epochs since last update + */ function epochsSinceUpdate() external view returns (uint256); } diff --git a/packages/contracts/contracts/gateway/BridgeEscrow.sol b/packages/contracts/contracts/gateway/BridgeEscrow.sol index 73bc0a3d7..d3b50edc8 100644 --- a/packages/contracts/contracts/gateway/BridgeEscrow.sol +++ b/packages/contracts/contracts/gateway/BridgeEscrow.sol @@ -9,7 +9,8 @@ import { Managed } from "../governance/Managed.sol"; /** * @title Bridge Escrow - * @dev This contracts acts as a gateway for an L2 bridge (or several). It simply holds GRT and has + * @author Edge & Node + * @notice This contracts acts as a gateway for an L2 bridge (or several). It simply holds GRT and has * a set of spenders that can transfer the tokens; the L1 side of each L2 bridge has to be * approved as a spender. */ diff --git a/packages/contracts/contracts/gateway/GraphTokenGateway.sol b/packages/contracts/contracts/gateway/GraphTokenGateway.sol index fb992afc2..f11f52f7d 100644 --- a/packages/contracts/contracts/gateway/GraphTokenGateway.sol +++ b/packages/contracts/contracts/gateway/GraphTokenGateway.sol @@ -9,7 +9,8 @@ import { Managed } from "../governance/Managed.sol"; /** * @title L1/L2 Graph Token Gateway - * @dev This includes everything that's shared between the L1 and L2 sides of the bridge. + * @author Edge & Node + * @notice This includes everything that's shared between the L1 and L2 sides of the bridge. */ abstract contract GraphTokenGateway is GraphUpgradeable, Pausable, Managed, ITokenGateway { /// @dev Storage gap added in case we need to add state variables to this contract @@ -52,7 +53,7 @@ abstract contract GraphTokenGateway is GraphUpgradeable, Pausable, Managed, ITok } /** - * @dev Override the default pausing from Managed to allow pausing this + * @notice Override the default pausing from Managed to allow pausing this * particular contract instead of pausing from the Controller. */ function _notPaused() internal view override { @@ -60,7 +61,7 @@ abstract contract GraphTokenGateway is GraphUpgradeable, Pausable, Managed, ITok } /** - * @dev Runs state validation before unpausing, reverts if + * @notice Runs state validation before unpausing, reverts if * something is not set properly */ function _checksBeforeUnpause() internal view virtual; diff --git a/packages/contracts/contracts/gateway/ICallhookReceiver.sol b/packages/contracts/contracts/gateway/ICallhookReceiver.sol index 8d003cb76..d3b674bcc 100644 --- a/packages/contracts/contracts/gateway/ICallhookReceiver.sol +++ b/packages/contracts/contracts/gateway/ICallhookReceiver.sol @@ -2,12 +2,18 @@ /** * @title Interface for contracts that can receive callhooks through the Arbitrum GRT bridge - * @dev Any contract that can receive a callhook on L2, sent through the bridge from L1, must + * @author Edge & Node + * @notice Any contract that can receive a callhook on L2, sent through the bridge from L1, must * be allowlisted by the governor, but also implement this interface that contains * the function that will actually be called by the L2GraphTokenGateway. */ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Callhook Receiver Interface + * @author Edge & Node + * @notice Interface for contracts that can receive tokens with callhook from the bridge + */ interface ICallhookReceiver { /** * @notice Receive tokens with a callhook from the bridge diff --git a/packages/contracts/contracts/gateway/L1GraphTokenGateway.sol b/packages/contracts/contracts/gateway/L1GraphTokenGateway.sol index 7fad927ad..20c25d2e6 100644 --- a/packages/contracts/contracts/gateway/L1GraphTokenGateway.sol +++ b/packages/contracts/contracts/gateway/L1GraphTokenGateway.sol @@ -3,6 +3,10 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events, gas-strict-inequalities +// solhint-disable named-parameters-mapping + import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; @@ -18,7 +22,8 @@ import { IGraphToken } from "../token/IGraphToken.sol"; /** * @title L1 Graph Token Gateway Contract - * @dev Provides the L1 side of the Ethereum-Arbitrum GRT bridge. Sends GRT to the L2 chain + * @author Edge & Node + * @notice Provides the L1 side of the Ethereum-Arbitrum GRT bridge. Sends GRT to the L2 chain * by escrowing them and sending a message to the L2 gateway, and receives tokens from L2 by * releasing them from escrow. * Based on Offchain Labs' reference implementation and Livepeer's arbitrum-lpt-bridge @@ -28,28 +33,35 @@ import { IGraphToken } from "../token/IGraphToken.sol"; contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMessenger { using SafeMathUpgradeable for uint256; - /// Address of the Graph Token contract on L2 + /// @notice Address of the Graph Token contract on L2 address public l2GRT; - /// Address of the Arbitrum Inbox + /// @notice Address of the Arbitrum Inbox address public inbox; - /// Address of the Arbitrum Gateway Router on L1 + /// @notice Address of the Arbitrum Gateway Router on L1 address public l1Router; - /// Address of the L2GraphTokenGateway on L2 that is the counterpart of this gateway + /// @notice Address of the L2GraphTokenGateway on L2 that is the counterpart of this gateway address public l2Counterpart; - /// Address of the BridgeEscrow contract that holds the GRT in the bridge + /// @notice Address of the BridgeEscrow contract that holds the GRT in the bridge address public escrow; - /// Addresses for which this mapping is true are allowed to send callhooks in outbound transfers + /// @notice Addresses for which this mapping is true are allowed to send callhooks in outbound transfers mapping(address => bool) public callhookAllowlist; - /// Total amount minted from L2 + /// @notice Total amount minted from L2 uint256 public totalMintedFromL2; - /// Accumulated allowance for tokens minted from L2 at lastL2MintAllowanceUpdateBlock + /// @notice Accumulated allowance for tokens minted from L2 at lastL2MintAllowanceUpdateBlock uint256 public accumulatedL2MintAllowanceSnapshot; - /// Block at which new L2 allowance starts accumulating + /// @notice Block at which new L2 allowance starts accumulating uint256 public lastL2MintAllowanceUpdateBlock; - /// New L2 mint allowance per block + /// @notice New L2 mint allowance per block uint256 public l2MintAllowancePerBlock; - /// Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 + /** + * @notice Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 + * @param l1Token Address of the L1 token being transferred + * @param from Address sending the tokens on L1 + * @param to Address receiving the tokens on L2 + * @param sequenceNumber Sequence number of the retryable ticket + * @param amount Amount of tokens transferred + */ event DepositInitiated( address l1Token, address indexed from, @@ -58,7 +70,14 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess uint256 amount ); - /// Emitted when an incoming transfer is finalized, i.e tokens are withdrawn from L2 to L1 + /** + * @notice Emitted when an incoming transfer is finalized, i.e tokens are withdrawn from L2 to L1 + * @param l1Token Address of the L1 token being transferred + * @param from Address sending the tokens on L2 + * @param to Address receiving the tokens on L1 + * @param exitNum Exit number (always 0 for this contract) + * @param amount Amount of tokens transferred + */ event WithdrawalFinalized( address l1Token, address indexed from, @@ -67,25 +86,58 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess uint256 amount ); - /// Emitted when the Arbitrum Inbox and Gateway Router addresses have been updated + /** + * @notice Emitted when the Arbitrum Inbox and Gateway Router addresses have been updated + * @param inbox Address of the Arbitrum Inbox + * @param l1Router Address of the L1 Gateway Router + */ event ArbitrumAddressesSet(address inbox, address l1Router); - /// Emitted when the L2 GRT address has been updated + + /** + * @notice Emitted when the L2 GRT address has been updated + * @param l2GRT Address of the L2 GRT contract + */ event L2TokenAddressSet(address l2GRT); - /// Emitted when the counterpart L2GraphTokenGateway address has been updated + + /** + * @notice Emitted when the counterpart L2GraphTokenGateway address has been updated + * @param l2Counterpart Address of the L2 counterpart gateway + */ event L2CounterpartAddressSet(address l2Counterpart); - /// Emitted when the escrow address has been updated + /** + * @notice Emitted when the escrow address has been updated + * @param escrow Address of the escrow contract + */ event EscrowAddressSet(address escrow); - /// Emitted when an address is added to the callhook allowlist + + /** + * @notice Emitted when an address is added to the callhook allowlist + * @param newAllowlisted Address added to the allowlist + */ event AddedToCallhookAllowlist(address newAllowlisted); - /// Emitted when an address is removed from the callhook allowlist + + /** + * @notice Emitted when an address is removed from the callhook allowlist + * @param notAllowlisted Address removed from the allowlist + */ event RemovedFromCallhookAllowlist(address notAllowlisted); - /// Emitted when the L2 mint allowance per block is updated + + /** + * @notice Emitted when the L2 mint allowance per block is updated + * @param accumulatedL2MintAllowanceSnapshot Accumulated allowance snapshot at update block + * @param l2MintAllowancePerBlock New allowance per block + * @param lastL2MintAllowanceUpdateBlock Block number when allowance was updated + */ event L2MintAllowanceUpdated( uint256 accumulatedL2MintAllowanceSnapshot, uint256 l2MintAllowancePerBlock, uint256 lastL2MintAllowanceUpdateBlock ); - /// Emitted when tokens are minted due to an incoming transfer from L2 + + /** + * @notice Emitted when tokens are minted due to an incoming transfer from L2 + * @param amount Amount of tokens minted + */ event TokensMintedFromL2(uint256 amount); /** @@ -199,7 +251,7 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @dev Updates the L2 mint allowance per block + * @notice Updates the L2 mint allowance per block * It is meant to be called _after_ the issuancePerBlock is updated in L2. * The caller should provide the new issuance per block and the block at which it was updated, * the function will automatically compute the values so that the bridge's mint allowance @@ -221,7 +273,7 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @dev Manually sets the parameters used to compute the L2 mint allowance + * @notice Manually sets the parameters used to compute the L2 mint allowance * The use of this function is not recommended, use updateL2MintAllowance instead; * this one is only meant to be used as a backup recovery if a previous call to * updateL2MintAllowance was done with incorrect values. @@ -246,10 +298,7 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox. - * The tokens are escrowed by the gateway until they are withdrawn back to L1. - * The ticket must be redeemed on L2 to receive tokens at the specified address. - * Note that the caller must previously allow the gateway to spend the specified amount of GRT. + * @inheritdoc ITokenGateway * @dev maxGas and gasPriceBid must be set using Arbitrum's NodeInterface.estimateRetryableTicket method. * Also note that allowlisted senders (some protocol contracts) can include additional calldata * for a callhook to be executed on the L2 side when the tokens are received. In this case, the L2 transaction @@ -257,13 +306,6 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess * never succeeds. This requires extra care when adding contracts to the allowlist, but is necessary to ensure that * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks * with token transfers. - * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) - * @param _to Recipient address on L2 - * @param _amount Amount of tokens to transfer - * @param _maxGas Gas limit for L2 execution of the ticket - * @param _gasPriceBid Price per gas on L2 - * @param _data Encoded maxSubmissionCost and sender address along with additional calldata - * @return Sequence number of the retryable ticket created by Inbox */ function outboundTransfer( address _l1Token, @@ -304,15 +346,10 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @notice Receives withdrawn tokens from L2 - * The equivalent tokens are released from escrow and sent to the destination. + * @inheritdoc ITokenGateway * @dev can only accept transactions coming from the L2 GRT Gateway. * The last parameter is unused but kept for compatibility with Arbitrum gateways, * and the encoded exitNum is assumed to be 0. - * @param _l1Token L1 Address of the GRT contract (needed for compatibility with Arbitrum Gateway Router) - * @param _from Address of the sender - * @param _to Recipient address on L1 - * @param _amount Amount of tokens transferred */ function finalizeInboundTransfer( address _l1Token, @@ -335,10 +372,8 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @notice Calculate the L2 address of a bridged token + * @inheritdoc ITokenGateway * @dev In our case, this would only work for GRT. - * @param _l1ERC20 address of L1 GRT contract - * @return L2 address of the bridged GRT token */ function calculateL2TokenAddress(address _l1ERC20) external view override returns (address) { IGraphToken token = graphToken(); @@ -387,10 +422,8 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess ); } - /** - * @dev Runs state validation before unpausing, reverts if - * something is not set properly - */ + /// @inheritdoc GraphTokenGateway + // solhint-disable-next-line use-natspec function _checksBeforeUnpause() internal view override { require(inbox != address(0), "INBOX_NOT_SET"); require(l1Router != address(0), "ROUTER_NOT_SET"); @@ -425,7 +458,7 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @dev Get the accumulated L2 mint allowance at a particular block number + * @notice Get the accumulated L2 mint allowance at a particular block number * @param _blockNum Block at which allowance will be computed * @return The accumulated GRT amount that can be minted from L2 at the specified block */ @@ -438,7 +471,7 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @dev Mint new L1 tokens coming from L2 + * @notice Mint new L1 tokens coming from L2 * This will check if the amount to mint is within the L2's mint allowance, and revert otherwise. * The tokens will be sent to the bridge escrow (from where they will then be sent to the destinatary * of the current inbound transfer). @@ -454,7 +487,7 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess } /** - * @dev Check if minting a certain amount of tokens from L2 is within allowance + * @notice Check if minting a certain amount of tokens from L2 is within allowance * @param _amount Number of tokens that would be minted * @return true if minting those tokens is allowed, or false if it would be over allowance */ diff --git a/packages/contracts/contracts/governance/Controller.sol b/packages/contracts/contracts/governance/Controller.sol index 707a27fff..3bd3c77cb 100644 --- a/packages/contracts/contracts/governance/Controller.sol +++ b/packages/contracts/contracts/governance/Controller.sol @@ -2,6 +2,12 @@ pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events, gas-small-strings +// solhint-disable named-parameters-mapping + +/* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 + import { IController } from "./IController.sol"; import { IManaged } from "./IManaged.sol"; import { Governed } from "./Governed.sol"; @@ -9,14 +15,19 @@ import { Pausable } from "./Pausable.sol"; /** * @title Graph Controller contract - * @dev Controller is a registry of contracts for convenience. Inspired by Livepeer: + * @author Edge & Node + * @notice Controller is a registry of contracts for convenience. Inspired by Livepeer: * https://github.com/livepeer/protocol/blob/streamflow/contracts/Controller.sol */ contract Controller is Governed, Pausable, IController { /// @dev Track contract ids to contract proxy address mapping(bytes32 => address) private _registry; - /// Emitted when the proxy address for a protocol contract has been set + /** + * @notice Emitted when the proxy address for a protocol contract has been set + * @param id Contract identifier + * @param contractAddress Address of the contract proxy + */ event SetContractProxy(bytes32 indexed id, address contractAddress); /** @@ -37,7 +48,7 @@ contract Controller is Governed, Pausable, IController { } /** - * @notice Getter to access governor + * @inheritdoc IController */ function getGovernor() external view override returns (address) { return governor; @@ -46,9 +57,7 @@ contract Controller is Governed, Pausable, IController { // -- Registry -- /** - * @notice Register contract id and mapped address - * @param _id Contract id (keccak256 hash of contract name) - * @param _contractAddress Contract address + * @inheritdoc IController */ function setContractProxy(bytes32 _id, address _contractAddress) external override onlyGovernor { require(_contractAddress != address(0), "Contract address must be set"); @@ -57,8 +66,7 @@ contract Controller is Governed, Pausable, IController { } /** - * @notice Unregister a contract address - * @param _id Contract id (keccak256 hash of contract name) + * @inheritdoc IController */ function unsetContractProxy(bytes32 _id) external override onlyGovernor { _registry[_id] = address(0); @@ -66,18 +74,14 @@ contract Controller is Governed, Pausable, IController { } /** - * @notice Get contract proxy address by its id - * @param _id Contract id - * @return Address of the proxy contract for the provided id + * @inheritdoc IController */ function getContractProxy(bytes32 _id) external view override returns (address) { return _registry[_id]; } /** - * @notice Update contract's controller - * @param _id Contract id (keccak256 hash of contract name) - * @param _controller Controller address + * @inheritdoc IController */ function updateController(bytes32 _id, address _controller) external override onlyGovernor { require(_controller != address(0), "Controller must be set"); @@ -96,17 +100,15 @@ contract Controller is Governed, Pausable, IController { } /** - * @notice Change the paused state of the contract - * Full pause most of protocol functions - * @param _toPause True if the contracts should be paused, false otherwise + * @inheritdoc IController + * @dev Full pause most of protocol functions */ function setPaused(bool _toPause) external override onlyGovernorOrGuardian { _setPaused(_toPause); } /** - * @notice Change the Pause Guardian - * @param _newPauseGuardian The address of the new Pause Guardian + * @inheritdoc IController */ function setPauseGuardian(address _newPauseGuardian) external override onlyGovernor { require(_newPauseGuardian != address(0), "PauseGuardian must be set"); @@ -114,16 +116,14 @@ contract Controller is Governed, Pausable, IController { } /** - * @notice Getter to access paused - * @return True if the contracts are paused, false otherwise + * @inheritdoc IController */ function paused() external view override returns (bool) { return _paused; } /** - * @notice Getter to access partial pause status - * @return True if the contracts are partially paused, false otherwise + * @inheritdoc IController */ function partialPaused() external view override returns (bool) { return _partialPaused; diff --git a/packages/contracts/contracts/governance/Governed.sol b/packages/contracts/contracts/governance/Governed.sol index 76a3247dd..8c3446b88 100644 --- a/packages/contracts/contracts/governance/Governed.sol +++ b/packages/contracts/contracts/governance/Governed.sol @@ -2,23 +2,39 @@ pragma solidity ^0.7.6 || 0.8.27; +/* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 + /** * @title Graph Governance contract - * @dev All contracts that will be owned by a Governor entity should extend this contract. + * @author Edge & Node + * @notice All contracts that will be owned by a Governor entity should extend this contract. */ abstract contract Governed { // -- State -- - /// Address of the governor + /** + * @notice Address of the governor + */ address public governor; - /// Address of the new governor that is pending acceptance + /** + * @notice Address of the new governor that is pending acceptance + */ address public pendingGovernor; // -- Events -- - /// Emitted when a new owner/governor has been set, but is pending acceptance + /** + * @notice Emitted when a new owner/governor has been set, but is pending acceptance + * @param from Previous pending governor address + * @param to New pending governor address + */ event NewPendingOwnership(address indexed from, address indexed to); - /// Emitted when a new owner/governor has accepted their role + + /** + * @notice Emitted when a new owner/governor has accepted their role + * @param from Previous governor address + * @param to New governor address + */ event NewOwnership(address indexed from, address indexed to); /** @@ -30,7 +46,7 @@ abstract contract Governed { } /** - * @dev Initialize the governor for this contract + * @notice Initialize the governor for this contract * @param _initGovernor Address of the governor */ function _initialize(address _initGovernor) internal { diff --git a/packages/contracts/contracts/governance/IController.sol b/packages/contracts/contracts/governance/IController.sol index 6ab72010e..af1c43a0e 100644 --- a/packages/contracts/contracts/governance/IController.sol +++ b/packages/contracts/contracts/governance/IController.sol @@ -2,28 +2,78 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Controller Interface + * @author Edge & Node + * @notice Interface for the Controller contract that manages protocol governance and contract registry + */ interface IController { + /** + * @notice Return the governor address + * @return The governor address + */ function getGovernor() external view returns (address); // -- Registry -- + /** + * @notice Register contract id and mapped address + * @param _id Contract id (keccak256 hash of contract name) + * @param _contractAddress Contract address + */ function setContractProxy(bytes32 _id, address _contractAddress) external; + /** + * @notice Unregister a contract address + * @param _id Contract id (keccak256 hash of contract name) + */ function unsetContractProxy(bytes32 _id) external; + /** + * @notice Update contract's controller + * @param _id Contract id (keccak256 hash of contract name) + * @param _controller Controller address + */ function updateController(bytes32 _id, address _controller) external; + /** + * @notice Get contract proxy address by its id + * @param _id Contract id + * @return Address of the proxy contract for the provided id + */ function getContractProxy(bytes32 _id) external view returns (address); // -- Pausing -- + /** + * @notice Change the partial paused state of the contract + * Partial pause is intended as a partial pause of the protocol + * @param _partialPaused True if the contracts should be (partially) paused, false otherwise + */ function setPartialPaused(bool _partialPaused) external; + /** + * @notice Change the paused state of the contract + * Full pause most of protocol functions + * @param _paused True if the contracts should be paused, false otherwise + */ function setPaused(bool _paused) external; + /** + * @notice Change the Pause Guardian + * @param _newPauseGuardian The address of the new Pause Guardian + */ function setPauseGuardian(address _newPauseGuardian) external; + /** + * @notice Return whether the protocol is paused + * @return True if the protocol is paused + */ function paused() external view returns (bool); + /** + * @notice Return whether the protocol is partially paused + * @return True if the protocol is partially paused + */ function partialPaused() external view returns (bool); } diff --git a/packages/contracts/contracts/governance/IManaged.sol b/packages/contracts/contracts/governance/IManaged.sol index ff6625d81..8bfe2ae0b 100644 --- a/packages/contracts/contracts/governance/IManaged.sol +++ b/packages/contracts/contracts/governance/IManaged.sol @@ -6,21 +6,21 @@ import { IController } from "./IController.sol"; /** * @title Managed Interface - * @dev Interface for contracts that can be managed by a controller. + * @author Edge & Node + * @notice Interface for contracts that can be managed by a controller. */ interface IManaged { /** - * @notice Set the controller that manages this contract - * @dev Only the current controller can set a new controller - * @param _controller Address of the new controller + * @notice Set Controller. Only callable by current controller. + * @param _controller Controller contract address */ function setController(address _controller) external; /** * @notice Sync protocol contract addresses from the Controller registry - * @dev This function will cache all the contracts using the latest addresses. + * @dev This function will cache all the contracts using the latest addresses * Anyone can call the function whenever a Proxy contract change in the - * controller to ensure the protocol is using the latest version. + * controller to ensure the protocol is using the latest version */ function syncAllContracts() external; diff --git a/packages/contracts/contracts/governance/Managed.sol b/packages/contracts/contracts/governance/Managed.sol index 9b0ea29c8..596dee0f7 100644 --- a/packages/contracts/contracts/governance/Managed.sol +++ b/packages/contracts/contracts/governance/Managed.sol @@ -2,11 +2,15 @@ pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events +// solhint-disable named-parameters-mapping + import { IController } from "./IController.sol"; import { ICuration } from "../curation/ICuration.sol"; import { IEpochManager } from "../epochs/IEpochManager.sol"; -import { IRewardsManager } from "../rewards/IRewardsManager.sol"; +import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { IStaking } from "../staking/IStaking.sol"; import { IGraphToken } from "../token/IGraphToken.sol"; import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; @@ -16,7 +20,8 @@ import { IManaged } from "./IManaged.sol"; /** * @title Graph Managed contract - * @dev The Managed contract provides an interface to interact with the Controller. + * @author Edge & Node + * @notice The Managed contract provides an interface to interact with the Controller. * It also provides local caching for contract addresses. This mechanism relies on calling the * public `syncAllContracts()` function whenever a contract changes in the controller. * @@ -26,7 +31,9 @@ import { IManaged } from "./IManaged.sol"; abstract contract Managed is IManaged { // -- State -- - /// Controller that manages this contract + /** + * @inheritdoc IManaged + */ IController public override controller; /// @dev Cache for the addresses of the contracts retrieved from the controller mapping(bytes32 => address) private _addressCache; @@ -34,28 +41,46 @@ abstract contract Managed is IManaged { uint256[10] private __gap; // Immutables + /// @dev Contract name hash for Curation contract bytes32 private immutable CURATION = keccak256("Curation"); + /// @dev Contract name hash for EpochManager contract bytes32 private immutable EPOCH_MANAGER = keccak256("EpochManager"); + /// @dev Contract name hash for RewardsManager contract bytes32 private immutable REWARDS_MANAGER = keccak256("RewardsManager"); + /// @dev Contract name hash for Staking contract bytes32 private immutable STAKING = keccak256("Staking"); + /// @dev Contract name hash for GraphToken contract bytes32 private immutable GRAPH_TOKEN = keccak256("GraphToken"); + /// @dev Contract name hash for GraphTokenGateway contract bytes32 private immutable GRAPH_TOKEN_GATEWAY = keccak256("GraphTokenGateway"); + /// @dev Contract name hash for GNS contract bytes32 private immutable GNS = keccak256("GNS"); // -- Events -- - /// Emitted when a contract parameter has been updated + /** + * @notice Emitted when a contract parameter has been updated + * @param param Name of the parameter that was updated + */ event ParameterUpdated(string param); - /// Emitted when the controller address has been set + + /** + * @notice Emitted when the controller address has been set + * @param controller Address of the new controller + */ event SetController(address controller); - /// Emitted when contract with `nameHash` is synced to `contractAddress`. + /** + * @notice Emitted when contract with `nameHash` is synced to `contractAddress`. + * @param nameHash Hash of the contract name + * @param contractAddress Address of the synced contract + */ event ContractSynced(bytes32 indexed nameHash, address contractAddress); // -- Modifiers -- /** - * @dev Revert if the controller is paused or partially paused + * @notice Revert if the controller is paused or partially paused */ function _notPartialPaused() internal view { require(!controller.paused(), "Paused"); @@ -63,21 +88,21 @@ abstract contract Managed is IManaged { } /** - * @dev Revert if the controller is paused + * @notice Revert if the controller is paused */ function _notPaused() internal view virtual { require(!controller.paused(), "Paused"); } /** - * @dev Revert if the caller is not the governor + * @notice Revert if the caller is not the governor */ function _onlyGovernor() internal view { require(msg.sender == controller.getGovernor(), "Only Controller governor"); } /** - * @dev Revert if the caller is not the Controller + * @notice Revert if the caller is not the Controller */ function _onlyController() internal view { require(msg.sender == address(controller), "Caller must be Controller"); @@ -118,7 +143,7 @@ abstract contract Managed is IManaged { // -- Functions -- /** - * @dev Initialize a Managed contract + * @notice Initialize a Managed contract * @param _controller Address for the Controller that manages this contract */ function _initialize(address _controller) internal { @@ -126,15 +151,14 @@ abstract contract Managed is IManaged { } /** - * @notice Set Controller. Only callable by current controller. - * @param _controller Controller contract address + * @inheritdoc IManaged */ function setController(address _controller) external override onlyController { _setController(_controller); } /** - * @dev Set controller. + * @notice Set controller. * @param _controller Controller contract address */ function _setController(address _controller) internal { @@ -144,7 +168,7 @@ abstract contract Managed is IManaged { } /** - * @dev Return Curation interface + * @notice Return Curation interface * @return Curation contract registered with Controller */ function curation() internal view returns (ICuration) { @@ -152,7 +176,7 @@ abstract contract Managed is IManaged { } /** - * @dev Return EpochManager interface + * @notice Return EpochManager interface * @return Epoch manager contract registered with Controller */ function epochManager() internal view returns (IEpochManager) { @@ -160,7 +184,7 @@ abstract contract Managed is IManaged { } /** - * @dev Return RewardsManager interface + * @notice Return RewardsManager interface * @return Rewards manager contract registered with Controller */ function rewardsManager() internal view returns (IRewardsManager) { @@ -168,7 +192,7 @@ abstract contract Managed is IManaged { } /** - * @dev Return Staking interface + * @notice Return Staking interface * @return Staking contract registered with Controller */ function staking() internal view returns (IStaking) { @@ -176,7 +200,7 @@ abstract contract Managed is IManaged { } /** - * @dev Return GraphToken interface + * @notice Return GraphToken interface * @return Graph token contract registered with Controller */ function graphToken() internal view returns (IGraphToken) { @@ -184,7 +208,7 @@ abstract contract Managed is IManaged { } /** - * @dev Return GraphTokenGateway (L1 or L2) interface + * @notice Return GraphTokenGateway (L1 or L2) interface * @return Graph token gateway contract registered with Controller */ function graphTokenGateway() internal view returns (ITokenGateway) { @@ -192,7 +216,7 @@ abstract contract Managed is IManaged { } /** - * @dev Return GNS (L1 or L2) interface. + * @notice Return GNS (L1 or L2) interface. * @return Address of the GNS contract registered with Controller, as an IGNS interface. */ function gns() internal view returns (IGNS) { @@ -200,7 +224,7 @@ abstract contract Managed is IManaged { } /** - * @dev Resolve a contract address from the cache or the Controller if not found. + * @notice Resolve a contract address from the cache or the Controller if not found. * @param _nameHash keccak256 hash of the contract name * @return Address of the contract */ @@ -213,7 +237,7 @@ abstract contract Managed is IManaged { } /** - * @dev Cache a contract address from the Controller registry. + * @notice Cache a contract address from the Controller registry. * @param _nameHash keccak256 hash of the name of the contract to sync into the cache */ function _syncContract(bytes32 _nameHash) internal { @@ -225,10 +249,7 @@ abstract contract Managed is IManaged { } /** - * @notice Sync protocol contract addresses from the Controller registry - * @dev This function will cache all the contracts using the latest addresses - * Anyone can call the function whenever a Proxy contract change in the - * controller to ensure the protocol is using the latest version + * @inheritdoc IManaged */ function syncAllContracts() external override { _syncContract(CURATION); diff --git a/packages/contracts/contracts/governance/Pausable.sol b/packages/contracts/contracts/governance/Pausable.sol index 2bc1795cd..bf260cb72 100644 --- a/packages/contracts/contracts/governance/Pausable.sol +++ b/packages/contracts/contracts/governance/Pausable.sol @@ -2,6 +2,14 @@ pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + +/** + * @title Pausable Contract + * @author Edge & Node + * @notice Abstract contract that provides pause functionality for protocol operations + */ abstract contract Pausable { /** * @dev "Partial paused" pauses exit and enter functions for GRT, but not internal @@ -13,24 +21,36 @@ abstract contract Pausable { */ bool internal _paused; - /// Timestamp for the last time the partial pause was set + /// @notice Timestamp for the last time the partial pause was set uint256 public lastPartialPauseTime; - /// Timestamp for the last time the full pause was set + /// @notice Timestamp for the last time the full pause was set uint256 public lastPauseTime; - /// Pause guardian is a separate entity from the governor that can + /// @notice Pause guardian is a separate entity from the governor that can /// pause and unpause the protocol, fully or partially address public pauseGuardian; - /// Emitted when the partial pause state changed + /** + * @notice Emitted when the partial pause state changed + * @param isPaused Whether the contract is partially paused + */ event PartialPauseChanged(bool isPaused); - /// Emitted when the full pause state changed + + /** + * @notice Emitted when the full pause state changed + * @param isPaused Whether the contract is fully paused + */ event PauseChanged(bool isPaused); - /// Emitted when the pause guardian is changed + + /** + * @notice Emitted when the pause guardian is changed + * @param oldPauseGuardian Address of the previous pause guardian + * @param pauseGuardian Address of the new pause guardian + */ event NewPauseGuardian(address indexed oldPauseGuardian, address indexed pauseGuardian); /** - * @dev Change the partial paused state of the contract + * @notice Change the partial paused state of the contract * @param _toPartialPause New value for the partial pause state (true means the contracts will be partially paused) */ function _setPartialPaused(bool _toPartialPause) internal { @@ -45,7 +65,7 @@ abstract contract Pausable { } /** - * @dev Change the paused state of the contract + * @notice Change the paused state of the contract * @param _toPause New value for the pause state (true means the contracts will be paused) */ function _setPaused(bool _toPause) internal { @@ -60,7 +80,7 @@ abstract contract Pausable { } /** - * @dev Change the Pause Guardian + * @notice Change the Pause Guardian * @param newPauseGuardian The address of the new Pause Guardian */ function _setPauseGuardian(address newPauseGuardian) internal { diff --git a/packages/contracts/contracts/l2/curation/IL2Curation.sol b/packages/contracts/contracts/l2/curation/IL2Curation.sol index 7f93f9603..3b39ac7a6 100644 --- a/packages/contracts/contracts/l2/curation/IL2Curation.sol +++ b/packages/contracts/contracts/l2/curation/IL2Curation.sol @@ -4,6 +4,8 @@ pragma solidity ^0.7.6 || 0.8.27; /** * @title Interface of the L2 Curation contract. + * @author Edge & Node + * @notice Interface for the L2 Curation contract that handles curation on Layer 2 */ interface IL2Curation { /** diff --git a/packages/contracts/contracts/l2/curation/L2Curation.sol b/packages/contracts/contracts/l2/curation/L2Curation.sol index 271545ea7..9a5f6a4e6 100644 --- a/packages/contracts/contracts/l2/curation/L2Curation.sol +++ b/packages/contracts/contracts/l2/curation/L2Curation.sol @@ -3,13 +3,16 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events, gas-small-strings, gas-strict-inequalities + import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; import { ClonesUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol"; import { GraphUpgradeable } from "../../upgrades/GraphUpgradeable.sol"; import { TokenUtils } from "../../utils/TokenUtils.sol"; -import { IRewardsManager } from "../../rewards/IRewardsManager.sol"; +import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { Managed } from "../../governance/Managed.sol"; import { IGraphToken } from "../../token/IGraphToken.sol"; import { CurationV3Storage } from "../../curation/CurationStorage.sol"; @@ -18,7 +21,8 @@ import { IL2Curation } from "./IL2Curation.sol"; /** * @title L2Curation contract - * @dev Allows curators to signal on subgraph deployments that might be relevant to indexers by + * @author Edge & Node + * @notice Allows curators to signal on subgraph deployments that might be relevant to indexers by * staking Graph Tokens (GRT). Additionally, curators earn fees from the Query Market related to the * subgraph deployment they curate. * A curators deposit goes to a curation pool along with the deposits of other curators, @@ -38,14 +42,20 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { uint256 private constant SIGNAL_PER_MINIMUM_DEPOSIT = 1; // 1e-18 signal as 18 decimal number /// @dev Reserve ratio for all subgraphs set to 100% for a flat bonding curve + // solhint-disable-next-line immutable-vars-naming uint32 private immutable fixedReserveRatio = MAX_PPM; // -- Events -- /** - * @dev Emitted when `curator` deposited `tokens` on `subgraphDeploymentID` as curation signal. + * @notice Emitted when `curator` deposited `tokens` on `subgraphDeploymentID` as curation signal. * The `curator` receives `signal` amount according to the curation pool bonding curve. * An amount of `curationTax` will be collected and burned. + * @param curator Address of the curator + * @param subgraphDeploymentID Subgraph deployment being signaled on + * @param tokens Amount of tokens deposited + * @param signal Amount of signal minted + * @param curationTax Amount of tokens burned as curation tax */ event Signalled( address indexed curator, @@ -56,19 +66,26 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { ); /** - * @dev Emitted when `curator` burned `signal` for a `subgraphDeploymentID`. + * @notice Emitted when `curator` burned `signal` for a `subgraphDeploymentID`. * The curator will receive `tokens` according to the value of the bonding curve. + * @param curator Address of the curator + * @param subgraphDeploymentID Subgraph deployment being signaled on + * @param tokens Amount of tokens received + * @param signal Amount of signal burned */ event Burned(address indexed curator, bytes32 indexed subgraphDeploymentID, uint256 tokens, uint256 signal); /** - * @dev Emitted when `tokens` amount were collected for `subgraphDeploymentID` as part of fees + * @notice Emitted when `tokens` amount were collected for `subgraphDeploymentID` as part of fees * distributed by an indexer from query fees received from state channels. + * @param subgraphDeploymentID Subgraph deployment that collected fees + * @param tokens Amount of tokens collected as fees */ event Collected(bytes32 indexed subgraphDeploymentID, uint256 tokens); /** - * @dev Emitted when the subgraph service is set. + * @notice Emitted when the subgraph service is set + * @param newSubgraphService Address of the new subgraph service */ event SubgraphServiceSet(address indexed newSubgraphService); @@ -107,7 +124,8 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { * @notice Set the default reserve ratio - not implemented in L2 * @dev We only keep this for compatibility with ICuration */ - function setDefaultReserveRatio(uint32) external view override onlyGovernor { + // solhint-disable-next-line use-natspec + function setDefaultReserveRatio(uint32 /* _defaultReserveRatio */) external view override onlyGovernor { revert("Not implemented in L2"); } @@ -153,7 +171,7 @@ 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 or Staking contract are authorized as caller + // 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" @@ -174,7 +192,8 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal * @param _tokensIn Amount of Graph Tokens to deposit * @param _signalOutMin Expected minimum amount of signal to receive - * @return Signal minted and deposit tax + * @return Signal minted + * @return Curation tax paid */ function mint( bytes32 _subgraphDeploymentID, @@ -228,12 +247,7 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { } /** - * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. - * @dev This function charges no tax and can only be called by GNS in specific scenarios (for now - * only during an L1-L2 transfer). - * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal - * @param _tokensIn Amount of Graph Tokens to deposit - * @return Signal minted + * @inheritdoc IL2Curation */ function mintTaxFree( bytes32 _subgraphDeploymentID, @@ -387,11 +401,7 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { } /** - * @notice Calculate amount of signal that can be bought with tokens in a curation pool, - * without accounting for curation tax. - * @param _subgraphDeploymentID Subgraph deployment to mint signal - * @param _tokensIn Amount of tokens used to mint signal - * @return Amount of signal that can be bought + * @inheritdoc IL2Curation */ function tokensToSignalNoTax( bytes32 _subgraphDeploymentID, @@ -401,12 +411,7 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { } /** - * @notice Calculate the amount of tokens that would be recovered if minting signal with - * the input tokens and then burning it. This can be used to compute rounding error. - * This function does not account for curation tax. - * @param _subgraphDeploymentID Subgraph deployment for which to mint signal - * @param _tokensIn Amount of tokens used to mint signal - * @return Amount of tokens that would be recovered after minting and burning signal + * @inheritdoc IL2Curation */ function tokensToSignalToTokensNoTax( bytes32 _subgraphDeploymentID, @@ -436,7 +441,7 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { } /** - * @dev Internal: Set the minimum deposit amount for curators. + * @notice Internal: Set the minimum deposit amount for curators. * Update the minimum deposit amount to `_minimumCurationDeposit` * @param _minimumCurationDeposit Minimum amount of tokens required deposit */ @@ -448,7 +453,7 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { } /** - * @dev Internal: Set the curation tax percentage to charge when a curator deposits GRT tokens. + * @notice Internal: Set the curation tax percentage to charge when a curator deposits GRT tokens. * @param _percentage Curation tax percentage charged when depositing GRT tokens */ function _setCurationTaxPercentage(uint32 _percentage) private { @@ -459,7 +464,7 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { } /** - * @dev Internal: Set the master copy to use as clones for the curation token. + * @notice Internal: Set the master copy to use as clones for the curation token. * @param _curationTokenMaster Address of implementation contract to use for curation tokens */ function _setCurationTokenMaster(address _curationTokenMaster) private { @@ -471,7 +476,7 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { } /** - * @dev Triggers an update of rewards due to a change in signal. + * @notice Triggers an update of rewards due to a change in signal. * @param _subgraphDeploymentID Subgraph deployment updated */ function _updateRewards(bytes32 _subgraphDeploymentID) private { @@ -482,7 +487,7 @@ contract L2Curation is CurationV3Storage, GraphUpgradeable, IL2Curation { } /** - * @dev Calculate amount of signal that can be bought with tokens in a curation pool. + * @notice Calculate amount of signal that can be bought with tokens in a curation pool. * @param _subgraphDeploymentID Subgraph deployment to mint signal * @param _tokensIn Amount of tokens used to mint signal * @return Amount of signal that can be bought with tokens diff --git a/packages/contracts/contracts/l2/discovery/IL2GNS.sol b/packages/contracts/contracts/l2/discovery/IL2GNS.sol index a24216fbb..9b3a26152 100644 --- a/packages/contracts/contracts/l2/discovery/IL2GNS.sol +++ b/packages/contracts/contracts/l2/discovery/IL2GNS.sol @@ -6,8 +6,15 @@ import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; /** * @title Interface for the L2GNS contract. + * @author Edge & Node + * @notice Interface for the L2 Graph Name System (GNS) contract */ interface IL2GNS is ICallhookReceiver { + /** + * @dev Message codes for L1 to L2 communication + * @param RECEIVE_SUBGRAPH_CODE Code for receiving subgraph transfers + * @param RECEIVE_CURATOR_BALANCE_CODE Code for receiving curator balance transfers + */ enum L1MessageCodes { RECEIVE_SUBGRAPH_CODE, RECEIVE_CURATOR_BALANCE_CODE @@ -16,6 +23,10 @@ interface IL2GNS is ICallhookReceiver { /** * @dev The SubgraphL2TransferData struct holds information * about a subgraph related to its transfer from L1 to L2. + * @param tokens GRT that will be sent to L2 to mint signal + * @param curatorBalanceClaimed True for curators whose balance has been claimed in L2 + * @param l2Done Transfer finished on L2 side + * @param subgraphReceivedOnL2BlockNumber Block number when the subgraph was received on L2 */ struct SubgraphL2TransferData { uint256 tokens; // GRT that will be sent to L2 to mint signal diff --git a/packages/contracts/contracts/l2/discovery/L2GNS.sol b/packages/contracts/contracts/l2/discovery/L2GNS.sol index 34d47d400..cf5528953 100644 --- a/packages/contracts/contracts/l2/discovery/L2GNS.sol +++ b/packages/contracts/contracts/l2/discovery/L2GNS.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events, gas-small-strings, gas-strict-inequalities + import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; import { GNS } from "../../discovery/GNS.sol"; @@ -14,7 +17,8 @@ import { IL2Curation } from "../curation/IL2Curation.sol"; /** * @title L2GNS - * @dev The Graph Name System contract provides a decentralized naming system for subgraphs + * @author Edge & Node + * @notice The Graph Name System contract provides a decentralized naming system for subgraphs * used in the scope of the Graph Network. It translates Subgraphs into Subgraph Versions. * Each version is associated with a Subgraph Deployment. The contract has no knowledge of * human-readable names. All human readable names emitted in events. @@ -26,35 +30,47 @@ import { IL2Curation } from "../curation/IL2Curation.sol"; contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { using SafeMathUpgradeable for uint256; - /// Offset added to an L1 subgraph ID to compute the L2 subgraph ID alias + /// @notice Offset added to an L1 subgraph ID to compute the L2 subgraph ID alias uint256 public constant SUBGRAPH_ID_ALIAS_OFFSET = uint256(0x1111000000000000000000000000000000000000000000000000000000001111); - /// Maximum rounding error when receiving signal tokens from L1, in parts-per-million. - /// If the error from minting signal is above this, tokens will be sent back to the curator. + /// @notice Maximum rounding error when receiving signal tokens from L1, in parts-per-million + /// @dev If the error from minting signal is above this, tokens will be sent back to the curator uint256 public constant MAX_ROUNDING_ERROR = 1000; /// @dev 100% expressed in parts-per-million uint256 private constant MAX_PPM = 1000000; - /// @dev Emitted when a subgraph is received from L1 through the bridge + /// @notice Emitted when a subgraph is received from L1 through the bridge + /// @param _l1SubgraphID Subgraph ID on L1 + /// @param _l2SubgraphID Subgraph ID on L2 (aliased) + /// @param _owner Address of the subgraph owner + /// @param _tokens Amount of tokens transferred with the subgraph event SubgraphReceivedFromL1( uint256 indexed _l1SubgraphID, uint256 indexed _l2SubgraphID, address indexed _owner, uint256 _tokens ); - /// @dev Emitted when a subgraph transfer from L1 is finalized, so the subgraph is published on L2 + /// @notice Emitted when a subgraph transfer from L1 is finalized, so the subgraph is published on L2 + /// @param _l2SubgraphID Subgraph ID on L2 event SubgraphL2TransferFinalized(uint256 indexed _l2SubgraphID); - /// @dev Emitted when the L1 balance for a curator has been claimed + /// @notice Emitted when the L1 balance for a curator has been claimed + /// @param _l1SubgraphId Subgraph ID on L1 + /// @param _l2SubgraphID Subgraph ID on L2 (aliased) + /// @param _l2Curator Address of the curator on L2 + /// @param _tokens Amount of tokens received event CuratorBalanceReceived( uint256 indexed _l1SubgraphId, uint256 indexed _l2SubgraphID, address indexed _l2Curator, uint256 _tokens ); - /// @dev Emitted when the L1 balance for a curator has been returned to the beneficiary. + /// @notice Emitted when the L1 balance for a curator has been returned to the beneficiary. /// This can happen if the subgraph transfer was not finished when the curator's tokens arrived. + /// @param _l1SubgraphID Subgraph ID on L1 + /// @param _l2Curator Address of the curator on L2 + /// @param _tokens Amount of tokens returned event CuratorBalanceReturnedToBeneficiary( uint256 indexed _l1SubgraphID, address indexed _l2Curator, @@ -103,13 +119,7 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { } /** - * @notice Finish a subgraph transfer from L1. - * The subgraph must have been previously sent through the bridge - * using the sendSubgraphToL2 function on L1GNS. - * @param _l2SubgraphID Subgraph ID (aliased from the L1 subgraph ID) - * @param _subgraphDeploymentID Latest subgraph deployment to assign to the subgraph - * @param _subgraphMetadata IPFS hash of the subgraph metadata - * @param _versionMetadata IPFS hash of the version metadata + * @inheritdoc IL2GNS */ function finishSubgraphTransferFromL1( uint256 _l2SubgraphID, @@ -220,25 +230,21 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { } /** - * @notice Return the aliased L2 subgraph ID from a transferred L1 subgraph ID - * @param _l1SubgraphID L1 subgraph ID - * @return L2 subgraph ID + * @inheritdoc IL2GNS */ function getAliasedL2SubgraphID(uint256 _l1SubgraphID) public pure override returns (uint256) { return _l1SubgraphID + SUBGRAPH_ID_ALIAS_OFFSET; } /** - * @notice Return the unaliased L1 subgraph ID from a transferred L2 subgraph ID - * @param _l2SubgraphID L2 subgraph ID - * @return L1subgraph ID + * @inheritdoc IL2GNS */ function getUnaliasedL1SubgraphID(uint256 _l2SubgraphID) public pure override returns (uint256) { return _l2SubgraphID - SUBGRAPH_ID_ALIAS_OFFSET; } /** - * @dev Receive a subgraph from L1. + * @notice Receive a subgraph from L1. * This function will initialize a subgraph received through the bridge, * and store the transfer data so that it's finalized later using finishSubgraphTransferFromL1. * @param _l1SubgraphID Subgraph ID in L1 (will be aliased) @@ -308,9 +314,9 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { } /** - * @dev Get subgraph data. - * Since there are no legacy subgraphs in L2, we override the base - * GNS method to save us the step of checking for legacy subgraphs. + * @notice Get subgraph data + * @dev Since there are no legacy subgraphs in L2, we override the base + * GNS method to save us the step of checking for legacy subgraphs * @param _subgraphID Subgraph ID * @return Subgraph Data */ diff --git a/packages/contracts/contracts/l2/discovery/L2GNSStorage.sol b/packages/contracts/contracts/l2/discovery/L2GNSStorage.sol index f658c49d9..26e7bf1e0 100644 --- a/packages/contracts/contracts/l2/discovery/L2GNSStorage.sol +++ b/packages/contracts/contracts/l2/discovery/L2GNSStorage.sol @@ -1,5 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable named-parameters-mapping + pragma solidity ^0.7.6; pragma abicoder v2; @@ -7,11 +10,11 @@ import { IL2GNS } from "./IL2GNS.sol"; /** * @title L2GNSV1Storage + * @author Edge & Node * @notice This contract holds all the L2-specific storage variables for the L2GNS contract, version 1 - * @dev */ abstract contract L2GNSV1Storage { - /// Data for subgraph transfer from L1 to L2 + /// @notice Data for subgraph transfer from L1 to L2 mapping(uint256 => IL2GNS.SubgraphL2TransferData) public subgraphL2TransferData; /// @dev Storage gap to keep storage slots fixed in future versions uint256[50] private __gap; diff --git a/packages/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol b/packages/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol index be8f212b8..3b4c1c0ed 100644 --- a/packages/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/packages/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; @@ -16,7 +19,8 @@ import { L2GraphToken } from "../token/L2GraphToken.sol"; /** * @title L2 Graph Token Gateway Contract - * @dev Provides the L2 side of the Ethereum-Arbitrum GRT bridge. Receives GRT from the L1 chain + * @author Edge & Node + * @notice Provides the L2 side of the Ethereum-Arbitrum GRT bridge. Receives GRT from the L1 chain * and mints them on the L2 side. Sends GRT back to L1 by burning them on the L2 side. * Based on Offchain Labs' reference implementation and Livepeer's arbitrum-lpt-bridge * (See: https://github.com/OffchainLabs/arbitrum/tree/master/packages/arb-bridge-peripherals/contracts/tokenbridge @@ -25,23 +29,42 @@ import { L2GraphToken } from "../token/L2GraphToken.sol"; contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, ReentrancyGuardUpgradeable { using SafeMathUpgradeable for uint256; - /// Address of the Graph Token contract on L1 + /// @notice Address of the Graph Token contract on L1 address public l1GRT; - /// Address of the L1GraphTokenGateway that is the counterpart of this gateway on L1 + /// @notice Address of the L1GraphTokenGateway that is the counterpart of this gateway on L1 address public l1Counterpart; - /// Address of the Arbitrum Gateway Router on L2 + /// @notice Address of the Arbitrum Gateway Router on L2 address public l2Router; /// @dev Calldata included in an outbound transfer, stored as a structure for convenience and stack depth + /** + * @dev Struct for outbound transfer calldata + * @param from Address sending the tokens + * @param extraData Additional data for the transfer + */ struct OutboundCalldata { address from; bytes extraData; } - /// Emitted when an incoming transfer is finalized, i.e. tokens were deposited from L1 to L2 + /** + * @notice Emitted when an incoming transfer is finalized, i.e. tokens were deposited from L1 to L2 + * @param l1Token Address of the L1 token + * @param from Address sending the tokens on L1 + * @param to Address receiving the tokens on L2 + * @param amount Amount of tokens transferred + */ event DepositFinalized(address indexed l1Token, address indexed from, address indexed to, uint256 amount); - /// Emitted when an outbound transfer is initiated, i.e. tokens are being withdrawn from L2 back to L1 + /** + * @notice Emitted when an outbound transfer is initiated, i.e. tokens are being withdrawn from L2 back to L1 + * @param l1Token Address of the L1 token + * @param from Address sending the tokens on L2 + * @param to Address receiving the tokens on L1 + * @param l2ToL1Id ID of the L2 to L1 message + * @param exitNum Exit number (always 0 for this contract) + * @param amount Amount of tokens transferred + */ event WithdrawalInitiated( address l1Token, address indexed from, @@ -51,11 +74,22 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran uint256 amount ); - /// Emitted when the Arbitrum Gateway Router address on L2 has been updated + /** + * @notice Emitted when the Arbitrum Gateway Router address on L2 has been updated + * @param l2Router Address of the L2 Gateway Router + */ event L2RouterSet(address l2Router); - /// Emitted when the L1 Graph Token address has been updated + + /** + * @notice Emitted when the L1 Graph Token address has been updated + * @param l1GRT Address of the L1 GRT contract + */ event L1TokenAddressSet(address l1GRT); - /// Emitted when the address of the counterpart gateway on L1 has been updated + + /** + * @notice Emitted when the address of the counterpart gateway on L1 has been updated + * @param l1Counterpart Address of the L1 counterpart gateway + */ event L1CounterpartAddressSet(address l1Counterpart); /** @@ -135,7 +169,7 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran } /** - * @notice Receives token amount from L1 and mints the equivalent tokens to the receiving address + * @inheritdoc ITokenGateway * @dev Only accepts transactions from the L1 GRT Gateway. * The function is payable for ITokenGateway compatibility, but msg.value must be zero. * Note that allowlisted senders (some protocol contracts) can include additional calldata @@ -144,11 +178,6 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran * never succeeds. This requires extra care when adding contracts to the allowlist, but is necessary to ensure that * the tickets can be retried in the case of a temporary failure, and to ensure the atomicity of callhooks * with token transfers. - * @param _l1Token L1 Address of GRT - * @param _from Address of the sender on L1 - * @param _to Recipient address on L2 - * @param _amount Amount of tokens transferred - * @param _data Extra callhook data, only used when the sender is allowlisted */ function finalizeInboundTransfer( address _l1Token, @@ -170,18 +199,14 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran } /** - * @notice Burns L2 tokens and initiates a transfer to L1. + * @inheritdoc ITokenGateway + * @dev Burns L2 tokens and initiates a transfer to L1. * The tokens will be available on L1 only after the wait period (7 days) is over, * and will require an Outbox.executeTransaction to finalize. * Note that the caller must previously allow the gateway to spend the specified amount of GRT. - * @dev no additional callhook data is allowed. The two unused params are needed + * No additional callhook data is allowed. The two unused params are needed * for compatibility with Arbitrum's gateway router. * The function is payable for ITokenGateway compatibility, but msg.value must be zero. - * @param _l1Token L1 Address of GRT (needed for compatibility with Arbitrum Gateway Router) - * @param _to Recipient address on L1 - * @param _amount Amount of tokens to burn - * @param _data Contains sender and additional data (always empty) to send to L1 - * @return ID of the withdraw transaction */ function outboundTransfer( address _l1Token, @@ -218,10 +243,8 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran } /** - * @notice Calculate the L2 address of a bridged token + * @inheritdoc ITokenGateway * @dev In our case, this would only work for GRT. - * @param l1ERC20 address of L1 GRT contract - * @return L2 address of the bridged GRT token */ function calculateL2TokenAddress(address l1ERC20) public view override returns (address) { if (l1ERC20 != l1GRT) { @@ -259,10 +282,8 @@ contract L2GraphTokenGateway is GraphTokenGateway, L2ArbitrumMessenger, Reentran ); } - /** - * @dev Runs state validation before unpausing, reverts if - * something is not set properly - */ + /// @inheritdoc GraphTokenGateway + // solhint-disable-next-line use-natspec function _checksBeforeUnpause() internal view override { require(l2Router != address(0), "L2_ROUTER_NOT_SET"); require(l1Counterpart != address(0), "L1_COUNTERPART_NOT_SET"); diff --git a/packages/contracts/contracts/l2/staking/IL2Staking.sol b/packages/contracts/contracts/l2/staking/IL2Staking.sol index 4b7748e31..522a9ca12 100644 --- a/packages/contracts/contracts/l2/staking/IL2Staking.sol +++ b/packages/contracts/contracts/l2/staking/IL2Staking.sol @@ -9,6 +9,7 @@ import { IL2StakingTypes } from "./IL2StakingTypes.sol"; /** * @title Interface for the L2 Staking contract + * @author Edge & Node * @notice This is the interface that should be used when interacting with the L2 Staking contract. * It extends the IStaking interface with the functions that are specific to L2, adding the callhook receiver * to receive transferred stake and delegation from L1. diff --git a/packages/contracts/contracts/l2/staking/IL2StakingBase.sol b/packages/contracts/contracts/l2/staking/IL2StakingBase.sol index f5c33c2d0..7f861db39 100644 --- a/packages/contracts/contracts/l2/staking/IL2StakingBase.sol +++ b/packages/contracts/contracts/l2/staking/IL2StakingBase.sol @@ -2,13 +2,23 @@ pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; /** * @title Base interface for the L2Staking contract. + * @author Edge & Node * @notice This interface is used to define the callhook receiver interface that is implemented by L2Staking. * @dev Note it includes only the L2-specific functionality, not the full IStaking interface. */ interface IL2StakingBase is ICallhookReceiver { + /** + * @notice Emitted when transferred delegation is returned to a delegator + * @param indexer Address of the indexer + * @param delegator Address of the delegator + * @param amount Amount of delegation returned + */ event TransferredDelegationReturnedToDelegator(address indexed indexer, address indexed delegator, uint256 amount); } diff --git a/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol b/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol index 500694e89..722a2dbf4 100644 --- a/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol +++ b/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol @@ -2,6 +2,11 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title IL2StakingTypes + * @author Edge & Node + * @notice Interface defining types and enums used by L2 staking contracts + */ interface IL2StakingTypes { /// @dev Message codes for the L1 -> L2 bridge callhook enum L1MessageCodes { diff --git a/packages/contracts/contracts/l2/staking/L2Staking.sol b/packages/contracts/contracts/l2/staking/L2Staking.sol index 278e26a50..fb3784456 100644 --- a/packages/contracts/contracts/l2/staking/L2Staking.sol +++ b/packages/contracts/contracts/l2/staking/L2Staking.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { Staking } from "../../staking/Staking.sol"; import { IL2StakingBase } from "./IL2StakingBase.sol"; @@ -10,9 +13,13 @@ import { Stakes } from "../../staking/libs/Stakes.sol"; import { IStakes } from "../../staking/libs/IStakes.sol"; import { IL2StakingTypes } from "./IL2StakingTypes.sol"; +// solhint-disable-next-line no-unused-import +import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; // Used by @inheritdoc + /** * @title L2Staking contract - * @dev This contract is the L2 variant of the Staking contract. It adds a function + * @author Edge & Node + * @notice This contract is the L2 variant of the Staking contract. It adds a function * to receive an indexer's stake or delegation from L1. Note that this contract inherits Staking, * which uses a StakingExtension contract to implement the full IStaking interface through delegatecalls. */ @@ -24,10 +31,14 @@ contract L2Staking is Staking, IL2StakingBase { uint256 private constant MINIMUM_DELEGATION = 1e18; /** - * @dev Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator + * @notice Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator * gets `shares` for the delegation pool proportionally to the tokens staked. * This is copied from IStakingExtension, but we can't inherit from it because we * don't implement the full interface here. + * @param indexer Address of the indexer receiving the delegation + * @param delegator Address of the delegator + * @param tokens Amount of tokens delegated + * @param shares Amount of shares issued to the delegator */ event StakeDelegated(address indexed indexer, address indexed delegator, uint256 tokens, uint256 shares); @@ -48,7 +59,7 @@ contract L2Staking is Staking, IL2StakingBase { } /** - * @notice Receive tokens with a callhook from the bridge. + * @inheritdoc ICallhookReceiver * @dev The encoded _data can contain information about an indexer's stake * or a delegator's delegation. * See L1MessageCodes in IL2Staking for the supported messages. @@ -82,7 +93,7 @@ contract L2Staking is Staking, IL2StakingBase { } /** - * @dev Receive an Indexer's stake from L1. + * @notice Receive an Indexer's stake from L1. * The specified amount is added to the indexer's stake; the indexer's * address is specified in the _indexerData struct. * @param _amount Amount of tokens that were transferred @@ -105,7 +116,7 @@ contract L2Staking is Staking, IL2StakingBase { } /** - * @dev Receive a Delegator's delegation from L1. + * @notice Receive a Delegator's delegation from L1. * The specified amount is added to the delegator's delegation; the delegator's * address and the indexer's address are specified in the _delegationData struct. * Note that no delegation tax is applied here. diff --git a/packages/contracts/contracts/l2/token/GraphTokenUpgradeable.sol b/packages/contracts/contracts/l2/token/GraphTokenUpgradeable.sol index 0f5cf0ecb..d26371533 100644 --- a/packages/contracts/contracts/l2/token/GraphTokenUpgradeable.sol +++ b/packages/contracts/contracts/l2/token/GraphTokenUpgradeable.sol @@ -2,6 +2,10 @@ pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one, gas-small-strings, gas-strict-inequalities +// solhint-disable named-parameters-mapping + import { ERC20BurnableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20BurnableUpgradeable.sol"; import { ECDSAUpgradeable } from "@openzeppelin/contracts-upgradeable/cryptography/ECDSAUpgradeable.sol"; @@ -10,7 +14,8 @@ import { Governed } from "../../governance/Governed.sol"; /** * @title GraphTokenUpgradeable contract - * @dev This is the implementation of the ERC20 Graph Token. + * @author Edge & Node + * @notice This is the implementation of the ERC20 Graph Token. * The implementation exposes a permit() function to allow for a spender to send a signed message * and approve funds to a spender following EIP2612 to make integration with other contracts easier. * @@ -47,16 +52,23 @@ abstract contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20Burn bytes32 private DOMAIN_SEPARATOR; // solhint-disable-line var-name-mixedcase /// @dev Addresses for which this mapping is true are allowed to mint tokens mapping(address => bool) private _minters; - /// Nonces for permit signatures for each token holder + /// @notice Nonces for permit signatures for each token holder mapping(address => uint256) public nonces; /// @dev Storage gap added in case we need to add state variables to this contract uint256[47] private __gap; // -- Events -- - /// Emitted when a new minter is added + /** + * @notice Emitted when a new minter is added + * @param account Address of the minter that was added + */ event MinterAdded(address indexed account); - /// Emitted when a minter is removed + + /** + * @notice Emitted when a minter is removed + * @param account Address of the minter that was removed + */ event MinterRemoved(address indexed account); /// @dev Reverts if the caller is not an authorized minter @@ -145,7 +157,7 @@ abstract contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20Burn } /** - * @dev Graph Token Contract initializer. + * @notice Graph Token Contract initializer. * @param _owner Owner of this contract, who will hold the initial supply and will be a minter * @param _initialSupply Initial supply of GRT */ @@ -173,7 +185,7 @@ abstract contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20Burn } /** - * @dev Add a new minter. + * @notice Add a new minter. * @param _account Address of the minter */ function _addMinter(address _account) private { @@ -182,7 +194,7 @@ abstract contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20Burn } /** - * @dev Remove a minter. + * @notice Remove a minter. * @param _account Address of the minter */ function _removeMinter(address _account) private { @@ -191,7 +203,7 @@ abstract contract GraphTokenUpgradeable is GraphUpgradeable, Governed, ERC20Burn } /** - * @dev Get the running network chain ID. + * @notice Get the running network chain ID. * @return The chain ID */ function _getChainID() private pure returns (uint256) { diff --git a/packages/contracts/contracts/l2/token/L2GraphToken.sol b/packages/contracts/contracts/l2/token/L2GraphToken.sol index 639444870..c16f0164c 100644 --- a/packages/contracts/contracts/l2/token/L2GraphToken.sol +++ b/packages/contracts/contracts/l2/token/L2GraphToken.sol @@ -2,27 +2,48 @@ pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { GraphTokenUpgradeable } from "./GraphTokenUpgradeable.sol"; import { IArbToken } from "../../arbitrum/IArbToken.sol"; /** * @title L2 Graph Token Contract - * @dev Provides the L2 version of the GRT token, meant to be minted/burned + * @author Edge & Node + * @notice Provides the L2 version of the GRT token, meant to be minted/burned * through the L2GraphTokenGateway. */ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { - /// Address of the gateway (on L2) that is allowed to mint tokens + /// @notice Address of the gateway (on L2) that is allowed to mint tokens address public gateway; - /// Address of the corresponding Graph Token contract on L1 + /// @notice Address of the corresponding Graph Token contract on L1 address public override l1Address; - /// Emitted when the bridge / gateway has minted new tokens, i.e. tokens were transferred to L2 + /** + * @notice Emitted when the bridge / gateway has minted new tokens, i.e. tokens were transferred to L2 + * @param account Address that received the minted tokens + * @param amount Amount of tokens minted + */ event BridgeMinted(address indexed account, uint256 amount); - /// Emitted when the bridge / gateway has burned tokens, i.e. tokens were transferred back to L1 + + /** + * @notice Emitted when the bridge / gateway has burned tokens, i.e. tokens were transferred back to L1 + * @param account Address from which tokens were burned + * @param amount Amount of tokens burned + */ event BridgeBurned(address indexed account, uint256 amount); - /// Emitted when the address of the gateway has been updated + + /** + * @notice Emitted when the address of the gateway has been updated + * @param gateway Address of the new gateway + */ event GatewaySet(address gateway); - /// Emitted when the address of the Graph Token contract on L1 has been updated + + /** + * @notice Emitted when the address of the Graph Token contract on L1 has been updated + * @param l1Address Address of the L1 Graph Token contract + */ event L1AddressSet(address l1Address); /** @@ -69,9 +90,8 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { } /** - * @notice Increases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L2) - * @param _account Address to credit with the new tokens - * @param _amount Number of tokens to mint + * @inheritdoc IArbToken + * @dev Only callable by the L2GraphTokenGateway when tokens are transferred to L2 */ function bridgeMint(address _account, uint256 _amount) external override onlyGateway { _mint(_account, _amount); @@ -79,9 +99,8 @@ contract L2GraphToken is GraphTokenUpgradeable, IArbToken { } /** - * @notice Decreases token supply, only callable by the L1/L2 bridge (when tokens are transferred to L1). - * @param _account Address from which to extract the tokens - * @param _amount Number of tokens to burn + * @inheritdoc IArbToken + * @dev Only callable by the L2GraphTokenGateway when tokens are transferred back to L1 */ function bridgeBurn(address _account, uint256 _amount) external override onlyGateway { burnFrom(_account, _amount); diff --git a/packages/contracts/contracts/libraries/Base58Encoder.sol b/packages/contracts/contracts/libraries/Base58Encoder.sol index 9af197855..91caa8855 100644 --- a/packages/contracts/contracts/libraries/Base58Encoder.sol +++ b/packages/contracts/contracts/libraries/Base58Encoder.sol @@ -2,14 +2,25 @@ pragma solidity ^0.7.6; -/// @title Base58Encoder -/// @author Original author - Martin Lundfall (martin.lundfall@gmail.com) -/// Based on https://github.com/MrChico/verifyIPFS +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one + +/** + * @title Base58Encoder + * @author Original author - Martin Lundfall (martin.lundfall@gmail.com) + * @notice Library for encoding bytes to Base58 format, used for IPFS hashes + * @dev Based on https://github.com/MrChico/verifyIPFS + */ library Base58Encoder { - bytes constant sha256MultiHash = hex"1220"; - bytes constant ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + /// @dev SHA-256 multihash prefix for IPFS hashes + // solhint-disable-next-line const-name-snakecase + bytes internal constant sha256MultiHash = hex"1220"; + /// @dev Base58 alphabet used for encoding + bytes internal constant ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - /// @dev Converts hex string to base 58 + /// @notice Converts hex string to base 58 + /// @param source The bytes to encode + /// @return The base58 encoded bytes function encode(bytes memory source) internal pure returns (bytes memory) { if (source.length == 0) return new bytes(0); uint8[] memory digits = new uint8[](64); @@ -32,6 +43,12 @@ library Base58Encoder { return toAlphabet(reverse(truncate(digits, digitlength))); } + /** + * @notice Truncate an array to a specific length + * @param array The array to truncate + * @param length The desired length + * @return The truncated array + */ function truncate(uint8[] memory array, uint8 length) internal pure returns (uint8[] memory) { uint8[] memory output = new uint8[](length); for (uint256 i = 0; i < length; i++) { @@ -40,6 +57,11 @@ library Base58Encoder { return output; } + /** + * @notice Reverse an array + * @param input The array to reverse + * @return The reversed array + */ function reverse(uint8[] memory input) internal pure returns (uint8[] memory) { uint8[] memory output = new uint8[](input.length); for (uint256 i = 0; i < input.length; i++) { @@ -48,6 +70,11 @@ library Base58Encoder { return output; } + /** + * @notice Convert indices to alphabet characters + * @param indices The indices to convert + * @return The alphabet characters as bytes + */ function toAlphabet(uint8[] memory indices) internal pure returns (bytes memory) { bytes memory output = new bytes(indices.length); for (uint256 i = 0; i < indices.length; i++) { diff --git a/packages/contracts/contracts/libraries/HexStrings.sol b/packages/contracts/contracts/libraries/HexStrings.sol index 4842883a9..2b5e314e6 100644 --- a/packages/contracts/contracts/libraries/HexStrings.sol +++ b/packages/contracts/contracts/libraries/HexStrings.sol @@ -2,12 +2,22 @@ pragma solidity ^0.7.6; -/// @title HexStrings -/// Based on https://github.com/OpenZeppelin/openzeppelin-contracts/blob/8dd744fc1843d285c38e54e9d439dea7f6b93495/contracts/utils/Strings.sol +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one + +/** + * @title HexStrings + * @author Edge & Node + * @notice Library for converting values to hexadecimal string representations + * @dev Based on https://github.com/OpenZeppelin/openzeppelin-contracts/blob/8dd744fc1843d285c38e54e9d439dea7f6b93495/contracts/utils/Strings.sol + */ library HexStrings { + /// @dev Hexadecimal symbols used for string conversion bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; - /// @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + /// @notice Converts a `uint256` to its ASCII `string` hexadecimal representation. + /// @param value The uint256 value to convert + /// @return The hexadecimal string representation function toString(uint256 value) internal pure returns (string memory) { if (value == 0) { return "0x00"; @@ -21,7 +31,10 @@ library HexStrings { return toHexString(value, length); } - /// @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + /// @notice Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + /// @param value The uint256 value to convert + /// @param length The fixed length of the output string + /// @return The hexadecimal string representation with fixed length function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; diff --git a/packages/contracts/contracts/payments/AllocationExchange.sol b/packages/contracts/contracts/payments/AllocationExchange.sol index 5f0b30b44..9bbd983a1 100644 --- a/packages/contracts/contracts/payments/AllocationExchange.sol +++ b/packages/contracts/contracts/payments/AllocationExchange.sol @@ -3,16 +3,20 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; - -import "../governance/Governed.sol"; -import "../staking/IStaking.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-calldata-parameters, gas-increment-by-one, gas-indexed-events, gas-small-strings +// solhint-disable named-parameters-mapping + +import { ECDSA } from "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { Governed } from "../governance/Governed.sol"; +import { IStaking } from "../staking/IStaking.sol"; import { IGraphToken } from "../token/IGraphToken.sol"; /** * @title Allocation Exchange - * @dev This contract holds tokens that anyone with a voucher signed by the + * @author Edge & Node + * @notice This contract holds tokens that anyone with a voucher signed by the * authority can redeem. The contract validates if the voucher presented is valid * and then sends tokens to the Staking contract by calling the collect() function * passing the voucher allocationID. The contract enforces that only one voucher for @@ -20,9 +24,14 @@ import { IGraphToken } from "../token/IGraphToken.sol"; * Only governance can change the authority. */ contract AllocationExchange is Governed { - // An allocation voucher represents a signed message that allows - // redeeming an amount of funds from this contract and collect - // them as part of an allocation + /** + * @dev An allocation voucher represents a signed message that allows + * redeeming an amount of funds from this contract and collect + * them as part of an allocation + * @param allocationID Address of the allocation + * @param amount Amount of tokens to redeem + * @param signature Signature from the authority (65 bytes) + */ struct AllocationVoucher { address allocationID; uint256 amount; @@ -31,20 +40,43 @@ contract AllocationExchange is Governed { // -- Constants -- + /// @dev Maximum uint256 value used for unlimited token approvals uint256 private constant MAX_UINT256 = 2 ** 256 - 1; + /// @dev Expected length of ECDSA signatures uint256 private constant SIGNATURE_LENGTH = 65; // -- State -- - IStaking private immutable staking; - IGraphToken private immutable graphToken; + /// @dev Reference to the Staking contract + IStaking private immutable STAKING; + /// @dev Reference to the Graph Token contract + IGraphToken private immutable GRAPH_TOKEN; + /// @notice Mapping of authorized accounts that can redeem allocations mapping(address => bool) public authority; + /// @notice Mapping of allocations that have been redeemed mapping(address => bool) public allocationsRedeemed; // -- Events + /** + * @notice Emitted when an authority is set or unset + * @param account Address of the authority + * @param authorized Whether the authority is authorized + */ event AuthoritySet(address indexed account, bool authorized); + + /** + * @notice Emitted when an allocation voucher is redeemed + * @param allocationID Address of the allocation + * @param amount Amount of tokens redeemed + */ event AllocationRedeemed(address indexed allocationID, uint256 amount); + + /** + * @notice Emitted when tokens are withdrawn from the contract + * @param to Address that received the tokens + * @param amount Amount of tokens withdrawn + */ event TokensWithdrawn(address indexed to, uint256 amount); // -- Functions @@ -60,8 +92,8 @@ contract AllocationExchange is Governed { require(_governor != address(0), "Exchange: governor must be set"); Governed._initialize(_governor); - graphToken = _graphToken; - staking = _staking; + GRAPH_TOKEN = _graphToken; + STAKING = _staking; _setAuthority(_authority, true); } @@ -70,7 +102,7 @@ contract AllocationExchange is Governed { * @dev Increased gas efficiency instead of approving on each voucher redeem */ function approveAll() external { - graphToken.approve(address(staking), MAX_UINT256); + GRAPH_TOKEN.approve(address(STAKING), MAX_UINT256); } /** @@ -82,7 +114,7 @@ contract AllocationExchange is Governed { function withdraw(address _to, uint256 _amount) external onlyGovernor { require(_to != address(0), "Exchange: empty destination"); require(_amount != 0, "Exchange: empty amount"); - require(graphToken.transfer(_to, _amount), "Exchange: cannot transfer"); + require(GRAPH_TOKEN.transfer(_to, _amount), "Exchange: cannot transfer"); emit TokensWithdrawn(_to, _amount); } @@ -155,7 +187,7 @@ contract AllocationExchange is Governed { // Make the staking contract collect funds from this contract // The Staking contract will validate if the allocation is valid - staking.collect(_voucher.amount, _voucher.allocationID); + STAKING.collect(_voucher.amount, _voucher.allocationID); emit AllocationRedeemed(_voucher.allocationID, _voucher.amount); } diff --git a/packages/contracts/contracts/rewards/IRewardsIssuer.sol b/packages/contracts/contracts/rewards/IRewardsIssuer.sol index d50410b33..614dbf40e 100644 --- a/packages/contracts/contracts/rewards/IRewardsIssuer.sol +++ b/packages/contracts/contracts/rewards/IRewardsIssuer.sol @@ -2,9 +2,14 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Rewards Issuer Interface + * @author Edge & Node + * @notice Interface for contracts that issue rewards based on allocation data + */ interface IRewardsIssuer { /** - * @dev Get allocation data to calculate rewards issuance + * @notice Get allocation data to calculate rewards issuance * * @param allocationId The allocation Id * @return isActive Whether the allocation is active or not diff --git a/packages/contracts/contracts/rewards/IRewardsManager.sol b/packages/contracts/contracts/rewards/IRewardsManager.sol deleted file mode 100644 index b31064d1b..000000000 --- a/packages/contracts/contracts/rewards/IRewardsManager.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -interface IRewardsManager { - /** - * @dev Stores accumulated rewards and snapshots related to a particular SubgraphDeployment. - */ - struct Subgraph { - uint256 accRewardsForSubgraph; - uint256 accRewardsForSubgraphSnapshot; - uint256 accRewardsPerSignalSnapshot; - uint256 accRewardsPerAllocatedToken; - } - - // -- Config -- - - function setIssuancePerBlock(uint256 _issuancePerBlock) external; - - function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; - - function setSubgraphService(address _subgraphService) external; - - // -- Denylist -- - - function setSubgraphAvailabilityOracle(address _subgraphAvailabilityOracle) external; - - function setDenied(bytes32 _subgraphDeploymentID, bool _deny) external; - - function isDenied(bytes32 _subgraphDeploymentID) external view returns (bool); - - // -- Getters -- - - function getNewRewardsPerSignal() external view returns (uint256); - - function getAccRewardsPerSignal() external view returns (uint256); - - function getAccRewardsForSubgraph(bytes32 _subgraphDeploymentID) external view returns (uint256); - - function getAccRewardsPerAllocatedToken(bytes32 _subgraphDeploymentID) external view returns (uint256, uint256); - - function getRewards(address _rewardsIssuer, address _allocationID) external view returns (uint256); - - function calcRewards(uint256 _tokens, uint256 _accRewardsPerAllocatedToken) external pure returns (uint256); - - // -- Updates -- - - function updateAccRewardsPerSignal() external returns (uint256); - - function takeRewards(address _allocationID) external returns (uint256); - - // -- Hooks -- - - function onSubgraphSignalUpdate(bytes32 _subgraphDeploymentID) external returns (uint256); - - function onSubgraphAllocationUpdate(bytes32 _subgraphDeploymentID) external returns (uint256); -} diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 58f654b91..083908b3b 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -3,6 +3,9 @@ pragma solidity 0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one, gas-indexed-events, gas-small-strings, gas-strict-inequalities + import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; @@ -11,11 +14,13 @@ import { MathUtils } from "../staking/libs/MathUtils.sol"; import { IGraphToken } from "../token/IGraphToken.sol"; import { RewardsManagerV5Storage } from "./RewardsManagerStorage.sol"; -import { IRewardsManager } from "./IRewardsManager.sol"; import { IRewardsIssuer } from "./IRewardsIssuer.sol"; +import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; /** * @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 @@ -30,6 +35,7 @@ import { IRewardsIssuer } from "./IRewardsIssuer.sol"; * - getRewards * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. + * custom:security-contact Please email security+contracts@ thegraph.com (remove space) if you find any bugs. We might have an active bug bounty program. */ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; @@ -40,27 +46,32 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa // -- Events -- /** - * @dev Emitted when rewards are assigned to an indexer. + * @notice Emitted when rewards are assigned to an indexer. * @dev We use the Horizon prefix to change the event signature which makes network subgraph development much easier + * @param indexer Address of the indexer receiving rewards + * @param allocationID Address of the allocation receiving rewards + * @param amount Amount of rewards assigned */ event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount); /** - * @dev Emitted when rewards are denied to an indexer + * @notice Emitted when rewards are denied to an indexer * @param indexer Address of the indexer being denied rewards * @param allocationID Address of the allocation being denied rewards */ event RewardsDenied(address indexed indexer, address indexed allocationID); /** - * @dev Emitted when a subgraph is denied for claiming rewards + * @notice Emitted when a subgraph is denied for claiming rewards * @param subgraphDeploymentID Subgraph deployment ID being denied * @param sinceBlock Block number since when the subgraph is denied */ event RewardsDenylistUpdated(bytes32 indexed subgraphDeploymentID, uint256 sinceBlock); /** - * @dev Emitted when the subgraph service is set + * @notice Emitted when the subgraph service is set + * @param oldSubgraphService Previous subgraph service address + * @param newSubgraphService New subgraph service address */ event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService); @@ -85,20 +96,20 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa // -- Config -- /** - * @dev Sets the GRT issuance per block. - * The issuance is defined as a fixed amount of rewards per block in GRT. + * @inheritdoc IRewardsManager + * + * @dev The issuance is defined as a fixed amount of rewards per block in GRT. * Whenever this function is called in layer 2, the updateL2MintAllowance function * _must_ be called on the L1GraphTokenGateway in L1, to ensure the bridge can mint the * right amount of tokens. - * @param _issuancePerBlock Issuance expressed in GRT per block (scaled by 1e18) */ function setIssuancePerBlock(uint256 _issuancePerBlock) external override onlyGovernor { _setIssuancePerBlock(_issuancePerBlock); } /** - * @dev Sets the GRT issuance per block. - * The issuance is defined as a fixed amount of rewards per block in GRT. + * @notice Sets the GRT issuance per block. + * @dev The issuance is defined as a fixed amount of rewards per block in GRT. * @param _issuancePerBlock Issuance expressed in GRT per block (scaled by 1e18) */ function _setIssuancePerBlock(uint256 _issuancePerBlock) private { @@ -110,8 +121,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa } /** - * @notice Sets the subgraph oracle allowed to deny distribution of rewards to subgraphs - * @param _subgraphAvailabilityOracle Address of the subgraph availability oracle + * @inheritdoc IRewardsManager */ function setSubgraphAvailabilityOracle(address _subgraphAvailabilityOracle) external override onlyGovernor { subgraphAvailabilityOracle = _subgraphAvailabilityOracle; @@ -119,7 +129,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa } /** - * @notice Sets the minimum signaled tokens on a subgraph to start accruing rewards + * @inheritdoc IRewardsManager * @dev Can be set to zero which means that this feature is not being used * @param _minimumSubgraphSignal Minimum signaled tokens */ @@ -133,6 +143,9 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa emit ParameterUpdated("minimumSubgraphSignal"); } + /** + * @inheritdoc IRewardsManager + */ function setSubgraphService(address _subgraphService) external override onlyGovernor { address oldSubgraphService = address(subgraphService); subgraphService = IRewardsIssuer(_subgraphService); @@ -142,17 +155,15 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa // -- Denylist -- /** - * @notice Denies to claim rewards for a subgraph + * @inheritdoc IRewardsManager * @dev Can only be called by the subgraph availability oracle - * @param _subgraphDeploymentID Subgraph deployment ID - * @param _deny Whether to set the subgraph as denied for claiming rewards or not */ function setDenied(bytes32 _subgraphDeploymentID, bool _deny) external override onlySubgraphAvailabilityOracle { _setDenied(_subgraphDeploymentID, _deny); } /** - * @dev Internal: Denies to claim rewards for a subgraph. + * @notice Internal: Denies to claim rewards for a subgraph. * @param _subgraphDeploymentID Subgraph deployment ID * @param _deny Whether to set the subgraph as denied for claiming rewards or not */ @@ -162,11 +173,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa emit RewardsDenylistUpdated(_subgraphDeploymentID, sinceBlock); } - /** - * @notice Tells if subgraph is in deny list - * @param _subgraphDeploymentID Subgraph deployment ID to check - * @return Whether the subgraph is denied for claiming rewards or not - */ + /// @inheritdoc IRewardsManager function isDenied(bytes32 _subgraphDeploymentID) public view override returns (bool) { return denylist[_subgraphDeploymentID] > 0; } @@ -174,7 +181,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa // -- Getters -- /** - * @notice Gets the issuance of rewards per signal since last updated + * @inheritdoc IRewardsManager * @dev Linear formula: `x = r * t` * * Notation: @@ -209,19 +216,12 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa return x.mul(FIXED_POINT_SCALING_FACTOR).div(signalledTokens); } - /** - * @notice Gets the currently accumulated rewards per signal - * @return Currently accumulated rewards per signal - */ + /// @inheritdoc IRewardsManager function getAccRewardsPerSignal() public view override returns (uint256) { return accRewardsPerSignal.add(getNewRewardsPerSignal()); } - /** - * @notice Gets the accumulated rewards for the subgraph - * @param _subgraphDeploymentID Subgraph deployment - * @return Accumulated rewards for subgraph - */ + /// @inheritdoc IRewardsManager function getAccRewardsForSubgraph(bytes32 _subgraphDeploymentID) public view override returns (uint256) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; @@ -237,12 +237,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa return subgraph.accRewardsForSubgraph.add(newRewards); } - /** - * @notice Gets the accumulated rewards per allocated token for the subgraph - * @param _subgraphDeploymentID Subgraph deployment - * @return Accumulated rewards per allocated token for the subgraph - * @return Accumulated rewards for subgraph - */ + /// @inheritdoc IRewardsManager function getAccRewardsPerAllocatedToken( bytes32 _subgraphDeploymentID ) public view override returns (uint256, uint256) { @@ -280,10 +275,9 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa // -- Updates -- /** - * @notice Updates the accumulated rewards per signal and save checkpoint block number + * @inheritdoc IRewardsManager * @dev Must be called before `issuancePerBlock` or `total signalled GRT` changes. * Called from the Curation contract on mint() and burn() - * @return Accumulated rewards per signal */ function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); @@ -292,11 +286,9 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa } /** - * @notice Triggers an update of rewards for a subgraph + * @inheritdoc IRewardsManager * @dev Must be called before `signalled GRT` on a subgraph changes. * Hook called from the Curation contract on mint() and burn() - * @param _subgraphDeploymentID Subgraph deployment - * @return Accumulated rewards for subgraph */ function onSubgraphSignalUpdate(bytes32 _subgraphDeploymentID) external override returns (uint256) { // Called since `total signalled GRT` will change @@ -310,12 +302,8 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa } /** - * @notice Triggers an update of rewards for a subgraph - * @dev Must be called before allocation on a subgraph changes. - * Hook called from the Staking contract on allocate() and close() - * - * @param _subgraphDeploymentID Subgraph deployment - * @return Accumulated rewards per allocated token for a subgraph + * @inheritdoc IRewardsManager + * @dev Hook called from the Staking contract on allocate() and close() */ function onSubgraphAllocationUpdate(bytes32 _subgraphDeploymentID) public override returns (uint256) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; @@ -327,13 +315,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa return subgraph.accRewardsPerAllocatedToken; } - /** - * @dev Calculate current rewards for a given allocation on demand. - * The allocation could be a legacy allocation or a new subgraph service allocation. - * Returns 0 if the allocation is not active. - * @param _allocationID Allocation - * @return Rewards amount for an allocation - */ + /// @inheritdoc IRewardsManager function getRewards(address _rewardsIssuer, address _allocationID) external view override returns (uint256) { require( _rewardsIssuer == address(staking()) || _rewardsIssuer == address(subgraphService), @@ -359,7 +341,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa } /** - * @dev Calculate rewards for a given accumulated rewards per allocated token. + * @notice Calculate rewards for a given accumulated rewards per allocated token * @param _tokens Tokens allocated * @param _accRewardsPerAllocatedToken Allocation accumulated rewards per token * @return Rewards amount @@ -372,7 +354,7 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa } /** - * @dev Calculate current rewards for a given allocation. + * @notice Calculate current rewards for a given allocation. * @param _tokens Tokens allocated * @param _startAccRewardsPerAllocatedToken Allocation start accumulated rewards * @param _endAccRewardsPerAllocatedToken Allocation end accumulated rewards @@ -388,13 +370,10 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa } /** - * @dev Pull rewards from the contract for a particular allocation. - * This function can only be called by an authorized rewards issuer which are + * @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). - * This function will mint the necessary tokens to reward based on the inflation calculation. * Mints 0 tokens if the allocation is not active. - * @param _allocationID Allocation - * @return Assigned rewards amount */ function takeRewards(address _allocationID) external override returns (uint256) { address rewardsIssuer = msg.sender; diff --git a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol index d2ffa2b42..5002d7890 100644 --- a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol +++ b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol @@ -1,44 +1,78 @@ // SPDX-License-Identifier: GPL-2.0-or-later +/* solhint-disable one-contract-per-file */ + +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable named-parameters-mapping + pragma solidity ^0.7.6 || 0.8.27; import { IRewardsIssuer } from "./IRewardsIssuer.sol"; -import { IRewardsManager } from "./IRewardsManager.sol"; +import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { Managed } from "../governance/Managed.sol"; +/** + * @title RewardsManagerV1Storage + * @author Edge & Node + * @notice Storage layout for RewardsManager V1 + */ contract RewardsManagerV1Storage is Managed { // -- State -- + /// @dev Deprecated issuance rate variable (no longer used) uint256 private __DEPRECATED_issuanceRate; // solhint-disable-line var-name-mixedcase + /// @notice Accumulated rewards per signal uint256 public accRewardsPerSignal; + /// @notice Block number when accumulated rewards per signal was last updated uint256 public accRewardsPerSignalLastBlockUpdated; - // Address of role allowed to deny rewards on subgraphs + /// @notice Address of role allowed to deny rewards on subgraphs address public subgraphAvailabilityOracle; - // Subgraph related rewards: subgraph deployment ID => subgraph rewards + /// @notice Subgraph related rewards: subgraph deployment ID => subgraph rewards mapping(bytes32 => IRewardsManager.Subgraph) public subgraphs; - // Subgraph denylist : subgraph deployment ID => block when added or zero (if not denied) + /// @notice Subgraph denylist: subgraph deployment ID => block when added or zero (if not denied) mapping(bytes32 => uint256) public denylist; } +/** + * @title RewardsManagerV2Storage + * @author Edge & Node + * @notice Storage layout for RewardsManager V2 + */ contract RewardsManagerV2Storage is RewardsManagerV1Storage { - // Minimum amount of signaled tokens on a subgraph required to accrue rewards + /// @notice Minimum amount of signaled tokens on a subgraph required to accrue rewards uint256 public minimumSubgraphSignal; } +/** + * @title RewardsManagerV3Storage + * @author Edge & Node + * @notice Storage layout for RewardsManager V3 + */ contract RewardsManagerV3Storage is RewardsManagerV2Storage { - // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated + /// @dev Deprecated token supply snapshot variable (no longer used) uint256 private __DEPRECATED_tokenSupplySnapshot; // solhint-disable-line var-name-mixedcase } +/** + * @title RewardsManagerV4Storage + * @author Edge & Node + * @notice Storage layout for RewardsManager V4 + */ contract RewardsManagerV4Storage is RewardsManagerV3Storage { - // GRT issued for indexer rewards per block + /// @notice GRT issued for indexer rewards per block + /// @dev Only used when issuanceAllocator is zero address. uint256 public issuancePerBlock; } +/** + * @title RewardsManagerV5Storage + * @author Edge & Node + * @notice Storage layout for RewardsManager V5 + */ contract RewardsManagerV5Storage is RewardsManagerV4Storage { - // Address of the subgraph service + /// @notice Address of the subgraph service IRewardsIssuer public subgraphService; } diff --git a/packages/contracts/contracts/rewards/SubgraphAvailabilityManager.sol b/packages/contracts/contracts/rewards/SubgraphAvailabilityManager.sol index 3c0c98cd2..d24577b42 100644 --- a/packages/contracts/contracts/rewards/SubgraphAvailabilityManager.sol +++ b/packages/contracts/contracts/rewards/SubgraphAvailabilityManager.sol @@ -2,12 +2,17 @@ pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one, gas-indexed-events, gas-small-strings, gas-strict-inequalities +// solhint-disable named-parameters-mapping + import { Governed } from "../governance/Governed.sol"; -import { IRewardsManager } from "./IRewardsManager.sol"; +import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; /** * @title Subgraph Availability Manager - * @dev Manages the availability of subgraphs by allowing oracles to vote on whether + * @author Edge & Node + * @notice Manages the availability of subgraphs by allowing oracles to vote on whether * a subgraph should be denied rewards or not. When enough oracles have voted to deny or * allow rewards for a subgraph, it calls the RewardsManager Contract to set the correct * state. The oracles and the execution threshold are set at deployment time. @@ -21,15 +26,16 @@ contract SubgraphAvailabilityManager is Governed { uint256 public constant NUM_ORACLES = 5; /// @notice Number of votes required to execute a deny or allow call to the RewardsManager - uint256 public immutable executionThreshold; + uint256 public immutable executionThreshold; // solhint-disable-line immutable-vars-naming /// @dev Address of the RewardsManager contract + // solhint-disable-next-line immutable-vars-naming IRewardsManager private immutable rewardsManager; // -- State -- - /// @dev Nonce for generating votes on subgraph deployment IDs. - /// Increased whenever oracles or voteTimeLimit change, to invalidate old votes. + /// @notice Nonce for generating votes on subgraph deployment IDs + /// @dev Increased whenever oracles or voteTimeLimit change, to invalidate old votes uint256 public currentNonce; /// @notice Time limit for a vote to be valid @@ -39,30 +45,30 @@ contract SubgraphAvailabilityManager is Governed { address[NUM_ORACLES] public oracles; /// @notice Mapping of current nonce to subgraph deployment ID to an array of timestamps of last deny vote - /// currentNonce => subgraphDeploymentId => timestamp[oracleIndex] + /// @dev currentNonce => subgraphDeploymentId => timestamp[oracleIndex] mapping(uint256 => mapping(bytes32 => uint256[NUM_ORACLES])) public lastDenyVote; - /// @notice Mapping of current nonce to subgraph deployment ID to an array of timestamp of last allow vote - /// currentNonce => subgraphDeploymentId => timestamp[oracleIndex] + /// @notice Mapping of current nonce to subgraph deployment ID to an array of timestamp of last allow vote + /// @dev currentNonce => subgraphDeploymentId => timestamp[oracleIndex] mapping(uint256 => mapping(bytes32 => uint256[NUM_ORACLES])) public lastAllowVote; // -- Events -- /** - * @dev Emitted when an oracle is set + * @notice Emitted when an oracle is set * @param index Index of the oracle * @param oracle Address of the oracle */ event OracleSet(uint256 indexed index, address indexed oracle); /** - * @dev Emitted when the vote time limit is set + * @notice Emitted when the vote time limit is set * @param voteTimeLimit Vote time limit in seconds */ event VoteTimeLimitSet(uint256 voteTimeLimit); /** - * @dev Emitted when an oracle votes to deny or allow a subgraph + * @notice Emitted when an oracle votes to deny or allow a subgraph * @param subgraphDeploymentID Subgraph deployment ID * @param deny True to deny, false to allow * @param oracleIndex Index of the oracle voting @@ -72,6 +78,10 @@ contract SubgraphAvailabilityManager is Governed { // -- Modifiers -- + /** + * @dev Modifier to restrict access to authorized oracles only + * @param _oracleIndex Index of the oracle in the oracles array + */ modifier onlyOracle(uint256 _oracleIndex) { require(_oracleIndex < NUM_ORACLES, "SAM: index out of bounds"); require(msg.sender == oracles[_oracleIndex], "SAM: caller must be oracle"); @@ -81,7 +91,7 @@ contract SubgraphAvailabilityManager is Governed { // -- Constructor -- /** - * @dev Contract constructor + * @notice Contract constructor * @param _governor Account that can set or remove oracles and set the vote time limit * @param _rewardsManager Address of the RewardsManager contract * @param _executionThreshold Number of votes required to execute a deny or allow call to the RewardsManager @@ -118,7 +128,7 @@ contract SubgraphAvailabilityManager is Governed { // -- Functions -- /** - * @dev Set the vote time limit. Refreshes all existing votes by incrementing the current nonce. + * @notice Set the vote time limit. Refreshes all existing votes by incrementing the current nonce. * @param _voteTimeLimit Vote time limit in seconds */ function setVoteTimeLimit(uint256 _voteTimeLimit) external onlyGovernor { @@ -128,7 +138,7 @@ contract SubgraphAvailabilityManager is Governed { } /** - * @dev Set oracle address with index. Refreshes all existing votes by incrementing the current nonce. + * @notice Set oracle address with index. Refreshes all existing votes by incrementing the current nonce. * @param _index Index of the oracle * @param _oracle Address of the oracle */ @@ -144,7 +154,7 @@ contract SubgraphAvailabilityManager is Governed { } /** - * @dev Vote deny or allow for a subgraph. + * @notice Vote deny or allow for a subgraph. * NOTE: Can only be called by an oracle. * @param _subgraphDeploymentID Subgraph deployment ID * @param _deny True to deny, false to allow @@ -155,7 +165,7 @@ contract SubgraphAvailabilityManager is Governed { } /** - * @dev Vote deny or allow for many subgraphs. + * @notice Vote deny or allow for many subgraphs. * NOTE: Can only be called by an oracle. * @param _subgraphDeploymentID Array of subgraph deployment IDs * @param _deny Array of booleans, true to deny, false to allow @@ -173,7 +183,7 @@ contract SubgraphAvailabilityManager is Governed { } /** - * @dev Vote deny or allow for a subgraph. + * @notice Vote deny or allow for a subgraph. * When oracles cast their votes we store the timestamp of the vote. * Check if the execution threshold has been reached for a subgraph. * If execution threshold is reached we call the RewardsManager to set the correct state. @@ -203,7 +213,7 @@ contract SubgraphAvailabilityManager is Governed { } /** - * @dev Check if the execution threshold has been reached for a subgraph. + * @notice Check if the execution threshold has been reached for a subgraph. * For a vote to be valid it needs to be within the vote time limit. * @param _subgraphDeploymentID Subgraph deployment ID * @param _deny True to deny, false to allow diff --git a/packages/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol b/packages/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol index b935682b9..9a2224048 100644 --- a/packages/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol +++ b/packages/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol @@ -5,7 +5,8 @@ pragma abicoder v2; /** * @title Interface for the L1GraphTokenLockTransferTool contract - * @dev This interface defines the function to get the L2 wallet address for a given L1 token lock wallet. + * @author Edge & Node + * @notice This interface defines the function to get the L2 wallet address for a given L1 token lock wallet. * The Transfer Tool contract is implemented in the token-distribution repo: https://github.com/graphprotocol/token-distribution/pull/64 * and is only included here to provide support in L1Staking for the transfer of stake and delegation * owned by token lock contracts. See GIP-0046 for details: https://forum.thegraph.com/t/4023 diff --git a/packages/contracts/contracts/staking/IL1Staking.sol b/packages/contracts/contracts/staking/IL1Staking.sol index 4a446f787..d64372f0a 100644 --- a/packages/contracts/contracts/staking/IL1Staking.sol +++ b/packages/contracts/contracts/staking/IL1Staking.sol @@ -8,6 +8,7 @@ import { IL1StakingBase } from "./IL1StakingBase.sol"; /** * @title Interface for the L1 Staking contract + * @author Edge & Node * @notice This is the interface that should be used when interacting with the L1 Staking contract. * It extends the IStaking interface with the functions that are specific to L1, adding the transfer tools * to send stake and delegation to L2. diff --git a/packages/contracts/contracts/staking/IL1StakingBase.sol b/packages/contracts/contracts/staking/IL1StakingBase.sol index fad2136c2..09b2c3cca 100644 --- a/packages/contracts/contracts/staking/IL1StakingBase.sol +++ b/packages/contracts/contracts/staking/IL1StakingBase.sol @@ -3,23 +3,39 @@ pragma solidity >=0.6.12 <0.8.0; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IL1GraphTokenLockTransferTool } from "./IL1GraphTokenLockTransferTool.sol"; /** * @title Base interface for the L1Staking contract. + * @author Edge & Node * @notice This interface is used to define the transfer tools that are implemented in L1Staking. * @dev Note it includes only the L1-specific functionality, not the full IStaking interface. */ interface IL1StakingBase { - /// @dev Emitted when an indexer transfers their stake to L2. - /// This can happen several times as indexers can transfer partial stake. + /** + * @notice Emitted when an indexer transfers their stake to L2. + * This can happen several times as indexers can transfer partial stake. + * @param indexer Address of the indexer on L1 + * @param l2Indexer Address of the indexer on L2 + * @param transferredStakeTokens Amount of stake tokens transferred + */ event IndexerStakeTransferredToL2( address indexed indexer, address indexed l2Indexer, uint256 transferredStakeTokens ); - /// @dev Emitted when a delegator transfers their delegation to L2 + /** + * @notice Emitted when a delegator transfers their delegation to L2 + * @param delegator Address of the delegator on L1 + * @param l2Delegator Address of the delegator on L2 + * @param indexer Address of the indexer on L1 + * @param l2Indexer Address of the indexer on L2 + * @param transferredDelegationTokens Amount of delegation tokens transferred + */ event DelegationTransferredToL2( address indexed delegator, address indexed l2Delegator, @@ -28,10 +44,17 @@ interface IL1StakingBase { uint256 transferredDelegationTokens ); - /// @dev Emitted when the L1GraphTokenLockTransferTool is set + /** + * @notice Emitted when the L1GraphTokenLockTransferTool is set + * @param l1GraphTokenLockTransferTool Address of the L1GraphTokenLockTransferTool contract + */ event L1GraphTokenLockTransferToolSet(address l1GraphTokenLockTransferTool); - /// @dev Emitted when a delegator unlocks their tokens ahead of time because the indexer has transferred to L2 + /** + * @notice Emitted when a delegator unlocks their tokens ahead of time because the indexer has transferred to L2 + * @param indexer Address of the indexer that transferred to L2 + * @param delegator Address of the delegator unlocking their tokens + */ event StakeDelegatedUnlockedDueToL2Transfer(address indexed indexer, address indexed delegator); /** diff --git a/packages/contracts/contracts/staking/IStaking.sol b/packages/contracts/contracts/staking/IStaking.sol index a7d89feea..16c1db02d 100644 --- a/packages/contracts/contracts/staking/IStaking.sol +++ b/packages/contracts/contracts/staking/IStaking.sol @@ -10,6 +10,7 @@ import { IManaged } from "../governance/IManaged.sol"; /** * @title Interface for the Staking contract + * @author Edge & Node * @notice This is the interface that should be used when interacting with the Staking contract. * @dev Note that Staking doesn't actually inherit this interface. This is because of * the custom setup of the Staking contract where part of the functionality is implemented diff --git a/packages/contracts/contracts/staking/IStakingBase.sol b/packages/contracts/contracts/staking/IStakingBase.sol index 588144b2a..23b867dd4 100644 --- a/packages/contracts/contracts/staking/IStakingBase.sol +++ b/packages/contracts/contracts/staking/IStakingBase.sol @@ -3,10 +3,15 @@ pragma solidity >=0.6.12 <0.8.0 || 0.8.27; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IStakingData } from "./IStakingData.sol"; /** * @title Base interface for the Staking contract. + * @author Edge & Node + * @notice Base interface for the Staking contract. * @dev This interface includes only what's implemented in the base Staking contract. * It does not include the L1 and L2 specific functionality. It also does not include * several functions that are implemented in the StakingExtension contract, and are called @@ -15,25 +20,38 @@ import { IStakingData } from "./IStakingData.sol"; */ interface IStakingBase is IStakingData { /** - * @dev Emitted when `indexer` stakes `tokens` amount. + * @notice Emitted when `indexer` stakes `tokens` amount. + * @param indexer Address of the indexer + * @param tokens Amount of tokens staked */ event StakeDeposited(address indexed indexer, uint256 tokens); /** - * @dev Emitted when `indexer` unstaked and locked `tokens` amount until `until` block. + * @notice Emitted when `indexer` unstaked and locked `tokens` amount until `until` block. + * @param indexer Address of the indexer + * @param tokens Amount of tokens locked + * @param until Block number until which tokens are locked */ event StakeLocked(address indexed indexer, uint256 tokens, uint256 until); /** - * @dev Emitted when `indexer` withdrew `tokens` staked. + * @notice Emitted when `indexer` withdrew `tokens` staked. + * @param indexer Address of the indexer + * @param tokens Amount of tokens withdrawn */ event StakeWithdrawn(address indexed indexer, uint256 tokens); /** - * @dev Emitted when `indexer` allocated `tokens` amount to `subgraphDeploymentID` + * @notice Emitted when `indexer` allocated `tokens` amount to `subgraphDeploymentID` * during `epoch`. * `allocationID` indexer derived address used to identify the allocation. * `metadata` additional information related to the allocation. + * @param indexer Address of the indexer + * @param subgraphDeploymentID Subgraph deployment ID + * @param epoch Epoch when allocation was created + * @param tokens Amount of tokens allocated + * @param allocationID Allocation identifier + * @param metadata IPFS hash for additional allocation information */ event AllocationCreated( address indexed indexer, @@ -45,10 +63,18 @@ interface IStakingBase is IStakingData { ); /** - * @dev Emitted when `indexer` close an allocation in `epoch` for `allocationID`. + * @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 Address of the indexer + * @param subgraphDeploymentID Subgraph deployment ID + * @param epoch Epoch when allocation was closed + * @param tokens Amount of tokens unallocated + * @param allocationID Allocation identifier + * @param sender Address that closed the allocation + * @param poi Proof of indexing submitted + * @param isPublic True if closed by someone other than the indexer */ event AllocationClosed( address indexed indexer, @@ -62,12 +88,23 @@ interface IStakingBase is IStakingData { ); /** - * @dev Emitted when `indexer` collects a rebate on `subgraphDeploymentID` for `allocationID`. + * @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 Address providing the rebate tokens + * @param indexer Address of the indexer collecting the rebate + * @param subgraphDeploymentID Subgraph deployment ID + * @param allocationID Allocation identifier + * @param epoch Epoch when rebate was collected + * @param tokens Total amount of tokens in the rebate + * @param protocolTax Amount burned as protocol tax + * @param curationFees Amount distributed to curators + * @param queryFees Amount available for rebate after fees + * @param queryRebates Amount distributed to the indexer + * @param delegationRewards Amount distributed to delegators */ event RebateCollected( address assetHolder, @@ -84,7 +121,11 @@ interface IStakingBase is IStakingData { ); /** - * @dev Emitted when `indexer` update the delegation parameters for its delegation pool. + * @notice Emitted when `indexer` update the delegation parameters for its delegation pool. + * @param indexer Address of the indexer + * @param indexingRewardCut Percentage of indexing rewards left for the indexer + * @param queryFeeCut Percentage of query fees left for the indexer + * @param __DEPRECATED_cooldownBlocks Deprecated parameter (no longer used) */ event DelegationParametersUpdated( address indexed indexer, @@ -94,18 +135,24 @@ interface IStakingBase is IStakingData { ); /** - * @dev Emitted when `indexer` set `operator` access. + * @notice Emitted when `indexer` set `operator` access. + * @param indexer Address of the indexer + * @param operator Address of the operator + * @param allowed Whether the operator is authorized */ event SetOperator(address indexed indexer, address indexed operator, bool allowed); /** - * @dev Emitted when `indexer` set an address to receive rewards. + * @notice Emitted when `indexer` set an address to receive rewards. + * @param indexer Address of the indexer + * @param destination Address to receive rewards */ event SetRewardsDestination(address indexed indexer, address indexed destination); /** - * @dev Emitted when `extensionImpl` was set as the address of the StakingExtension contract + * @notice Emitted when `extensionImpl` was set as the address of the StakingExtension contract * to which extended functionality is delegated. + * @param extensionImpl Address of the StakingExtension implementation */ event ExtensionImplementationSet(address indexed extensionImpl); @@ -260,12 +307,9 @@ interface IStakingBase is IStakingData { * @notice Set the delegation parameters for the caller. * @param _indexingRewardCut Percentage of indexing rewards left for the indexer * @param _queryFeeCut Percentage of query fees left for the indexer + * @param _cooldownBlocks Deprecated cooldown blocks parameter (no longer used) */ - function setDelegationParameters( - uint32 _indexingRewardCut, - uint32 _queryFeeCut, - uint32 // _cooldownBlocks, deprecated - ) external; + function setDelegationParameters(uint32 _indexingRewardCut, uint32 _queryFeeCut, uint32 _cooldownBlocks) external; /** * @notice Allocate available tokens to a subgraph deployment. @@ -362,18 +406,29 @@ interface IStakingBase is IStakingData { function getAllocation(address _allocationID) external view returns (Allocation memory); /** + * @notice Get the allocation data for the rewards manager * @dev New function to get the allocation data for the rewards manager * @dev Note that this is only to make tests pass, as the staking contract with * this changes will never get deployed. HorizonStaking is taking it's place. + * @param _allocationID The allocation identifier + * @return isActive Whether the allocation is active + * @return indexer Address of the indexer + * @return subgraphDeploymentID Subgraph deployment ID + * @return tokens Amount of tokens allocated + * @return createdAtEpoch Epoch when allocation was created + * @return closedAtEpoch Epoch when allocation was closed (0 if still active) */ function getAllocationData( address _allocationID ) external view returns (bool, address, bytes32, uint256, uint256, uint256); /** + * @notice Get the allocation active status for the rewards manager * @dev New function to get the allocation active status for the rewards manager * @dev Note that this is only to make tests pass, as the staking contract with * this changes will never get deployed. HorizonStaking is taking it's place. + * @param _allocationID The allocation identifier + * @return True if the allocation is active, false otherwise */ function isActiveAllocation(address _allocationID) external view returns (bool); diff --git a/packages/contracts/contracts/staking/IStakingData.sol b/packages/contracts/contracts/staking/IStakingData.sol index 149e3b8a6..edac435c7 100644 --- a/packages/contracts/contracts/staking/IStakingData.sol +++ b/packages/contracts/contracts/staking/IStakingData.sol @@ -4,12 +4,22 @@ pragma solidity >=0.6.12 <0.8.0 || 0.8.27; /** * @title Staking Data interface - * @dev This interface defines some structures used by the Staking contract. + * @author Edge & Node + * @notice This interface defines some structures used by the Staking contract. */ interface IStakingData { /** * @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 Address of the indexer that owns the allocation + * @param subgraphDeploymentID Subgraph deployment ID being allocated to + * @param tokens Tokens allocated to a SubgraphDeployment + * @param createdAtEpoch Epoch when it was created + * @param closedAtEpoch Epoch when it was closed + * @param collectedFees Collected fees for the allocation + * @param __DEPRECATED_effectiveAllocation Deprecated field for effective allocation + * @param accRewardsPerAllocatedToken Snapshot used for reward calc + * @param distributedRebates Collected rebates that have been rebated */ struct Allocation { address indexer; @@ -27,6 +37,13 @@ interface IStakingData { /** * @dev Delegation pool information. One per indexer. + * @param __DEPRECATED_cooldownBlocks Deprecated field for cooldown blocks + * @param indexingRewardCut Indexing reward cut in PPM + * @param queryFeeCut Query fee cut in PPM + * @param updatedAtBlock Block when the pool was last updated + * @param tokens Total tokens as pool reserves + * @param shares Total shares minted in the pool + * @param delegators Mapping of delegator => Delegation */ struct DelegationPool { uint32 __DEPRECATED_cooldownBlocks; // solhint-disable-line var-name-mixedcase @@ -40,6 +57,9 @@ interface IStakingData { /** * @dev Individual delegation data of a delegator in a pool. + * @param shares Shares owned by a delegator in the pool + * @param tokensLocked Tokens locked for undelegation + * @param tokensLockedUntil Epoch when locked tokens can be withdrawn */ struct Delegation { uint256 shares; // Shares owned by a delegator in the pool @@ -49,6 +69,10 @@ interface IStakingData { /** * @dev Rebates parameters. Used to avoid stack too deep errors in Staking initialize function. + * @param alphaNumerator Alpha parameter numerator for rebate calculation + * @param alphaDenominator Alpha parameter denominator for rebate calculation + * @param lambdaNumerator Lambda parameter numerator for rebate calculation + * @param lambdaDenominator Lambda parameter denominator for rebate calculation */ struct RebatesParameters { uint32 alphaNumerator; diff --git a/packages/contracts/contracts/staking/IStakingExtension.sol b/packages/contracts/contracts/staking/IStakingExtension.sol index 9a998aac5..3053e7acf 100644 --- a/packages/contracts/contracts/staking/IStakingExtension.sol +++ b/packages/contracts/contracts/staking/IStakingExtension.sol @@ -3,12 +3,16 @@ pragma solidity >=0.6.12 <0.8.0 || 0.8.27; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IStakingData } from "./IStakingData.sol"; import { IStakes } from "./libs/IStakes.sol"; /** * @title Interface for the StakingExtension contract - * @dev This interface defines the events and functions implemented + * @author Edge & Node + * @notice This interface defines the events and functions implemented * in the StakingExtension contract, which is used to extend the functionality * of the Staking contract while keeping it within the 24kB mainnet size limit. * In particular, this interface includes delegation functions and various storage @@ -18,6 +22,12 @@ interface IStakingExtension is IStakingData { /** * @dev DelegationPool struct as returned by delegationPools(), since * the original DelegationPool in IStakingData.sol contains a nested mapping. + * @param __DEPRECATED_cooldownBlocks Deprecated field for cooldown blocks + * @param indexingRewardCut Indexing reward cut in PPM + * @param queryFeeCut Query fee cut in PPM + * @param updatedAtBlock Block when the pool was last updated + * @param tokens Total tokens as pool reserves + * @param shares Total shares minted in the pool */ struct DelegationPoolReturn { uint32 __DEPRECATED_cooldownBlocks; // solhint-disable-line var-name-mixedcase @@ -29,14 +39,23 @@ interface IStakingExtension is IStakingData { } /** - * @dev Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator + * @notice Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator * gets `shares` for the delegation pool proportionally to the tokens staked. + * @param indexer Address of the indexer receiving the delegation + * @param delegator Address of the delegator + * @param tokens Amount of tokens delegated + * @param shares Amount of shares issued to the delegator */ event StakeDelegated(address indexed indexer, address indexed delegator, uint256 tokens, uint256 shares); /** - * @dev Emitted when `delegator` undelegated `tokens` from `indexer`. + * @notice Emitted when `delegator` undelegated `tokens` from `indexer`. * Tokens get locked for withdrawal after a period of time. + * @param indexer Address of the indexer from which tokens are undelegated + * @param delegator Address of the delegator + * @param tokens Amount of tokens undelegated + * @param shares Amount of shares returned + * @param until Epoch until which tokens are locked */ event StakeDelegatedLocked( address indexed indexer, @@ -47,18 +66,28 @@ interface IStakingExtension is IStakingData { ); /** - * @dev Emitted when `delegator` withdrew delegated `tokens` from `indexer`. + * @notice Emitted when `delegator` withdrew delegated `tokens` from `indexer`. + * @param indexer Address of the indexer from which tokens are withdrawn + * @param delegator Address of the delegator + * @param tokens Amount of tokens withdrawn */ event StakeDelegatedWithdrawn(address indexed indexer, address indexed delegator, uint256 tokens); /** - * @dev Emitted when `indexer` was slashed for a total of `tokens` amount. + * @notice Emitted when `indexer` was slashed for a total of `tokens` amount. * Tracks `reward` amount of tokens given to `beneficiary`. + * @param indexer Address of the indexer that was slashed + * @param tokens Total amount of tokens slashed + * @param reward Amount of tokens given as reward + * @param beneficiary Address receiving the reward */ event StakeSlashed(address indexed indexer, uint256 tokens, uint256 reward, address beneficiary); /** - * @dev Emitted when `caller` set `slasher` address as `allowed` to slash stakes. + * @notice Emitted when `caller` set `slasher` address as `allowed` to slash stakes. + * @param caller Address that updated the slasher status + * @param slasher Address of the slasher + * @param allowed Whether the slasher is allowed to slash */ event SlasherUpdate(address indexed caller, address indexed slasher, bool allowed); @@ -114,6 +143,7 @@ interface IStakingExtension is IStakingData { * re-delegate to a new indexer. * @param _indexer Withdraw available tokens delegated to indexer * @param _newIndexer Re-delegate to indexer address if non-zero, withdraw if zero address + * @return Amount of tokens withdrawn */ function withdrawDelegated(address _indexer, address _newIndexer) external returns (uint256); diff --git a/packages/contracts/contracts/staking/L1Staking.sol b/packages/contracts/contracts/staking/L1Staking.sol index 78ce8bc63..2ffc3b62a 100644 --- a/packages/contracts/contracts/staking/L1Staking.sol +++ b/packages/contracts/contracts/staking/L1Staking.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable function-max-lines, gas-strict-inequalities + import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; @@ -19,7 +22,8 @@ import { IL2StakingTypes } from "../l2/staking/IL2StakingTypes.sol"; /** * @title L1Staking contract - * @dev This contract is the L1 variant of the Staking contract. It adds functions + * @author Edge & Node + * @notice This contract is the L1 variant of the Staking contract. It adds functions * to send an indexer's stake to L2, and to send delegation to L2 as well. */ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { @@ -36,9 +40,7 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @notice Set the L1GraphTokenLockTransferTool contract address - * @dev This function can only be called by the governor. - * @param _l1GraphTokenLockTransferTool Address of the L1GraphTokenLockTransferTool contract + * @inheritdoc IL1StakingBase */ function setL1GraphTokenLockTransferTool( IL1GraphTokenLockTransferTool _l1GraphTokenLockTransferTool @@ -48,21 +50,7 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @notice Send an indexer's stake to L2. - * @dev This function can only be called by the indexer (not an operator). - * It will validate that the remaining stake is sufficient to cover all the allocated - * stake, so the indexer might have to close some allocations before transferring. - * It will also check that the indexer's stake is not locked for withdrawal. - * Since the indexer address might be an L1-only contract, the function takes a beneficiary - * address that will be the indexer's address in L2. - * The caller must provide an amount of ETH to use for the L2 retryable ticket, that - * must be at _exactly_ `_maxSubmissionCost + _gasPriceBid * _maxGas`. - * Any refunds for the submission fee or L2 gas will be lost. - * @param _l2Beneficiary Address of the indexer in L2. If the indexer has previously transferred stake, this must match the previously-used value. - * @param _amount Amount of stake GRT to transfer to L2 - * @param _maxGas Max gas to use for the L2 retryable ticket - * @param _gasPriceBid Gas price bid for the L2 retryable ticket - * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket + * @inheritdoc IL1StakingBase */ function transferStakeToL2( address _l2Beneficiary, @@ -76,22 +64,7 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @notice Send an indexer's stake to L2, from a GraphTokenLockWallet vesting contract. - * @dev This function can only be called by the indexer (not an operator). - * It will validate that the remaining stake is sufficient to cover all the allocated - * stake, so the indexer might have to close some allocations before transferring. - * It will also check that the indexer's stake is not locked for withdrawal. - * The L2 beneficiary for the stake will be determined by calling the L1GraphTokenLockTransferTool contract, - * so the caller must have previously transferred tokens through that first - * (see GIP-0046 for details: https://forum.thegraph.com/t/4023). - * The ETH for the L2 gas will be pulled from the L1GraphTokenLockTransferTool, so the owner of - * the GraphTokenLockWallet must have previously deposited at least `_maxSubmissionCost + _gasPriceBid * _maxGas` - * ETH into the L1GraphTokenLockTransferTool contract (using its depositETH function). - * Any refunds for the submission fee or L2 gas will be lost. - * @param _amount Amount of stake GRT to transfer to L2 - * @param _maxGas Max gas to use for the L2 retryable ticket - * @param _gasPriceBid Gas price bid for the L2 retryable ticket - * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket + * @inheritdoc IL1StakingBase */ function transferLockedStakeToL2( uint256 _amount, @@ -109,20 +82,7 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @notice Send a delegator's delegated tokens to L2 - * @dev This function can only be called by the delegator. - * This function will validate that the indexer has transferred their stake using transferStakeToL2, - * and that the delegation is not locked for undelegation. - * Since the delegator's address might be an L1-only contract, the function takes a beneficiary - * address that will be the delegator's address in L2. - * The caller must provide an amount of ETH to use for the L2 retryable ticket, that - * must be _exactly_ `_maxSubmissionCost + _gasPriceBid * _maxGas`. - * Any refunds for the submission fee or L2 gas will be lost. - * @param _indexer Address of the indexer (in L1, before transferring to L2) - * @param _l2Beneficiary Address of the delegator in L2 - * @param _maxGas Max gas to use for the L2 retryable ticket - * @param _gasPriceBid Gas price bid for the L2 retryable ticket - * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket + * @inheritdoc IL1StakingBase */ function transferDelegationToL2( address _indexer, @@ -144,21 +104,7 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @notice Send a delegator's delegated tokens to L2, for a GraphTokenLockWallet vesting contract - * @dev This function can only be called by the delegator. - * This function will validate that the indexer has transferred their stake using transferStakeToL2, - * and that the delegation is not locked for undelegation. - * The L2 beneficiary for the delegation will be determined by calling the L1GraphTokenLockTransferTool contract, - * so the caller must have previously transferred tokens through that first - * (see GIP-0046 for details: https://forum.thegraph.com/t/4023). - * The ETH for the L2 gas will be pulled from the L1GraphTokenLockTransferTool, so the owner of - * the GraphTokenLockWallet must have previously deposited at least `_maxSubmissionCost + _gasPriceBid * _maxGas` - * ETH into the L1GraphTokenLockTransferTool contract (using its depositETH function). - * Any refunds for the submission fee or L2 gas will be lost. - * @param _indexer Address of the indexer (in L1, before transferring to L2) - * @param _maxGas Max gas to use for the L2 retryable ticket - * @param _gasPriceBid Gas price bid for the L2 retryable ticket - * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket + * @inheritdoc IL1StakingBase */ function transferLockedDelegationToL2( address _indexer, @@ -184,13 +130,7 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @notice Unlock a delegator's delegated tokens, if the indexer has transferred to L2 - * @dev This function can only be called by the delegator. - * This function will validate that the indexer has transferred their stake using transferStakeToL2, - * and that the indexer has no remaining stake in L1. - * The tokens must previously be locked for undelegation by calling `undelegate()`, - * and can be withdrawn with `withdrawDelegated()` immediately after calling this. - * @param _indexer Address of the indexer (in L1, before transferring to L2) + * @inheritdoc IL1StakingBase */ function unlockDelegationToTransferredIndexer(address _indexer) external override notPartialPaused { require( @@ -209,13 +149,14 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @dev Implements sending an indexer's stake to L2. + * @notice Implements sending an indexer's stake to L2. * This function can only be called by the indexer (not an operator). * It will validate that the remaining stake is sufficient to cover all the allocated * stake, so the indexer might have to close some allocations before transferring. * It will also check that the indexer's stake is not locked for withdrawal. * Since the indexer address might be an L1-only contract, the function takes a beneficiary * address that will be the indexer's address in L2. + * @param _indexer Address of the indexer transferring stake * @param _l2Beneficiary Address of the indexer in L2. If the indexer has previously transferred stake, this must match the previously-used value. * @param _amount Amount of stake GRT to transfer to L2 * @param _maxGas Max gas to use for the L2 retryable ticket @@ -282,12 +223,13 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @dev Implements sending a delegator's delegated tokens to L2. + * @notice Implements sending a delegator's delegated tokens to L2. * This function can only be called by the delegator. * This function will validate that the indexer has transferred their stake using transferStakeToL2, * and that the delegation is not locked for undelegation. * Since the delegator's address might be an L1-only contract, the function takes a beneficiary * address that will be the delegator's address in L2. + * @param _delegator Address of the delegator transferring delegation * @param _indexer Address of the indexer (in L1, before transferring to L2) * @param _l2Beneficiary Address of the delegator in L2 * @param _maxGas Max gas to use for the L2 retryable ticket @@ -352,7 +294,7 @@ contract L1Staking is Staking, L1StakingV1Storage, IL1StakingBase { } /** - * @dev Sends a message to the L2Staking with some extra data, + * @notice Sends a message to the L2Staking with some extra data, * also sending some tokens, using the L1GraphTokenGateway. * @param _tokens Amount of tokens to send to L2 * @param _maxGas Max gas to use for the L2 retryable ticket diff --git a/packages/contracts/contracts/staking/L1StakingStorage.sol b/packages/contracts/contracts/staking/L1StakingStorage.sol index bd7a7f0ee..b07ffa5e1 100644 --- a/packages/contracts/contracts/staking/L1StakingStorage.sol +++ b/packages/contracts/contracts/staking/L1StakingStorage.sol @@ -1,5 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable named-parameters-mapping + pragma solidity ^0.7.6; pragma abicoder v2; @@ -7,12 +10,13 @@ import { IL1GraphTokenLockTransferTool } from "./IL1GraphTokenLockTransferTool.s /** * @title L1StakingV1Storage + * @author Edge & Node * @notice This contract holds all the L1-specific storage variables for the L1Staking contract, version 1 * @dev When adding new versions, make sure to move the gap to the new version and * reduce the size of the gap accordingly. */ abstract contract L1StakingV1Storage { - /// If an indexer has transferred to L2, this mapping will hold the indexer's address in L2 + /// @notice If an indexer has transferred to L2, this mapping will hold the indexer's address in L2 mapping(address => address) public indexerTransferredToL2; /// @dev For locked indexers/delegations, this contract holds the mapping of L1 to L2 addresses IL1GraphTokenLockTransferTool internal l1GraphTokenLockTransferTool; diff --git a/packages/contracts/contracts/staking/Staking.sol b/packages/contracts/contracts/staking/Staking.sol index c61fe0ded..4f6107bd0 100644 --- a/packages/contracts/contracts/staking/Staking.sol +++ b/packages/contracts/contracts/staking/Staking.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable function-max-lines, gas-strict-inequalities + import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { ECDSA } from "@openzeppelin/contracts/cryptography/ECDSA.sol"; @@ -17,13 +20,14 @@ import { Stakes } from "./libs/Stakes.sol"; import { IStakes } from "./libs/IStakes.sol"; import { Managed } from "../governance/Managed.sol"; import { ICuration } from "../curation/ICuration.sol"; -import { IRewardsManager } from "../rewards/IRewardsManager.sol"; +import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { StakingExtension } from "./StakingExtension.sol"; import { LibExponential } from "./libs/Exponential.sol"; /** * @title Base Staking contract - * @dev The Staking contract allows Indexers to Stake on Subgraphs. Indexers Stake by creating + * @author Edge & Node + * @notice The Staking contract allows Indexers to Stake on Subgraphs. Indexers Stake by creating * Allocations on a Subgraph. It also allows Delegators to Delegate towards an Indexer. The * contract also has the slashing functionality. * The contract is abstract as the implementation that is deployed depends on each layer: L1Staking on mainnet @@ -45,9 +49,11 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M * @dev This function does not return to its internal call site, it will return directly to the * external caller. */ - // solhint-disable-next-line payable-fallback, no-complex-fallback fallback() external { + // solhint-disable-previous-line payable-fallback, no-complex-fallback + require(_implementation() != address(0), "only through proxy"); + // solhint-disable-next-line no-inline-assembly assembly { // (a) get free memory pointer @@ -80,17 +86,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Initialize this contract. - * @param _controller Address of the controller that manages this contract - * @param _minimumIndexerStake Minimum amount of tokens that an indexer must stake - * @param _thawingPeriod Number of epochs that tokens get locked after unstaking - * @param _protocolPercentage Percentage of query fees that are burned as protocol fee (in PPM) - * @param _curationPercentage Percentage of query fees that are given to curators (in PPM) - * @param _maxAllocationEpochs The maximum number of epochs that an allocation can be active - * @param _delegationUnbondingPeriod The period in epochs that tokens get locked after undelegating - * @param _delegationRatio The ratio between an indexer's own stake and the delegation they can use - * @param _rebatesParameters Alpha and lambda parameters for rebates function - * @param _extensionImpl Address of the StakingExtension implementation + * @inheritdoc IStakingBase */ function initialize( address _controller, @@ -140,9 +136,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Set the address of the StakingExtension implementation. - * @dev This function can only be called by the governor. - * @param _extensionImpl Address of the StakingExtension implementation + * @inheritdoc IStakingBase */ function setExtensionImpl(address _extensionImpl) external override onlyGovernor { extensionImpl = _extensionImpl; @@ -150,9 +144,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Set the address of the counterpart (L1 or L2) staking contract. - * @dev This function can only be called by the governor. - * @param _counterpart Address of the counterpart staking contract in the other chain, without any aliasing. + * @inheritdoc IStakingBase */ function setCounterpartStakingAddress(address _counterpart) external override onlyGovernor { counterpartStakingAddress = _counterpart; @@ -160,52 +152,42 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Set the minimum stake required to be an indexer. - * @param _minimumIndexerStake Minimum indexer stake + * @inheritdoc IStakingBase */ function setMinimumIndexerStake(uint256 _minimumIndexerStake) external override onlyGovernor { _setMinimumIndexerStake(_minimumIndexerStake); } /** - * @notice Set the thawing period for unstaking. - * @param _thawingPeriod Period in blocks to wait for token withdrawals after unstaking + * @inheritdoc IStakingBase */ function setThawingPeriod(uint32 _thawingPeriod) external override onlyGovernor { _setThawingPeriod(_thawingPeriod); } /** - * @notice Set the curation percentage of query fees sent to curators. - * @param _percentage Percentage of query fees sent to curators + * @inheritdoc IStakingBase */ function setCurationPercentage(uint32 _percentage) external override onlyGovernor { _setCurationPercentage(_percentage); } /** - * @notice Set a protocol percentage to burn when collecting query fees. - * @param _percentage Percentage of query fees to burn as protocol fee + * @inheritdoc IStakingBase */ function setProtocolPercentage(uint32 _percentage) external override onlyGovernor { _setProtocolPercentage(_percentage); } /** - * @notice Set the max time allowed for indexers to allocate on a subgraph - * before others are allowed to close the allocation. - * @param _maxAllocationEpochs Allocation duration limit in epochs + * @inheritdoc IStakingBase */ function setMaxAllocationEpochs(uint32 _maxAllocationEpochs) external override onlyGovernor { _setMaxAllocationEpochs(_maxAllocationEpochs); } /** - * @dev Set the rebate parameters. - * @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 + * @inheritdoc IStakingBase */ function setRebateParameters( uint32 _alphaNumerator, @@ -217,9 +199,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Authorize or unauthorize an address to be an operator for the caller. - * @param _operator Address to authorize or unauthorize - * @param _allowed Whether the operator is authorized or not + * @inheritdoc IStakingBase */ function setOperator(address _operator, bool _allowed) external override { require(_operator != msg.sender, "operator == sender"); @@ -228,21 +208,18 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Deposit tokens on the indexer's stake. - * The amount staked must be over the minimumIndexerStake. - * @param _tokens Amount of tokens to stake + * @inheritdoc IStakingBase */ function stake(uint256 _tokens) external override { stakeTo(msg.sender, _tokens); } /** - * @notice Unstake tokens from the indexer stake, lock them until the thawing period expires. + * @inheritdoc IStakingBase * @dev NOTE: The function accepts an amount greater than the currently staked tokens. * If that happens, it will try to unstake the max amount of tokens it can. * The reason for this behaviour is to avoid time conditions while the transaction * is in flight. - * @param _tokens Amount of tokens to unstake */ function unstake(uint256 _tokens) external override notPartialPaused { address indexer = msg.sender; @@ -271,15 +248,14 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Withdraw indexer tokens once the thawing period has passed. + * @inheritdoc IStakingBase */ function withdraw() external override notPaused { _withdraw(msg.sender); } /** - * @notice Set the destination where to send rewards for an indexer. - * @param _destination Rewards destination address. If set to zero, rewards will be restaked + * @inheritdoc IStakingBase */ function setRewardsDestination(address _destination) external override { __rewardsDestination[msg.sender] = _destination; @@ -287,12 +263,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Allocate available tokens to a subgraph deployment. - * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated - * @param _tokens Amount of tokens to allocate - * @param _allocationID The allocation identifier - * @param _metadata IPFS hash for additional information about the allocation - * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` + * @inheritdoc IStakingBase */ function allocate( bytes32 _subgraphDeploymentID, @@ -305,14 +276,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Allocate available tokens to a subgraph deployment from and indexer's stake. - * The caller must be the indexer or the indexer's operator. - * @param _indexer Indexer address to allocate funds from. - * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated - * @param _tokens Amount of tokens to allocate - * @param _allocationID The allocation identifier - * @param _metadata IPFS hash for additional information about the allocation - * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` + * @inheritdoc IStakingBase */ function allocateFrom( address _indexer, @@ -326,25 +290,20 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Close an allocation and free the staked tokens. - * To be eligible for rewards a proof of indexing must be presented. + * @inheritdoc IStakingBase + * @dev 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 override notPaused { _closeAllocation(_allocationID, _poi); } /** - * @dev Collect and rebate query fees from state channels to the indexer - * To avoid reverting on the withdrawal from channel flow 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. + * @inheritdoc IStakingBase + * @dev 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 override { // Allocation identifier validation @@ -447,36 +406,28 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Return if allocationID is used. - * @param _allocationID Address used as signer by the indexer for an allocation - * @return True if allocationID already used + * @inheritdoc IStakingBase */ function isAllocation(address _allocationID) external view override returns (bool) { return _getAllocationState(_allocationID) != AllocationState.Null; } /** - * @notice Getter that returns if an indexer has any stake. - * @param _indexer Address of the indexer - * @return True if indexer has staked tokens + * @inheritdoc IStakingBase */ function hasStake(address _indexer) external view override returns (bool) { return __stakes[_indexer].tokensStaked > 0; } /** - * @notice Return the allocation by ID. - * @param _allocationID Address used as allocation identifier - * @return Allocation data + * @inheritdoc IStakingBase */ function getAllocation(address _allocationID) external view override returns (Allocation memory) { return __allocations[_allocationID]; } /** - * @dev New function to get the allocation data for the rewards manager - * @dev Note that this is only to make tests pass, as the staking contract with - * this changes will never get deployed. HorizonStaking is taking it's place. + * @inheritdoc IStakingBase */ function getAllocationData( address _allocationID @@ -495,46 +446,35 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev New function to get the allocation active status for the rewards manager - * @dev Note that this is only to make tests pass, as the staking contract with - * this changes will never get deployed. HorizonStaking is taking it's place. + * @inheritdoc IStakingBase */ function isActiveAllocation(address _allocationID) external view override returns (bool) { return _getAllocationState(_allocationID) == AllocationState.Active; } /** - * @notice Return the current state of an allocation - * @param _allocationID Allocation identifier - * @return AllocationState enum with the state of the allocation + * @inheritdoc IStakingBase */ function getAllocationState(address _allocationID) external view override returns (AllocationState) { return _getAllocationState(_allocationID); } /** - * @notice Return the total amount of tokens allocated to subgraph. - * @param _subgraphDeploymentID Deployment ID for the subgraph - * @return Total tokens allocated to subgraph + * @inheritdoc IStakingBase */ function getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentID) external view override returns (uint256) { return __subgraphAllocations[_subgraphDeploymentID]; } /** - * @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 + * @inheritdoc IStakingBase */ function getIndexerStakedTokens(address _indexer) external view override returns (uint256) { return __stakes[_indexer].tokensStaked; } /** - * @notice Deposit tokens on the Indexer stake, on behalf of the Indexer. - * The amount staked must be over the minimumIndexerStake. - * @param _indexer Address of the indexer - * @param _tokens Amount of tokens to stake + * @inheritdoc IStakingBase */ function stakeTo(address _indexer, uint256 _tokens) public override notPartialPaused { require(_tokens > 0, "!tokens"); @@ -547,9 +487,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Set the delegation parameters for the caller. - * @param _indexingRewardCut Percentage of indexing rewards left for the indexer - * @param _queryFeeCut Percentage of query fees left for the indexer + * @inheritdoc IStakingBase */ function setDelegationParameters( uint32 _indexingRewardCut, @@ -560,10 +498,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Get the total amount of tokens available to use in allocations. - * This considers the indexer stake and delegated tokens according to delegation ratio - * @param _indexer Address of the indexer - * @return Amount of tokens available to allocate including delegation + * @inheritdoc IStakingBase */ function getIndexerCapacity(address _indexer) public view override returns (uint256) { IStakes.Indexer memory indexerStake = __stakes[_indexer]; @@ -576,17 +511,14 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @notice Return true if operator is allowed for indexer. - * @param _operator Address of the operator - * @param _indexer Address of the indexer - * @return True if operator is allowed for indexer, false otherwise + * @inheritdoc IStakingBase */ function isOperator(address _operator, address _indexer) public view override returns (bool) { return __operatorAuth[_indexer][_operator]; } /** - * @dev Internal: Set the minimum indexer stake required. + * @notice Internal: Set the minimum indexer stake required. * @param _minimumIndexerStake Minimum indexer stake */ function _setMinimumIndexerStake(uint256 _minimumIndexerStake) private { @@ -596,7 +528,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Internal: Set the thawing period for unstaking. + * @notice Internal: Set the thawing period for unstaking. * @param _thawingPeriod Period in blocks to wait for token withdrawals after unstaking */ function _setThawingPeriod(uint32 _thawingPeriod) private { @@ -606,7 +538,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Internal: Set the curation percentage of query fees sent to curators. + * @notice Internal: Set the curation percentage of query fees sent to curators. * @param _percentage Percentage of query fees sent to curators */ function _setCurationPercentage(uint32 _percentage) private { @@ -617,7 +549,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Internal: Set a protocol percentage to burn when collecting query fees. + * @notice Internal: Set a protocol percentage to burn when collecting query fees. * @param _percentage Percentage of query fees to burn as protocol fee */ function _setProtocolPercentage(uint32 _percentage) private { @@ -628,7 +560,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Internal: Set the max time allowed for indexers stake on allocations. + * @notice Internal: Set the max time allowed for indexers stake on allocations. * @param _maxAllocationEpochs Allocation duration limit in epochs */ function _setMaxAllocationEpochs(uint32 _maxAllocationEpochs) private { @@ -637,7 +569,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Set the rebate parameters. + * @notice Set the rebate parameters. * @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 @@ -660,7 +592,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Set the delegation parameters for a particular indexer. + * @notice Set the delegation parameters for a particular indexer. * @param _indexer Indexer to set delegation parameters * @param _indexingRewardCut Percentage of indexing rewards left for delegators * @param _queryFeeCut Percentage of query fees left for delegators @@ -681,7 +613,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Stake tokens on the indexer. + * @notice Stake tokens on the indexer. * This function does not check minimum indexer stake requirement to allow * to be called by functions that increase the stake when collecting rewards * without reverting @@ -704,7 +636,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Withdraw indexer tokens once the thawing period has passed. + * @notice Withdraw indexer tokens once the thawing period has passed. * @param _indexer Address of indexer to withdraw funds from */ function _withdraw(address _indexer) private { @@ -719,7 +651,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Allocate available tokens to a subgraph deployment. + * @notice Allocate available tokens to a subgraph deployment. * @param _indexer Indexer address to allocate funds from. * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated * @param _tokens Amount of tokens to allocate @@ -794,7 +726,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Close an allocation and free the staked 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 */ @@ -861,7 +793,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Collect the delegation rewards for query fees. + * @notice Collect the delegation rewards for query fees. * 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 @@ -879,7 +811,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Collect the delegation rewards for indexing. + * @notice Collect the delegation rewards for indexing. * 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 @@ -897,7 +829,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Collect the curation fees for a subgraph deployment from an amount of tokens. + * @notice Collect the curation fees for a subgraph deployment from an amount of tokens. * This function transfer curation fees to the Curation contract by calling Curation.collect * @param _graphToken Token to collect * @param _subgraphDeploymentID Subgraph deployment to which the curation fees are related @@ -926,7 +858,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M 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 + // Then we call collect() to do the transfer bookkeeping rewardsManager().onSubgraphSignalUpdate(_subgraphDeploymentID); TokenUtils.pushTokens(_graphToken, address(curation), curationFees); curation.collect(_subgraphDeploymentID, curationFees); @@ -937,7 +869,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Collect tax to burn for an amount of tokens. + * @notice Collect tax to burn for an amount of tokens. * @param _graphToken Token to burn * @param _tokens Total tokens received used to calculate the amount of tax to collect * @param _percentage Percentage of tokens to burn as tax @@ -953,7 +885,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Triggers an update of rewards due to a change in allocations. + * @notice Triggers an update of rewards due to a change in allocations. * @param _subgraphDeploymentID Subgraph deployment updated * @return Accumulated rewards per allocated token for the subgraph deployment */ @@ -966,7 +898,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Assign rewards for the closed allocation to indexer and delegators. + * @notice Assign rewards for the closed allocation to indexer and delegators. * @param _allocationID Allocation * @param _indexer Address of the indexer that did the allocation */ @@ -993,7 +925,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Send rewards to the appropriate destination. + * @notice Send rewards to the appropriate destination. * @param _graphToken Graph token * @param _amount Number of rewards tokens * @param _beneficiary Address of the beneficiary of rewards @@ -1013,7 +945,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Check if the caller is authorized to operate on behalf of + * @notice Check if the caller is authorized to operate on behalf of * an indexer (i.e. the caller is the indexer or an operator) * @param _indexer Indexer address * @return True if the caller is authorized to operate on behalf of the indexer @@ -1023,7 +955,7 @@ abstract contract Staking is StakingV4Storage, GraphUpgradeable, IStakingBase, M } /** - * @dev Return the current state of an allocation + * @notice Return the current state of an allocation * @param _allocationID Allocation identifier * @return AllocationState enum with the state of the allocation */ diff --git a/packages/contracts/contracts/staking/StakingExtension.sol b/packages/contracts/contracts/staking/StakingExtension.sol index b06fbe894..fb8dea3e8 100644 --- a/packages/contracts/contracts/staking/StakingExtension.sol +++ b/packages/contracts/contracts/staking/StakingExtension.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { StakingV4Storage } from "./StakingStorage.sol"; import { IStakingExtension } from "./IStakingExtension.sol"; @@ -16,7 +19,8 @@ import { MathUtils } from "./libs/MathUtils.sol"; /** * @title StakingExtension contract - * @dev This contract provides the logic to manage delegations and other Staking + * @author Edge & Node + * @notice This contract provides the logic to manage delegations and other Staking * extension features (e.g. storage getters). It is meant to be called through delegatecall from the * Staking contract, and is only kept separate to keep the Staking contract size * within limits. @@ -44,12 +48,14 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi * initialize() function, so it uses the same access control check to ensure it is * being called by the Staking implementation as part of the proxy upgrade process. * @param _delegationUnbondingPeriod Delegation unbonding period in blocks + * @param _cooldownBlocks Deprecated parameter (no longer used) * @param _delegationRatio Delegation capacity multiplier (e.g. 10 means 10x the indexer stake) * @param _delegationTaxPercentage Percentage of delegated tokens to burn as delegation tax, expressed in parts per million */ function initialize( uint32 _delegationUnbondingPeriod, - uint32, //_cooldownBlocks, deprecated + // solhint-disable-next-line no-unused-vars + uint32 _cooldownBlocks, // deprecated uint32 _delegationRatio, uint32 _delegationTaxPercentage ) external onlyImpl { @@ -59,38 +65,28 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @notice Set a delegation tax percentage to burn when delegated funds are deposited. - * @dev This function is only callable by the governor - * @param _percentage Percentage of delegated tokens to burn as delegation tax, expressed in parts per million + * @inheritdoc IStakingExtension */ function setDelegationTaxPercentage(uint32 _percentage) external override onlyGovernor { _setDelegationTaxPercentage(_percentage); } /** - * @notice Set the delegation ratio. - * If set to 10 it means the indexer can use up to 10x the indexer staked amount - * from their delegated tokens - * @dev This function is only callable by the governor - * @param _delegationRatio Delegation capacity multiplier + * @inheritdoc IStakingExtension */ function setDelegationRatio(uint32 _delegationRatio) external override onlyGovernor { _setDelegationRatio(_delegationRatio); } /** - * @notice Set the time, in epochs, a Delegator needs to wait to withdraw tokens after undelegating. - * @dev This function is only callable by the governor - * @param _delegationUnbondingPeriod Period in epochs to wait for token withdrawals after undelegating + * @inheritdoc IStakingExtension */ function setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) external override onlyGovernor { _setDelegationUnbondingPeriod(_delegationUnbondingPeriod); } /** - * @notice Set or unset an address as allowed slasher. - * @param _slasher Address of the party allowed to slash indexers - * @param _allowed True if slasher is allowed + * @inheritdoc IStakingExtension */ function setSlasher(address _slasher, bool _allowed) external override onlyGovernor { require(_slasher != address(0), "!slasher"); @@ -99,10 +95,7 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @notice Delegate tokens to an indexer. - * @param _indexer Address of the indexer to which tokens are delegated - * @param _tokens Amount of tokens to delegate - * @return Amount of shares issued from the delegation pool + * @inheritdoc IStakingExtension */ function delegate(address _indexer, uint256 _tokens) external override notPartialPaused returns (uint256) { address delegator = msg.sender; @@ -115,32 +108,21 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @notice Undelegate tokens from an indexer. Tokens will be locked for the unbonding period. - * @param _indexer Address of the indexer to which tokens had been delegated - * @param _shares Amount of shares to return and undelegate tokens - * @return Amount of tokens returned for the shares of the delegation pool + * @inheritdoc IStakingExtension */ function undelegate(address _indexer, uint256 _shares) external override notPartialPaused returns (uint256) { return _undelegate(msg.sender, _indexer, _shares); } /** - * @notice Withdraw undelegated tokens once the unbonding period has passed, and optionally - * re-delegate to a new indexer. - * @param _indexer Withdraw available tokens delegated to indexer - * @param _newIndexer Re-delegate to indexer address if non-zero, withdraw if zero address + * @inheritdoc IStakingExtension */ function withdrawDelegated(address _indexer, address _newIndexer) external override notPaused returns (uint256) { return _withdrawDelegated(msg.sender, _indexer, _newIndexer); } /** - * @notice Slash the indexer stake. Delegated tokens are not subject to slashing. - * @dev Can only be called by the slasher role. - * @param _indexer Address of indexer to slash - * @param _tokens Amount of tokens to slash from the indexer stake - * @param _reward Amount of reward tokens to send to a beneficiary - * @param _beneficiary Address of a beneficiary to receive a reward for the slashing + * @inheritdoc IStakingExtension */ function slash( address _indexer, @@ -188,48 +170,35 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @notice Return the delegation from a delegator to an indexer. - * @param _indexer Address of the indexer where funds have been delegated - * @param _delegator Address of the delegator - * @return Delegation data + * @inheritdoc IStakingExtension */ function getDelegation(address _indexer, address _delegator) external view override returns (Delegation memory) { return __delegationPools[_indexer].delegators[_delegator]; } /** - * @notice Getter for the delegationRatio, i.e. the delegation capacity multiplier: - * If delegation ratio is 100, and an Indexer has staked 5 GRT, - * then they can use up to 500 GRT from the delegated stake - * @return Delegation ratio + * @inheritdoc IStakingExtension */ function delegationRatio() external view override returns (uint32) { return __delegationRatio; } /** - * @notice Getter for delegationUnbondingPeriod: - * Time in epochs a delegator needs to wait to withdraw delegated stake - * @return Delegation unbonding period in epochs + * @inheritdoc IStakingExtension */ function delegationUnbondingPeriod() external view override returns (uint32) { return __delegationUnbondingPeriod; } /** - * @notice Getter for delegationTaxPercentage: - * Percentage of tokens to tax a delegation deposit, expressed in parts per million - * @return Delegation tax percentage in parts per million + * @inheritdoc IStakingExtension */ function delegationTaxPercentage() external view override returns (uint32) { return __delegationTaxPercentage; } /** - * @notice Getter for delegationPools[_indexer]: - * gets the delegation pool structure for a particular indexer. - * @param _indexer Address of the indexer for which to query the delegation pool - * @return Delegation pool as a DelegationPoolReturn struct + * @inheritdoc IStakingExtension */ function delegationPools(address _indexer) external view override returns (DelegationPoolReturn memory) { DelegationPool storage pool = __delegationPools[_indexer]; @@ -245,120 +214,91 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @notice Getter for rewardsDestination[_indexer]: - * returns the address where the indexer's rewards are sent. - * @param _indexer The indexer address for which to query the rewards destination - * @return The address where the indexer's rewards are sent, zero if none is set in which case rewards are re-staked + * @inheritdoc IStakingExtension */ function rewardsDestination(address _indexer) external view override returns (address) { return __rewardsDestination[_indexer]; } /** - * @notice Getter for operatorAuth[_indexer][_maybeOperator]: - * returns true if the operator is authorized to operate on behalf of the indexer. - * @param _indexer The indexer address for which to query authorization - * @param _maybeOperator The address that may or may not be an operator - * @return True if the operator is authorized to operate on behalf of the indexer + * @inheritdoc IStakingExtension */ function operatorAuth(address _indexer, address _maybeOperator) external view override returns (bool) { return __operatorAuth[_indexer][_maybeOperator]; } /** - * @notice Getter for subgraphAllocations[_subgraphDeploymentId]: - * returns the amount of tokens allocated to a subgraph deployment. - * @param _subgraphDeploymentId The subgraph deployment for which to query the allocations - * @return The amount of tokens allocated to the subgraph deployment + * @inheritdoc IStakingExtension */ function subgraphAllocations(bytes32 _subgraphDeploymentId) external view override returns (uint256) { return __subgraphAllocations[_subgraphDeploymentId]; } /** - * @notice Getter for slashers[_maybeSlasher]: - * returns true if the address is a slasher, i.e. an entity that can slash indexers - * @param _maybeSlasher Address for which to check the slasher role - * @return True if the address is a slasher + * @inheritdoc IStakingExtension */ function slashers(address _maybeSlasher) external view override returns (bool) { return __slashers[_maybeSlasher]; } /** - * @notice Getter for minimumIndexerStake: the minimum - * amount of GRT that an indexer needs to stake. - * @return Minimum indexer stake in GRT + * @inheritdoc IStakingExtension */ function minimumIndexerStake() external view override returns (uint256) { return __minimumIndexerStake; } /** - * @notice Getter for thawingPeriod: the time in blocks an - * indexer needs to wait to unstake tokens. - * @return Thawing period in blocks + * @inheritdoc IStakingExtension */ function thawingPeriod() external view override returns (uint32) { return __thawingPeriod; } /** - * @notice Getter for curationPercentage: the percentage of - * query fees that are distributed to curators. - * @return Curation percentage in parts per million + * @inheritdoc IStakingExtension */ function curationPercentage() external view override returns (uint32) { return __curationPercentage; } /** - * @notice Getter for protocolPercentage: the percentage of - * query fees that are burned as protocol fees. - * @return Protocol percentage in parts per million + * @inheritdoc IStakingExtension */ function protocolPercentage() external view override returns (uint32) { return __protocolPercentage; } /** - * @notice Getter for maxAllocationEpochs: the maximum time in epochs - * that an allocation can be open before anyone is allowed to close it. This - * also caps the effective allocation when sending the allocation's query fees - * to the rebate pool. - * @return Maximum allocation period in epochs + * @inheritdoc IStakingExtension */ function maxAllocationEpochs() external view override returns (uint32) { return __maxAllocationEpochs; } /** - * @notice Getter for the numerator of the rebates alpha parameter - * @return Alpha numerator + * @inheritdoc IStakingExtension */ function alphaNumerator() external view override returns (uint32) { return __alphaNumerator; } /** - * @notice Getter for the denominator of the rebates alpha parameter - * @return Alpha denominator + * @inheritdoc IStakingExtension */ function alphaDenominator() external view override returns (uint32) { return __alphaDenominator; } /** - * @notice Getter for the numerator of the rebates lambda parameter - * @return Lambda numerator + * @inheritdoc IStakingExtension */ function lambdaNumerator() external view override returns (uint32) { return __lambdaNumerator; } /** - * @notice Getter for the denominator of the rebates lambda parameter - * @return Lambda denominator + * @inheritdoc IStakingExtension */ function lambdaDenominator() external view override returns (uint32) { return __lambdaDenominator; @@ -369,35 +309,28 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi * gets the stake information for an indexer as an IStakes.Indexer struct. * @param _indexer Indexer address for which to query the stake information * @return Stake information for the specified indexer, as an IStakes.Indexer struct + * @inheritdoc IStakingExtension */ function stakes(address _indexer) external view override returns (IStakes.Indexer memory) { return __stakes[_indexer]; } /** - * @notice Getter for allocations[_allocationID]: - * gets an allocation's information as an IStakingData.Allocation struct. - * @param _allocationID Allocation ID for which to query the allocation information - * @return The specified allocation, as an IStakingData.Allocation struct + * @inheritdoc IStakingExtension */ function allocations(address _allocationID) external view override returns (IStakingData.Allocation memory) { return __allocations[_allocationID]; } /** - * @notice Return whether the delegator has delegated to the indexer. - * @param _indexer Address of the indexer where funds have been delegated - * @param _delegator Address of the delegator - * @return True if delegator has tokens delegated to the indexer + * @inheritdoc IStakingExtension */ function isDelegator(address _indexer, address _delegator) public view override returns (bool) { return __delegationPools[_indexer].delegators[_delegator].shares > 0; } /** - * @notice Returns amount of delegated tokens ready to be withdrawn after unbonding period. - * @param _delegation Delegation of tokens from delegator to indexer - * @return Amount of tokens to withdraw + * @inheritdoc IStakingExtension */ function getWithdraweableDelegatedTokens(Delegation memory _delegation) public view override returns (uint256) { // There must be locked tokens and period passed @@ -409,7 +342,7 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @dev Internal: Set a delegation tax percentage to burn when delegated funds are deposited. + * @notice Internal: Set a delegation tax percentage to burn when delegated funds are deposited. * @param _percentage Percentage of delegated tokens to burn as delegation tax */ function _setDelegationTaxPercentage(uint32 _percentage) private { @@ -420,7 +353,7 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @dev Internal: Set the delegation ratio. + * @notice Internal: Set the delegation ratio. * If set to 10 it means the indexer can use up to 10x the indexer staked amount * from their delegated tokens * @param _delegationRatio Delegation capacity multiplier @@ -431,7 +364,7 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @dev Internal: Set the period for undelegation of stake from indexer. + * @notice Internal: Set the period for undelegation of stake from indexer. * @param _delegationUnbondingPeriod Period in epochs to wait for token withdrawals after undelegating */ function _setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) private { @@ -441,7 +374,7 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @dev Delegate tokens to an indexer. + * @notice Delegate tokens to an indexer. * @param _delegator Address of the delegator * @param _indexer Address of the indexer to delegate tokens to * @param _tokens Amount of tokens to delegate @@ -480,7 +413,7 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @dev Undelegate tokens from an indexer. + * @notice Undelegate tokens from an indexer. * @param _delegator Address of the delegator * @param _indexer Address of the indexer where tokens had been delegated * @param _shares Amount of shares to return and undelegate tokens @@ -531,7 +464,7 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @dev Withdraw delegated tokens once the unbonding period has passed. + * @notice Withdraw delegated tokens once the unbonding period has passed. * @param _delegator Delegator that is withdrawing tokens * @param _indexer Withdraw available tokens delegated to indexer * @param _delegateToIndexer Re-delegate to indexer address if non-zero, withdraw if zero address @@ -570,7 +503,7 @@ contract StakingExtension is StakingV4Storage, GraphUpgradeable, IStakingExtensi } /** - * @dev Collect tax to burn for an amount of tokens. + * @notice Collect tax to burn for an amount of tokens. * @param _graphToken Token to burn * @param _tokens Total tokens received used to calculate the amount of tax to collect * @param _percentage Percentage of tokens to burn as tax diff --git a/packages/contracts/contracts/staking/StakingStorage.sol b/packages/contracts/contracts/staking/StakingStorage.sol index 949a63614..221359b50 100644 --- a/packages/contracts/contracts/staking/StakingStorage.sol +++ b/packages/contracts/contracts/staking/StakingStorage.sol @@ -1,7 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// solhint-disable one-contract-per-file pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable one-contract-per-file, max-states-count +// solhint-disable named-parameters-mapping + import { Managed } from "../governance/Managed.sol"; import { IStakingData } from "./IStakingData.sol"; @@ -9,11 +14,11 @@ import { IStakes } from "./libs/IStakes.sol"; /** * @title StakingV1Storage + * @author Edge & Node * @notice This contract holds all the storage variables for the Staking contract, version 1 * @dev Note that we use a double underscore prefix for variable names; this prefix identifies * variables that used to be public but are now internal, getters can be found on StakingExtension.sol. */ -// solhint-disable-next-line max-states-count contract StakingV1Storage is Managed { // -- Staking -- @@ -54,7 +59,7 @@ contract StakingV1Storage is Managed { /// @dev Subgraph Allocations: subgraphDeploymentID => tokens mapping(bytes32 => uint256) internal __subgraphAllocations; - // Rebate pools : epoch => Pool + /// @dev Deprecated rebate pools mapping (no longer used) mapping(uint256 => uint256) private __DEPRECATED_rebates; // solhint-disable-line var-name-mixedcase // -- Slashing -- @@ -95,6 +100,7 @@ contract StakingV1Storage is Managed { /** * @title StakingV2Storage + * @author Edge & Node * @notice This contract holds all the storage variables for the Staking contract, version 2 * @dev Note that we use a double underscore prefix for variable names; this prefix identifies * variables that used to be public but are now internal, getters can be found on StakingExtension.sol. @@ -106,6 +112,7 @@ contract StakingV2Storage is StakingV1Storage { /** * @title StakingV3Storage + * @author Edge & Node * @notice This contract holds all the storage variables for the base Staking contract, version 3. */ contract StakingV3Storage is StakingV2Storage { @@ -117,13 +124,15 @@ contract StakingV3Storage is StakingV2Storage { /** * @title StakingV4Storage + * @author Edge & Node * @notice This contract holds all the storage variables for the base Staking contract, version 4. * @dev Note that it includes a storage gap - if adding future versions, make sure to move the gap * to the new version and reduce the size of the gap accordingly. */ contract StakingV4Storage is StakingV3Storage { - // Additional rebate parameters for exponential rebates + /// @dev Numerator for the lambda parameter in exponential rebate calculations uint32 internal __lambdaNumerator; + /// @dev Denominator for the lambda parameter in exponential rebate calculations uint32 internal __lambdaDenominator; /// @dev Gap to allow adding variables in future upgrades (since L1Staking and L2Staking can have their own storage as well) diff --git a/packages/contracts/contracts/staking/libs/Exponential.sol b/packages/contracts/contracts/staking/libs/Exponential.sol index c9370342e..2b0222daa 100644 --- a/packages/contracts/contracts/staking/libs/Exponential.sol +++ b/packages/contracts/contracts/staking/libs/Exponential.sol @@ -6,13 +6,14 @@ import { LibFixedMath } from "./LibFixedMath.sol"; /** * @title LibExponential library + * @author Edge & Node * @notice A library to compute query fee rebates using an exponential formula */ library LibExponential { /// @dev Maximum value of the exponent for which to compute the exponential before clamping to zero. uint32 private constant MAX_EXPONENT = 15; - /// @dev The exponential formula used to compute fee-based rewards for + /// @notice The exponential formula used to compute fee-based rewards for /// staking pools in a given epoch. This function does not perform /// bounds checking on the inputs, but the following conditions /// need to be true: diff --git a/packages/contracts/contracts/staking/libs/IStakes.sol b/packages/contracts/contracts/staking/libs/IStakes.sol index 701336409..10364ebad 100644 --- a/packages/contracts/contracts/staking/libs/IStakes.sol +++ b/packages/contracts/contracts/staking/libs/IStakes.sol @@ -3,6 +3,11 @@ pragma solidity ^0.7.6 || 0.8.27; pragma abicoder v2; +/** + * @title Interface for staking data structures + * @author Edge & Node + * @notice Defines the data structures used for indexer staking + */ interface IStakes { struct Indexer { uint256 tokensStaked; // Tokens on the indexer stake (staked by the indexer) diff --git a/packages/contracts/contracts/staking/libs/LibFixedMath.sol b/packages/contracts/contracts/staking/libs/LibFixedMath.sol index ae8c9b69e..55628ea6e 100644 --- a/packages/contracts/contracts/staking/libs/LibFixedMath.sol +++ b/packages/contracts/contracts/staking/libs/LibFixedMath.sol @@ -20,36 +20,49 @@ pragma solidity ^0.7.6; -// solhint-disable indent -/// @dev Signed, fixed-point, 127-bit precision math library. +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable function-max-lines, gas-strict-inequalities + +/** + * @title LibFixedMath + * @author Edge & Node + * @notice Signed, fixed-point, 127-bit precision math library + */ library LibFixedMath { - // 1 + /// @dev Fixed-point representation of 1 int256 private constant FIXED_1 = int256(0x0000000000000000000000000000000080000000000000000000000000000000); - // 2**255 + /// @dev Minimum fixed-point value (2**255) int256 private constant MIN_FIXED_VAL = int256(0x8000000000000000000000000000000000000000000000000000000000000000); - // 1^2 (in fixed-point) + /// @dev Fixed-point representation of 1^2 int256 private constant FIXED_1_SQUARED = int256(0x4000000000000000000000000000000000000000000000000000000000000000); - // 1 + /// @dev Maximum value for natural logarithm calculation int256 private constant LN_MAX_VAL = FIXED_1; - // e ^ -63.875 + /// @dev Minimum value for natural logarithm calculation (e ^ -63.875) int256 private constant LN_MIN_VAL = int256(0x0000000000000000000000000000000000000000000000000000000733048c5a); - // 0 + /// @dev Maximum value for exponentiation calculation int256 private constant EXP_MAX_VAL = 0; - // -63.875 + /// @dev Minimum value for exponentiation calculation (-63.875) int256 private constant EXP_MIN_VAL = -int256(0x0000000000000000000000000000001ff0000000000000000000000000000000); - /// @dev Get one as a fixed-point number. + /// @notice Get one as a fixed-point number. + /// @return f The fixed-point representation of 1 function one() internal pure returns (int256 f) { f = FIXED_1; } - /// @dev Returns the addition of two fixed point numbers, reverting on overflow. + /// @notice Returns the addition of two fixed point numbers, reverting on overflow. + /// @param a First fixed-point number + /// @param b Second fixed-point number + /// @return c The sum of a and b function add(int256 a, int256 b) internal pure returns (int256 c) { c = _add(a, b); } - /// @dev Returns the addition of two fixed point numbers, reverting on overflow. + /// @notice Returns the subtraction of two fixed point numbers, reverting on overflow. + /// @param a First fixed-point number + /// @param b Second fixed-point number + /// @return c The difference a - b function sub(int256 a, int256 b) internal pure returns (int256 c) { if (b == MIN_FIXED_VAL) { revert("out-of-bounds"); @@ -57,24 +70,37 @@ library LibFixedMath { c = _add(a, -b); } - /// @dev Returns the multiplication of two fixed point numbers, reverting on overflow. + /// @notice Returns the multiplication of two fixed point numbers, reverting on overflow. + /// @param a First fixed-point number + /// @param b Second fixed-point number + /// @return c The product of a and b function mul(int256 a, int256 b) internal pure returns (int256 c) { c = _mul(a, b) / FIXED_1; } - /// @dev Returns the division of two fixed point numbers. + /// @notice Returns the division of two fixed point numbers. + /// @param a Dividend fixed-point number + /// @param b Divisor fixed-point number + /// @return c The quotient a / b function div(int256 a, int256 b) internal pure returns (int256 c) { c = _div(_mul(a, FIXED_1), b); } - /// @dev Performs (a * n) / d, without scaling for precision. + /// @notice Performs (a * n) / d, without scaling for precision. + /// @param a First operand + /// @param n Numerator + /// @param d Denominator + /// @return c The result of (a * n) / d function mulDiv(int256 a, int256 n, int256 d) internal pure returns (int256 c) { c = _div(_mul(a, n), d); } - /// @dev Returns the unsigned integer result of multiplying a fixed-point + /// @notice Returns the unsigned integer result of multiplying a fixed-point /// number with an integer, reverting if the multiplication overflows. /// Negative results are clamped to zero. + /// @param f Fixed-point number + /// @param u Unsigned integer + /// @return The result of f * u as an unsigned integer function uintMul(int256 f, uint256 u) internal pure returns (uint256) { if (int256(u) < int256(0)) { revert("out-of-bounds"); @@ -86,7 +112,9 @@ library LibFixedMath { return uint256(uint256(c) >> 127); } - /// @dev Returns the absolute value of a fixed point number. + /// @notice Returns the absolute value of a fixed point number. + /// @param f Fixed-point number + /// @return c The absolute value of f function abs(int256 f) internal pure returns (int256 c) { if (f == MIN_FIXED_VAL) { revert("out-of-bounds"); @@ -98,23 +126,32 @@ library LibFixedMath { } } - /// @dev Returns 1 / `x`, where `x` is a fixed-point number. + /// @notice Returns 1 / `x`, where `x` is a fixed-point number. + /// @param f Fixed-point number to invert + /// @return c The reciprocal of f function invert(int256 f) internal pure returns (int256 c) { c = _div(FIXED_1_SQUARED, f); } - /// @dev Convert signed `n` / 1 to a fixed-point number. + /// @notice Convert signed `n` / 1 to a fixed-point number. + /// @param n Signed integer to convert + /// @return f The fixed-point representation of n function toFixed(int256 n) internal pure returns (int256 f) { f = _mul(n, FIXED_1); } - /// @dev Convert signed `n` / `d` to a fixed-point number. + /// @notice Convert signed `n` / `d` to a fixed-point number. + /// @param n Numerator + /// @param d Denominator + /// @return f The fixed-point representation of n/d function toFixed(int256 n, int256 d) internal pure returns (int256 f) { f = _div(_mul(n, FIXED_1), d); } - /// @dev Convert unsigned `n` / 1 to a fixed-point number. + /// @notice Convert unsigned `n` / 1 to a fixed-point number. /// Reverts if `n` is too large to fit in a fixed-point number. + /// @param n Unsigned integer to convert + /// @return f The fixed-point representation of n function toFixed(uint256 n) internal pure returns (int256 f) { if (int256(n) < int256(0)) { revert("out-of-bounds"); @@ -122,8 +159,11 @@ library LibFixedMath { f = _mul(int256(n), FIXED_1); } - /// @dev Convert unsigned `n` / `d` to a fixed-point number. + /// @notice Convert unsigned `n` / `d` to a fixed-point number. /// Reverts if `n` / `d` is too large to fit in a fixed-point number. + /// @param n Numerator + /// @param d Denominator + /// @return f The fixed-point representation of n/d function toFixed(uint256 n, uint256 d) internal pure returns (int256 f) { if (int256(n) < int256(0)) { revert("out-of-bounds"); @@ -134,12 +174,16 @@ library LibFixedMath { f = _div(_mul(int256(n), FIXED_1), int256(d)); } - /// @dev Convert a fixed-point number to an integer. + /// @notice Convert a fixed-point number to an integer. + /// @param f Fixed-point number to convert + /// @return n The integer representation of f function toInteger(int256 f) internal pure returns (int256 n) { return f / FIXED_1; } - /// @dev Get the natural logarithm of a fixed-point number 0 < `x` <= LN_MAX_VAL + /// @notice Get the natural logarithm of a fixed-point number 0 < `x` <= LN_MAX_VAL + /// @param x Fixed-point number to compute logarithm of + /// @return r The natural logarithm of x function ln(int256 x) internal pure returns (int256 r) { if (x > LN_MAX_VAL) { revert("out-of-bounds"); @@ -228,7 +272,9 @@ library LibFixedMath { r += (z * (0x088888888888888888888888888888888 - y)) / 0x800000000000000000000000000000000; // add y^15 / 15 - y^16 / 16 } - /// @dev Compute the natural exponent for a fixed-point number EXP_MIN_VAL <= `x` <= 1 + /// @notice Compute the natural exponent for a fixed-point number EXP_MIN_VAL <= `x` <= 1 + /// @param x Fixed-point number to compute exponent of + /// @return r The natural exponent of x function exp(int256 x) internal pure returns (int256 r) { if (x < EXP_MIN_VAL) { // Saturate to zero below EXP_MIN_VAL. @@ -350,7 +396,10 @@ library LibFixedMath { } } - /// @dev Returns the multiplication two numbers, reverting on overflow. + /// @notice Returns the multiplication two numbers, reverting on overflow. + /// @param a First operand + /// @param b Second operand + /// @return c The product of a and b function _mul(int256 a, int256 b) private pure returns (int256 c) { if (a == 0 || b == 0) { return 0; @@ -361,7 +410,10 @@ library LibFixedMath { } } - /// @dev Returns the division of two numbers, reverting on division by zero. + /// @notice Returns the division of two numbers, reverting on division by zero. + /// @param a Dividend + /// @param b Divisor + /// @return c The quotient of a and b function _div(int256 a, int256 b) private pure returns (int256 c) { if (b == 0) { revert("overflow"); @@ -372,7 +424,10 @@ library LibFixedMath { c = a / b; } - /// @dev Adds two numbers, reverting on overflow. + /// @notice Adds two numbers, reverting on overflow. + /// @param a First operand + /// @param b Second operand + /// @return c The sum of a and b function _add(int256 a, int256 b) private pure returns (int256 c) { c = a + b; if ((a < 0 && b < 0 && c > a) || (a > 0 && b > 0 && c < a)) { diff --git a/packages/contracts/contracts/staking/libs/MathUtils.sol b/packages/contracts/contracts/staking/libs/MathUtils.sol index 0fb20389a..467e1ae2a 100644 --- a/packages/contracts/contracts/staking/libs/MathUtils.sol +++ b/packages/contracts/contracts/staking/libs/MathUtils.sol @@ -2,17 +2,21 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts/math/SafeMath.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; /** * @title MathUtils Library + * @author Edge & Node * @notice A collection of functions to perform math operations */ library MathUtils { using SafeMath for uint256; /** - * @dev Calculates the weighted average of two values pondering each of these + * @notice Calculates the weighted average of two values pondering each of these * values based on configured weights. The contribution of each value N is * weightN/(weightA + weightB). The calculation rounds up to ensure the result * is always greater than the smallest of the two values. @@ -20,6 +24,7 @@ library MathUtils { * @param weightA The weight to use for value A * @param valueB The amount for value B * @param weightB The weight to use for value B + * @return The weighted average of the two values, rounded up */ function weightedAverageRoundingUp( uint256 valueA, @@ -31,14 +36,20 @@ library MathUtils { } /** - * @dev Returns the minimum of two numbers. + * @notice Returns the minimum of two numbers. + * @param x First number + * @param y Second number + * @return The smaller of the two numbers */ function min(uint256 x, uint256 y) internal pure returns (uint256) { return x <= y ? x : y; } /** - * @dev Returns the difference between two numbers or zero if negative. + * @notice Returns the difference between two numbers or zero if negative. + * @param x First number + * @param y Second number + * @return The difference x - y, or 0 if y > x */ function diffOrZero(uint256 x, uint256 y) internal pure returns (uint256) { return (x > y) ? x.sub(y) : 0; diff --git a/packages/contracts/contracts/staking/libs/Stakes.sol b/packages/contracts/contracts/staking/libs/Stakes.sol index b09101032..175e0bc21 100644 --- a/packages/contracts/contracts/staking/libs/Stakes.sol +++ b/packages/contracts/contracts/staking/libs/Stakes.sol @@ -3,13 +3,15 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/math/SafeMath.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import "./MathUtils.sol"; -import "./IStakes.sol"; +import { MathUtils } from "./MathUtils.sol"; +import { IStakes } from "./IStakes.sol"; /** * @title A collection of data structures and functions to manage the Indexer Stake state. + * @author Edge & Node + * @notice A collection of data structures and functions to manage the Indexer Stake state. * Used for low-level state changes, require() conditions should be evaluated * at the caller function scope. */ @@ -18,7 +20,7 @@ library Stakes { using Stakes for IStakes.Indexer; /** - * @dev Deposit tokens to the indexer stake. + * @notice Deposit tokens to the indexer stake. * @param stake Stake data * @param _tokens Amount of tokens to deposit */ @@ -27,7 +29,7 @@ library Stakes { } /** - * @dev Release tokens from the indexer stake. + * @notice Release tokens from the indexer stake. * @param stake Stake data * @param _tokens Amount of tokens to release */ @@ -36,7 +38,7 @@ library Stakes { } /** - * @dev Allocate tokens from the main stack to a SubgraphDeployment. + * @notice Allocate tokens from the main stack to a SubgraphDeployment. * @param stake Stake data * @param _tokens Amount of tokens to allocate */ @@ -45,7 +47,7 @@ library Stakes { } /** - * @dev Unallocate tokens from a SubgraphDeployment back to the main stack. + * @notice Unallocate tokens from a SubgraphDeployment back to the main stack. * @param stake Stake data * @param _tokens Amount of tokens to unallocate */ @@ -54,7 +56,7 @@ library Stakes { } /** - * @dev Lock tokens until a thawing period pass. + * @notice Lock tokens until a thawing period pass. * @param stake Stake data * @param _tokens Amount of tokens to unstake * @param _period Period in blocks that need to pass before withdrawal @@ -77,7 +79,7 @@ library Stakes { } /** - * @dev Unlock tokens. + * @notice Unlock tokens. * @param stake Stake data * @param _tokens Amount of tokens to unlock */ @@ -89,7 +91,7 @@ library Stakes { } /** - * @dev Take all tokens out from the locked stake for withdrawal. + * @notice Take all tokens out from the locked stake for withdrawal. * @param stake Stake data * @return Amount of tokens being withdrawn */ @@ -109,7 +111,7 @@ library Stakes { } /** - * @dev Return the amount of tokens used in allocations and locked for withdrawal. + * @notice Return the amount of tokens used in allocations and locked for withdrawal. * @param stake Stake data * @return Token amount */ @@ -118,7 +120,7 @@ library Stakes { } /** - * @dev Return the amount of tokens staked not considering the ones that are already going + * @notice Return the amount of tokens staked not considering the ones that are already going * through the thawing period or are ready for withdrawal. We call it secure stake because * it is not subject to change by a withdraw call from the indexer. * @param stake Stake data @@ -129,7 +131,7 @@ library Stakes { } /** - * @dev Tokens free balance on the indexer stake that can be used for any purpose. + * @notice Tokens free balance on the indexer stake that can be used for any purpose. * Any token that is allocated cannot be used as well as tokens that are going through the * thawing period or are withdrawable * Calc: tokensStaked - tokensAllocated - tokensLocked @@ -141,7 +143,7 @@ library Stakes { } /** - * @dev Tokens free balance on the indexer stake that can be used for allocations. + * @notice Tokens free balance on the indexer stake that can be used for allocations. * This function accepts a parameter for extra delegated capacity that takes into * account delegated tokens * @param stake Stake data @@ -171,7 +173,7 @@ library Stakes { } /** - * @dev Tokens available for withdrawal after thawing period. + * @notice Tokens available for withdrawal after thawing period. * @param stake Stake data * @return Token amount */ diff --git a/packages/contracts/contracts/tests/CallhookReceiverMock.sol b/packages/contracts/contracts/tests/CallhookReceiverMock.sol index e2418f3c8..b87d57cf0 100644 --- a/packages/contracts/contracts/tests/CallhookReceiverMock.sol +++ b/packages/contracts/contracts/tests/CallhookReceiverMock.sol @@ -2,21 +2,29 @@ pragma solidity ^0.7.6; -import "../gateway/ICallhookReceiver.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events, use-natspec + +import { ICallhookReceiver } from "../gateway/ICallhookReceiver.sol"; /** - * @title GovernedMock contract + * @title CallhookReceiverMock contract + * @dev Mock contract for testing callhook receiver functionality */ contract CallhookReceiverMock is ICallhookReceiver { + /** + * @dev Emitted when a transfer is received + * @param from Address that sent the transfer + * @param amount Amount of tokens transferred + * @param foo First test parameter + * @param bar Second test parameter + */ event TransferReceived(address from, uint256 amount, uint256 foo, uint256 bar); /** - * @dev Receive tokens with a callhook from the bridge - * Expects two uint256 values encoded in _data. + * @inheritdoc ICallhookReceiver + * @dev Expects two uint256 values encoded in _data. * Reverts if the first of these values is zero. - * @param _from Token sender in L1 - * @param _amount Amount of tokens that were transferred - * @param _data ABI-encoded callhook data */ function onTokenTransfer(address _from, uint256 _amount, bytes calldata _data) external override { uint256 foo; diff --git a/packages/contracts/contracts/tests/GovernedMock.sol b/packages/contracts/contracts/tests/GovernedMock.sol index cc908287b..9e6c2cc18 100644 --- a/packages/contracts/contracts/tests/GovernedMock.sol +++ b/packages/contracts/contracts/tests/GovernedMock.sol @@ -2,12 +2,19 @@ pragma solidity ^0.7.6; -import "../governance/Governed.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + +import { Governed } from "../governance/Governed.sol"; /** * @title GovernedMock contract + * @dev Mock contract for testing Governed functionality */ contract GovernedMock is Governed { + /** + * @dev Constructor that initializes the contract with the deployer as governor + */ constructor() { Governed._initialize(msg.sender); } diff --git a/packages/contracts/contracts/tests/L1GraphTokenLockTransferToolBadMock.sol b/packages/contracts/contracts/tests/L1GraphTokenLockTransferToolBadMock.sol index f540b1b96..12b06b332 100644 --- a/packages/contracts/contracts/tests/L1GraphTokenLockTransferToolBadMock.sol +++ b/packages/contracts/contracts/tests/L1GraphTokenLockTransferToolBadMock.sol @@ -3,17 +3,37 @@ pragma solidity ^0.7.6; pragma experimental ABIEncoderV2; +// solhint-disable named-parameters-mapping +// solhint-disable gas-small-strings + +/** + * @title L1GraphTokenLockTransferToolBadMock + * @author Edge & Node + * @notice Mock contract for testing L1 Graph Token Lock Transfer Tool with bad behavior + */ contract L1GraphTokenLockTransferToolBadMock { + /** + * @notice Mapping from L1 wallet address to L2 wallet address + */ mapping(address => address) public l2WalletAddress; - function setL2WalletAddress(address _l1Address, address _l2Address) external { - l2WalletAddress[_l1Address] = _l2Address; + /** + * @notice Set the L2 wallet address for an L1 wallet + * @param l1Address L1 wallet address + * @param l2Address L2 wallet address + */ + function setL2WalletAddress(address l1Address, address l2Address) external { + l2WalletAddress[l1Address] = l2Address; } - // Sends 1 wei less than requested - function pullETH(address _l1Wallet, uint256 _amount) external { - require(l2WalletAddress[_l1Wallet] != address(0), "L1GraphTokenLockTransferToolMock: unknown L1 wallet"); - (bool success, ) = payable(msg.sender).call{ value: _amount - 1 }(""); + /** + * @notice Pull ETH from the contract to the caller (sends 1 wei less than requested for testing) + * @param l1Wallet L1 wallet address to check + * @param amount Amount of ETH to pull + */ + function pullETH(address l1Wallet, uint256 amount) external { + require(l2WalletAddress[l1Wallet] != address(0), "L1GraphTokenLockTransferToolMock: unknown L1 wallet"); + (bool success, ) = payable(msg.sender).call{ value: amount - 1 }(""); require(success, "L1GraphTokenLockTransferToolMock: ETH pull failed"); } } diff --git a/packages/contracts/contracts/tests/L1GraphTokenLockTransferToolMock.sol b/packages/contracts/contracts/tests/L1GraphTokenLockTransferToolMock.sol index a1321d62f..92e8f73f7 100644 --- a/packages/contracts/contracts/tests/L1GraphTokenLockTransferToolMock.sol +++ b/packages/contracts/contracts/tests/L1GraphTokenLockTransferToolMock.sol @@ -3,16 +3,37 @@ pragma solidity ^0.7.6; pragma experimental ABIEncoderV2; +// solhint-disable named-parameters-mapping +// solhint-disable gas-small-strings + +/** + * @title L1GraphTokenLockTransferToolMock + * @author Edge & Node + * @notice Mock contract for testing L1 Graph Token Lock Transfer Tool functionality + */ contract L1GraphTokenLockTransferToolMock { + /** + * @notice Mapping from L1 wallet address to L2 wallet address + */ mapping(address => address) public l2WalletAddress; - function setL2WalletAddress(address _l1Address, address _l2Address) external { - l2WalletAddress[_l1Address] = _l2Address; + /** + * @notice Set the L2 wallet address for an L1 wallet + * @param l1Address L1 wallet address + * @param l2Address L2 wallet address + */ + function setL2WalletAddress(address l1Address, address l2Address) external { + l2WalletAddress[l1Address] = l2Address; } - function pullETH(address _l1Wallet, uint256 _amount) external { - require(l2WalletAddress[_l1Wallet] != address(0), "L1GraphTokenLockTransferToolMock: unknown L1 wallet"); - (bool success, ) = payable(msg.sender).call{ value: _amount }(""); + /** + * @notice Pull ETH from the contract to the caller + * @param l1Wallet L1 wallet address to check + * @param amount Amount of ETH to pull + */ + function pullETH(address l1Wallet, uint256 amount) external { + require(l2WalletAddress[l1Wallet] != address(0), "L1GraphTokenLockTransferToolMock: unknown L1 wallet"); + (bool success, ) = payable(msg.sender).call{ value: amount }(""); require(success, "L1GraphTokenLockTransferToolMock: ETH pull failed"); } } diff --git a/packages/contracts/contracts/tests/LegacyGNSMock.sol b/packages/contracts/contracts/tests/LegacyGNSMock.sol index b2b4088b9..30e619e6e 100644 --- a/packages/contracts/contracts/tests/LegacyGNSMock.sol +++ b/packages/contracts/contracts/tests/LegacyGNSMock.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + import { L1GNS } from "../discovery/L1GNS.sol"; import { IGNS } from "../discovery/IGNS.sol"; diff --git a/packages/contracts/contracts/tests/arbitrum/ArbSysMock.sol b/packages/contracts/contracts/tests/arbitrum/ArbSysMock.sol index b5af6114e..3c256fa74 100644 --- a/packages/contracts/contracts/tests/arbitrum/ArbSysMock.sol +++ b/packages/contracts/contracts/tests/arbitrum/ArbSysMock.sol @@ -2,12 +2,22 @@ pragma solidity ^0.7.6; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + /** * @title ArbSys Mock Contract * @dev This is a mock implementation of the ArbSys precompiled contract used in Arbitrum * It's used for testing the L2GraphTokenGateway contract */ contract ArbSysMock { + /** + * @dev Emitted when a transaction is sent from L2 to L1 + * @param from Address sending the transaction on L2 + * @param to Address receiving the transaction on L1 + * @param id Unique identifier for the L2-to-L1 transaction + * @param data Transaction data + */ event L2ToL1Tx(address indexed from, address indexed to, uint256 indexed id, bytes data); /** diff --git a/packages/contracts/contracts/tests/arbitrum/BridgeMock.sol b/packages/contracts/contracts/tests/arbitrum/BridgeMock.sol index 77be89b4e..141cf2dda 100644 --- a/packages/contracts/contracts/tests/arbitrum/BridgeMock.sol +++ b/packages/contracts/contracts/tests/arbitrum/BridgeMock.sol @@ -2,29 +2,35 @@ pragma solidity ^0.7.6; -import "../../arbitrum/IBridge.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one, use-natspec + +import { IBridge } from "../../arbitrum/IBridge.sol"; /** * @title Arbitrum Bridge mock contract * @dev This contract implements Arbitrum's IBridge interface for testing purposes */ contract BridgeMock is IBridge { - // Address of the (mock) Arbitrum Inbox + /** + * @notice Address of the (mock) Arbitrum Inbox + */ address public inbox; - // Address of the (mock) Arbitrum Outbox + /** + * @notice Address of the (mock) Arbitrum Outbox + */ address public outbox; - // Index of the next message on the inbox messages array + /** + * @notice Index of the next message on the inbox messages array + */ uint256 public messageIndex; - // Inbox messages array + /** + * @inheritdoc IBridge + */ bytes32[] public override inboxAccs; /** - * @dev Deliver a message to the inbox. The encoded message will be - * added to the inbox array, and messageIndex will be incremented. - * @param _kind Type of the message - * @param _sender Address that is sending the message - * @param _messageDataHash keccak256 hash of the message data - * @return The next index for the inbox array + * @inheritdoc IBridge */ function deliverMessageToInbox( uint8 _kind, @@ -38,13 +44,7 @@ contract BridgeMock is IBridge { } /** - * @dev Executes an L1 function call incoing from L2. This can only be called - * by the Outbox. - * @param _destAddr Contract to call - * @param _amount ETH value to send - * @param _data Calldata for the function call - * @return True if the call was successful, false otherwise - * @return Return data from the call + * @inheritdoc IBridge */ function executeCall( address _destAddr, @@ -62,9 +62,7 @@ contract BridgeMock is IBridge { } /** - * @dev Set the address of the inbox. Anyone can call this, because it's a mock. - * @param _inbox Address of the inbox - * @param _enabled Enable the inbox (ignored) + * @inheritdoc IBridge */ function setInbox(address _inbox, bool _enabled) external override { inbox = _inbox; @@ -72,9 +70,7 @@ contract BridgeMock is IBridge { } /** - * @dev Set the address of the outbox. Anyone can call this, because it's a mock. - * @param _outbox Address of the outbox - * @param _enabled Enable the outbox (ignored) + * @inheritdoc IBridge */ function setOutbox(address _outbox, bool _enabled) external override { outbox = _outbox; @@ -84,33 +80,28 @@ contract BridgeMock is IBridge { // View functions /** - * @dev Getter for the active outbox (in this case there's only one) + * @inheritdoc IBridge */ function activeOutbox() external view override returns (address) { return outbox; } /** - * @dev Getter for whether an address is an allowed inbox (in this case there's only one) - * @param _inbox Address to check - * @return True if the address is the allowed inbox, false otherwise + * @inheritdoc IBridge */ function allowedInboxes(address _inbox) external view override returns (bool) { return _inbox == inbox; } /** - * @dev Getter for whether an address is an allowed outbox (in this case there's only one) - * @param _outbox Address to check - * @return True if the address is the allowed outbox, false otherwise + * @inheritdoc IBridge */ function allowedOutboxes(address _outbox) external view override returns (bool) { return _outbox == outbox; } /** - * @dev Getter for the count of messages in the inboxAccs - * @return Number of messages in inboxAccs + * @inheritdoc IBridge */ function messageCount() external view override returns (uint256) { return inboxAccs.length; diff --git a/packages/contracts/contracts/tests/arbitrum/InboxMock.sol b/packages/contracts/contracts/tests/arbitrum/InboxMock.sol index 57af6941c..c920ea314 100644 --- a/packages/contracts/contracts/tests/arbitrum/InboxMock.sol +++ b/packages/contracts/contracts/tests/arbitrum/InboxMock.sol @@ -2,26 +2,28 @@ pragma solidity ^0.7.6; -import "../../arbitrum/IInbox.sol"; -import "../../arbitrum/AddressAliasHelper.sol"; +import { IInbox } from "../../arbitrum/IInbox.sol"; +import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; +import { IBridge } from "../../arbitrum/IBridge.sol"; /** * @title Arbitrum Inbox mock contract - * @dev This contract implements (a subset of) Arbitrum's IInbox interface for testing purposes + * @author Edge & Node + * @notice This contract implements (a subset of) Arbitrum's IInbox interface for testing purposes */ contract InboxMock is IInbox { - // Type indicator for a standard L2 message + /// @dev Type indicator for a standard L2 message uint8 internal constant L2_MSG = 3; - // Type indicator for a retryable ticket message + /// @dev Type indicator for a retryable ticket message // solhint-disable-next-line const-name-snakecase uint8 internal constant L1MessageType_submitRetryableTx = 9; - // Address of the Bridge (mock) contract + /** + * @inheritdoc IInbox + */ IBridge public override bridge; /** - * @dev Send a message to L2 (by delivering it to the Bridge) - * @param _messageData Encoded data to send in the message - * @return message number returned by the inbox + * @inheritdoc IInbox */ function sendL2Message(bytes calldata _messageData) external override returns (uint256) { uint256 msgNum = deliverToBridge(L2_MSG, msg.sender, keccak256(_messageData)); @@ -30,7 +32,7 @@ contract InboxMock is IInbox { } /** - * @dev Set the address of the (mock) bridge + * @notice Set the address of the (mock) bridge * @param _bridge Address of the bridge */ function setBridge(address _bridge) external { @@ -38,6 +40,7 @@ contract InboxMock is IInbox { } /** + * @inheritdoc IInbox * @dev Unimplemented in this mock */ function sendUnsignedTransaction( @@ -52,6 +55,7 @@ contract InboxMock is IInbox { } /** + * @inheritdoc IInbox * @dev Unimplemented in this mock */ function sendContractTransaction( @@ -65,6 +69,7 @@ contract InboxMock is IInbox { } /** + * @inheritdoc IInbox * @dev Unimplemented in this mock */ function sendL1FundedUnsignedTransaction( @@ -78,6 +83,7 @@ contract InboxMock is IInbox { } /** + * @inheritdoc IInbox * @dev Unimplemented in this mock */ function sendL1FundedContractTransaction( @@ -90,16 +96,7 @@ contract InboxMock is IInbox { } /** - * @dev Creates a retryable ticket for an L2 transaction - * @param _destAddr Address of the contract to call in L2 - * @param _arbTxCallValue Callvalue to use in the L2 transaction - * @param _maxSubmissionCost Max cost of submitting the ticket, in Wei - * @param _submissionRefundAddress L2 address to refund for any remaining value from the submission cost - * @param _valueRefundAddress L2 address to refund if the ticket times out or gets cancelled - * @param _maxGas Max gas for the L2 transcation - * @param _gasPriceBid Gas price bid on L2 - * @param _data Encoded calldata for the L2 transaction (including function selector) - * @return message number returned by the bridge + * @inheritdoc IInbox */ function createRetryableTicket( address _destAddr, @@ -132,11 +129,16 @@ contract InboxMock is IInbox { ); } + /** + * @inheritdoc IInbox + * @dev Unimplemented in this mock + */ function depositEth(uint256) external payable override returns (uint256) { revert("Unimplemented"); } /** + * @inheritdoc IInbox * @dev Unimplemented in this mock */ function pauseCreateRetryables() external pure override { @@ -144,6 +146,7 @@ contract InboxMock is IInbox { } /** + * @inheritdoc IInbox * @dev Unimplemented in this mock */ function unpauseCreateRetryables() external pure override { @@ -151,6 +154,7 @@ contract InboxMock is IInbox { } /** + * @inheritdoc IInbox * @dev Unimplemented in this mock */ function startRewriteAddress() external pure override { @@ -158,6 +162,7 @@ contract InboxMock is IInbox { } /** + * @inheritdoc IInbox * @dev Unimplemented in this mock */ function stopRewriteAddress() external pure override { @@ -165,7 +170,7 @@ contract InboxMock is IInbox { } /** - * @dev Deliver a message to the bridge + * @notice Deliver a message to the bridge * @param _kind Type of the message * @param _sender Address that is sending the message * @param _messageData Encoded message data @@ -178,7 +183,7 @@ contract InboxMock is IInbox { } /** - * @dev Deliver a message to the bridge + * @notice Deliver a message to the bridge * @param _kind Type of the message * @param _sender Address that is sending the message * @param _messageDataHash keccak256 hash of the encoded message data diff --git a/packages/contracts/contracts/tests/arbitrum/OutboxMock.sol b/packages/contracts/contracts/tests/arbitrum/OutboxMock.sol index 92b9bb246..4191e9e0a 100644 --- a/packages/contracts/contracts/tests/arbitrum/OutboxMock.sol +++ b/packages/contracts/contracts/tests/arbitrum/OutboxMock.sol @@ -2,15 +2,26 @@ pragma solidity ^0.7.6; -import "../../arbitrum/IOutbox.sol"; -import "../../arbitrum/IBridge.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + +import { IOutbox } from "../../arbitrum/IOutbox.sol"; +import { IBridge } from "../../arbitrum/IBridge.sol"; /** * @title Arbitrum Outbox mock contract * @dev This contract implements (a subset of) Arbitrum's IOutbox interface for testing purposes */ contract OutboxMock is IOutbox { - // Context of an L2-to-L1 function call + /** + * @dev Context of an L2-to-L1 function call + * @param l2Block L2 block number + * @param l1Block L1 block number + * @param timestamp Timestamp of the call + * @param batchNum Batch number + * @param outputId Output ID + * @param sender Address of the sender + */ struct L2ToL1Context { uint128 l2Block; uint128 l1Block; @@ -19,7 +30,7 @@ contract OutboxMock is IOutbox { bytes32 outputId; address sender; } - // Context of the current L2-to-L1 function call (set and cleared in each transaction) + /// @dev Context of the current L2-to-L1 function call (set and cleared in each transaction) L2ToL1Context internal context; // Address of the (mock) Arbitrum Bridge @@ -33,59 +44,42 @@ contract OutboxMock is IOutbox { bridge = IBridge(_bridge); } - /** - * @dev Getter for the L2 sender of the current incoming message - */ + /// @inheritdoc IOutbox function l2ToL1Sender() external view override returns (address) { return context.sender; } - /** - * @dev Getter for the L2 block of the current incoming message - */ + /// @inheritdoc IOutbox function l2ToL1Block() external view override returns (uint256) { return context.l2Block; } - /** - * @dev Getter for the L1 block of the current incoming message - */ + /// @inheritdoc IOutbox function l2ToL1EthBlock() external view override returns (uint256) { return context.l1Block; } - /** - * @dev Getter for the L1 timestamp of the current incoming message - */ + /// @inheritdoc IOutbox function l2ToL1Timestamp() external view override returns (uint256) { return context.timestamp; } - /** - * @dev Getter for the L2 batch number of the current incoming message - */ + /// @inheritdoc IOutbox function l2ToL1BatchNum() external view override returns (uint256) { return context.batchNum; } - /** - * @dev Getter for the output ID of the current incoming message - */ + /// @inheritdoc IOutbox function l2ToL1OutputId() external view override returns (bytes32) { return context.outputId; } - /** - * @dev Unimplemented in this mock - */ + /// @inheritdoc IOutbox function processOutgoingMessages(bytes calldata, uint256[] calldata) external pure override { revert("Unimplemented"); } - /** - * @dev Check whether an outbox entry for a message exists. - * This mock returns always true. - */ + /// @inheritdoc IOutbox function outboxEntryExists(uint256) external pure override returns (bool) { return true; } diff --git a/packages/contracts/contracts/tests/ens/IENS.sol b/packages/contracts/contracts/tests/ens/IENS.sol index f03cb651c..042a9170f 100644 --- a/packages/contracts/contracts/tests/ens/IENS.sol +++ b/packages/contracts/contracts/tests/ens/IENS.sol @@ -1,9 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + pragma solidity ^0.7.6; // Needed for abi and typechain in the npm package +/** + * @title ENS Registry Interface + * @author Edge & Node + * @notice Interface for the Ethereum Name Service registry + */ interface IENS { + /** + * @notice Get the owner of a node + * @param node The node to query + * @return The address of the owner + */ function owner(bytes32 node) external view returns (address); - // Must call setRecord, not setOwner, We must namehash it ourselves as well - function setSubnodeRecord(bytes32 node, bytes32 label, address owner, address resolver, uint64 ttl) external; + /** + * @notice Set the record for a subnode + * @dev Must call setRecord, not setOwner. We must namehash it ourselves as well. + * @param node The parent node + * @param label The label hash of the subnode + * @param _owner The address of the new owner + * @param resolver The address of the resolver + * @param ttl The TTL in seconds + */ + function setSubnodeRecord(bytes32 node, bytes32 label, address _owner, address resolver, uint64 ttl) external; } diff --git a/packages/contracts/contracts/tests/ens/IPublicResolver.sol b/packages/contracts/contracts/tests/ens/IPublicResolver.sol index 06ce2243b..7a449fb8d 100644 --- a/packages/contracts/contracts/tests/ens/IPublicResolver.sol +++ b/packages/contracts/contracts/tests/ens/IPublicResolver.sol @@ -1,8 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + pragma solidity ^0.7.6; // Needed for abi and typechain in the npm package +/** + * @title ENS Public Resolver Interface + * @author Edge & Node + * @notice Interface for the ENS public resolver contract + */ interface IPublicResolver { + /** + * @notice Get the text record for a node + * @param node The node to query + * @param key The key of the text record + * @return The text record value + */ function text(bytes32 node, string calldata key) external view returns (string memory); + /** + * @notice Set the text record for a node + * @param node The node to set the record for + * @param key The key of the text record + * @param value The value to set + */ function setText(bytes32 node, string calldata key, string calldata value) external; } diff --git a/packages/contracts/contracts/tests/ens/ITestRegistrar.sol b/packages/contracts/contracts/tests/ens/ITestRegistrar.sol index 8a795cc85..406a27fb7 100644 --- a/packages/contracts/contracts/tests/ens/ITestRegistrar.sol +++ b/packages/contracts/contracts/tests/ens/ITestRegistrar.sol @@ -1,5 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + pragma solidity ^0.7.6; +/** + * @title Test Registrar Interface + * @author Edge & Node + * @notice Interface for a test ENS registrar contract + */ interface ITestRegistrar { + /** + * @notice Register a name with the registrar + * @param label The label hash to register + * @param owner The address to assign ownership to + */ function register(bytes32 label, address owner) external; } diff --git a/packages/contracts/contracts/token/GraphToken.sol b/packages/contracts/contracts/token/GraphToken.sol index 53496b9a5..652fa5477 100644 --- a/packages/contracts/contracts/token/GraphToken.sol +++ b/packages/contracts/contracts/token/GraphToken.sol @@ -2,16 +2,21 @@ pragma solidity ^0.7.6; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol"; -import "@openzeppelin/contracts/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/math/SafeMath.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-small-strings, gas-strict-inequalities +// solhint-disable named-parameters-mapping -import "../governance/Governed.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol"; +import { ECDSA } from "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { Governed } from "../governance/Governed.sol"; /** * @title GraphToken contract - * @dev This is the implementation of the ERC20 Graph Token. + * @author Edge & Node + * @notice This is the implementation of the ERC20 Graph Token. * The implementation exposes a Permit() function to allow for a spender to send a signed message * and approve funds to a spender following EIP2612 to make integration with other contracts easier. * @@ -28,32 +33,53 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { // -- EIP712 -- // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-domainseparator + /// @dev EIP-712 domain type hash for signature verification bytes32 private constant DOMAIN_TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)"); + /// @dev EIP-712 domain name hash for signature verification bytes32 private constant DOMAIN_NAME_HASH = keccak256("Graph Token"); + /// @dev EIP-712 domain version hash for signature verification bytes32 private constant DOMAIN_VERSION_HASH = keccak256("0"); + /// @dev EIP-712 domain salt for signature verification (randomly generated) bytes32 private constant DOMAIN_SALT = 0x51f3d585afe6dfeb2af01bba0889a36c1db03beec88c6a4d0c53817069026afa; // Randomly generated salt + /// @dev EIP-712 permit typehash for signature verification bytes32 private constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); // -- State -- - bytes32 private DOMAIN_SEPARATOR; + /// @dev EIP-712 domain separator for signature verification + bytes32 private domainSeparator; + /// @dev Mapping of addresses authorized to mint tokens mapping(address => bool) private _minters; + /** + * @notice Nonces for permit functionality (EIP-2612) + * @dev Mapping from owner address to current nonce for permit signatures + */ mapping(address => uint256) public nonces; // -- Events -- + /** + * @notice Emitted when a new minter is added + * @param account Address of the minter that was added + */ event MinterAdded(address indexed account); + + /** + * @notice Emitted when a minter is removed + * @param account Address of the minter that was removed + */ event MinterRemoved(address indexed account); + /// @dev Modifier to restrict access to minters only modifier onlyMinter() { require(isMinter(msg.sender), "Only minter can call"); _; } /** - * @dev Graph Token Contract Constructor. + * @notice Graph Token Contract Constructor. * @param _initialSupply Initial supply of GRT */ constructor(uint256 _initialSupply) ERC20("Graph Token", "GRT") { @@ -66,7 +92,7 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { _addMinter(msg.sender); // EIP-712 domain separator - DOMAIN_SEPARATOR = keccak256( + domainSeparator = keccak256( abi.encode( DOMAIN_TYPE_HASH, DOMAIN_NAME_HASH, @@ -79,7 +105,7 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { } /** - * @dev Approve token allowance by validating a message signed by the holder. + * @notice Approve token allowance by validating a message signed by the holder. * @param _owner Address of the token holder * @param _spender Address of the approved spender * @param _value Amount of tokens to approve the spender @@ -100,7 +126,7 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { bytes32 digest = keccak256( abi.encodePacked( "\x19\x01", - DOMAIN_SEPARATOR, + domainSeparator, keccak256(abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonces[_owner], _deadline)) ) ); @@ -114,7 +140,7 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { } /** - * @dev Add a new minter. + * @notice Add a new minter. * @param _account Address of the minter */ function addMinter(address _account) external onlyGovernor { @@ -122,7 +148,7 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { } /** - * @dev Remove a minter. + * @notice Remove a minter. * @param _account Address of the minter */ function removeMinter(address _account) external onlyGovernor { @@ -130,14 +156,14 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { } /** - * @dev Renounce to be a minter. + * @notice Renounce to be a minter. */ function renounceMinter() external { _removeMinter(msg.sender); } /** - * @dev Mint new tokens. + * @notice Mint new tokens. * @param _to Address to send the newly minted tokens * @param _amount Amount of tokens to mint */ @@ -146,7 +172,7 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { } /** - * @dev Return if the `_account` is a minter or not. + * @notice Return if the `_account` is a minter or not. * @param _account Address to check * @return True if the `_account` is minter */ @@ -155,7 +181,7 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { } /** - * @dev Add a new minter. + * @notice Add a new minter. * @param _account Address of the minter */ function _addMinter(address _account) private { @@ -164,7 +190,7 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { } /** - * @dev Remove a minter. + * @notice Remove a minter. * @param _account Address of the minter */ function _removeMinter(address _account) private { @@ -173,11 +199,12 @@ contract GraphToken is Governed, ERC20, ERC20Burnable { } /** - * @dev Get the running network chain ID. + * @notice Get the running network chain ID. * @return The chain ID */ function _getChainID() private pure returns (uint256) { uint256 id; + // solhint-disable-next-line no-inline-assembly assembly { id := chainid() } diff --git a/packages/contracts/contracts/token/IGraphToken.sol b/packages/contracts/contracts/token/IGraphToken.sol index df3b7643f..924183e46 100644 --- a/packages/contracts/contracts/token/IGraphToken.sol +++ b/packages/contracts/contracts/token/IGraphToken.sol @@ -1,30 +1,82 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - +pragma solidity ^0.7.6 || ^0.8.0; + +// Solhint linting fails for 0.8.0. +// solhint-disable-next-line import-path-check +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title IGraphToken + * @author Edge & Node + * @notice Interface for the Graph Token contract + * @dev Extends IERC20 with additional functionality for minting, burning, and permit + */ interface IGraphToken is IERC20 { // -- Mint and Burn -- + /** + * @notice Burns tokens from the caller's account + * @param amount The amount of tokens to burn + */ function burn(uint256 amount) external; + /** + * @notice Burns tokens from a specified account (requires allowance) + * @param _from The account to burn tokens from + * @param amount The amount of tokens to burn + */ function burnFrom(address _from, uint256 amount) external; + /** + * @notice Mints new tokens to a specified account + * @dev Only callable by accounts with minter role + * @param _to The account to mint tokens to + * @param _amount The amount of tokens to mint + */ function mint(address _to, uint256 _amount) external; // -- Mint Admin -- + /** + * @notice Adds a new minter account + * @dev Only callable by accounts with appropriate permissions + * @param _account The account to grant minter role to + */ function addMinter(address _account) external; + /** + * @notice Removes minter role from an account + * @dev Only callable by accounts with appropriate permissions + * @param _account The account to revoke minter role from + */ function removeMinter(address _account) external; + /** + * @notice Renounces minter role for the caller + * @dev Allows a minter to voluntarily give up their minting privileges + */ function renounceMinter() external; + /** + * @notice Checks if an account has minter role + * @param _account The account to check + * @return True if the account is a minter, false otherwise + */ function isMinter(address _account) external view returns (bool); // -- Permit -- + /** + * @notice Allows approval via signature (EIP-2612) + * @param _owner The token owner's address + * @param _spender The spender's address + * @param _value The allowance amount + * @param _deadline The deadline timestamp for the permit + * @param _v The recovery byte of the signature + * @param _r Half of the ECDSA signature pair + * @param _s Half of the ECDSA signature pair + */ function permit( address _owner, address _spender, @@ -37,7 +89,19 @@ interface IGraphToken is IERC20 { // -- Allowance -- + /** + * @notice Increases the allowance granted to a spender + * @param spender The account whose allowance will be increased + * @param addedValue The amount to increase the allowance by + * @return True if the operation succeeded + */ function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + /** + * @notice Decreases the allowance granted to a spender + * @param spender The account whose allowance will be decreased + * @param subtractedValue The amount to decrease the allowance by + * @return True if the operation succeeded + */ function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); } diff --git a/packages/contracts/contracts/upgrades/GraphProxy.sol b/packages/contracts/contracts/upgrades/GraphProxy.sol index d6fbfac7f..733e3c0be 100644 --- a/packages/contracts/contracts/upgrades/GraphProxy.sol +++ b/packages/contracts/contracts/upgrades/GraphProxy.sol @@ -2,13 +2,19 @@ pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-small-strings + +/* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 + import { GraphProxyStorage } from "./GraphProxyStorage.sol"; import { IGraphProxy } from "./IGraphProxy.sol"; /** * @title Graph Proxy - * @dev Graph Proxy contract used to delegate call implementation contracts and support upgrades. + * @author Edge & Node + * @notice Graph Proxy contract used to delegate call implementation contracts and support upgrades. * This contract should NOT define storage as it is managed by GraphProxyStorage. * This contract implements a proxy that is upgradeable by an admin. * https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#transparent-proxies-and-function-clashes @@ -69,56 +75,49 @@ contract GraphProxy is GraphProxyStorage, IGraphProxy { } /** - * @notice Get the current admin - * + * @inheritdoc IGraphProxy * @dev NOTE: Only the admin and implementation can call this function. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` - * - * @return The address of the current admin */ - function admin() external override ifAdminOrPendingImpl returns (address) { + function admin() external override ifAdminOrPendingImpl returns (address adminAddress) { return _getAdmin(); } /** - * @notice Get the current implementation. - * + * @inheritdoc IGraphProxy * @dev NOTE: Only the admin can call this function. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` - * - * @return The address of the current implementation for this proxy */ - function implementation() external override ifAdminOrPendingImpl returns (address) { + function implementation() external override ifAdminOrPendingImpl returns (address implementationAddress) { return _getImplementation(); } /** - * @notice Get the current pending implementation. - * + * @inheritdoc IGraphProxy * @dev NOTE: Only the admin can call this function. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x9e5eddc59e0b171f57125ab86bee043d9128098c3a6b9adb4f2e86333c2f6f8c` - * - * @return The address of the current pending implementation for this proxy */ - function pendingImplementation() external override ifAdminOrPendingImpl returns (address) { + function pendingImplementation() + external + override + ifAdminOrPendingImpl + returns (address pendingImplementationAddress) + { return _getPendingImplementation(); } /** - * @notice Changes the admin of the proxy. - * + * @inheritdoc IGraphProxy * @dev NOTE: Only the admin can call this function. - * - * @param _newAdmin Address of the new admin */ function setAdmin(address _newAdmin) external override ifAdmin { require(_newAdmin != address(0), "Admin cant be the zero address"); @@ -126,25 +125,22 @@ contract GraphProxy is GraphProxyStorage, IGraphProxy { } /** - * @notice Upgrades to a new implementation contract. + * @inheritdoc IGraphProxy * @dev NOTE: Only the admin can call this function. - * @param _newImplementation Address of implementation contract */ function upgradeTo(address _newImplementation) external override ifAdmin { _setPendingImplementation(_newImplementation); } /** - * @notice Admin function for new implementation to accept its role as implementation. + * @inheritdoc IGraphProxy */ function acceptUpgrade() external override ifAdminOrPendingImpl { _acceptUpgrade(); } /** - * @notice Admin function for new implementation to accept its role as implementation, - * calling a function on the new implementation. - * @param data Calldata (including selector) for the function to delegatecall into the implementation + * @inheritdoc IGraphProxy */ function acceptUpgradeAndCall(bytes calldata data) external override ifAdminOrPendingImpl { _acceptUpgrade(); @@ -154,7 +150,7 @@ contract GraphProxy is GraphProxyStorage, IGraphProxy { } /** - * @dev Admin function for new implementation to accept its role as implementation. + * @notice Admin function for new implementation to accept its role as implementation. */ function _acceptUpgrade() internal { address _pendingImplementation = _getPendingImplementation(); @@ -166,7 +162,7 @@ contract GraphProxy is GraphProxyStorage, IGraphProxy { } /** - * @dev Delegates the current call to implementation. + * @notice Delegates the current call to implementation. * This function does not return to its internal call site, it will return directly to the * external caller. */ diff --git a/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol b/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol index db8e9dcb3..83550a3e5 100644 --- a/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol +++ b/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol @@ -2,6 +2,8 @@ pragma solidity ^0.7.6 || 0.8.27; +/* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 + import { Governed } from "../governance/Governed.sol"; import { IGraphProxy } from "./IGraphProxy.sol"; @@ -9,7 +11,8 @@ import { GraphUpgradeable } from "./GraphUpgradeable.sol"; /** * @title GraphProxyAdmin - * @dev This is the owner of upgradeable proxy contracts. + * @author Edge & Node + * @notice This is the owner of upgradeable proxy contracts. * Proxy contracts use a TransparentProxy pattern, any admin related call * like upgrading a contract or changing the admin needs to be send through * this contract. diff --git a/packages/contracts/contracts/upgrades/GraphProxyStorage.sol b/packages/contracts/contracts/upgrades/GraphProxyStorage.sol index 7871e4996..828af8e23 100644 --- a/packages/contracts/contracts/upgrades/GraphProxyStorage.sol +++ b/packages/contracts/contracts/upgrades/GraphProxyStorage.sol @@ -2,9 +2,12 @@ pragma solidity ^0.7.6 || 0.8.27; +/* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 + /** * @title Graph Proxy Storage - * @dev Contract functions related to getting and setting proxy storage. + * @author Edge & Node + * @notice Contract functions related to getting and setting proxy storage. * This contract does not actually define state variables managed by the compiler * but uses fixed slot locations. */ @@ -32,7 +35,9 @@ abstract contract GraphProxyStorage { bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; /** - * @dev Emitted when pendingImplementation is changed. + * @notice Emitted when pendingImplementation is changed. + * @param oldPendingImplementation Address of the previous pending implementation + * @param newPendingImplementation Address of the new pending implementation */ event PendingImplementationUpdated( address indexed oldPendingImplementation, @@ -40,13 +45,17 @@ abstract contract GraphProxyStorage { ); /** - * @dev Emitted when pendingImplementation is accepted, + * @notice Emitted when pendingImplementation is accepted, * which means contract implementation is updated. + * @param oldImplementation Address of the previous implementation + * @param newImplementation Address of the new implementation */ event ImplementationUpdated(address indexed oldImplementation, address indexed newImplementation); /** - * @dev Emitted when the admin account has changed. + * @notice Emitted when the admin account has changed. + * @param oldAdmin Address of the previous admin + * @param newAdmin Address of the new admin */ event AdminUpdated(address indexed oldAdmin, address indexed newAdmin); @@ -59,6 +68,7 @@ abstract contract GraphProxyStorage { } /** + * @notice Returns the current admin address * @return adm The admin slot. */ function _getAdmin() internal view returns (address adm) { @@ -70,7 +80,7 @@ abstract contract GraphProxyStorage { } /** - * @dev Sets the address of the proxy admin. + * @notice Sets the address of the proxy admin. * @param _newAdmin Address of the new proxy admin */ function _setAdmin(address _newAdmin) internal { @@ -85,7 +95,7 @@ abstract contract GraphProxyStorage { } /** - * @dev Returns the current implementation. + * @notice Returns the current implementation. * @return impl Address of the current implementation */ function _getImplementation() internal view returns (address impl) { @@ -97,7 +107,7 @@ abstract contract GraphProxyStorage { } /** - * @dev Returns the current pending implementation. + * @notice Returns the current pending implementation. * @return impl Address of the current pending implementation */ function _getPendingImplementation() internal view returns (address impl) { @@ -109,7 +119,7 @@ abstract contract GraphProxyStorage { } /** - * @dev Sets the implementation address of the proxy. + * @notice Sets the implementation address of the proxy. * @param _newImplementation Address of the new implementation */ function _setImplementation(address _newImplementation) internal { @@ -125,7 +135,7 @@ abstract contract GraphProxyStorage { } /** - * @dev Sets the pending implementation address of the proxy. + * @notice Sets the pending implementation address of the proxy. * @param _newImplementation Address of the new pending implementation */ function _setPendingImplementation(address _newImplementation) internal { diff --git a/packages/contracts/contracts/upgrades/GraphUpgradeable.sol b/packages/contracts/contracts/upgrades/GraphUpgradeable.sol index 60dfbe888..ada2b04fd 100644 --- a/packages/contracts/contracts/upgrades/GraphUpgradeable.sol +++ b/packages/contracts/contracts/upgrades/GraphUpgradeable.sol @@ -2,11 +2,14 @@ pragma solidity ^0.7.6 || 0.8.27; +/* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 + import { IGraphProxy } from "./IGraphProxy.sol"; /** * @title Graph Upgradeable - * @dev This contract is intended to be inherited from upgradeable contracts. + * @author Edge & Node + * @notice This contract is intended to be inherited from upgradeable contracts. */ abstract contract GraphUpgradeable { /** @@ -18,6 +21,7 @@ abstract contract GraphUpgradeable { /** * @dev Check if the caller is the proxy admin. + * @param _proxy The proxy contract to check admin for */ modifier onlyProxyAdmin(IGraphProxy _proxy) { require(msg.sender == _proxy.admin(), "Caller must be the proxy admin"); @@ -33,7 +37,7 @@ abstract contract GraphUpgradeable { } /** - * @dev Returns the current implementation. + * @notice Returns the current implementation. * @return impl Address of the current implementation */ function _implementation() internal view returns (address impl) { diff --git a/packages/contracts/contracts/upgrades/IGraphProxy.sol b/packages/contracts/contracts/upgrades/IGraphProxy.sol index 4f501ed7c..a3f2fdd8e 100644 --- a/packages/contracts/contracts/upgrades/IGraphProxy.sol +++ b/packages/contracts/contracts/upgrades/IGraphProxy.sol @@ -2,18 +2,76 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Graph Proxy Interface + * @author Edge & Node + * @notice Interface for the Graph Proxy contract that handles upgradeable proxy functionality + */ interface IGraphProxy { + /** + * @notice Get the current admin. + * + * @dev NOTE: Only the admin can call this function. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + * + * @return adminAddress The address of the current admin + */ function admin() external returns (address); + /** + * @notice Change the admin of the proxy. + * + * @dev NOTE: Only the admin can call this function. + * + * @param _newAdmin Address of the new admin + */ function setAdmin(address _newAdmin) external; + /** + * @notice Get the current implementation. + * + * @dev NOTE: Only the admin can call this function. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` + * + * @return implementationAddress The address of the current implementation for this proxy + */ function implementation() external returns (address); + /** + * @notice Get the current pending implementation. + * + * @dev NOTE: Only the admin can call this function. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x9e5eddc59e0b171f57125ab86bee043d9128098c3a6b9adb4f2e86333c2f6f8c` + * + * @return pendingImplementationAddress The address of the current pending implementation for this proxy + */ function pendingImplementation() external returns (address); + /** + * @notice Upgrades to a new implementation contract. + * @dev NOTE: Only the admin can call this function. + * @param _newImplementation Address of implementation contract + */ function upgradeTo(address _newImplementation) external; + /** + * @notice Admin function for new implementation to accept its role as implementation. + */ function acceptUpgrade() external; + /** + * @notice Admin function for new implementation to accept its role as implementation, + * calling a function on the new implementation. + * @param data Calldata (including selector) for the function to delegatecall into the implementation + */ function acceptUpgradeAndCall(bytes calldata data) external; } diff --git a/packages/contracts/contracts/utils/TokenUtils.sol b/packages/contracts/contracts/utils/TokenUtils.sol index ef2f03211..50ad277b4 100644 --- a/packages/contracts/contracts/utils/TokenUtils.sol +++ b/packages/contracts/contracts/utils/TokenUtils.sol @@ -2,17 +2,20 @@ pragma solidity ^0.7.6 || 0.8.27; +/* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 + import { IGraphToken } from "../token/IGraphToken.sol"; /** * @title TokenUtils library + * @author Edge & Node * @notice This library contains utility functions for handling tokens (transfers and burns). * It is specifically adapted for the GraphToken, so does not need to handle edge cases * for other tokens. */ library TokenUtils { /** - * @dev Pull tokens from an address to this contract. + * @notice Pull tokens from an address to this contract. * @param _graphToken Token to transfer * @param _from Address sending the tokens * @param _amount Amount of tokens to transfer @@ -24,7 +27,7 @@ library TokenUtils { } /** - * @dev Push tokens from this contract to a receiving address. + * @notice Push tokens from this contract to a receiving address. * @param _graphToken Token to transfer * @param _to Address receiving the tokens * @param _amount Amount of tokens to transfer @@ -36,7 +39,7 @@ library TokenUtils { } /** - * @dev Burn tokens held by this contract. + * @notice Burn tokens held by this contract. * @param _graphToken Token to burn * @param _amount Amount of tokens to burn */ diff --git a/packages/data-edge/contracts/DataEdge.sol b/packages/data-edge/contracts/DataEdge.sol index fc39b7386..8b02c3ce0 100644 --- a/packages/data-edge/contracts/DataEdge.sol +++ b/packages/data-edge/contracts/DataEdge.sol @@ -5,8 +5,10 @@ pragma solidity ^0.8.12; /// @title Data Edge contract is only used to store on-chain data, it does not /// perform execution. On-chain client services can read the data /// and decode the payload for different purposes. +/// @author Edge & Node +/// @notice Contract for storing on-chain data without execution contract DataEdge { - /// @dev Fallback function, accepts any payload + /// @notice Fallback function, accepts any payload fallback() external payable { // no-op } diff --git a/packages/data-edge/contracts/EventfulDataEdge.sol b/packages/data-edge/contracts/EventfulDataEdge.sol index d995be665..d3725f151 100644 --- a/packages/data-edge/contracts/EventfulDataEdge.sol +++ b/packages/data-edge/contracts/EventfulDataEdge.sol @@ -6,9 +6,14 @@ pragma solidity ^0.8.12; /// perform execution. On-chain client services can read the data /// and decode the payload for different purposes. /// NOTE: This version emits an event with the calldata. +/// @author Edge & Node +/// @notice Contract for storing on-chain data with event logging contract EventfulDataEdge { + /// @notice Emitted when data is received + /// @param data The calldata received by the contract event Log(bytes data); + /// @notice Accepts any payload and emits it as an event /// @dev Fallback function, accepts any payload fallback() external payable { emit Log(msg.data); diff --git a/packages/horizon/contracts/data-service/DataService.sol b/packages/horizon/contracts/data-service/DataService.sol index 6b14365d5..21205dbfe 100644 --- a/packages/horizon/contracts/data-service/DataService.sol +++ b/packages/horizon/contracts/data-service/DataService.sol @@ -9,6 +9,7 @@ import { ProvisionManager } from "./utilities/ProvisionManager.sol"; /** * @title DataService contract + * @author Edge & Node * @dev Implementation of the {IDataService} interface. * @notice This implementation provides base functionality for a data service: * - GraphDirectory, allows the data service to interact with Graph Horizon contracts @@ -32,6 +33,7 @@ import { ProvisionManager } from "./utilities/ProvisionManager.sol"; */ abstract contract DataService is GraphDirectory, ProvisionManager, DataServiceV1Storage, IDataService { /** + * @notice Constructor for the DataService contract * @dev Addresses in GraphDirectory are immutables, they can only be set in this constructor. * @param controller The address of the Graph Horizon controller contract. */ diff --git a/packages/horizon/contracts/data-service/DataServiceStorage.sol b/packages/horizon/contracts/data-service/DataServiceStorage.sol index df759b892..0b6d27e4b 100644 --- a/packages/horizon/contracts/data-service/DataServiceStorage.sol +++ b/packages/horizon/contracts/data-service/DataServiceStorage.sol @@ -3,7 +3,8 @@ pragma solidity 0.8.27; /** * @title DataServiceStorage - * @dev This contract holds the storage variables for the DataService contract. + * @author Edge & Node + * @notice This contract holds the storage variables for the DataService contract. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index a82c15361..91f5c5f4e 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -12,6 +12,7 @@ import { DataServiceFeesV1Storage } from "./DataServiceFeesStorage.sol"; /** * @title DataServiceFees contract + * @author Edge & Node * @dev Implementation of the {IDataServiceFees} interface. * @notice Extension for the {IDataService} contract to handle payment collateralization * using a Horizon provision. See {IDataServiceFees} for more details. diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol index 78d826f03..bafb8fe52 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol @@ -7,6 +7,8 @@ import { ILinkedList } from "@graphprotocol/interfaces/contracts/horizon/interna /** * @title Storage layout for the {DataServiceFees} extension contract. + * @author Edge & Node + * @notice Storage layout for the DataServiceFees extension contract * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol index a4c60f7db..b1bd4203a 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol @@ -8,6 +8,7 @@ import { DataService } from "../DataService.sol"; /** * @title DataServicePausable contract + * @author Edge & Node * @dev Implementation of the {IDataServicePausable} interface. * @notice Extension for the {IDataService} contract, adds pausing functionality * to the data service. Pausing is controlled by privileged accounts called diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol index 2ca8e09c6..ad792f914 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol @@ -8,8 +8,9 @@ import { DataService } from "../DataService.sol"; /** * @title DataServicePausableUpgradeable contract + * @author Edge & Node + * @notice Upgradeable version of the {DataServicePausable} contract. * @dev Implementation of the {IDataServicePausable} interface. - * @dev Upgradeable version of the {DataServicePausable} contract. * @dev This contract inherits from {DataService} which needs to be initialized, please see * {DataService} for detailed instructions. * @custom:security-contact Please email security+contracts@thegraph.com if you find any diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol b/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol index 0bb600a01..a6af01533 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol @@ -12,8 +12,9 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s /** * @title Rescuable contract - * @dev Allows a contract to have a function to rescue tokens sent by mistake. - * The contract must implement the external rescueTokens function or similar, + * @author Edge & Node + * @notice Allows a contract to have a function to rescue tokens sent by mistake. + * @dev The contract must implement the external rescueTokens function or similar, * that calls this contract's _rescueTokens. * @dev Note that this extension does not provide an external function to set * rescuers. This should be implemented in the derived contract. @@ -63,7 +64,7 @@ abstract contract DataServiceRescuable is DataService, IDataServiceRescuable { } /** - * @dev Allows rescuing tokens sent to this contract + * @notice Allows rescuing tokens sent to this contract * @param _to Destination address to send the tokens * @param _token Address of the token being rescued * @param _tokens Amount of tokens to pull diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol index 06612913d..42f4a7de9 100644 --- a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol +++ b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; /** * @title ProvisionTracker library + * @author Edge & Node * @notice A library to facilitate tracking of "used tokens" on Graph Horizon provisions. This can be used to * ensure data services have enough economic security (provisioned stake) to back the payments they collect for * their services. diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index db5a9e8f5..9d0db415b 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events +// solhint-disable gas-strict-inequalities + import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { UintRange } from "../../libraries/UintRange.sol"; @@ -12,6 +16,7 @@ import { ProvisionManagerV1Storage } from "./ProvisionManagerStorage.sol"; /** * @title ProvisionManager contract + * @author Edge & Node * @notice A helper contract that implements several provision management functions. * @dev Provides utilities to verify provision parameters are within an acceptable range. Each * parameter has an overridable setter and getter for the validity range, and a checker that reverts diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol index 5931c66e5..d2d3495ba 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.27; /** * @title Storage layout for the {ProvisionManager} helper contract. + * @author Edge & Node + * @notice Storage layout for the ProvisionManager helper contract * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/horizon/contracts/libraries/Denominations.sol b/packages/horizon/contracts/libraries/Denominations.sol index 46cff3516..abd0ac9a6 100644 --- a/packages/horizon/contracts/libraries/Denominations.sol +++ b/packages/horizon/contracts/libraries/Denominations.sol @@ -3,7 +3,8 @@ pragma solidity 0.8.27; /** * @title Denominations library - * @dev Provides a list of ground denominations for those tokens that cannot be represented by an ERC20. + * @author Edge & Node + * @notice Provides a list of ground denominations for those tokens that cannot be represented by an ERC20 * For now, the only needed is the native token that could be ETH, MATIC, or other depending on the layer being operated. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. diff --git a/packages/horizon/contracts/libraries/LibFixedMath.sol b/packages/horizon/contracts/libraries/LibFixedMath.sol index 704617b40..2468721b2 100644 --- a/packages/horizon/contracts/libraries/LibFixedMath.sol +++ b/packages/horizon/contracts/libraries/LibFixedMath.sol @@ -20,8 +20,12 @@ pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable function-max-lines, gas-strict-inequalities + /** * @title LibFixedMath + * @author Edge & Node * @notice This library provides fixed-point arithmetic operations. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. @@ -36,12 +40,20 @@ library LibFixedMath { // -63.875 int256 private constant EXP_MIN_VAL = -int256(0x0000000000000000000000000000001ff0000000000000000000000000000000); - /// @dev Get one as a fixed-point number. + /** + * @notice Get one as a fixed-point number + * @return f The fixed-point representation of one + */ function one() internal pure returns (int256 f) { f = FIXED_1; } - /// @dev Returns the addition of two fixed point numbers, reverting on overflow. + /** + * @notice Returns the subtraction of two fixed point numbers, reverting on overflow + * @param a The first fixed point number + * @param b The second fixed point number to subtract + * @return c The result of a - b + */ function sub(int256 a, int256 b) internal pure returns (int256 c) { if (b == MIN_FIXED_VAL) { revert("out-of-bounds"); @@ -49,19 +61,34 @@ library LibFixedMath { c = _add(a, -b); } - /// @dev Returns the multiplication of two fixed point numbers, reverting on overflow. + /** + * @notice Returns the multiplication of two fixed point numbers, reverting on overflow + * @param a The first fixed point number + * @param b The second fixed point number + * @return c The result of a * b + */ function mul(int256 a, int256 b) internal pure returns (int256 c) { c = _mul(a, b) / FIXED_1; } - /// @dev Performs (a * n) / d, without scaling for precision. + /** + * @notice Performs (a * n) / d, without scaling for precision + * @param a The first fixed point number + * @param n The numerator + * @param d The denominator + * @return c The result of (a * n) / d + */ function mulDiv(int256 a, int256 n, int256 d) internal pure returns (int256 c) { c = _div(_mul(a, n), d); } - /// @dev Returns the unsigned integer result of multiplying a fixed-point - /// number with an integer, reverting if the multiplication overflows. - /// Negative results are clamped to zero. + /** + * @notice Returns the unsigned integer result of multiplying a fixed-point number with an integer + * @dev Negative results are clamped to zero. Reverts if the multiplication overflows. + * @param f Fixed-point number + * @param u Unsigned integer + * @return Unsigned integer result, clamped to zero if negative + */ function uintMul(int256 f, uint256 u) internal pure returns (uint256) { if (int256(u) < int256(0)) { revert("out-of-bounds"); @@ -73,17 +100,30 @@ library LibFixedMath { return uint256(uint256(c) >> 127); } - /// @dev Convert signed `n` / `d` to a fixed-point number. + /** + * @notice Convert signed `n` / `d` to a fixed-point number + * @param n Numerator + * @param d Denominator + * @return f Fixed-point representation of n/d + */ function toFixed(int256 n, int256 d) internal pure returns (int256 f) { f = _div(_mul(n, FIXED_1), d); } - /// @dev Convert a fixed-point number to an integer. + /** + * @notice Convert a fixed-point number to an integer + * @param f Fixed-point number + * @return n Integer representation + */ function toInteger(int256 f) internal pure returns (int256 n) { return f / FIXED_1; } - /// @dev Compute the natural exponent for a fixed-point number EXP_MIN_VAL <= `x` <= 1 + /** + * @notice Compute the natural exponent for a fixed-point number EXP_MIN_VAL <= `x` <= 1 + * @param x Fixed-point number to compute exponent for + * @return r The natural exponent of x + */ function exp(int256 x) internal pure returns (int256 r) { if (x < EXP_MIN_VAL) { // Saturate to zero below EXP_MIN_VAL. @@ -205,7 +245,12 @@ library LibFixedMath { } } - /// @dev Returns the multiplication two numbers, reverting on overflow. + /** + * @notice Returns the multiplication of two numbers, reverting on overflow + * @param a First number + * @param b Second number + * @return c The result of a * b + */ function _mul(int256 a, int256 b) private pure returns (int256 c) { if (a == 0 || b == 0) { return 0; @@ -218,7 +263,12 @@ library LibFixedMath { } } - /// @dev Returns the division of two numbers, reverting on division by zero. + /** + * @notice Returns the division of two numbers, reverting on division by zero + * @param a Dividend + * @param b Divisor + * @return c The result of a / b + */ function _div(int256 a, int256 b) private pure returns (int256 c) { if (b == 0) { revert("overflow"); @@ -231,7 +281,12 @@ library LibFixedMath { } } - /// @dev Adds two numbers, reverting on overflow. + /** + * @notice Adds two numbers, reverting on overflow + * @param a First number + * @param b Second number + * @return c The result of a + b + */ function _add(int256 a, int256 b) private pure returns (int256 c) { unchecked { c = a + b; diff --git a/packages/horizon/contracts/libraries/LinkedList.sol b/packages/horizon/contracts/libraries/LinkedList.sol index 9d3305329..083b1f436 100644 --- a/packages/horizon/contracts/libraries/LinkedList.sol +++ b/packages/horizon/contracts/libraries/LinkedList.sol @@ -2,10 +2,14 @@ pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one, gas-strict-inequalities + import { ILinkedList } from "@graphprotocol/interfaces/contracts/horizon/internal/ILinkedList.sol"; /** * @title LinkedList library + * @author Edge & Node * @notice A library to manage singly linked lists. * * The library makes no assumptions about the contents of the items, the only diff --git a/packages/horizon/contracts/libraries/MathUtils.sol b/packages/horizon/contracts/libraries/MathUtils.sol index fc81e9608..6c0a09a1a 100644 --- a/packages/horizon/contracts/libraries/MathUtils.sol +++ b/packages/horizon/contracts/libraries/MathUtils.sol @@ -1,23 +1,29 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + pragma solidity 0.8.27; /** * @title MathUtils Library + * @author Edge & Node * @notice A collection of functions to perform math operations * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ library MathUtils { /** - * @dev Calculates the weighted average of two values pondering each of these - * values based on configured weights. The contribution of each value N is + * @notice Calculates the weighted average of two values pondering each of these + * values based on configured weights + * @dev The contribution of each value N is * weightN/(weightA + weightB). The calculation rounds up to ensure the result * is always equal or greater than the smallest of the two values. * @param valueA The amount for value A * @param weightA The weight to use for value A * @param valueB The amount for value B * @param weightB The weight to use for value B + * @return The weighted average result */ function weightedAverageRoundingUp( uint256 valueA, @@ -29,7 +35,7 @@ library MathUtils { } /** - * @dev Returns the minimum of two numbers. + * @notice Returns the minimum of two numbers * @param x The first number * @param y The second number * @return The minimum of the two numbers @@ -39,7 +45,7 @@ library MathUtils { } /** - * @dev Returns the difference between two numbers or zero if negative. + * @notice Returns the difference between two numbers or zero if negative * @param x The first number * @param y The second number * @return The difference between the two numbers or zero if negative diff --git a/packages/horizon/contracts/libraries/PPMMath.sol b/packages/horizon/contracts/libraries/PPMMath.sol index a7966c91d..998a912e8 100644 --- a/packages/horizon/contracts/libraries/PPMMath.sol +++ b/packages/horizon/contracts/libraries/PPMMath.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + /** * @title PPMMath library + * @author Edge & Node * @notice A library for handling calculations with parts per million (PPM) amounts. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. diff --git a/packages/horizon/contracts/libraries/UintRange.sol b/packages/horizon/contracts/libraries/UintRange.sol index 69d3f5d8a..7c9bdfdd8 100644 --- a/packages/horizon/contracts/libraries/UintRange.sol +++ b/packages/horizon/contracts/libraries/UintRange.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + /** * @title UintRange library + * @author Edge & Node * @notice A library for handling range checks on uint256 values. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. diff --git a/packages/horizon/contracts/mocks/ControllerMock.sol b/packages/horizon/contracts/mocks/ControllerMock.sol index 638387f0c..415e48c5e 100644 --- a/packages/horizon/contracts/mocks/ControllerMock.sol +++ b/packages/horizon/contracts/mocks/ControllerMock.sol @@ -7,22 +7,30 @@ import { IManaged } from "@graphprotocol/interfaces/contracts/contracts/governan /** * @title Graph Controller contract (mock) + * @author Edge & Node + * @notice Mock implementation of the Graph Controller contract for testing * @dev Controller is a registry of contracts for convenience. Inspired by Livepeer: * https://github.com/livepeer/protocol/blob/streamflow/contracts/Controller.sol */ contract ControllerMock is IController { /// @dev Track contract ids to contract proxy address mapping(bytes32 contractName => address contractAddress) private _registry; + + /// @notice Address of the governor address public governor; bool internal _paused; bool internal _partialPaused; address internal _pauseGuardian; - /// Emitted when the proxy address for a protocol contract has been set - event SetContractProxy(bytes32 indexed id, address contractAddress); + /** + * @notice Emitted when the proxy address for a protocol contract has been set + * @param id The contract identifier + * @param contractAddress The new contract address + */ + event SetContractProxy(bytes32 indexed id, address indexed contractAddress); /** - * Constructor for the Controller mock + * @notice Constructor for the Controller mock * @param governor_ Address of the governor */ constructor(address governor_) { diff --git a/packages/horizon/contracts/mocks/CurationMock.sol b/packages/horizon/contracts/mocks/CurationMock.sol index ea3df0587..9de0fea16 100644 --- a/packages/horizon/contracts/mocks/CurationMock.sol +++ b/packages/horizon/contracts/mocks/CurationMock.sol @@ -2,17 +2,38 @@ pragma solidity 0.8.27; +/** + * @title CurationMock + * @author Edge & Node + * @notice Mock implementation of curation functionality for testing + */ contract CurationMock { + /// @notice Mapping of subgraph deployment ID to curation tokens mapping(bytes32 subgraphDeploymentID => uint256 tokens) public curation; + /** + * @notice Signal curation tokens for a subgraph deployment + * @param subgraphDeploymentID The subgraph deployment ID + * @param tokens The amount of tokens to signal + */ function signal(bytes32 subgraphDeploymentID, uint256 tokens) public { curation[subgraphDeploymentID] += tokens; } + /** + * @notice Check if a subgraph deployment is curated + * @param subgraphDeploymentID The subgraph deployment ID + * @return True if the subgraph deployment has curation tokens + */ function isCurated(bytes32 subgraphDeploymentID) public view returns (bool) { return curation[subgraphDeploymentID] != 0; } + /** + * @notice Collect curation tokens for a subgraph deployment + * @param subgraphDeploymentID The subgraph deployment ID + * @param tokens The amount of tokens to collect + */ function collect(bytes32 subgraphDeploymentID, uint256 tokens) external { curation[subgraphDeploymentID] += tokens; } diff --git a/packages/horizon/contracts/mocks/Dummy.sol b/packages/horizon/contracts/mocks/Dummy.sol index e6a575d0f..72b4e1a67 100644 --- a/packages/horizon/contracts/mocks/Dummy.sol +++ b/packages/horizon/contracts/mocks/Dummy.sol @@ -2,4 +2,9 @@ pragma solidity 0.8.27; +/** + * @title Dummy + * @author Edge & Node + * @notice Empty dummy contract for testing purposes + */ contract Dummy {} diff --git a/packages/horizon/contracts/mocks/EpochManagerMock.sol b/packages/horizon/contracts/mocks/EpochManagerMock.sol index 4c00fb29c..4030dc19c 100644 --- a/packages/horizon/contracts/mocks/EpochManagerMock.sol +++ b/packages/horizon/contracts/mocks/EpochManagerMock.sol @@ -4,16 +4,29 @@ pragma solidity 0.8.27; import { IEpochManager } from "@graphprotocol/interfaces/contracts/contracts/epochs/IEpochManager.sol"; +/** + * @title EpochManagerMock + * @author Edge & Node + * @notice Mock implementation of the EpochManager for testing + */ contract EpochManagerMock is IEpochManager { // -- Variables -- + /// @notice Length of an epoch in blocks uint256 public epochLength; + /// @notice Last epoch that was run uint256 public lastRunEpoch; + /// @notice Last epoch when the length was updated uint256 public lastLengthUpdateEpoch; + /// @notice Block number when the length was last updated uint256 public lastLengthUpdateBlock; // -- Configuration -- + /** + * @notice Set the epoch length + * @param epochLength_ New epoch length in blocks + */ function setEpochLength(uint256 epochLength_) public { lastLengthUpdateEpoch = 1; lastLengthUpdateBlock = blockNum(); @@ -22,41 +35,78 @@ contract EpochManagerMock is IEpochManager { // -- Epochs + /** + * @notice Run the current epoch + */ function runEpoch() public { lastRunEpoch = currentEpoch(); } // -- Getters -- + /** + * @notice Check if the current epoch has been run + * @return True if the current epoch has been run + */ function isCurrentEpochRun() public view returns (bool) { return lastRunEpoch == currentEpoch(); } + /** + * @notice Get the current block number + * @return The current block number + */ function blockNum() public view returns (uint256) { return block.number; } + /** + * @notice Get the hash of a specific block + * @param block_ Block number to get hash for + * @return The block hash + */ function blockHash(uint256 block_) public view returns (bytes32) { return blockhash(block_); } + /** + * @notice Get the current epoch number + * @return The current epoch number + */ function currentEpoch() public view returns (uint256) { return lastLengthUpdateEpoch + epochsSinceUpdate(); } + /** + * @notice Get the block number when the current epoch started + * @return The block number when the current epoch started + */ function currentEpochBlock() public view returns (uint256) { return lastLengthUpdateBlock + (epochsSinceUpdate() * epochLength); } + /** + * @notice Get the number of blocks since the current epoch started + * @return The number of blocks since the current epoch started + */ function currentEpochBlockSinceStart() public view returns (uint256) { return blockNum() - currentEpochBlock(); } + /** + * @notice Get the number of epochs since a given epoch + * @param epoch_ The epoch to compare against + * @return The number of epochs since the given epoch + */ function epochsSince(uint256 epoch_) public view returns (uint256) { uint256 epoch = currentEpoch(); return epoch_ < epoch ? (epoch - epoch_) : 0; } + /** + * @notice Get the number of epochs since the last length update + * @return The number of epochs since the last length update + */ function epochsSinceUpdate() public view returns (uint256) { return (blockNum() - lastLengthUpdateBlock) / epochLength; } diff --git a/packages/horizon/contracts/mocks/MockGRTToken.sol b/packages/horizon/contracts/mocks/MockGRTToken.sol index 235999ae5..385b3b4e2 100644 --- a/packages/horizon/contracts/mocks/MockGRTToken.sol +++ b/packages/horizon/contracts/mocks/MockGRTToken.sol @@ -4,27 +4,65 @@ pragma solidity 0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +/** + * @title MockGRTToken + * @author Edge & Node + * @notice Mock implementation of the Graph Token for testing + */ contract MockGRTToken is ERC20, IGraphToken { + /** + * @notice Constructor for the MockGRTToken + */ constructor() ERC20("Graph Token", "GRT") {} + /** + * @notice Burn tokens from the caller's account + * @param tokens Amount of tokens to burn + */ function burn(uint256 tokens) external { _burn(msg.sender, tokens); } + /** + * @notice Burn tokens from a specific account + * @param from Account to burn tokens from + * @param tokens Amount of tokens to burn + */ function burnFrom(address from, uint256 tokens) external { _burn(from, tokens); } // -- Mint Admin -- + /** + * @notice Add a minter (mock implementation - does nothing) + * @param account Account to add as minter + */ function addMinter(address account) external {} + /** + * @notice Remove a minter (mock implementation - does nothing) + * @param account Account to remove as minter + */ function removeMinter(address account) external {} + /** + * @notice Renounce minter role (mock implementation - does nothing) + */ function renounceMinter() external {} // -- Permit -- + /** + * @notice Permit function for gasless approvals (mock implementation - does nothing) + * @param owner Token owner + * @param spender Spender address + * @param value Amount to approve + * @param deadline Deadline for the permit + * @param v Recovery byte of the signature + * @param r First 32 bytes of the signature + * @param s Second 32 bytes of the signature + */ function permit( address owner, address spender, @@ -37,12 +75,34 @@ contract MockGRTToken is ERC20, IGraphToken { // -- Allowance -- + /** + * @notice Increase allowance (mock implementation - does nothing) + * @param spender Spender address + * @param addedValue Amount to add to allowance + * @return Always returns false in mock + */ function increaseAllowance(address spender, uint256 addedValue) external returns (bool) {} + /** + * @notice Decrease allowance (mock implementation - does nothing) + * @param spender Spender address + * @param subtractedValue Amount to subtract from allowance + * @return Always returns false in mock + */ function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) {} + /** + * @notice Check if an account is a minter (mock implementation - always returns false) + * @param account Account to check + * @return Always returns false in mock + */ function isMinter(address account) external view returns (bool) {} + /** + * @notice Mint tokens to an account + * @param to Account to mint tokens to + * @param tokens Amount of tokens to mint + */ function mint(address to, uint256 tokens) public { _mint(to, tokens); } diff --git a/packages/horizon/contracts/mocks/RewardsManagerMock.sol b/packages/horizon/contracts/mocks/RewardsManagerMock.sol index 300e7efe2..89f4a7e4a 100644 --- a/packages/horizon/contracts/mocks/RewardsManagerMock.sol +++ b/packages/horizon/contracts/mocks/RewardsManagerMock.sol @@ -4,23 +4,51 @@ pragma solidity 0.8.27; import { MockGRTToken } from "./MockGRTToken.sol"; +/** + * @title RewardsManagerMock + * @author Edge & Node + * @notice Mock implementation of the RewardsManager for testing + */ contract RewardsManagerMock { // -- Variables -- + /// @notice The mock GRT token contract MockGRTToken public token; uint256 private _rewards; // -- Constructor -- + /** + * @notice Constructor for the RewardsManager mock + * @param token_ The mock GRT token contract + * @param rewards The amount of rewards to distribute + */ constructor(MockGRTToken token_, uint256 rewards) { token = token_; _rewards = rewards; } - function takeRewards(address) external returns (uint256) { + /** + * @notice Take rewards for an allocation + * @param allocationID The allocation ID (unused in mock) + * @return The amount of rewards taken + */ + function takeRewards(address allocationID) external returns (uint256) { + // solhint-disable-previous-line no-unused-vars token.mint(msg.sender, _rewards); return _rewards; } - function onSubgraphAllocationUpdate(bytes32) public returns (uint256) {} + /** + * @notice Handle subgraph allocation update (mock implementation) + * @param subgraphDeploymentID The subgraph deployment ID + * @return Always returns 0 in mock + */ + function onSubgraphAllocationUpdate(bytes32 subgraphDeploymentID) public returns (uint256) {} + + /** + * @notice Handle subgraph signal update (mock implementation) + * @param subgraphDeploymentID The subgraph deployment ID + * @return Always returns 0 in mock + */ function onSubgraphSignalUpdate(bytes32 subgraphDeploymentID) external returns (uint256) {} } diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol index e82758b0e..f8268efca 100644 --- a/packages/horizon/contracts/payments/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable function-max-lines + import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; @@ -14,6 +17,7 @@ import { GraphDirectory } from "../utilities/GraphDirectory.sol"; /** * @title GraphPayments contract + * @author Edge & Node * @notice This contract is part of the Graph Horizon payments protocol. It's designed * to pull funds (GRT) from the {PaymentsEscrow} and distribute them according to a * set of pre established rules. diff --git a/packages/horizon/contracts/payments/PaymentsEscrow.sol b/packages/horizon/contracts/payments/PaymentsEscrow.sol index bcced0412..c36b9bd73 100644 --- a/packages/horizon/contracts/payments/PaymentsEscrow.sol +++ b/packages/horizon/contracts/payments/PaymentsEscrow.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IPaymentsEscrow } from "@graphprotocol/interfaces/contracts/horizon/IPaymentsEscrow.sol"; @@ -13,6 +16,7 @@ import { GraphDirectory } from "../utilities/GraphDirectory.sol"; /** * @title PaymentsEscrow contract + * @author Edge & Node * @dev Implements the {IPaymentsEscrow} interface * @notice This contract is part of the Graph Horizon payments protocol. It holds the funds (GRT) * for payments made through the payments protocol for services provided diff --git a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol index 0ede499e0..eb33d931c 100644 --- a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol +++ b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-small-strings +// solhint-disable gas-strict-inequalities +// solhint-disable function-max-lines + import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IGraphTallyCollector } from "@graphprotocol/interfaces/contracts/horizon/IGraphTallyCollector.sol"; import { IPaymentsCollector } from "@graphprotocol/interfaces/contracts/horizon/IPaymentsCollector.sol"; @@ -14,6 +19,7 @@ import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /** * @title GraphTallyCollector contract + * @author Edge & Node * @dev Implements the {IGraphTallyCollector}, {IPaymentCollector} and {IAuthorizable} interfaces. * @notice A payments collector contract that can be used to collect payments using a GraphTally RAV (Receipt Aggregate Voucher). * @dev Note that the contract expects the RAV aggregate value to be monotonically increasing, each successive RAV for the same @@ -185,9 +191,9 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall } /** - * @dev Recovers the signer address of a signed ReceiptAggregateVoucher (RAV). - * @param _signedRAV The SignedRAV containing the RAV and its signature. - * @return The address of the signer. + * @notice Recovers the signer address of a signed ReceiptAggregateVoucher (RAV) + * @param _signedRAV The SignedRAV containing the RAV and its signature + * @return The address of the signer */ function _recoverRAVSigner(SignedRAV memory _signedRAV) private view returns (address) { bytes32 messageHash = _encodeRAV(_signedRAV.rav); @@ -195,9 +201,9 @@ contract GraphTallyCollector is EIP712, GraphDirectory, Authorizable, IGraphTall } /** - * @dev Computes the hash of a ReceiptAggregateVoucher (RAV). - * @param _rav The RAV for which to compute the hash. - * @return The hash of the RAV. + * @notice Computes the hash of a ReceiptAggregateVoucher (RAV) + * @param _rav The RAV for which to compute the hash + * @return The hash of the RAV */ function _encodeRAV(ReceiptAggregateVoucher memory _rav) private view returns (bytes32) { return diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index ff675fdd6..1c3e2e8af 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -1,5 +1,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities +// solhint-disable gas-increment-by-one +// solhint-disable function-max-lines + pragma solidity 0.8.27; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; @@ -17,6 +22,7 @@ import { HorizonStakingBase } from "./HorizonStakingBase.sol"; /** * @title HorizonStaking contract + * @author Edge & Node * @notice The {HorizonStaking} contract allows service providers to stake and provision tokens to verifiers to be used * as economic security for a service. It also allows delegators to delegate towards a service provider provision. * @dev Implements the {IHorizonStakingMain} interface. @@ -69,11 +75,10 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { } /** - * @dev 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. + * @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, @@ -88,8 +93,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * @dev This function does not return to its internal call site, it will return directly to the * external caller. */ - // solhint-disable-next-line payable-fallback, no-complex-fallback fallback() external { + // solhint-disable-previous-line payable-fallback, no-complex-fallback address extensionImpl = STAKING_EXTENSION_ADDRESS; // solhint-disable-next-line no-inline-assembly assembly { @@ -681,8 +686,8 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { * @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 _verifier The verifier address for which the tokens are provisioned (who will be able to slash the tokens) * @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) * @param _maxVerifierCut The maximum cut, expressed in PPM, that a verifier can transfer instead of burning when slashing * @param _thawingPeriod The period in seconds that the tokens will be thawing before they can be removed from the provision */ diff --git a/packages/horizon/contracts/staking/HorizonStakingBase.sol b/packages/horizon/contracts/staking/HorizonStakingBase.sol index 2428e1e98..9c52a2171 100644 --- a/packages/horizon/contracts/staking/HorizonStakingBase.sol +++ b/packages/horizon/contracts/staking/HorizonStakingBase.sol @@ -1,5 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + pragma solidity 0.8.27; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; @@ -17,6 +20,7 @@ import { HorizonStakingV1Storage } from "./HorizonStakingStorage.sol"; /** * @title HorizonStakingBase contract + * @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. @@ -42,10 +46,9 @@ abstract contract HorizonStakingBase is address internal immutable SUBGRAPH_DATA_SERVICE_ADDRESS; /** - * @dev 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. + * @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) Managed(controller) { SUBGRAPH_DATA_SERVICE_ADDRESS = subgraphDataServiceAddress; diff --git a/packages/horizon/contracts/staking/HorizonStakingExtension.sol b/packages/horizon/contracts/staking/HorizonStakingExtension.sol index 3480a5c4d..867a01f23 100644 --- a/packages/horizon/contracts/staking/HorizonStakingExtension.sol +++ b/packages/horizon/contracts/staking/HorizonStakingExtension.sol @@ -2,6 +2,9 @@ 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/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; @@ -16,6 +19,7 @@ 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 @@ -39,10 +43,9 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev 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. + * @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, @@ -265,7 +268,7 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Collect tax to burn for an amount of tokens. + * @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 @@ -277,7 +280,7 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Triggers an update of rewards due to a change in allocations. + * @notice Triggers an update of rewards due to a change in allocations * @param _subgraphDeploymentID Subgraph deployment updated */ function _updateRewards(bytes32 _subgraphDeploymentID) private { @@ -285,7 +288,7 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Assign rewards for the closed allocation to indexer and delegators. + * @notice Assign rewards for the closed allocation to indexer and delegators * @param _allocationID Allocation * @param _indexer Address of the indexer that did the allocation */ @@ -307,7 +310,7 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Send rewards to the appropriate destination. + * @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 @@ -326,7 +329,7 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Close an allocation and free the staked 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 */ @@ -390,8 +393,8 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Collect the delegation rewards for query fees. - * This function will assign the collected fees to the delegation pool. + * @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 @@ -408,8 +411,8 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Collect the delegation rewards for indexing. - * This function will assign the collected fees to the delegation pool. + * @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 @@ -426,8 +429,8 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Collect the curation fees for a subgraph deployment from an amount of tokens. - * This function transfer curation fees to the Curation contract by calling Curation.collect + * @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 @@ -461,7 +464,7 @@ contract HorizonStakingExtension is HorizonStakingBase, IHorizonStakingExtension } /** - * @dev Return the current state of an allocation + * @notice Return the current state of an allocation * @param _allocationID Allocation identifier * @return AllocationState enum with the state of the allocation */ diff --git a/packages/horizon/contracts/staking/HorizonStakingStorage.sol b/packages/horizon/contracts/staking/HorizonStakingStorage.sol index 19dc65f11..5f63af9df 100644 --- a/packages/horizon/contracts/staking/HorizonStakingStorage.sol +++ b/packages/horizon/contracts/staking/HorizonStakingStorage.sol @@ -11,7 +11,8 @@ import { ILinkedList } from "@graphprotocol/interfaces/contracts/horizon/interna /** * @title HorizonStakingV1Storage - * @notice This contract holds all the storage variables for the Staking contract. + * @author Edge & Node + * @notice This contract holds all the storage variables for the Staking contract * @dev Deprecated variables are kept to support the transition to Horizon Staking. * They can eventually be collapsed into a single storage slot. * @custom:security-contact Please email security+contracts@thegraph.com if you find any diff --git a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol index b137079b3..974e7197b 100644 --- a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol +++ b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol @@ -6,6 +6,7 @@ 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. @@ -16,21 +17,22 @@ library ExponentialRebates { /// @dev Maximum value of the exponent for which to compute the exponential before clamping to zero. uint32 private constant MAX_EXPONENT = 15; - /// @dev The exponential formula used to compute fee-based rewards for - /// staking pools in a given epoch. 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. + /** + * @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, diff --git a/packages/horizon/contracts/staking/utilities/Managed.sol b/packages/horizon/contracts/staking/utilities/Managed.sol index b2e36056f..88d2fb7c1 100644 --- a/packages/horizon/contracts/staking/utilities/Managed.sol +++ b/packages/horizon/contracts/staking/utilities/Managed.sol @@ -8,8 +8,9 @@ import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; /** * @title Graph Managed contract - * @dev The Managed contract provides an interface to interact with the Controller. - * For Graph Horizon this contract is mostly a shell that uses {GraphDirectory}, however since the {HorizonStaking} + * @author Edge & Node + * @notice The Managed contract provides an interface to interact with the Controller + * @dev For Graph Horizon this contract is mostly a shell that uses {GraphDirectory}, however since the {HorizonStaking} * contract uses it we need to preserve the storage layout. * Inspired by Livepeer: https://github.com/livepeer/protocol/blob/streamflow/contracts/Controller.sol * @custom:security-contact Please email security+contracts@thegraph.com if you find any @@ -59,8 +60,8 @@ abstract contract Managed is GraphDirectory { } /** - * @dev Initialize the contract - * @param controller_ The address of the Graph controller contract. + * @notice Initialize the contract + * @param controller_ The address of the Graph controller contract */ constructor(address controller_) GraphDirectory(controller_) {} } diff --git a/packages/horizon/contracts/utilities/Authorizable.sol b/packages/horizon/contracts/utilities/Authorizable.sol index 3c77e951e..6af2e677f 100644 --- a/packages/horizon/contracts/utilities/Authorizable.sol +++ b/packages/horizon/contracts/utilities/Authorizable.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + import { IAuthorizable } from "@graphprotocol/interfaces/contracts/horizon/IAuthorizable.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; @@ -8,6 +11,7 @@ import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/Mes /** * @title Authorizable contract + * @author Edge & Node * @dev Implements the {IAuthorizable} interface. * @notice A mechanism to authorize signers to sign messages on behalf of an authorizer. * Signers cannot be reused for different authorizers. diff --git a/packages/horizon/contracts/utilities/GraphDirectory.sol b/packages/horizon/contracts/utilities/GraphDirectory.sol index 469410c8b..1071605d9 100644 --- a/packages/horizon/contracts/utilities/GraphDirectory.sol +++ b/packages/horizon/contracts/utilities/GraphDirectory.sol @@ -17,6 +17,7 @@ import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curatio /** * @title GraphDirectory contract + * @author Edge & Node * @notice This contract is meant to be inherited by other contracts that * need to keep track of the addresses in Graph Horizon contracts. * It fetches the addresses from the Controller supplied during construction, diff --git a/packages/interfaces/contracts/contracts/arbitrum/IArbToken.sol b/packages/interfaces/contracts/contracts/arbitrum/IArbToken.sol index 35fd7beb3..a45f948f9 100644 --- a/packages/interfaces/contracts/contracts/arbitrum/IArbToken.sol +++ b/packages/interfaces/contracts/contracts/arbitrum/IArbToken.sol @@ -29,18 +29,28 @@ */ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Arbitrum Token Interface + * @author Edge & Node + * @notice Interface for tokens that can be minted and burned on Arbitrum L2 + */ interface IArbToken { /** * @notice should increase token supply by amount, and should (probably) only be callable by the L1 bridge. + * @param account Account to mint tokens to + * @param amount Amount of tokens to mint */ function bridgeMint(address account, uint256 amount) external; /** * @notice should decrease token supply by amount, and should (probably) only be callable by the L1 bridge. + * @param account Account to burn tokens from + * @param amount Amount of tokens to burn */ function bridgeBurn(address account, uint256 amount) external; /** + * @notice Get the L1 token address * @return address of layer 1 token */ function l1Address() external view returns (address); diff --git a/packages/interfaces/contracts/contracts/arbitrum/IBridge.sol b/packages/interfaces/contracts/contracts/arbitrum/IBridge.sol index 791c2b1a1..d49695069 100644 --- a/packages/interfaces/contracts/contracts/arbitrum/IBridge.sol +++ b/packages/interfaces/contracts/contracts/arbitrum/IBridge.sol @@ -25,7 +25,24 @@ pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + +/** + * @title Bridge Interface + * @author Edge & Node + * @notice Interface for the Arbitrum Bridge contract + */ interface IBridge { + /** + * @notice Emitted when a message is delivered to the inbox + * @param messageIndex Index of the message + * @param beforeInboxAcc Inbox accumulator before this message + * @param inbox Address of the inbox + * @param kind Type of the message + * @param sender Address that sent the message + * @param messageDataHash Hash of the message data + */ event MessageDelivered( uint256 indexed messageIndex, bytes32 indexed beforeInboxAcc, @@ -35,38 +52,102 @@ interface IBridge { bytes32 messageDataHash ); + /** + * @notice Emitted when a bridge call is triggered + * @param outbox Address of the outbox + * @param destAddr Destination address for the call + * @param amount ETH amount sent with the call + * @param data Calldata for the function call + */ event BridgeCallTriggered(address indexed outbox, address indexed destAddr, uint256 amount, bytes data); + /** + * @notice Emitted when an inbox is enabled or disabled + * @param inbox Address of the inbox + * @param enabled Whether the inbox is enabled + */ event InboxToggle(address indexed inbox, bool enabled); + /** + * @notice Emitted when an outbox is enabled or disabled + * @param outbox Address of the outbox + * @param enabled Whether the outbox is enabled + */ event OutboxToggle(address indexed outbox, bool enabled); + /** + * @notice Deliver a message to the inbox + * @param kind Type of the message + * @param sender Address that is sending the message + * @param messageDataHash keccak256 hash of the message data + * @return The message index + */ function deliverMessageToInbox( uint8 kind, address sender, bytes32 messageDataHash ) external payable returns (uint256); + /** + * @notice Execute a call from L2 to L1 + * @param destAddr Contract to call + * @param amount ETH value to send + * @param data Calldata for the function call + * @return success True if the call was successful, false otherwise + * @return returnData Return data from the call + */ function executeCall( address destAddr, uint256 amount, bytes calldata data ) external returns (bool success, bytes memory returnData); - // These are only callable by the admin + /** + * @notice Set the address of an inbox + * @param inbox Address of the inbox + * @param enabled Whether to enable the inbox + */ function setInbox(address inbox, bool enabled) external; + /** + * @notice Set the address of an outbox + * @param inbox Address of the outbox + * @param enabled Whether to enable the outbox + */ function setOutbox(address inbox, bool enabled) external; // View functions + /** + * @notice Get the active outbox address + * @return The active outbox address + */ function activeOutbox() external view returns (address); + /** + * @notice Check if an address is an allowed inbox + * @param inbox Address to check + * @return True if the address is an allowed inbox, false otherwise + */ function allowedInboxes(address inbox) external view returns (bool); + /** + * @notice Check if an address is an allowed outbox + * @param outbox Address to check + * @return True if the address is an allowed outbox, false otherwise + */ function allowedOutboxes(address outbox) external view returns (bool); + /** + * @notice Get the inbox accumulator at a specific index + * @param index Index to query + * @return The inbox accumulator at the given index + */ function inboxAccs(uint256 index) external view returns (bytes32); + /** + * @notice Get the count of messages in the inbox + * @return Number of messages in the inbox + */ function messageCount() external view returns (uint256); } diff --git a/packages/interfaces/contracts/contracts/arbitrum/IInbox.sol b/packages/interfaces/contracts/contracts/arbitrum/IInbox.sol index f216128cf..41ed88a65 100644 --- a/packages/interfaces/contracts/contracts/arbitrum/IInbox.sol +++ b/packages/interfaces/contracts/contracts/arbitrum/IInbox.sol @@ -28,9 +28,29 @@ pragma solidity ^0.7.6 || 0.8.27; import { IBridge } from "./IBridge.sol"; import { IMessageProvider } from "./IMessageProvider.sol"; +/** + * @title Inbox Interface + * @author Edge & Node + * @notice Interface for the Arbitrum Inbox contract + */ interface IInbox is IMessageProvider { + /** + * @notice Send a message to L2 + * @param messageData Encoded data to send in the message + * @return Message number returned by the inbox + */ function sendL2Message(bytes calldata messageData) external returns (uint256); + /** + * @notice Send an unsigned transaction to L2 + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param nonce Nonce for the transaction + * @param destAddr Destination address on L2 + * @param amount Amount of ETH to send + * @param data Transaction data + * @return Message number returned by the inbox + */ function sendUnsignedTransaction( uint256 maxGas, uint256 gasPriceBid, @@ -40,6 +60,15 @@ interface IInbox is IMessageProvider { bytes calldata data ) external returns (uint256); + /** + * @notice Send a contract transaction to L2 + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param destAddr Destination address on L2 + * @param amount Amount of ETH to send + * @param data Transaction data + * @return Message number returned by the inbox + */ function sendContractTransaction( uint256 maxGas, uint256 gasPriceBid, @@ -48,6 +77,15 @@ interface IInbox is IMessageProvider { bytes calldata data ) external returns (uint256); + /** + * @notice Send an L1-funded unsigned transaction to L2 + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param nonce Nonce for the transaction + * @param destAddr Destination address on L2 + * @param data Transaction data + * @return Message number returned by the inbox + */ function sendL1FundedUnsignedTransaction( uint256 maxGas, uint256 gasPriceBid, @@ -56,6 +94,14 @@ interface IInbox is IMessageProvider { bytes calldata data ) external payable returns (uint256); + /** + * @notice Send an L1-funded contract transaction to L2 + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param destAddr Destination address on L2 + * @param data Transaction data + * @return Message number returned by the inbox + */ function sendL1FundedContractTransaction( uint256 maxGas, uint256 gasPriceBid, @@ -63,6 +109,18 @@ interface IInbox is IMessageProvider { bytes calldata data ) external payable returns (uint256); + /** + * @notice Create a retryable ticket for an L2 transaction + * @param destAddr Destination address on L2 + * @param arbTxCallValue Call value for the L2 transaction + * @param maxSubmissionCost Maximum cost for submitting the ticket + * @param submissionRefundAddress Address to refund submission cost to + * @param valueRefundAddress Address to refund excess value to + * @param maxGas Maximum gas for the L2 transaction + * @param gasPriceBid Gas price bid for the L2 transaction + * @param data Transaction data + * @return Message number returned by the inbox + */ function createRetryableTicket( address destAddr, uint256 arbTxCallValue, @@ -74,15 +132,36 @@ interface IInbox is IMessageProvider { bytes calldata data ) external payable returns (uint256); + /** + * @notice Deposit ETH to L2 + * @param maxSubmissionCost Maximum cost for submitting the deposit + * @return Message number returned by the inbox + */ function depositEth(uint256 maxSubmissionCost) external payable returns (uint256); + /** + * @notice Get the bridge contract + * @return The bridge contract address + */ function bridge() external view returns (IBridge); + /** + * @notice Pause the creation of retryable tickets + */ function pauseCreateRetryables() external; + /** + * @notice Unpause the creation of retryable tickets + */ function unpauseCreateRetryables() external; + /** + * @notice Start rewriting addresses + */ function startRewriteAddress() external; + /** + * @notice Stop rewriting addresses + */ function stopRewriteAddress() external; } diff --git a/packages/interfaces/contracts/contracts/arbitrum/IMessageProvider.sol b/packages/interfaces/contracts/contracts/arbitrum/IMessageProvider.sol index 50b674c70..98199f50c 100644 --- a/packages/interfaces/contracts/contracts/arbitrum/IMessageProvider.sol +++ b/packages/interfaces/contracts/contracts/arbitrum/IMessageProvider.sol @@ -25,8 +25,22 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Message Provider Interface + * @author Edge & Node + * @notice Interface for Arbitrum message providers + */ interface IMessageProvider { + /** + * @notice Emitted when a message is delivered to the inbox + * @param messageNum Message number + * @param data Message data + */ event InboxMessageDelivered(uint256 indexed messageNum, bytes data); + /** + * @notice Emitted when a message is delivered from origin + * @param messageNum Message number + */ event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); } diff --git a/packages/interfaces/contracts/contracts/arbitrum/IOutbox.sol b/packages/interfaces/contracts/contracts/arbitrum/IOutbox.sol index 49617a976..3727f2f6b 100644 --- a/packages/interfaces/contracts/contracts/arbitrum/IOutbox.sol +++ b/packages/interfaces/contracts/contracts/arbitrum/IOutbox.sol @@ -25,13 +25,36 @@ pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + +/** + * @title Arbitrum Outbox Interface + * @author Edge & Node + * @notice Interface for the Arbitrum outbox contract + */ interface IOutbox { + /** + * @notice Emitted when an outbox entry is created + * @param batchNum Batch number + * @param outboxEntryIndex Index of the outbox entry + * @param outputRoot Output root hash + * @param numInBatch Number of messages in the batch + */ event OutboxEntryCreated( uint256 indexed batchNum, uint256 outboxEntryIndex, bytes32 outputRoot, uint256 numInBatch ); + + /** + * @notice Emitted when an outbox transaction is executed + * @param destAddr Destination address + * @param l2Sender L2 sender address + * @param outboxEntryIndex Index of the outbox entry + * @param transactionIndex Index of the transaction + */ event OutBoxTransactionExecuted( address indexed destAddr, address indexed l2Sender, @@ -39,19 +62,53 @@ interface IOutbox { uint256 transactionIndex ); + /** + * @notice Get the L2 to L1 sender address + * @return The sender address + */ function l2ToL1Sender() external view returns (address); + /** + * @notice Get the L2 to L1 block number + * @return The block number + */ function l2ToL1Block() external view returns (uint256); + /** + * @notice Get the L2 to L1 Ethereum block number + * @return The Ethereum block number + */ function l2ToL1EthBlock() external view returns (uint256); + /** + * @notice Get the L2 to L1 timestamp + * @return The timestamp + */ function l2ToL1Timestamp() external view returns (uint256); + /** + * @notice Get the L2 to L1 batch number + * @return The batch number + */ function l2ToL1BatchNum() external view returns (uint256); + /** + * @notice Get the L2 to L1 output ID + * @return The output ID + */ function l2ToL1OutputId() external view returns (bytes32); + /** + * @notice Process outgoing messages + * @param sendsData Encoded message data + * @param sendLengths Array of message lengths + */ function processOutgoingMessages(bytes calldata sendsData, uint256[] calldata sendLengths) external; + /** + * @notice Check if an outbox entry exists + * @param batchNum Batch number to check + * @return True if the entry exists + */ function outboxEntryExists(uint256 batchNum) external view returns (bool); } diff --git a/packages/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol b/packages/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol index 3b12e578e..2a0d1a2f3 100644 --- a/packages/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol +++ b/packages/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol @@ -25,6 +25,11 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Token Gateway Interface + * @author Edge & Node + * @notice Interface for token gateways that handle cross-chain token transfers + */ interface ITokenGateway { /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated // event OutboundTransferInitiated( @@ -46,15 +51,33 @@ interface ITokenGateway { // bytes _data // ); + /** + * @notice Transfer tokens from L1 to L2 or L2 to L1 + * @param token Address of the token being transferred + * @param to Recipient address on the destination chain + * @param amount Amount of tokens to transfer + * @param maxGas Maximum gas for the transaction + * @param gasPriceBid Gas price bid for the transaction + * @param data Additional data for the transfer + * @return Transaction data + */ function outboundTransfer( address token, address to, - uint256 amunt, - uint256 maxas, - uint256 gasPiceBid, + uint256 amount, + uint256 maxGas, + uint256 gasPriceBid, bytes calldata data ) external payable returns (bytes memory); + /** + * @notice Finalize an inbound token transfer + * @param token Address of the token being transferred + * @param from Sender address on the source chain + * @param to Recipient address on the destination chain + * @param amount Amount of tokens being transferred + * @param data Additional data for the transfer + */ function finalizeInboundTransfer( address token, address from, diff --git a/packages/interfaces/contracts/contracts/base/IMulticall.sol b/packages/interfaces/contracts/contracts/base/IMulticall.sol index 10f7fa469..07f40ea36 100644 --- a/packages/interfaces/contracts/contracts/base/IMulticall.sol +++ b/packages/interfaces/contracts/contracts/base/IMulticall.sol @@ -5,6 +5,7 @@ pragma abicoder v2; /** * @title Multicall interface + * @author Edge & Node * @notice Enables calling multiple methods in a single call to the contract */ interface IMulticall { diff --git a/packages/interfaces/contracts/contracts/curation/ICuration.sol b/packages/interfaces/contracts/contracts/curation/ICuration.sol index cb6271991..91227f23a 100644 --- a/packages/interfaces/contracts/contracts/curation/ICuration.sol +++ b/packages/interfaces/contracts/contracts/curation/ICuration.sol @@ -4,7 +4,8 @@ pragma solidity ^0.7.6 || 0.8.27; /** * @title Curation Interface - * @dev Interface for the Curation contract (and L2Curation too) + * @author Edge & Node + * @notice Interface for the Curation contract (and L2Curation too) */ interface ICuration { // -- Configuration -- diff --git a/packages/interfaces/contracts/contracts/curation/IGraphCurationToken.sol b/packages/interfaces/contracts/contracts/curation/IGraphCurationToken.sol index 644593f54..d83fce16a 100644 --- a/packages/interfaces/contracts/contracts/curation/IGraphCurationToken.sol +++ b/packages/interfaces/contracts/contracts/curation/IGraphCurationToken.sol @@ -4,10 +4,29 @@ pragma solidity ^0.7.6 || 0.8.27; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +/** + * @title Graph Curation Token Interface + * @author Edge & Node + * @notice Interface for curation tokens that represent shares in subgraph curation pools + */ interface IGraphCurationToken is IERC20Upgradeable { + /** + * @notice Graph Curation Token Contract initializer. + * @param owner Address of the contract issuing this token + */ function initialize(address owner) external; + /** + * @notice Burn tokens from an address. + * @param account Address from where tokens will be burned + * @param amount Amount of tokens to burn + */ function burnFrom(address account, uint256 amount) external; + /** + * @notice Mint new tokens. + * @param to Address to send the newly minted tokens + * @param amount Amount of tokens to mint + */ function mint(address to, uint256 amount) external; } diff --git a/packages/interfaces/contracts/contracts/discovery/IGNS.sol b/packages/interfaces/contracts/contracts/discovery/IGNS.sol index 6c00ce694..c502cd7af 100644 --- a/packages/interfaces/contracts/contracts/discovery/IGNS.sol +++ b/packages/interfaces/contracts/contracts/discovery/IGNS.sol @@ -4,6 +4,8 @@ pragma solidity ^0.7.6 || 0.8.27; /** * @title Interface for GNS + * @author Edge & Node + * @notice Interface for the Graph Name System (GNS) contract */ interface IGNS { // -- Pool -- @@ -12,6 +14,13 @@ interface IGNS { * @dev The SubgraphData struct holds information about subgraphs * and their signal; both nSignal (i.e. name signal at the GNS level) * and vSignal (i.e. version signal at the Curation contract level) + * @param vSignal The token of the subgraph-deployment bonding curve + * @param nSignal The token of the subgraph bonding curve + * @param curatorNSignal Mapping of curator addresses to their name signal amounts + * @param subgraphDeploymentID The deployment ID this subgraph points to + * @param __DEPRECATED_reserveRatio Deprecated reserve ratio field + * @param disabled Whether the subgraph is disabled/deprecated + * @param withdrawableGRT Amount of GRT available for withdrawal after deprecation */ struct SubgraphData { uint256 vSignal; // The token of the subgraph-deployment bonding curve @@ -26,6 +35,8 @@ interface IGNS { /** * @dev The LegacySubgraphKey struct holds the account and sequence ID * used to generate subgraph IDs in legacy subgraphs. + * @param account The account that created the legacy subgraph + * @param accountSeqID The sequence ID for the account's subgraphs */ struct LegacySubgraphKey { address account; @@ -35,7 +46,10 @@ interface IGNS { // -- Events -- /** - * @dev Emitted when a subgraph version is updated. + * @notice Emitted when a subgraph version is updated. + * @param subgraphID The subgraph ID + * @param subgraphDeploymentID The subgraph deployment ID + * @param versionMetadata The version metadata */ event SubgraphVersionUpdated( uint256 indexed subgraphID, @@ -127,7 +141,7 @@ interface IGNS { function burnSignal(uint256 subgraphID, uint256 nSignal, uint256 tokensOutMin) external; /** - * @notice Move subgraph signal from sender to recipient + * @notice Move subgraph signal from sender to `recipient` * @param subgraphID Subgraph ID * @param recipient Address to send the signal to * @param amount The amount of nSignal to transfer @@ -169,7 +183,9 @@ interface IGNS { * @notice Calculate subgraph signal to be returned for an amount of tokens. * @param subgraphID Subgraph ID * @param tokensIn Tokens being exchanged for subgraph signal - * @return Amount of subgraph signal and curation tax + * @return Amount of subgraph signal that can be bought + * @return Amount of version signal that can be bought + * @return Amount of curation tax */ function tokensToNSignal(uint256 subgraphID, uint256 tokensIn) external view returns (uint256, uint256, uint256); @@ -178,6 +194,7 @@ interface IGNS { * @param subgraphID Subgraph ID * @param nSignalIn Subgraph signal being exchanged for tokens * @return Amount of tokens returned for an amount of subgraph signal + * @return Amount of version signal returned */ function nSignalToTokens(uint256 subgraphID, uint256 nSignalIn) external view returns (uint256, uint256); diff --git a/packages/interfaces/contracts/contracts/discovery/IServiceRegistry.sol b/packages/interfaces/contracts/contracts/discovery/IServiceRegistry.sol index b3f5377c0..1389fe015 100644 --- a/packages/interfaces/contracts/contracts/discovery/IServiceRegistry.sol +++ b/packages/interfaces/contracts/contracts/discovery/IServiceRegistry.sol @@ -2,19 +2,52 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Service Registry Interface + * @author Edge & Node + * @notice Interface for the Service Registry contract that manages indexer service information + */ interface IServiceRegistry { + /** + * @dev Indexer service information + * @param url URL of the indexer service + * @param geohash Geohash of the indexer service location + */ struct IndexerService { string url; string geohash; } + /** + * @notice Register an indexer service + * @param url URL of the indexer service + * @param geohash Geohash of the indexer service location + */ function register(string calldata url, string calldata geohash) external; + /** + * @notice Register an indexer service + * @param indexer Address of the indexer + * @param url URL of the indexer service + * @param geohash Geohash of the indexer service location + */ function registerFor(address indexer, string calldata url, string calldata geohash) external; + /** + * @notice Unregister an indexer service + */ function unregister() external; + /** + * @notice Unregister an indexer service + * @param indexer Address of the indexer + */ function unregisterFor(address indexer) external; + /** + * @notice Return the registration status of an indexer service + * @param indexer Address of the indexer + * @return True if the indexer service is registered + */ function isRegistered(address indexer) external view returns (bool); } diff --git a/packages/interfaces/contracts/contracts/discovery/ISubgraphNFT.sol b/packages/interfaces/contracts/contracts/discovery/ISubgraphNFT.sol index 0f7d5a73d..a3a9b659e 100644 --- a/packages/interfaces/contracts/contracts/discovery/ISubgraphNFT.sol +++ b/packages/interfaces/contracts/contracts/discovery/ISubgraphNFT.sol @@ -4,22 +4,64 @@ pragma solidity ^0.7.6 || 0.8.27; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +/** + * @title Subgraph NFT Interface + * @author Edge & Node + * @notice Interface for the Subgraph NFT contract that represents subgraph ownership + */ interface ISubgraphNFT is IERC721 { // -- Config -- + /** + * @notice Set the minter allowed to perform actions on the NFT + * @dev Minter can mint, burn and update the metadata + * @param minter Address of the allowed minter + */ function setMinter(address minter) external; + /** + * @notice Set the token descriptor contract + * @dev Token descriptor can be zero. If set, it must be a contract + * @param tokenDescriptor Address of the contract that creates the NFT token URI + */ function setTokenDescriptor(address tokenDescriptor) external; + /** + * @notice Set the base URI + * @dev Can be set to empty + * @param baseURI Base URI to use to build the token URI + */ function setBaseURI(string memory baseURI) external; // -- Actions -- + /** + * @notice Mint `tokenId` and transfers it to `to` + * @dev `tokenId` must not exist and `to` cannot be the zero address + * @param to Address receiving the minted NFT + * @param tokenId ID of the NFT + */ function mint(address to, uint256 tokenId) external; + /** + * @notice Burn `tokenId` + * @dev The approval is cleared when the token is burned + * @param tokenId ID of the NFT + */ function burn(uint256 tokenId) external; + /** + * @notice Set the metadata for a subgraph represented by `tokenId` + * @dev `tokenId` must exist + * @param tokenId ID of the NFT + * @param subgraphMetadata IPFS hash for the metadata + */ function setSubgraphMetadata(uint256 tokenId, bytes32 subgraphMetadata) external; + /** + * @notice Returns the Uniform Resource Identifier (URI) for `tokenId` token + * @param tokenId ID of the NFT + * @return The URI for the token + */ function tokenURI(uint256 tokenId) external view returns (string memory); } diff --git a/packages/interfaces/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol b/packages/interfaces/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol index 429e22898..3217b50df 100644 --- a/packages/interfaces/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol +++ b/packages/interfaces/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol @@ -2,7 +2,11 @@ pragma solidity ^0.7.6 || 0.8.27; -/// @title Describes subgraph NFT tokens via URI +/** + * @title Describes subgraph NFT tokens via URI + * @author Edge & Node + * @notice Interface for describing subgraph NFT tokens via URI + */ interface ISubgraphNFTDescriptor { /// @notice Produces the URI describing a particular token ID for a Subgraph /// @dev Note this URI may be data: URI with the JSON contents directly inlined diff --git a/packages/interfaces/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol b/packages/interfaces/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol index c4bdf1ba8..18b084a18 100644 --- a/packages/interfaces/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol +++ b/packages/interfaces/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol @@ -2,8 +2,25 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Ethereum DID Registry Interface + * @author Edge & Node + * @notice Interface for the Ethereum DID Registry contract + */ interface IEthereumDIDRegistry { + /** + * @notice Get the owner of an identity + * @param identity The identity address + * @return The address of the identity owner + */ function identityOwner(address identity) external view returns (address); + /** + * @notice Set an attribute for an identity + * @param identity The identity address + * @param name The attribute name + * @param value The attribute value + * @param validity The validity period in seconds + */ function setAttribute(address identity, bytes32 name, bytes calldata value, uint256 validity) external; } diff --git a/packages/interfaces/contracts/contracts/disputes/IDisputeManager.sol b/packages/interfaces/contracts/contracts/disputes/IDisputeManager.sol index eb86be5d0..a9a1a94e5 100644 --- a/packages/interfaces/contracts/contracts/disputes/IDisputeManager.sol +++ b/packages/interfaces/contracts/contracts/disputes/IDisputeManager.sol @@ -3,15 +3,26 @@ pragma solidity >=0.6.12 <0.8.0 || 0.8.27; pragma abicoder v2; +/** + * @title Dispute Manager Interface + * @author Edge & Node + * @notice Interface for the Dispute Manager contract that handles indexing and query disputes + */ interface IDisputeManager { // -- Dispute -- + /** + * @dev Types of disputes that can be created + */ enum DisputeType { Null, IndexingDispute, QueryDispute } + /** + * @dev Status of a dispute + */ enum DisputeStatus { Null, Accepted, @@ -20,7 +31,15 @@ interface IDisputeManager { Pending } - // Disputes contain info necessary for the Arbitrator to verify and resolve + /** + * @dev Disputes contain info necessary for the Arbitrator to verify and resolve + * @param indexer Address of the indexer being disputed + * @param fisherman Address of the challenger creating the dispute + * @param deposit Amount of tokens staked as deposit + * @param relatedDisputeID ID of related dispute (for conflicting attestations) + * @param disputeType Type of dispute (Query or Indexing) + * @param status Current status of the dispute + */ struct Dispute { address indexer; address fisherman; @@ -32,14 +51,27 @@ interface IDisputeManager { // -- Attestation -- - // Receipt content sent from indexer in response to request + /** + * @dev Receipt content sent from indexer in response to request + * @param requestCID Content ID of the request + * @param responseCID Content ID of the response + * @param subgraphDeploymentID ID of the subgraph deployment + */ struct Receipt { bytes32 requestCID; bytes32 responseCID; bytes32 subgraphDeploymentID; } - // Attestation sent from indexer in response to a request + /** + * @dev Attestation sent from indexer in response to a request + * @param requestCID Content ID of the request + * @param responseCID Content ID of the response + * @param subgraphDeploymentID ID of the subgraph deployment + * @param r R component of the signature + * @param s S component of the signature + * @param v Recovery ID of the signature + */ struct Attestation { bytes32 requestCID; bytes32 responseCID; @@ -51,41 +83,121 @@ interface IDisputeManager { // -- Configuration -- + /** + * @dev Set the arbitrator address. + * @notice Update the arbitrator to `arbitrator` + * @param arbitrator The address of the arbitration contract or party + */ function setArbitrator(address arbitrator) external; + /** + * @dev Set the minimum deposit required to create a dispute. + * @notice Update the minimum deposit to `minimumDeposit` Graph Tokens + * @param minimumDeposit The minimum deposit in Graph Tokens + */ function setMinimumDeposit(uint256 minimumDeposit) external; + /** + * @dev Set the percent reward that the fisherman gets when slashing occurs. + * @notice Update the reward percentage to `percentage` + * @param percentage Reward as a percentage of indexer stake + */ function setFishermanRewardPercentage(uint32 percentage) external; + /** + * @notice Set the percentage used for slashing indexers. + * @param qryPercentage Percentage slashing for query disputes + * @param idxPercentage Percentage slashing for indexing disputes + */ function setSlashingPercentage(uint32 qryPercentage, uint32 idxPercentage) external; // -- Getters -- + /** + * @notice Check if a dispute has been created + * @param disputeID Dispute identifier + * @return True if the dispute exists + */ function isDisputeCreated(bytes32 disputeID) external view returns (bool); + /** + * @notice Encode a receipt into a hash for EIP-712 signature verification + * @param receipt The receipt to encode + * @return The encoded hash + */ function encodeHashReceipt(Receipt memory receipt) external view returns (bytes32); + /** + * @notice Check if two attestations are conflicting + * @param attestation1 First attestation + * @param attestation2 Second attestation + * @return True if attestations are conflicting + */ function areConflictingAttestations( Attestation memory attestation1, Attestation memory attestation2 ) external pure returns (bool); + /** + * @notice Get the indexer address from an attestation + * @param attestation The attestation to extract indexer from + * @return The indexer address + */ function getAttestationIndexer(Attestation memory attestation) external view returns (address); // -- Dispute -- + /** + * @notice Create a query dispute for the arbitrator to resolve. + * This function is called by a fisherman that will need to `deposit` at + * least `minimumDeposit` GRT tokens. + * @param attestationData Attestation bytes submitted by the fisherman + * @param deposit Amount of tokens staked as deposit + * @return The dispute ID + */ function createQueryDispute(bytes calldata attestationData, uint256 deposit) external returns (bytes32); + /** + * @notice Create query disputes for two conflicting attestations. + * A conflicting attestation is a proof presented by two different indexers + * where for the same request on a subgraph the response is different. + * For this type of dispute the submitter is not required to present a deposit + * as one of the attestation is considered to be right. + * Two linked disputes will be created and if the arbitrator resolve one, the other + * one will be automatically resolved. + * @param attestationData1 First attestation data submitted + * @param attestationData2 Second attestation data submitted + * @return First dispute ID + * @return Second dispute ID + */ function createQueryDisputeConflict( bytes calldata attestationData1, bytes calldata attestationData2 ) external returns (bytes32, bytes32); + /** + * @notice Create an indexing dispute + * @param allocationID Allocation ID being disputed + * @param deposit Deposit amount for the dispute + * @return The dispute ID + */ function createIndexingDispute(address allocationID, uint256 deposit) external returns (bytes32); + /** + * @notice Accept a dispute (arbitrator only) + * @param disputeID ID of the dispute to accept + */ function acceptDispute(bytes32 disputeID) external; + /** + * @notice Reject a dispute (arbitrator only) + * @param disputeID ID of the dispute to reject + */ function rejectDispute(bytes32 disputeID) external; + /** + * @notice Draw a dispute (arbitrator only) + * @param disputeID ID of the dispute to draw + */ function drawDispute(bytes32 disputeID) external; } diff --git a/packages/interfaces/contracts/contracts/epochs/IEpochManager.sol b/packages/interfaces/contracts/contracts/epochs/IEpochManager.sol index 06f3fd70a..b7f3e0ab5 100644 --- a/packages/interfaces/contracts/contracts/epochs/IEpochManager.sol +++ b/packages/interfaces/contracts/contracts/epochs/IEpochManager.sol @@ -2,30 +2,77 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Epoch Manager Interface + * @author Edge & Node + * @notice Interface for the Epoch Manager contract that handles protocol epochs + */ interface IEpochManager { // -- Configuration -- + /** + * @notice Set epoch length to `epochLength` blocks + * @param epochLength Epoch length in blocks + */ function setEpochLength(uint256 epochLength) external; // -- Epochs + /** + * @dev Run a new epoch, should be called once at the start of any epoch. + * @notice Perform state changes for the current epoch + */ function runEpoch() external; // -- Getters -- + /** + * @notice Check if the current epoch has been run + * @return True if current epoch has been run, false otherwise + */ function isCurrentEpochRun() external view returns (bool); + /** + * @notice Get the current block number + * @return Current block number + */ function blockNum() external view returns (uint256); + /** + * @notice Get the hash of a specific block + * @param block Block number to get hash for + * @return Block hash + */ function blockHash(uint256 block) external view returns (bytes32); + /** + * @notice Get the current epoch number + * @return Current epoch number + */ function currentEpoch() external view returns (uint256); + /** + * @notice Get the block number when the current epoch started + * @return Block number of current epoch start + */ function currentEpochBlock() external view returns (uint256); + /** + * @notice Get the number of blocks since the current epoch started + * @return Number of blocks since current epoch start + */ function currentEpochBlockSinceStart() external view returns (uint256); + /** + * @notice Get the number of epochs since a given epoch + * @param epoch Epoch to calculate from + * @return Number of epochs since the given epoch + */ function epochsSince(uint256 epoch) external view returns (uint256); + /** + * @notice Get the number of epochs since the last epoch length update + * @return Number of epochs since last update + */ function epochsSinceUpdate() external view returns (uint256); } diff --git a/packages/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol b/packages/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol index 86bc654a0..f96672a49 100644 --- a/packages/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol +++ b/packages/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol @@ -2,12 +2,18 @@ /** * @title Interface for contracts that can receive callhooks through the Arbitrum GRT bridge - * @dev Any contract that can receive a callhook on L2, sent through the bridge from L1, must + * @author Edge & Node + * @notice Any contract that can receive a callhook on L2, sent through the bridge from L1, must * be allowlisted by the governor, but also implement this interface that contains * the function that will actually be called by the L2GraphTokenGateway. */ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Callhook Receiver Interface + * @author Edge & Node + * @notice Interface for contracts that can receive tokens with callhook from the bridge + */ interface ICallhookReceiver { /** * @notice Receive tokens with a callhook from the bridge diff --git a/packages/interfaces/contracts/contracts/governance/IController.sol b/packages/interfaces/contracts/contracts/governance/IController.sol index 2f6d3b6b7..d13eee1b0 100644 --- a/packages/interfaces/contracts/contracts/governance/IController.sol +++ b/packages/interfaces/contracts/contracts/governance/IController.sol @@ -2,28 +2,78 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Controller Interface + * @author Edge & Node + * @notice Interface for the Controller contract that manages protocol governance and contract registry + */ interface IController { + /** + * @notice Return the governor address + * @return The governor address + */ function getGovernor() external view returns (address); // -- Registry -- + /** + * @notice Register contract id and mapped address + * @param id Contract id (keccak256 hash of contract name) + * @param contractAddress Contract address + */ function setContractProxy(bytes32 id, address contractAddress) external; + /** + * @notice Unregister a contract address + * @param id Contract id (keccak256 hash of contract name) + */ function unsetContractProxy(bytes32 id) external; + /** + * @notice Update contract's controller + * @param id Contract id (keccak256 hash of contract name) + * @param controller Controller address + */ function updateController(bytes32 id, address controller) external; + /** + * @notice Get contract proxy address by its id + * @param id Contract id + * @return Address of the proxy contract for the provided id + */ function getContractProxy(bytes32 id) external view returns (address); // -- Pausing -- + /** + * @notice Change the partial paused state of the contract + * Partial pause is intended as a partial pause of the protocol + * @param partialPaused True if the contracts should be (partially) paused, false otherwise + */ function setPartialPaused(bool partialPaused) external; + /** + * @notice Change the paused state of the contract + * Full pause most of protocol functions + * @param paused True if the contracts should be paused, false otherwise + */ function setPaused(bool paused) external; + /** + * @notice Change the Pause Guardian + * @param newPauseGuardian The address of the new Pause Guardian + */ function setPauseGuardian(address newPauseGuardian) external; + /** + * @notice Return whether the protocol is paused + * @return True if the protocol is paused + */ function paused() external view returns (bool); + /** + * @notice Return whether the protocol is partially paused + * @return True if the protocol is partially paused + */ function partialPaused() external view returns (bool); } diff --git a/packages/interfaces/contracts/contracts/governance/IGoverned.sol b/packages/interfaces/contracts/contracts/governance/IGoverned.sol index 200f74ecf..1f8967e40 100644 --- a/packages/interfaces/contracts/contracts/governance/IGoverned.sol +++ b/packages/interfaces/contracts/contracts/governance/IGoverned.sol @@ -3,13 +3,22 @@ pragma solidity ^0.7.6 || 0.8.27; /** * @title IGoverned - * @dev Interface for the Governed contract. + * @author Edge & Node + * @notice Interface for governed contracts */ interface IGoverned { // -- State getters -- + /** + * @notice Get the current governor address + * @return The address of the current governor + */ function governor() external view returns (address); + /** + * @notice Get the pending governor address + * @return The address of the pending governor + */ function pendingGovernor() external view returns (address); // -- External functions -- @@ -27,7 +36,17 @@ interface IGoverned { // -- Events -- + /** + * @notice Emitted when a new pending governor is set + * @param from The address of the current governor + * @param to The address of the new pending governor + */ event NewPendingOwnership(address indexed from, address indexed to); + /** + * @notice Emitted when governance is transferred to a new governor + * @param from The address of the previous governor + * @param to The address of the new governor + */ event NewOwnership(address indexed from, address indexed to); } diff --git a/packages/interfaces/contracts/contracts/governance/IManaged.sol b/packages/interfaces/contracts/contracts/governance/IManaged.sol index 9bfe57083..5e862ba1d 100644 --- a/packages/interfaces/contracts/contracts/governance/IManaged.sol +++ b/packages/interfaces/contracts/contracts/governance/IManaged.sol @@ -6,7 +6,8 @@ import { IController } from "./IController.sol"; /** * @title Managed Interface - * @dev Interface for contracts that can be managed by a controller. + * @author Edge & Node + * @notice Interface for contracts that can be managed by a controller. */ interface IManaged { /** diff --git a/packages/interfaces/contracts/contracts/l2/curation/IL2Curation.sol b/packages/interfaces/contracts/contracts/l2/curation/IL2Curation.sol index 18f6882cc..6051d444a 100644 --- a/packages/interfaces/contracts/contracts/l2/curation/IL2Curation.sol +++ b/packages/interfaces/contracts/contracts/l2/curation/IL2Curation.sol @@ -4,6 +4,8 @@ pragma solidity ^0.7.6 || 0.8.27; /** * @title Interface of the L2 Curation contract. + * @author Edge & Node + * @notice Interface for the L2 Curation contract that handles curation on Layer 2 */ interface IL2Curation { /** diff --git a/packages/interfaces/contracts/contracts/l2/discovery/IL2GNS.sol b/packages/interfaces/contracts/contracts/l2/discovery/IL2GNS.sol index 12a1aeec9..43fd6dffd 100644 --- a/packages/interfaces/contracts/contracts/l2/discovery/IL2GNS.sol +++ b/packages/interfaces/contracts/contracts/l2/discovery/IL2GNS.sol @@ -6,8 +6,15 @@ import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; /** * @title Interface for the L2GNS contract. + * @author Edge & Node + * @notice Interface for the L2 Graph Name System (GNS) contract */ interface IL2GNS is ICallhookReceiver { + /** + * @dev Message codes for L1 to L2 communication + * @param RECEIVE_SUBGRAPH_CODE Code for receiving subgraph transfers + * @param RECEIVE_CURATOR_BALANCE_CODE Code for receiving curator balance transfers + */ enum L1MessageCodes { RECEIVE_SUBGRAPH_CODE, RECEIVE_CURATOR_BALANCE_CODE @@ -16,6 +23,10 @@ interface IL2GNS is ICallhookReceiver { /** * @dev The SubgraphL2TransferData struct holds information * about a subgraph related to its transfer from L1 to L2. + * @param tokens GRT that will be sent to L2 to mint signal + * @param curatorBalanceClaimed True for curators whose balance has been claimed in L2 + * @param l2Done Transfer finished on L2 side + * @param subgraphReceivedOnL2BlockNumber Block number when the subgraph was received on L2 */ struct SubgraphL2TransferData { uint256 tokens; // GRT that will be sent to L2 to mint signal diff --git a/packages/interfaces/contracts/contracts/l2/gateway/IL2GraphTokenGateway.sol b/packages/interfaces/contracts/contracts/l2/gateway/IL2GraphTokenGateway.sol index b1e088212..d43e19a47 100644 --- a/packages/interfaces/contracts/contracts/l2/gateway/IL2GraphTokenGateway.sol +++ b/packages/interfaces/contracts/contracts/l2/gateway/IL2GraphTokenGateway.sol @@ -2,6 +2,14 @@ pragma solidity ^0.7.6 || 0.8.27; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + +/** + * @title IL2GraphTokenGateway + * @author Edge & Node + * @notice Interface for the L2 Graph Token Gateway contract that handles token bridging on L2 + */ interface IL2GraphTokenGateway { // Structs struct OutboundCalldata { @@ -10,7 +18,24 @@ interface IL2GraphTokenGateway { } // Events + /** + * @notice Emitted when a deposit from L1 is finalized on L2 + * @param l1Token The L1 token address + * @param from The sender address on L1 + * @param to The recipient address on L2 + * @param amount The amount of tokens deposited + */ event DepositFinalized(address indexed l1Token, address indexed from, address indexed to, uint256 amount); + + /** + * @notice Emitted when a withdrawal from L2 to L1 is initiated + * @param l1Token The L1 token address + * @param from The sender address on L2 + * @param to The recipient address on L1 + * @param l2ToL1Id The L2 to L1 message ID + * @param exitNum The exit number + * @param amount The amount of tokens withdrawn + */ event WithdrawalInitiated( address l1Token, address indexed from, @@ -19,19 +44,58 @@ interface IL2GraphTokenGateway { uint256 exitNum, uint256 amount ); + + /** + * @notice Emitted when the L2 router address is set + * @param l2Router The new L2 router address + */ event L2RouterSet(address l2Router); + + /** + * @notice Emitted when the L1 token address is set + * @param l1GRT The L1 GRT token address + */ event L1TokenAddressSet(address l1GRT); + + /** + * @notice Emitted when the L1 counterpart address is set + * @param l1Counterpart The L1 counterpart gateway address + */ event L1CounterpartAddressSet(address l1Counterpart); // Functions + /** + * @notice Initialize the gateway contract + * @param controller The controller contract address + */ function initialize(address controller) external; + /** + * @notice Set the L2 router address + * @param l2Router The L2 router contract address + */ function setL2Router(address l2Router) external; + /** + * @notice Set the L1 token address + * @param l1GRT The L1 GRT token contract address + */ function setL1TokenAddress(address l1GRT) external; + /** + * @notice Set the L1 counterpart gateway address + * @param l1Counterpart The L1 counterpart gateway contract address + */ function setL1CounterpartAddress(address l1Counterpart) external; + /** + * @notice Transfer tokens from L2 to L1 + * @param l1Token The L1 token address + * @param to The recipient address on L1 + * @param amount The amount of tokens to transfer + * @param data Additional data for the transfer + * @return The encoded outbound transfer data + */ function outboundTransfer( address l1Token, address to, @@ -39,6 +103,14 @@ interface IL2GraphTokenGateway { bytes calldata data ) external returns (bytes memory); + /** + * @notice Finalize an inbound transfer from L1 to L2 + * @param l1Token The L1 token address + * @param from The sender address on L1 + * @param to The recipient address on L2 + * @param amount The amount of tokens to transfer + * @param data Additional data for the transfer + */ function finalizeInboundTransfer( address l1Token, address from, @@ -47,6 +119,16 @@ interface IL2GraphTokenGateway { bytes calldata data ) external payable; + /** + * @notice Transfer tokens from L2 to L1 (overloaded version with unused parameters) + * @param l1Token The L1 token address + * @param to The recipient address on L1 + * @param amount The amount of tokens to transfer + * @param unused1 Unused parameter for compatibility + * @param unused2 Unused parameter for compatibility + * @param data Additional data for the transfer + * @return The encoded outbound transfer data + */ function outboundTransfer( address l1Token, address to, @@ -56,8 +138,22 @@ interface IL2GraphTokenGateway { bytes calldata data ) external payable returns (bytes memory); + /** + * @notice Calculate the L2 token address for a given L1 token + * @param l1ERC20 The L1 token address + * @return The corresponding L2 token address + */ function calculateL2TokenAddress(address l1ERC20) external view returns (address); + /** + * @notice Get the encoded calldata for an outbound transfer + * @param token The token address + * @param from The sender address + * @param to The recipient address + * @param amount The amount of tokens + * @param data Additional transfer data + * @return The encoded calldata + */ function getOutboundCalldata( address token, address from, diff --git a/packages/interfaces/contracts/contracts/l2/staking/IL2Staking.sol b/packages/interfaces/contracts/contracts/l2/staking/IL2Staking.sol index 0b1718571..4290e5842 100644 --- a/packages/interfaces/contracts/contracts/l2/staking/IL2Staking.sol +++ b/packages/interfaces/contracts/contracts/l2/staking/IL2Staking.sol @@ -9,6 +9,7 @@ import { IL2StakingTypes } from "./IL2StakingTypes.sol"; /** * @title Interface for the L2 Staking contract + * @author Edge & Node * @notice This is the interface that should be used when interacting with the L2 Staking contract. * It extends the IStaking interface with the functions that are specific to L2, adding the callhook receiver * to receive transferred stake and delegation from L1. diff --git a/packages/interfaces/contracts/contracts/l2/staking/IL2StakingBase.sol b/packages/interfaces/contracts/contracts/l2/staking/IL2StakingBase.sol index f5c33c2d0..7f861db39 100644 --- a/packages/interfaces/contracts/contracts/l2/staking/IL2StakingBase.sol +++ b/packages/interfaces/contracts/contracts/l2/staking/IL2StakingBase.sol @@ -2,13 +2,23 @@ pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; /** * @title Base interface for the L2Staking contract. + * @author Edge & Node * @notice This interface is used to define the callhook receiver interface that is implemented by L2Staking. * @dev Note it includes only the L2-specific functionality, not the full IStaking interface. */ interface IL2StakingBase is ICallhookReceiver { + /** + * @notice Emitted when transferred delegation is returned to a delegator + * @param indexer Address of the indexer + * @param delegator Address of the delegator + * @param amount Amount of delegation returned + */ event TransferredDelegationReturnedToDelegator(address indexed indexer, address indexed delegator, uint256 amount); } diff --git a/packages/interfaces/contracts/contracts/l2/staking/IL2StakingTypes.sol b/packages/interfaces/contracts/contracts/l2/staking/IL2StakingTypes.sol index 500694e89..722a2dbf4 100644 --- a/packages/interfaces/contracts/contracts/l2/staking/IL2StakingTypes.sol +++ b/packages/interfaces/contracts/contracts/l2/staking/IL2StakingTypes.sol @@ -2,6 +2,11 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title IL2StakingTypes + * @author Edge & Node + * @notice Interface defining types and enums used by L2 staking contracts + */ interface IL2StakingTypes { /// @dev Message codes for the L1 -> L2 bridge callhook enum L1MessageCodes { diff --git a/packages/interfaces/contracts/contracts/l2/token/IL2GraphToken.sol b/packages/interfaces/contracts/contracts/l2/token/IL2GraphToken.sol index 92b12946d..3bcee6c5f 100644 --- a/packages/interfaces/contracts/contracts/l2/token/IL2GraphToken.sol +++ b/packages/interfaces/contracts/contracts/l2/token/IL2GraphToken.sol @@ -1,20 +1,63 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IGraphToken } from "../../token/IGraphToken.sol"; +/** + * @title IL2GraphToken + * @author Edge & Node + * @notice Interface for the L2 Graph Token contract that extends IGraphToken with L2-specific functionality + */ interface IL2GraphToken is IGraphToken { // Events + /** + * @notice Emitted when tokens are minted through the bridge + * @param account The account that received the minted tokens + * @param amount The amount of tokens minted + */ event BridgeMinted(address indexed account, uint256 amount); + + /** + * @notice Emitted when tokens are burned through the bridge + * @param account The account from which tokens were burned + * @param amount The amount of tokens burned + */ event BridgeBurned(address indexed account, uint256 amount); + + /** + * @notice Emitted when the gateway address is set + * @param gateway The new gateway address + */ event GatewaySet(address gateway); + + /** + * @notice Emitted when the L1 address is set + * @param l1Address The new L1 address + */ event L1AddressSet(address l1Address); // Public state variables (view functions) + /** + * @notice Get the gateway contract address + * @return The address of the gateway contract + */ function gateway() external view returns (address); + + /** + * @notice Get the L1 contract address + * @return The address of the L1 contract + */ function l1Address() external view returns (address); // Functions + + /** + * @notice Initialize the L2 token contract + * @param owner The owner address for the contract + */ function initialize(address owner) external; /** diff --git a/packages/interfaces/contracts/contracts/rewards/ILegacyRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/ILegacyRewardsManager.sol index 7ea628e20..57bb9ef1b 100644 --- a/packages/interfaces/contracts/contracts/rewards/ILegacyRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/ILegacyRewardsManager.sol @@ -2,6 +2,16 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title ILegacyRewardsManager + * @author Edge & Node + * @notice Interface for the legacy rewards manager contract + */ interface ILegacyRewardsManager { + /** + * @notice Get the accumulated rewards for a given allocation + * @param allocationID The allocation identifier + * @return The amount of accumulated rewards + */ function getRewards(address allocationID) external view returns (uint256); } diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol index 0f1ed9d8f..075654619 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol @@ -2,9 +2,14 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Rewards Issuer Interface + * @author Edge & Node + * @notice Interface for contracts that issue rewards based on allocation data + */ interface IRewardsIssuer { /** - * @dev Get allocation data to calculate rewards issuance + * @notice Get allocation data to calculate rewards issuance * * @param allocationId The allocation Id * @return isActive Whether the allocation is active or not diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index 32b618f60..72a73e19b 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -1,10 +1,19 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || ^0.8.0; +/** + * @title IRewardsManager + * @author Edge & Node + * @notice Interface for the RewardsManager contract that handles reward distribution + */ interface IRewardsManager { /** - * @dev Stores accumulated rewards and snapshots related to a particular SubgraphDeployment. + * @dev Stores accumulated rewards and snapshots related to a particular SubgraphDeployment + * @param accRewardsForSubgraph Accumulated rewards for the subgraph + * @param accRewardsForSubgraphSnapshot Snapshot of accumulated rewards for the subgraph + * @param accRewardsPerSignalSnapshot Snapshot of accumulated rewards per signal + * @param accRewardsPerAllocatedToken Accumulated rewards per allocated token */ struct Subgraph { uint256 accRewardsForSubgraph; @@ -15,43 +24,128 @@ interface IRewardsManager { // -- Config -- + /** + * @notice Set the issuance per block for rewards distribution + * @param issuancePerBlock The amount of tokens to issue per block + */ function setIssuancePerBlock(uint256 issuancePerBlock) external; + /** + * @notice Sets the minimum signaled tokens on a subgraph to start accruing rewards + * @dev Can be set to zero which means that this feature is not being used + * @param minimumSubgraphSignal Minimum signaled tokens + */ function setMinimumSubgraphSignal(uint256 minimumSubgraphSignal) external; + /** + * @notice Set the subgraph service address + * @param subgraphService Address of the subgraph service contract + */ function setSubgraphService(address subgraphService) external; // -- Denylist -- + /** + * @notice Set the subgraph availability oracle address + * @param subgraphAvailabilityOracle The address of the subgraph availability oracle + */ function setSubgraphAvailabilityOracle(address subgraphAvailabilityOracle) external; + /** + * @notice Set the denied status for a subgraph deployment + * @param subgraphDeploymentID The subgraph deployment ID + * @param deny True to deny, false to allow + */ function setDenied(bytes32 subgraphDeploymentID, bool deny) external; + /** + * @notice Check if a subgraph deployment is denied + * @param subgraphDeploymentID The subgraph deployment ID to check + * @return True if the subgraph is denied, false otherwise + */ function isDenied(bytes32 subgraphDeploymentID) external view returns (bool); // -- Getters -- + /** + * @notice Gets the issuance of rewards per signal since last updated + * @return newly accrued rewards per signal since last update + */ function getNewRewardsPerSignal() external view returns (uint256); + /** + * @notice Gets the currently accumulated rewards per signal + * @return Currently accumulated rewards per signal + */ function getAccRewardsPerSignal() external view returns (uint256); + /** + * @notice Get the accumulated rewards for a specific subgraph + * @param subgraphDeploymentID The subgraph deployment ID + * @return The accumulated rewards for the subgraph + */ function getAccRewardsForSubgraph(bytes32 subgraphDeploymentID) external view returns (uint256); + /** + * @notice Gets the accumulated rewards per allocated token for the subgraph + * @param subgraphDeploymentID Subgraph deployment + * @return Accumulated rewards per allocated token for the subgraph + * @return Accumulated rewards for subgraph + */ function getAccRewardsPerAllocatedToken(bytes32 subgraphDeploymentID) external view returns (uint256, uint256); + /** + * @notice Calculate current rewards for a given allocation on demand + * @param rewardsIssuer The rewards issuer contract + * @param allocationID Allocation + * @return Rewards amount for an allocation + */ function getRewards(address rewardsIssuer, address allocationID) external view returns (uint256); + /** + * @notice Calculate rewards based on tokens and accumulated rewards per allocated token + * @param tokens The number of tokens allocated + * @param accRewardsPerAllocatedToken The accumulated rewards per allocated token + * @return The calculated rewards amount + */ function calcRewards(uint256 tokens, uint256 accRewardsPerAllocatedToken) external pure returns (uint256); // -- Updates -- + /** + * @notice Updates the accumulated rewards per signal and save checkpoint block number + * @dev Must be called before `issuancePerBlock` or `total signalled GRT` changes. + * Called from the Curation contract on mint() and burn() + * @return Accumulated rewards per signal + */ function updateAccRewardsPerSignal() external returns (uint256); + /** + * @notice Pull rewards from the contract for a particular allocation + * @dev This function can only be called by the Staking contract. + * This function will mint the necessary tokens to reward based on the inflation calculation. + * @param allocationID Allocation + * @return Assigned rewards amount + */ function takeRewards(address allocationID) external returns (uint256); // -- Hooks -- + /** + * @notice Triggers an update of rewards for a subgraph + * @dev Must be called before `signalled GRT` on a subgraph changes. + * Hook called from the Curation contract on mint() and burn() + * @param subgraphDeploymentID Subgraph deployment + * @return Accumulated rewards for subgraph + */ function onSubgraphSignalUpdate(bytes32 subgraphDeploymentID) external returns (uint256); + /** + * @notice Triggers an update of rewards for a subgraph + * @dev Must be called before allocation on a subgraph changes. + * Hook called from the Staking contract on allocate() and close() + * @param subgraphDeploymentID Subgraph deployment + * @return Accumulated rewards per allocated token for a subgraph + */ function onSubgraphAllocationUpdate(bytes32 subgraphDeploymentID) external returns (uint256); } diff --git a/packages/interfaces/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol b/packages/interfaces/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol index aca3b0ffd..d59f9f391 100644 --- a/packages/interfaces/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol +++ b/packages/interfaces/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol @@ -5,7 +5,8 @@ pragma abicoder v2; /** * @title Interface for the L1GraphTokenLockTransferTool contract - * @dev This interface defines the function to get the L2 wallet address for a given L1 token lock wallet. + * @author Edge & Node + * @notice This interface defines the function to get the L2 wallet address for a given L1 token lock wallet. * The Transfer Tool contract is implemented in the token-distribution repo: https://github.com/graphprotocol/token-distribution/pull/64 * and is only included here to provide support in L1Staking for the transfer of stake and delegation * owned by token lock contracts. See GIP-0046 for details: https://forum.thegraph.com/t/4023 diff --git a/packages/interfaces/contracts/contracts/staking/IL1Staking.sol b/packages/interfaces/contracts/contracts/staking/IL1Staking.sol index fd7859220..62dcf9c77 100644 --- a/packages/interfaces/contracts/contracts/staking/IL1Staking.sol +++ b/packages/interfaces/contracts/contracts/staking/IL1Staking.sol @@ -8,6 +8,7 @@ import { IL1StakingBase } from "./IL1StakingBase.sol"; /** * @title Interface for the L1 Staking contract + * @author Edge & Node * @notice This is the interface that should be used when interacting with the L1 Staking contract. * It extends the IStaking interface with the functions that are specific to L1, adding the transfer tools * to send stake and delegation to L2. diff --git a/packages/interfaces/contracts/contracts/staking/IL1StakingBase.sol b/packages/interfaces/contracts/contracts/staking/IL1StakingBase.sol index 9ed037258..a00b6e3cb 100644 --- a/packages/interfaces/contracts/contracts/staking/IL1StakingBase.sol +++ b/packages/interfaces/contracts/contracts/staking/IL1StakingBase.sol @@ -3,23 +3,39 @@ pragma solidity ^0.7.6 || 0.8.27; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IL1GraphTokenLockTransferTool } from "./IL1GraphTokenLockTransferTool.sol"; /** * @title Base interface for the L1Staking contract. + * @author Edge & Node * @notice This interface is used to define the transfer tools that are implemented in L1Staking. * @dev Note it includes only the L1-specific functionality, not the full IStaking interface. */ interface IL1StakingBase { - /// @dev Emitted when an indexer transfers their stake to L2. - /// This can happen several times as indexers can transfer partial stake. + /** + * @notice Emitted when an indexer transfers their stake to L2. + * This can happen several times as indexers can transfer partial stake. + * @param indexer Address of the indexer on L1 + * @param l2Indexer Address of the indexer on L2 + * @param transferredStakeTokens Amount of stake tokens transferred + */ event IndexerStakeTransferredToL2( address indexed indexer, address indexed l2Indexer, uint256 transferredStakeTokens ); - /// @dev Emitted when a delegator transfers their delegation to L2 + /** + * @notice Emitted when a delegator transfers their delegation to L2 + * @param delegator Address of the delegator on L1 + * @param l2Delegator Address of the delegator on L2 + * @param indexer Address of the indexer on L1 + * @param l2Indexer Address of the indexer on L2 + * @param transferredDelegationTokens Amount of delegation tokens transferred + */ event DelegationTransferredToL2( address indexed delegator, address indexed l2Delegator, @@ -28,10 +44,17 @@ interface IL1StakingBase { uint256 transferredDelegationTokens ); - /// @dev Emitted when the L1GraphTokenLockTransferTool is set + /** + * @notice Emitted when the L1GraphTokenLockTransferTool is set + * @param l1GraphTokenLockTransferTool Address of the L1GraphTokenLockTransferTool contract + */ event L1GraphTokenLockTransferToolSet(address l1GraphTokenLockTransferTool); - /// @dev Emitted when a delegator unlocks their tokens ahead of time because the indexer has transferred to L2 + /** + * @notice Emitted when a delegator unlocks their tokens ahead of time because the indexer has transferred to L2 + * @param indexer Address of the indexer that transferred to L2 + * @param delegator Address of the delegator unlocking their tokens + */ event StakeDelegatedUnlockedDueToL2Transfer(address indexed indexer, address indexed delegator); /** diff --git a/packages/interfaces/contracts/contracts/staking/IStaking.sol b/packages/interfaces/contracts/contracts/staking/IStaking.sol index c18e382b3..9b9a6fc5c 100644 --- a/packages/interfaces/contracts/contracts/staking/IStaking.sol +++ b/packages/interfaces/contracts/contracts/staking/IStaking.sol @@ -10,6 +10,7 @@ import { IManaged } from "../governance/IManaged.sol"; /** * @title Interface for the Staking contract + * @author Edge & Node * @notice This is the interface that should be used when interacting with the Staking contract. * @dev Note that Staking doesn't actually inherit this interface. This is because of * the custom setup of the Staking contract where part of the functionality is implemented diff --git a/packages/interfaces/contracts/contracts/staking/IStakingBase.sol b/packages/interfaces/contracts/contracts/staking/IStakingBase.sol index d7728d049..25d643c6f 100644 --- a/packages/interfaces/contracts/contracts/staking/IStakingBase.sol +++ b/packages/interfaces/contracts/contracts/staking/IStakingBase.sol @@ -3,10 +3,15 @@ pragma solidity ^0.7.6 || 0.8.27; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IStakingData } from "./IStakingData.sol"; /** * @title Base interface for the Staking contract. + * @author Edge & Node + * @notice Base interface for the Staking contract. * @dev This interface includes only what's implemented in the base Staking contract. * It does not include the L1 and L2 specific functionality. It also does not include * several functions that are implemented in the StakingExtension contract, and are called @@ -15,25 +20,38 @@ import { IStakingData } from "./IStakingData.sol"; */ interface IStakingBase is IStakingData { /** - * @dev Emitted when `indexer` stakes `tokens` amount. + * @notice Emitted when `indexer` stakes `tokens` amount. + * @param indexer Address of the indexer + * @param tokens Amount of tokens staked */ event StakeDeposited(address indexed indexer, uint256 tokens); /** - * @dev Emitted when `indexer` unstaked and locked `tokens` amount until `until` block. + * @notice Emitted when `indexer` unstaked and locked `tokens` amount until `until` block. + * @param indexer Address of the indexer + * @param tokens Amount of tokens locked + * @param until Block number until which tokens are locked */ event StakeLocked(address indexed indexer, uint256 tokens, uint256 until); /** - * @dev Emitted when `indexer` withdrew `tokens` staked. + * @notice Emitted when `indexer` withdrew `tokens` staked. + * @param indexer Address of the indexer + * @param tokens Amount of tokens withdrawn */ event StakeWithdrawn(address indexed indexer, uint256 tokens); /** - * @dev Emitted when `indexer` allocated `tokens` amount to `subgraphDeploymentID` + * @notice Emitted when `indexer` allocated `tokens` amount to `subgraphDeploymentID` * during `epoch`. * `allocationID` indexer derived address used to identify the allocation. * `metadata` additional information related to the allocation. + * @param indexer Address of the indexer + * @param subgraphDeploymentID Subgraph deployment ID + * @param epoch Epoch when allocation was created + * @param tokens Amount of tokens allocated + * @param allocationID Allocation identifier + * @param metadata IPFS hash for additional allocation information */ event AllocationCreated( address indexed indexer, @@ -45,10 +63,18 @@ interface IStakingBase is IStakingData { ); /** - * @dev Emitted when `indexer` close an allocation in `epoch` for `allocationID`. + * @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 Address of the indexer + * @param subgraphDeploymentID Subgraph deployment ID + * @param epoch Epoch when allocation was closed + * @param tokens Amount of tokens unallocated + * @param allocationID Allocation identifier + * @param sender Address that closed the allocation + * @param poi Proof of indexing submitted + * @param isPublic True if closed by someone other than the indexer */ event AllocationClosed( address indexed indexer, @@ -62,12 +88,23 @@ interface IStakingBase is IStakingData { ); /** - * @dev Emitted when `indexer` collects a rebate on `subgraphDeploymentID` for `allocationID`. + * @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 Address providing the rebate tokens + * @param indexer Address of the indexer collecting the rebate + * @param subgraphDeploymentID Subgraph deployment ID + * @param allocationID Allocation identifier + * @param epoch Epoch when rebate was collected + * @param tokens Total amount of tokens in the rebate + * @param protocolTax Amount burned as protocol tax + * @param curationFees Amount distributed to curators + * @param queryFees Amount available for rebate after fees + * @param queryRebates Amount distributed to the indexer + * @param delegationRewards Amount distributed to delegators */ event RebateCollected( address assetHolder, @@ -84,7 +121,11 @@ interface IStakingBase is IStakingData { ); /** - * @dev Emitted when `indexer` update the delegation parameters for its delegation pool. + * @notice Emitted when `indexer` update the delegation parameters for its delegation pool. + * @param indexer Address of the indexer + * @param indexingRewardCut Percentage of indexing rewards left for the indexer + * @param queryFeeCut Percentage of query fees left for the indexer + * @param __DEPRECATED_cooldownBlocks Deprecated parameter (no longer used) */ event DelegationParametersUpdated( address indexed indexer, @@ -94,18 +135,24 @@ interface IStakingBase is IStakingData { ); /** - * @dev Emitted when `indexer` set `operator` access. + * @notice Emitted when `indexer` set `operator` access. + * @param indexer Address of the indexer + * @param operator Address of the operator + * @param allowed Whether the operator is authorized */ event SetOperator(address indexed indexer, address indexed operator, bool allowed); /** - * @dev Emitted when `indexer` set an address to receive rewards. + * @notice Emitted when `indexer` set an address to receive rewards. + * @param indexer Address of the indexer + * @param destination Address to receive rewards */ event SetRewardsDestination(address indexed indexer, address indexed destination); /** - * @dev Emitted when `extensionImpl` was set as the address of the StakingExtension contract + * @notice Emitted when `extensionImpl` was set as the address of the StakingExtension contract * to which extended functionality is delegated. + * @param extensionImpl Address of the StakingExtension implementation */ event ExtensionImplementationSet(address indexed extensionImpl); @@ -260,12 +307,9 @@ interface IStakingBase is IStakingData { * @notice Set the delegation parameters for the caller. * @param indexingRewardCut Percentage of indexing rewards left for the indexer * @param queryFeeCut Percentage of query fees left for the indexer + * @param cooldownBlocks Deprecated cooldown blocks parameter (no longer used) */ - function setDelegationParameters( - uint32 indexingRewardCut, - uint32 queryFeeCut, - uint32 // cooldownBlocks, deprecated - ) external; + function setDelegationParameters(uint32 indexingRewardCut, uint32 queryFeeCut, uint32 cooldownBlocks) external; /** * @notice Allocate available tokens to a subgraph deployment. @@ -362,18 +406,27 @@ interface IStakingBase is IStakingData { function getAllocation(address allocationID) external view returns (Allocation memory); /** - * @dev New function to get the allocation data for the rewards manager + * @notice Get the allocation data for the rewards manager * @dev Note that this is only to make tests pass, as the staking contract with * this changes will never get deployed. HorizonStaking is taking it's place. + * @param allocationID The allocation ID + * @return active Whether the allocation is active + * @return indexer The indexer address + * @return subgraphDeploymentID The subgraph deployment ID + * @return tokens The allocated tokens + * @return createdAtEpoch The epoch when allocation was created + * @return closedAtEpoch The epoch when allocation was closed */ function getAllocationData( address allocationID ) external view returns (bool, address, bytes32, uint256, uint256, uint256); /** - * @dev New function to get the allocation active status for the rewards manager + * @notice Get the allocation active status for the rewards manager * @dev Note that this is only to make tests pass, as the staking contract with * this changes will never get deployed. HorizonStaking is taking it's place. + * @param allocationID The allocation ID + * @return Whether the allocation is active */ function isActiveAllocation(address allocationID) external view returns (bool); diff --git a/packages/interfaces/contracts/contracts/staking/IStakingData.sol b/packages/interfaces/contracts/contracts/staking/IStakingData.sol index 7db4d0ca1..b24c6cb39 100644 --- a/packages/interfaces/contracts/contracts/staking/IStakingData.sol +++ b/packages/interfaces/contracts/contracts/staking/IStakingData.sol @@ -4,12 +4,22 @@ pragma solidity ^0.7.6 || 0.8.27; /** * @title Staking Data interface - * @dev This interface defines some structures used by the Staking contract. + * @author Edge & Node + * @notice This interface defines some structures used by the Staking contract. */ interface IStakingData { /** * @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 Address of the indexer that owns the allocation + * @param subgraphDeploymentID Subgraph deployment ID being allocated to + * @param tokens Tokens allocated to a SubgraphDeployment + * @param createdAtEpoch Epoch when it was created + * @param closedAtEpoch Epoch when it was closed + * @param collectedFees Collected fees for the allocation + * @param __DEPRECATED_effectiveAllocation Deprecated field for effective allocation + * @param accRewardsPerAllocatedToken Snapshot used for reward calc + * @param distributedRebates Collected rebates that have been rebated */ struct Allocation { address indexer; @@ -27,6 +37,13 @@ interface IStakingData { /** * @dev Delegation pool information. One per indexer. + * @param __DEPRECATED_cooldownBlocks Deprecated field for cooldown blocks + * @param indexingRewardCut Indexing reward cut in PPM + * @param queryFeeCut Query fee cut in PPM + * @param updatedAtBlock Block when the pool was last updated + * @param tokens Total tokens as pool reserves + * @param shares Total shares minted in the pool + * @param delegators Mapping of delegator => Delegation */ struct DelegationPool { uint32 __DEPRECATED_cooldownBlocks; // solhint-disable-line var-name-mixedcase @@ -40,6 +57,9 @@ interface IStakingData { /** * @dev Individual delegation data of a delegator in a pool. + * @param shares Shares owned by a delegator in the pool + * @param tokensLocked Tokens locked for undelegation + * @param tokensLockedUntil Epoch when locked tokens can be withdrawn */ struct Delegation { uint256 shares; // Shares owned by a delegator in the pool @@ -49,6 +69,10 @@ interface IStakingData { /** * @dev Rebates parameters. Used to avoid stack too deep errors in Staking initialize function. + * @param alphaNumerator Alpha parameter numerator for rebate calculation + * @param alphaDenominator Alpha parameter denominator for rebate calculation + * @param lambdaNumerator Lambda parameter numerator for rebate calculation + * @param lambdaDenominator Lambda parameter denominator for rebate calculation */ struct RebatesParameters { uint32 alphaNumerator; diff --git a/packages/interfaces/contracts/contracts/staking/IStakingExtension.sol b/packages/interfaces/contracts/contracts/staking/IStakingExtension.sol index 0a13ab744..396a030a6 100644 --- a/packages/interfaces/contracts/contracts/staking/IStakingExtension.sol +++ b/packages/interfaces/contracts/contracts/staking/IStakingExtension.sol @@ -3,12 +3,16 @@ pragma solidity ^0.7.6 || 0.8.27; pragma abicoder v2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IStakingData } from "./IStakingData.sol"; import { IStakes } from "./libs/IStakes.sol"; /** * @title Interface for the StakingExtension contract - * @dev This interface defines the events and functions implemented + * @author Edge & Node + * @notice This interface defines the events and functions implemented * in the StakingExtension contract, which is used to extend the functionality * of the Staking contract while keeping it within the 24kB mainnet size limit. * In particular, this interface includes delegation functions and various storage @@ -18,6 +22,12 @@ interface IStakingExtension is IStakingData { /** * @dev DelegationPool struct as returned by delegationPools(), since * the original DelegationPool in IStakingData.sol contains a nested mapping. + * @param __DEPRECATED_cooldownBlocks Deprecated field for cooldown blocks + * @param indexingRewardCut Indexing reward cut in PPM + * @param queryFeeCut Query fee cut in PPM + * @param updatedAtBlock Block when the pool was last updated + * @param tokens Total tokens as pool reserves + * @param shares Total shares minted in the pool */ struct DelegationPoolReturn { uint32 __DEPRECATED_cooldownBlocks; // solhint-disable-line var-name-mixedcase @@ -29,14 +39,23 @@ interface IStakingExtension is IStakingData { } /** - * @dev Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator + * @notice Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator * gets `shares` for the delegation pool proportionally to the tokens staked. + * @param indexer Address of the indexer receiving the delegation + * @param delegator Address of the delegator + * @param tokens Amount of tokens delegated + * @param shares Amount of shares issued to the delegator */ event StakeDelegated(address indexed indexer, address indexed delegator, uint256 tokens, uint256 shares); /** - * @dev Emitted when `delegator` undelegated `tokens` from `indexer`. + * @notice Emitted when `delegator` undelegated `tokens` from `indexer`. * Tokens get locked for withdrawal after a period of time. + * @param indexer Address of the indexer from which tokens are undelegated + * @param delegator Address of the delegator + * @param tokens Amount of tokens undelegated + * @param shares Amount of shares returned + * @param until Epoch until which tokens are locked */ event StakeDelegatedLocked( address indexed indexer, @@ -47,18 +66,28 @@ interface IStakingExtension is IStakingData { ); /** - * @dev Emitted when `delegator` withdrew delegated `tokens` from `indexer`. + * @notice Emitted when `delegator` withdrew delegated `tokens` from `indexer`. + * @param indexer Address of the indexer from which tokens are withdrawn + * @param delegator Address of the delegator + * @param tokens Amount of tokens withdrawn */ event StakeDelegatedWithdrawn(address indexed indexer, address indexed delegator, uint256 tokens); /** - * @dev Emitted when `indexer` was slashed for a total of `tokens` amount. + * @notice Emitted when `indexer` was slashed for a total of `tokens` amount. * Tracks `reward` amount of tokens given to `beneficiary`. + * @param indexer Address of the indexer that was slashed + * @param tokens Total amount of tokens slashed + * @param reward Amount of tokens given as reward + * @param beneficiary Address receiving the reward */ event StakeSlashed(address indexed indexer, uint256 tokens, uint256 reward, address beneficiary); /** - * @dev Emitted when `caller` set `slasher` address as `allowed` to slash stakes. + * @notice Emitted when `caller` set `slasher` address as `allowed` to slash stakes. + * @param caller Address that updated the slasher status + * @param slasher Address of the slasher + * @param allowed Whether the slasher is allowed to slash */ event SlasherUpdate(address indexed caller, address indexed slasher, bool allowed); @@ -114,6 +143,7 @@ interface IStakingExtension is IStakingData { * re-delegate to a new indexer. * @param indexer Withdraw available tokens delegated to indexer * @param newIndexer Re-delegate to indexer address if non-zero, withdraw if zero address + * @return Amount of tokens withdrawn */ function withdrawDelegated(address indexer, address newIndexer) external returns (uint256); diff --git a/packages/interfaces/contracts/contracts/staking/libs/IStakes.sol b/packages/interfaces/contracts/contracts/staking/libs/IStakes.sol index 701336409..10364ebad 100644 --- a/packages/interfaces/contracts/contracts/staking/libs/IStakes.sol +++ b/packages/interfaces/contracts/contracts/staking/libs/IStakes.sol @@ -3,6 +3,11 @@ pragma solidity ^0.7.6 || 0.8.27; pragma abicoder v2; +/** + * @title Interface for staking data structures + * @author Edge & Node + * @notice Defines the data structures used for indexer staking + */ interface IStakes { struct Indexer { uint256 tokensStaked; // Tokens on the indexer stake (staked by the indexer) diff --git a/packages/interfaces/contracts/contracts/token/IGraphToken.sol b/packages/interfaces/contracts/contracts/token/IGraphToken.sol index 8d5d1b845..12cd2b2be 100644 --- a/packages/interfaces/contracts/contracts/token/IGraphToken.sol +++ b/packages/interfaces/contracts/contracts/token/IGraphToken.sol @@ -2,25 +2,67 @@ pragma solidity ^0.7.6 || 0.8.27; +// Solhint linting fails for 0.8.0. +// solhint-disable-next-line import-path-check import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/** + * @title IGraphToken + * @author Edge & Node + * @notice Interface for the Graph Token contract + * @dev Extends IERC20 with additional functionality for minting, burning, and permit + */ interface IGraphToken is IERC20 { // -- Mint and Burn -- + /** + * @notice Burns tokens from the caller's account + * @param amount The amount of tokens to burn + */ function burn(uint256 amount) external; + /** + * @notice Burns tokens from a specified account (requires allowance) + * @param from The account to burn tokens from + * @param amount The amount of tokens to burn + */ function burnFrom(address from, uint256 amount) external; + /** + * @notice Mints new tokens to a specified account + * @dev Only callable by accounts with minter role + * @param to The account to mint tokens to + * @param amount The amount of tokens to mint + */ function mint(address to, uint256 amount) external; // -- Mint Admin -- + /** + * @notice Adds a new minter account + * @dev Only callable by accounts with appropriate permissions + * @param account The account to grant minter role to + */ function addMinter(address account) external; + /** + * @notice Removes minter role from an account + * @dev Only callable by accounts with appropriate permissions + * @param account The account to revoke minter role from + */ function removeMinter(address account) external; + /** + * @notice Renounces minter role for the caller + * @dev Allows a minter to voluntarily give up their minting privileges + */ function renounceMinter() external; + /** + * @notice Checks if an account has minter role + * @param account The account to check + * @return True if the account is a minter, false otherwise + */ function isMinter(address account) external view returns (bool); // -- Permit -- @@ -47,7 +89,19 @@ interface IGraphToken is IERC20 { // -- Allowance -- + /** + * @notice Increases the allowance granted to a spender + * @param spender The account whose allowance will be increased + * @param addedValue The amount to increase the allowance by + * @return True if the operation succeeded + */ function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + /** + * @notice Decreases the allowance granted to a spender + * @param spender The account whose allowance will be decreased + * @param subtractedValue The amount to decrease the allowance by + * @return True if the operation succeeded + */ function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); } diff --git a/packages/interfaces/contracts/contracts/upgrades/IGraphProxy.sol b/packages/interfaces/contracts/contracts/upgrades/IGraphProxy.sol index 108708c03..eb0210d72 100644 --- a/packages/interfaces/contracts/contracts/upgrades/IGraphProxy.sol +++ b/packages/interfaces/contracts/contracts/upgrades/IGraphProxy.sol @@ -2,18 +2,76 @@ pragma solidity ^0.7.6 || 0.8.27; +/** + * @title Graph Proxy Interface + * @author Edge & Node + * @notice Interface for the Graph Proxy contract that handles upgradeable proxy functionality + */ interface IGraphProxy { + /** + * @notice Get the current admin. + * + * @dev NOTE: Only the admin can call this function. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + * + * @return adminAddress The address of the current admin + */ function admin() external returns (address); + /** + * @notice Change the admin of the proxy. + * + * @dev NOTE: Only the admin can call this function. + * + * @param newAdmin Address of the new admin + */ function setAdmin(address newAdmin) external; + /** + * @notice Get the current implementation. + * + * @dev NOTE: Only the admin can call this function. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` + * + * @return implementationAddress The address of the current implementation for this proxy + */ function implementation() external returns (address); + /** + * @notice Get the current pending implementation. + * + * @dev NOTE: Only the admin can call this function. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x9e5eddc59e0b171f57125ab86bee043d9128098c3a6b9adb4f2e86333c2f6f8c` + * + * @return pendingImplementationAddress The address of the current pending implementation for this proxy + */ function pendingImplementation() external returns (address); + /** + * @notice Upgrades to a new implementation contract. + * @dev NOTE: Only the admin can call this function. + * @param newImplementation Address of implementation contract + */ function upgradeTo(address newImplementation) external; + /** + * @notice Admin function for new implementation to accept its role as implementation. + */ function acceptUpgrade() external; + /** + * @notice Admin function for new implementation to accept its role as implementation, + * calling a function on the new implementation. + * @param data Calldata (including selector) for the function to delegatecall into the implementation + */ function acceptUpgradeAndCall(bytes calldata data) external; } diff --git a/packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol b/packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol index 44e0df0e4..a7139f554 100644 --- a/packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol +++ b/packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol @@ -7,30 +7,81 @@ import { IGoverned } from "../governance/IGoverned.sol"; /** * @title IGraphProxyAdmin - * @dev GraphProxyAdmin contract interface + * @author Edge & Node + * @notice GraphProxyAdmin contract interface for managing proxy contracts * @dev Note that this interface is not used by the contract implementation, just used for types and abi generation * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ interface IGraphProxyAdmin is IGoverned { + /** + * @notice Get the implementation address of a proxy + * @param proxy The proxy contract to query + * @return The implementation address + */ function getProxyImplementation(IGraphProxy proxy) external view returns (address); + /** + * @notice Get the pending implementation address of a proxy + * @param proxy The proxy contract to query + * @return The pending implementation address + */ function getProxyPendingImplementation(IGraphProxy proxy) external view returns (address); + /** + * @notice Get the admin address of a proxy + * @param proxy The proxy contract to query + * @return The admin address + */ function getProxyAdmin(IGraphProxy proxy) external view returns (address); + /** + * @notice Change the admin of a proxy contract + * @param proxy The proxy contract to modify + * @param newAdmin The new admin address + */ function changeProxyAdmin(IGraphProxy proxy, address newAdmin) external; + /** + * @notice Upgrade a proxy to a new implementation + * @param proxy The proxy contract to upgrade + * @param implementation The new implementation address + */ function upgrade(IGraphProxy proxy, address implementation) external; + /** + * @notice Upgrade a proxy to a new implementation + * @param proxy The proxy contract to upgrade + * @param implementation The new implementation address + */ function upgradeTo(IGraphProxy proxy, address implementation) external; + /** + * @notice Upgrade a proxy to a new implementation and call a function + * @param proxy The proxy contract to upgrade + * @param implementation The new implementation address + * @param data The calldata to execute on the new implementation + */ function upgradeToAndCall(IGraphProxy proxy, address implementation, bytes calldata data) external; + /** + * @notice Accept ownership of a proxy contract + * @param proxy The proxy contract to accept + */ function acceptProxy(IGraphProxy proxy) external; + /** + * @notice Accept ownership of a proxy contract and call a function + * @param proxy The proxy contract to accept + * @param data The calldata to execute after accepting + */ function acceptProxyAndCall(IGraphProxy proxy, bytes calldata data) external; // storage + + /** + * @notice Get the governor address + * @return The address of the governor + */ function governor() external view returns (address); } diff --git a/packages/interfaces/contracts/data-service/IDataService.sol b/packages/interfaces/contracts/data-service/IDataService.sol index 778987f71..564ba1a89 100644 --- a/packages/interfaces/contracts/data-service/IDataService.sol +++ b/packages/interfaces/contracts/data-service/IDataService.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IGraphPayments } from "../horizon/IGraphPayments.sol"; /** * @title Interface of the base {DataService} contract as defined by the Graph Horizon specification. + * @author Edge & Node * @notice This interface provides a guardrail for contracts that use the Data Service framework * to implement a data service on Graph Horizon. Much of the specification is intentionally loose * to allow for greater flexibility when designing a data service. It's not possible to guarantee that diff --git a/packages/interfaces/contracts/data-service/IDataServiceFees.sol b/packages/interfaces/contracts/data-service/IDataServiceFees.sol index 9d235f4f7..fad127aba 100644 --- a/packages/interfaces/contracts/data-service/IDataServiceFees.sol +++ b/packages/interfaces/contracts/data-service/IDataServiceFees.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IDataService } from "./IDataService.sol"; /** * @title Interface for the {DataServiceFees} contract. + * @author Edge & Node * @notice Extension for the {IDataService} contract to handle payment collateralization * using a Horizon provision. * diff --git a/packages/interfaces/contracts/data-service/IDataServicePausable.sol b/packages/interfaces/contracts/data-service/IDataServicePausable.sol index 906e864a8..ad355d9f0 100644 --- a/packages/interfaces/contracts/data-service/IDataServicePausable.sol +++ b/packages/interfaces/contracts/data-service/IDataServicePausable.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IDataService } from "./IDataService.sol"; /** * @title Interface for the {DataServicePausable} contract. + * @author Edge & Node * @notice Extension for the {IDataService} contract, adds pausing functionality * to the data service. Pausing is controlled by privileged accounts called * pause guardians. diff --git a/packages/interfaces/contracts/data-service/IDataServiceRescuable.sol b/packages/interfaces/contracts/data-service/IDataServiceRescuable.sol index f2cd7b06e..478e429da 100644 --- a/packages/interfaces/contracts/data-service/IDataServiceRescuable.sol +++ b/packages/interfaces/contracts/data-service/IDataServiceRescuable.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IDataService } from "./IDataService.sol"; /** * @title Interface for the {IDataServicePausable} contract. + * @author Edge & Node * @notice Extension for the {IDataService} contract, adds the ability to rescue * any ERC20 token or ETH from the contract, controlled by a rescuer privileged role. * @custom:security-contact Please email security+contracts@thegraph.com if you find any diff --git a/packages/interfaces/contracts/horizon/IAuthorizable.sol b/packages/interfaces/contracts/horizon/IAuthorizable.sol index 7a7a77798..5b32a69ff 100644 --- a/packages/interfaces/contracts/horizon/IAuthorizable.sol +++ b/packages/interfaces/contracts/horizon/IAuthorizable.sol @@ -1,8 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events +// solhint-disable gas-struct-packing + /** * @title Interface for the {Authorizable} contract + * @author Edge & Node * @notice Implements an authorization scheme that allows authorizers to * authorize signers to sign on their behalf. * @custom:security-contact Please email security+contracts@thegraph.com if you find any diff --git a/packages/interfaces/contracts/horizon/IGraphPayments.sol b/packages/interfaces/contracts/horizon/IGraphPayments.sol index 7a583f883..9338877ef 100644 --- a/packages/interfaces/contracts/horizon/IGraphPayments.sol +++ b/packages/interfaces/contracts/horizon/IGraphPayments.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; /** * @title Interface for the {GraphPayments} contract + * @author Edge & Node * @notice This contract is part of the Graph Horizon payments protocol. It's designed * to pull funds (GRT) from the {PaymentsEscrow} and distribute them according to a * set of pre established rules. diff --git a/packages/interfaces/contracts/horizon/IGraphTallyCollector.sol b/packages/interfaces/contracts/horizon/IGraphTallyCollector.sol index 86d0ebc3a..1f77cb693 100644 --- a/packages/interfaces/contracts/horizon/IGraphTallyCollector.sol +++ b/packages/interfaces/contracts/horizon/IGraphTallyCollector.sol @@ -7,6 +7,7 @@ import { IAuthorizable } from "./IAuthorizable.sol"; /** * @title Interface for the {GraphTallyCollector} contract + * @author Edge & Node * @dev Implements the {IPaymentCollector} interface as defined by the Graph * Horizon payments protocol. * @notice Implements a payments collector contract that can be used to collect @@ -49,8 +50,8 @@ interface IGraphTallyCollector is IPaymentsCollector, IAuthorizable { * @notice Emitted when a RAV is collected * @param collectionId The ID of the collection "bucket" the RAV belongs to. * @param payer The address of the payer - * @param dataService The address of the data service * @param serviceProvider The address of the service provider + * @param dataService The address of the data service * @param timestampNs The timestamp of the RAV * @param valueAggregate The total amount owed to the service provider * @param metadata Arbitrary metadata @@ -122,14 +123,14 @@ interface IGraphTallyCollector is IPaymentsCollector, IAuthorizable { ) external returns (uint256); /** - * @dev Recovers the signer address of a signed ReceiptAggregateVoucher (RAV). + * @notice Recovers the signer address of a signed ReceiptAggregateVoucher (RAV). * @param signedRAV The SignedRAV containing the RAV and its signature. * @return The address of the signer. */ function recoverRAVSigner(SignedRAV calldata signedRAV) external view returns (address); /** - * @dev Computes the hash of a ReceiptAggregateVoucher (RAV). + * @notice Computes the hash of a ReceiptAggregateVoucher (RAV). * @param rav The RAV for which to compute the hash. * @return The hash of the RAV. */ diff --git a/packages/interfaces/contracts/horizon/IHorizonStaking.sol b/packages/interfaces/contracts/horizon/IHorizonStaking.sol index 0ba4e26b3..ac98a5a7d 100644 --- a/packages/interfaces/contracts/horizon/IHorizonStaking.sol +++ b/packages/interfaces/contracts/horizon/IHorizonStaking.sol @@ -9,6 +9,7 @@ import { IHorizonStakingExtension } from "./internal/IHorizonStakingExtension.so /** * @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. * @dev Use this interface to interact with the Horizon Staking contract. diff --git a/packages/interfaces/contracts/horizon/IPaymentsCollector.sol b/packages/interfaces/contracts/horizon/IPaymentsCollector.sol index d37688462..1a3b6c8b8 100644 --- a/packages/interfaces/contracts/horizon/IPaymentsCollector.sol +++ b/packages/interfaces/contracts/horizon/IPaymentsCollector.sol @@ -6,6 +6,7 @@ import { IGraphPayments } from "./IGraphPayments.sol"; /** * @title Interface for a payments collector contract as defined by Graph Horizon payments protocol + * @author Edge & Node * @notice Contracts implementing this interface can be used with the payments protocol. First, a payer must * approve the collector to collect payments on their behalf. Only then can payment collection be initiated * using the collector contract. diff --git a/packages/interfaces/contracts/horizon/IPaymentsEscrow.sol b/packages/interfaces/contracts/horizon/IPaymentsEscrow.sol index 601783b12..7c1243b55 100644 --- a/packages/interfaces/contracts/horizon/IPaymentsEscrow.sol +++ b/packages/interfaces/contracts/horizon/IPaymentsEscrow.sol @@ -5,6 +5,7 @@ import { IGraphPayments } from "./IGraphPayments.sol"; /** * @title Interface for the {PaymentsEscrow} contract + * @author Edge & Node * @notice This contract is part of the Graph Horizon payments protocol. It holds the funds (GRT) * for payments made through the payments protocol for services provided * via a Graph Horizon data service. diff --git a/packages/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol b/packages/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol index ba050ae3a..f5f32232b 100644 --- a/packages/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol +++ b/packages/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol @@ -2,6 +2,9 @@ pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IHorizonStakingTypes } from "./IHorizonStakingTypes.sol"; import { IGraphPayments } from "../IGraphPayments.sol"; @@ -9,6 +12,7 @@ import { ILinkedList } from "./ILinkedList.sol"; /** * @title Interface for the {HorizonStakingBase} contract. + * @author Edge & Node * @notice Provides getters for {HorizonStaking} and {HorizonStakingExtension} storage variables. * @dev Most functions operate over {HorizonStaking} provisions. To uniquely identify a provision * functions take `serviceProvider` and `verifier` addresses. diff --git a/packages/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol b/packages/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol index d590a76a5..eb5e811c6 100644 --- a/packages/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol +++ b/packages/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol @@ -2,10 +2,14 @@ pragma solidity 0.8.27; +// 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. @@ -50,7 +54,7 @@ interface IHorizonStakingExtension is IRewardsIssuer { } /** - * @dev Emitted when `indexer` close an allocation in `epoch` for `allocationID`. + * @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. @@ -75,7 +79,7 @@ interface IHorizonStakingExtension is IRewardsIssuer { ); /** - * @dev Emitted when `indexer` collects a rebate on `subgraphDeploymentID` for `allocationID`. + * @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. @@ -108,7 +112,7 @@ interface IHorizonStakingExtension is IRewardsIssuer { ); /** - * @dev Emitted when `indexer` was slashed for a total of `tokens` amount. + * @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 @@ -128,7 +132,7 @@ interface IHorizonStakingExtension is IRewardsIssuer { function closeAllocation(address allocationID, bytes32 poi) external; /** - * @dev Collect and rebate query fees to the indexer + * @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 diff --git a/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol b/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol index 1a84bcd67..31d498c09 100644 --- a/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol +++ b/packages/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol @@ -2,11 +2,15 @@ pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IGraphPayments } from "../IGraphPayments.sol"; 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}. @@ -110,7 +114,7 @@ interface IHorizonStakingMain { ); /** - * @dev Emitted when an operator is allowed or denied by a service provider for a particular verifier + * @notice Emitted when an operator is allowed or denied by a service provider for a particular verifier * @param serviceProvider The address of the service provider * @param verifier The address of the verifier * @param operator The address of the operator @@ -289,12 +293,12 @@ interface IHorizonStakingMain { /** * @notice Emitted when a series of thaw requests are fulfilled. + * @param requestType The type of thaw request * @param serviceProvider The address of the service provider * @param verifier The address of the verifier * @param owner The address of the owner of the thaw requests * @param thawRequestsFulfilled The number of thaw requests fulfilled * @param tokens The total amount of tokens being released - * @param requestType The type of thaw request */ event ThawRequestsFulfilled( IHorizonStakingTypes.ThawRequestType indexed requestType, diff --git a/packages/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol b/packages/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol index 0ab84fc1b..07bd6b193 100644 --- a/packages/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol +++ b/packages/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.27; /** * @title Defines the data types used in the Horizon staking contract + * @author Edge & Node + * @notice Interface defining data types and structures for Horizon staking * @dev In order to preserve storage compatibility some data structures keep deprecated fields. * These structures have then two representations, an internal one used by the contract storage and a public one. * Getter functions should retrieve internal representations, remove deprecated fields and return the public representation. diff --git a/packages/interfaces/contracts/horizon/internal/ILinkedList.sol b/packages/interfaces/contracts/horizon/internal/ILinkedList.sol index 4a993df29..da21e0d9e 100644 --- a/packages/interfaces/contracts/horizon/internal/ILinkedList.sol +++ b/packages/interfaces/contracts/horizon/internal/ILinkedList.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.27; /** * @title Interface for the {LinkedList} library contract. + * @author Edge & Node + * @notice Interface for managing linked list data structures * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol b/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol index e82c96fa9..82026abba 100644 --- a/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol +++ b/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol @@ -2,10 +2,14 @@ pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IAttestation } from "./internal/IAttestation.sol"; /** * @title IDisputeManager + * @author Edge & Node * @notice Interface for the {Dispute Manager} contract. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. @@ -90,7 +94,7 @@ interface IDisputeManager { event SubgraphServiceSet(address indexed subgraphService); /** - * @dev Emitted when a query dispute is created for `subgraphDeploymentId` and `indexer` + * @notice Emitted when a query dispute is created for `subgraphDeploymentId` and `indexer` * by `fisherman`. * The event emits the amount of `tokens` deposited by the fisherman and `attestation` submitted. * @param disputeId The dispute id @@ -99,8 +103,8 @@ interface IDisputeManager { * @param tokens The amount of tokens deposited by the fisherman * @param subgraphDeploymentId The subgraph deployment id * @param attestation The attestation - * @param cancellableAt The timestamp when the dispute can be cancelled * @param stakeSnapshot The stake snapshot of the indexer at the time of the dispute + * @param cancellableAt The timestamp when the dispute can be cancelled */ event QueryDisputeCreated( bytes32 indexed disputeId, @@ -114,7 +118,7 @@ interface IDisputeManager { ); /** - * @dev Emitted when an indexing dispute is created for `allocationId` and `indexer` + * @notice Emitted when an indexing dispute is created for `allocationId` and `indexer` * by `fisherman`. * The event emits the amount of `tokens` deposited by the fisherman. * @param disputeId The dispute id @@ -140,7 +144,7 @@ interface IDisputeManager { ); /** - * @dev Emitted when a legacy dispute is created for `allocationId` and `fisherman`. + * @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 @@ -159,7 +163,7 @@ interface IDisputeManager { ); /** - * @dev Emitted when arbitrator accepts a `disputeId` to `indexer` created by `fisherman`. + * @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. * @param disputeId The dispute id * @param indexer The indexer address @@ -174,7 +178,7 @@ interface IDisputeManager { ); /** - * @dev Emitted when arbitrator rejects a `disputeId` for `indexer` created by `fisherman`. + * @notice Emitted when arbitrator rejects a `disputeId` for `indexer` created by `fisherman`. * The event emits the amount `tokens` burned from the fisherman deposit. * @param disputeId The dispute id * @param indexer The indexer address @@ -189,7 +193,7 @@ interface IDisputeManager { ); /** - * @dev Emitted when arbitrator draw a `disputeId` for `indexer` created by `fisherman`. + * @notice Emitted when arbitrator draw a `disputeId` for `indexer` created by `fisherman`. * The event emits the amount `tokens` used as deposit and returned to the fisherman. * @param disputeId The dispute id * @param indexer The indexer address @@ -199,7 +203,7 @@ interface IDisputeManager { event DisputeDrawn(bytes32 indexed disputeId, address indexed indexer, address indexed fisherman, uint256 tokens); /** - * @dev Emitted when two disputes are in conflict to link them. + * @notice Emitted when two disputes are in conflict to link them. * This event will be emitted after each DisputeCreated event is emitted * for each of the individual disputes. * @param disputeId1 The first dispute id @@ -208,7 +212,7 @@ interface IDisputeManager { event DisputeLinked(bytes32 indexed disputeId1, bytes32 indexed disputeId2); /** - * @dev Emitted when a dispute is cancelled by the fisherman. + * @notice Emitted when a dispute is cancelled by the fisherman. * The event emits the amount `tokens` returned to the fisherman. * @param disputeId The dispute id * @param indexer The indexer address diff --git a/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol b/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol index 88e48d6d4..3a439a2b3 100644 --- a/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol +++ b/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IDataServiceFees } from "../data-service/IDataServiceFees.sol"; import { IGraphPayments } from "../horizon/IGraphPayments.sol"; @@ -9,6 +12,7 @@ import { ILegacyAllocation } from "./internal/ILegacyAllocation.sol"; /** * @title Interface for the {SubgraphService} contract + * @author Edge & Node * @dev This interface extends {IDataServiceFees} and {IDataService}. * @notice The Subgraph Service is a data service built on top of Graph Horizon that supports the use case of * subgraph indexing and querying. The {SubgraphService} contract implements the flows described in the Data diff --git a/packages/interfaces/contracts/subgraph-service/internal/IAllocation.sol b/packages/interfaces/contracts/subgraph-service/internal/IAllocation.sol index 97b4f1176..e0f55e16a 100644 --- a/packages/interfaces/contracts/subgraph-service/internal/IAllocation.sol +++ b/packages/interfaces/contracts/subgraph-service/internal/IAllocation.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.27; /** * @title Interface for the {Allocation} library contract. + * @author Edge & Node + * @notice Interface for managing allocation data and operations * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/interfaces/contracts/subgraph-service/internal/IAttestation.sol b/packages/interfaces/contracts/subgraph-service/internal/IAttestation.sol index e425a917a..8d7f4cc55 100644 --- a/packages/interfaces/contracts/subgraph-service/internal/IAttestation.sol +++ b/packages/interfaces/contracts/subgraph-service/internal/IAttestation.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.27; /** * @title Interface for the {Attestation} library contract. + * @author Edge & Node + * @notice Interface for managing attestation data and verification * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol b/packages/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol index 74405a439..ad720e170 100644 --- a/packages/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol +++ b/packages/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.27; /** * @title Interface for the {LegacyAllocation} library contract. + * @author Edge & Node + * @notice Interface for managing legacy allocation data * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/interfaces/contracts/token-distribution/IGraphTokenLockWallet.sol b/packages/interfaces/contracts/token-distribution/IGraphTokenLockWallet.sol index 735d35046..4f1f7b0fc 100644 --- a/packages/interfaces/contracts/token-distribution/IGraphTokenLockWallet.sol +++ b/packages/interfaces/contracts/token-distribution/IGraphTokenLockWallet.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + /** * @title IGraphTokenLockWallet + * @author Edge & Node * @notice Interface for the GraphTokenLockWallet contract that manages locked tokens with vesting schedules * @dev This interface includes core vesting functionality. Protocol interaction functions are in IGraphTokenLockWalletToolshed */ @@ -17,50 +21,157 @@ interface IGraphTokenLockWallet { } // Events + + /// @notice Emitted when the manager is updated + /// @param _oldManager The previous manager address + /// @param _newManager The new manager address event ManagerUpdated(address indexed _oldManager, address indexed _newManager); + + /// @notice Emitted when ownership is transferred + /// @param previousOwner The previous owner address + /// @param newOwner The new owner address event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @notice Emitted when token destinations are approved event TokenDestinationsApproved(); + + /// @notice Emitted when token destinations are revoked event TokenDestinationsRevoked(); + + /// @notice Emitted when tokens are released to beneficiary + /// @param beneficiary The beneficiary address + /// @param amount The amount of tokens released event TokensReleased(address indexed beneficiary, uint256 amount); + + /// @notice Emitted when tokens are revoked + /// @param beneficiary The beneficiary address + /// @param amount The amount of tokens revoked event TokensRevoked(address indexed beneficiary, uint256 amount); + + /// @notice Emitted when tokens are withdrawn + /// @param beneficiary The beneficiary address + /// @param amount The amount of tokens withdrawn event TokensWithdrawn(address indexed beneficiary, uint256 amount); // View functions - Vesting Details + + /// @notice Get the beneficiary address + /// @return The beneficiary address function beneficiary() external view returns (address); + + /// @notice Get the token contract address + /// @return The token contract address function token() external view returns (address); + + /// @notice Get the total amount of tokens managed by this contract + /// @return The managed token amount function managedAmount() external view returns (uint256); + + /// @notice Get the vesting start time + /// @return The start time timestamp function startTime() external view returns (uint256); + + /// @notice Get the vesting end time + /// @return The end time timestamp function endTime() external view returns (uint256); + + /// @notice Get the number of vesting periods + /// @return The number of periods function periods() external view returns (uint256); + + /// @notice Get the release start time + /// @return The release start time timestamp function releaseStartTime() external view returns (uint256); + + /// @notice Get the vesting cliff time + /// @return The cliff time timestamp function vestingCliffTime() external view returns (uint256); + + /// @notice Get the revocability status + /// @return The revocability status function revocable() external view returns (Revocability); + + /// @notice Check if the vesting has been revoked + /// @return True if revoked, false otherwise function isRevoked() external view returns (bool); // View functions - Vesting Calculations + + /// @notice Get the current timestamp + /// @return The current timestamp function currentTime() external view returns (uint256); + + /// @notice Get the total vesting duration + /// @return The duration in seconds function duration() external view returns (uint256); + + /// @notice Get the time elapsed since vesting start + /// @return The elapsed time in seconds function sinceStartTime() external view returns (uint256); + + /// @notice Get the amount of tokens released per period + /// @return The amount per period function amountPerPeriod() external view returns (uint256); + + /// @notice Get the duration of each vesting period + /// @return The period duration in seconds function periodDuration() external view returns (uint256); + + /// @notice Get the current vesting period + /// @return The current period number function currentPeriod() external view returns (uint256); + + /// @notice Get the number of periods that have passed + /// @return The number of passed periods function passedPeriods() external view returns (uint256); // View functions - Token Amounts + + /// @notice Get the amount of tokens that can be released + /// @return The releasable token amount function releasableAmount() external view returns (uint256); + + /// @notice Get the amount of tokens that have vested + /// @return The vested token amount function vestedAmount() external view returns (uint256); + + /// @notice Get the amount of tokens that have been released + /// @return The released token amount function releasedAmount() external view returns (uint256); + + /// @notice Get the amount of tokens that have been used + /// @return The used token amount function usedAmount() external view returns (uint256); + + /// @notice Get the current token balance of the contract + /// @return The current balance function currentBalance() external view returns (uint256); + + /// @notice Get the surplus amount of tokens + /// @return The surplus token amount function surplusAmount() external view returns (uint256); + + /// @notice Get the total outstanding token amount + /// @return The total outstanding amount function totalOutstandingAmount() external view returns (uint256); // State-changing functions + + /// @notice Release vested tokens to the beneficiary function release() external; + + /// @notice Withdraw surplus tokens + /// @param _amount The amount of surplus tokens to withdraw function withdrawSurplus(uint256 _amount) external; + + /// @notice Approve protocol interactions function approveProtocol() external; + + /// @notice Revoke protocol interactions function revokeProtocol() external; // Fallback for forwarding calls + + /// @notice Fallback function for forwarding calls fallback() external payable; } diff --git a/packages/interfaces/contracts/toolshed/IControllerToolshed.sol b/packages/interfaces/contracts/toolshed/IControllerToolshed.sol index e4dbd8d41..665911755 100644 --- a/packages/interfaces/contracts/toolshed/IControllerToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IControllerToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// solhint-disable use-natspec + import { IController } from "../contracts/governance/IController.sol"; import { IGoverned } from "../contracts/governance/IGoverned.sol"; diff --git a/packages/interfaces/contracts/toolshed/IDisputeManagerToolshed.sol b/packages/interfaces/contracts/toolshed/IDisputeManagerToolshed.sol index 0496d3326..a1c1d7601 100644 --- a/packages/interfaces/contracts/toolshed/IDisputeManagerToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IDisputeManagerToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.27; +// solhint-disable use-natspec + import { IDisputeManager } from "../subgraph-service/IDisputeManager.sol"; import { IOwnable } from "./internal/IOwnable.sol"; diff --git a/packages/interfaces/contracts/toolshed/IEpochManagerToolshed.sol b/packages/interfaces/contracts/toolshed/IEpochManagerToolshed.sol index 427df1f64..b7be112d2 100644 --- a/packages/interfaces/contracts/toolshed/IEpochManagerToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IEpochManagerToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// solhint-disable use-natspec + import { IEpochManager } from "../contracts/epochs/IEpochManager.sol"; interface IEpochManagerToolshed is IEpochManager { diff --git a/packages/interfaces/contracts/toolshed/IGraphTallyCollectorToolshed.sol b/packages/interfaces/contracts/toolshed/IGraphTallyCollectorToolshed.sol index f6be272e4..527e34a78 100644 --- a/packages/interfaces/contracts/toolshed/IGraphTallyCollectorToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IGraphTallyCollectorToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// solhint-disable use-natspec + import { IGraphTallyCollector } from "../horizon/IGraphTallyCollector.sol"; interface IGraphTallyCollectorToolshed is IGraphTallyCollector { diff --git a/packages/interfaces/contracts/toolshed/IGraphTokenLockWalletToolshed.sol b/packages/interfaces/contracts/toolshed/IGraphTokenLockWalletToolshed.sol index d442f0c0f..2b2d1af90 100644 --- a/packages/interfaces/contracts/toolshed/IGraphTokenLockWalletToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IGraphTokenLockWalletToolshed.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// solhint-disable use-natspec + import { IGraphTokenLockWallet } from "../token-distribution/IGraphTokenLockWallet.sol"; import { IGraphPayments } from "../horizon/IGraphPayments.sol"; /** * @title IGraphTokenLockWalletToolshed + * @author Edge & Node * @notice Extended interface for GraphTokenLockWallet that includes Horizon protocol interaction functions * @dev Functions included are based on the GraphTokenLockManager whitelist for vesting contracts on Horizon */ diff --git a/packages/interfaces/contracts/toolshed/IHorizonStakingToolshed.sol b/packages/interfaces/contracts/toolshed/IHorizonStakingToolshed.sol index b021d3af9..4cd5c5dc4 100644 --- a/packages/interfaces/contracts/toolshed/IHorizonStakingToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IHorizonStakingToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// solhint-disable use-natspec + import { IHorizonStaking } from "../horizon/IHorizonStaking.sol"; import { IMulticall } from "../contracts/base/IMulticall.sol"; diff --git a/packages/interfaces/contracts/toolshed/IL2CurationToolshed.sol b/packages/interfaces/contracts/toolshed/IL2CurationToolshed.sol index 9b4b81782..b2f4f847b 100644 --- a/packages/interfaces/contracts/toolshed/IL2CurationToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IL2CurationToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// solhint-disable use-natspec + import { ICuration } from "../contracts/curation/ICuration.sol"; import { IL2Curation } from "../contracts/l2/curation/IL2Curation.sol"; diff --git a/packages/interfaces/contracts/toolshed/IL2GNSToolshed.sol b/packages/interfaces/contracts/toolshed/IL2GNSToolshed.sol index 7cfb3a579..573f79292 100644 --- a/packages/interfaces/contracts/toolshed/IL2GNSToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IL2GNSToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// solhint-disable use-natspec + import { IGNS } from "../contracts/discovery/IGNS.sol"; import { IL2GNS } from "../contracts/l2/discovery/IL2GNS.sol"; import { IMulticall } from "../contracts/base/IMulticall.sol"; diff --git a/packages/interfaces/contracts/toolshed/IPaymentsEscrowToolshed.sol b/packages/interfaces/contracts/toolshed/IPaymentsEscrowToolshed.sol index e77a0a1b0..63e4ee36e 100644 --- a/packages/interfaces/contracts/toolshed/IPaymentsEscrowToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IPaymentsEscrowToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// solhint-disable use-natspec + import { IPaymentsEscrow } from "../horizon/IPaymentsEscrow.sol"; interface IPaymentsEscrowToolshed is IPaymentsEscrow { diff --git a/packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol b/packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol index 9fd0b6e1b..0f26080f9 100644 --- a/packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// solhint-disable use-natspec + +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IRewardsManager } from "../contracts/rewards/IRewardsManager.sol"; interface IRewardsManagerToolshed is IRewardsManager { @@ -10,23 +15,23 @@ interface IRewardsManagerToolshed is IRewardsManager { event RewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount); /** - * @dev Emitted when rewards are assigned to an indexer. + * @notice Emitted when rewards are assigned to an indexer (Horizon version) * @dev We use the Horizon prefix to change the event signature which makes network subgraph development much easier */ event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount); /** - * @dev Emitted when rewards are denied to an indexer. + * @notice Emitted when rewards are denied to an indexer */ event RewardsDenied(address indexed indexer, address indexed allocationID); /** - * @dev Emitted when a subgraph is denied for claiming rewards. + * @notice Emitted when a subgraph is denied for claiming rewards */ event RewardsDenylistUpdated(bytes32 indexed subgraphDeploymentID, uint256 sinceBlock); /** - * @dev Emitted when the subgraph service is set + * @notice Emitted when the subgraph service is set */ event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService); diff --git a/packages/interfaces/contracts/toolshed/IServiceRegistryToolshed.sol b/packages/interfaces/contracts/toolshed/IServiceRegistryToolshed.sol index 59155f1f7..cae333ef0 100644 --- a/packages/interfaces/contracts/toolshed/IServiceRegistryToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IServiceRegistryToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +// solhint-disable use-natspec + import { IServiceRegistry } from "../contracts/discovery/IServiceRegistry.sol"; interface IServiceRegistryToolshed is IServiceRegistry { diff --git a/packages/interfaces/contracts/toolshed/ISubgraphServiceToolshed.sol b/packages/interfaces/contracts/toolshed/ISubgraphServiceToolshed.sol index 6d684c7ba..c2da4029e 100644 --- a/packages/interfaces/contracts/toolshed/ISubgraphServiceToolshed.sol +++ b/packages/interfaces/contracts/toolshed/ISubgraphServiceToolshed.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// solhint-disable use-natspec + import { ISubgraphService } from "../subgraph-service/ISubgraphService.sol"; import { IOwnable } from "./internal/IOwnable.sol"; import { IPausable } from "./internal/IPausable.sol"; diff --git a/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol b/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol index d00b9a9f4..217c44ee6 100644 --- a/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol +++ b/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// solhint-disable use-natspec + +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + interface IAllocationManager { // Events event AllocationCreated( diff --git a/packages/interfaces/contracts/toolshed/internal/IOwnable.sol b/packages/interfaces/contracts/toolshed/internal/IOwnable.sol index d5b894c58..5c66558af 100644 --- a/packages/interfaces/contracts/toolshed/internal/IOwnable.sol +++ b/packages/interfaces/contracts/toolshed/internal/IOwnable.sol @@ -1,4 +1,6 @@ // SPDX-License-Identifier: MIT +// solhint-disable use-natspec + pragma solidity 0.8.27; /// @title IOwnable diff --git a/packages/interfaces/contracts/toolshed/internal/IPausable.sol b/packages/interfaces/contracts/toolshed/internal/IPausable.sol index aee534ce3..28def0a70 100644 --- a/packages/interfaces/contracts/toolshed/internal/IPausable.sol +++ b/packages/interfaces/contracts/toolshed/internal/IPausable.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.27; +// solhint-disable use-natspec + /// @title IPausable /// @notice Interface for Pausable contract interface IPausable { diff --git a/packages/interfaces/contracts/toolshed/internal/IProvisionManager.sol b/packages/interfaces/contracts/toolshed/internal/IProvisionManager.sol index 4a536e5e1..31d4b11f7 100644 --- a/packages/interfaces/contracts/toolshed/internal/IProvisionManager.sol +++ b/packages/interfaces/contracts/toolshed/internal/IProvisionManager.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// solhint-disable use-natspec + +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + interface IProvisionManager { // Events event ProvisionTokensRangeSet(uint256 min, uint256 max); diff --git a/packages/interfaces/contracts/toolshed/internal/IProvisionTracker.sol b/packages/interfaces/contracts/toolshed/internal/IProvisionTracker.sol index a8c5a48d5..5cb7d99b0 100644 --- a/packages/interfaces/contracts/toolshed/internal/IProvisionTracker.sol +++ b/packages/interfaces/contracts/toolshed/internal/IProvisionTracker.sol @@ -1,4 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable use-natspec + pragma solidity 0.8.27; interface IProvisionTracker { diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 390efb8ce..a581f1c2c 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -1,6 +1,9 @@ // 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 { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { IDisputeManager } from "@graphprotocol/interfaces/contracts/subgraph-service/IDisputeManager.sol"; @@ -21,7 +24,8 @@ import { AttestationManager } from "./utilities/AttestationManager.sol"; /** * @title DisputeManager - * @notice Provides a way to permissionlessly create disputes for incorrect behavior in the Subgraph Service. + * @author Edge & Node + * @notice Provides a way to permissionlessly create disputes for incorrect behavior in the Subgraph Service * * There are two types of disputes that can be created: Query disputes and Indexing disputes. * diff --git a/packages/subgraph-service/contracts/DisputeManagerStorage.sol b/packages/subgraph-service/contracts/DisputeManagerStorage.sol index 7441efd00..38b6e3115 100644 --- a/packages/subgraph-service/contracts/DisputeManagerStorage.sol +++ b/packages/subgraph-service/contracts/DisputeManagerStorage.sol @@ -7,7 +7,8 @@ import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-s /** * @title DisputeManagerStorage - * @notice This contract holds all the storage variables for the Dispute Manager contract. + * @author Edge & Node + * @notice This contract holds all the storage variables for the Dispute Manager contract * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index aeb4eb827..37eb8f28e 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities +// solhint-disable function-max-lines + import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IGraphTallyCollector } from "@graphprotocol/interfaces/contracts/horizon/IGraphTallyCollector.sol"; @@ -26,6 +30,8 @@ import { Allocation } from "./libraries/Allocation.sol"; /** * @title SubgraphService contract + * @author Edge & Node + * @notice A data service contract for subgraph indexing and querying * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol index 133963347..04dc4abf9 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol @@ -5,7 +5,8 @@ import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-s /** * @title SubgraphServiceStorage - * @notice This contract holds all the storage variables for the Subgraph Service contract. + * @author Edge & Node + * @notice This contract holds all the storage variables for the Subgraph Service contract * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/subgraph-service/contracts/libraries/Allocation.sol b/packages/subgraph-service/contracts/libraries/Allocation.sol index 88f7195b8..5a4e3cb52 100644 --- a/packages/subgraph-service/contracts/libraries/Allocation.sol +++ b/packages/subgraph-service/contracts/libraries/Allocation.sol @@ -7,7 +7,8 @@ import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; /** * @title Allocation library - * @notice A library to handle Allocations. + * @author Edge & Node + * @notice A library to handle Allocations * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/subgraph-service/contracts/libraries/Attestation.sol b/packages/subgraph-service/contracts/libraries/Attestation.sol index d821fbfbc..25bb6651f 100644 --- a/packages/subgraph-service/contracts/libraries/Attestation.sol +++ b/packages/subgraph-service/contracts/libraries/Attestation.sol @@ -1,11 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities + import { IAttestation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAttestation.sol"; /** * @title Attestation library - * @notice A library to handle Attestation. + * @author Edge & Node + * @notice A library to handle Attestation * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ @@ -44,8 +48,8 @@ library Attestation { uint256 private constant BYTES32_BYTE_LENGTH = 32; /** - * @dev Returns if two attestations are conflicting. - * Everything must match except for the responseId. + * @notice Returns if two attestations are conflicting + * @dev Everything must match except for the responseId * @param _attestation1 Attestation * @param _attestation2 Attestation * @return True if the two attestations are conflicting @@ -60,7 +64,7 @@ library Attestation { } /** - * @dev Parse the bytes attestation into a struct from `_data`. + * @notice Parse the bytes attestation into a struct from `_data` * @param _data The bytes to parse * @return Attestation struct */ @@ -87,7 +91,7 @@ library Attestation { } /** - * @dev Parse a uint8 from `_bytes` starting at offset `_start`. + * @notice Parse a uint8 from `_bytes` starting at offset `_start` * @param _bytes The bytes to parse * @param _start The start offset * @return uint8 value @@ -111,7 +115,7 @@ library Attestation { } /** - * @dev Parse a bytes32 from `_bytes` starting at offset `_start`. + * @notice Parse a bytes32 from `_bytes` starting at offset `_start` * @param _bytes The bytes to parse * @param _start The start offset * @return bytes32 value diff --git a/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol b/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol index 2d73abd91..4717cefed 100644 --- a/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol +++ b/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol @@ -6,7 +6,8 @@ import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph- /** * @title LegacyAllocation library - * @notice A library to handle legacy Allocations. + * @author Edge & Node + * @notice A library to handle legacy Allocations * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index e095b3282..746c31f14 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events +// solhint-disable gas-small-strings +// solhint-disable function-max-lines + import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; @@ -20,7 +25,8 @@ import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/ /** * @title AllocationManager contract - * @notice A helper contract implementing allocation lifecycle management. + * @author Edge & Node + * @notice A helper contract implementing allocation lifecycle management * Allows opening, resizing, and closing allocations, as well as collecting indexing rewards by presenting a Proof * of Indexing (POI). * @custom:security-contact Please email security+contracts@thegraph.com if you find any @@ -63,8 +69,8 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @param tokensIndexerRewards The amount of tokens collected for the indexer * @param tokensDelegationRewards The amount of tokens collected for delegators * @param poi The POI presented - * @param currentEpoch The current epoch * @param poiMetadata The metadata associated with the POI + * @param currentEpoch The current epoch */ event IndexingRewardsCollected( address indexed indexer, @@ -95,7 +101,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca ); /** - * @dev Emitted when an indexer closes an allocation + * @notice Emitted when an indexer closes an allocation * @param indexer The address of the indexer * @param allocationId The id of the allocation * @param subgraphDeploymentId The id of the subgraph deployment diff --git a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol index 4c0870291..a56e649fd 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol @@ -6,7 +6,8 @@ import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph- /** * @title AllocationManagerStorage - * @notice This contract holds all the storage variables for the Allocation Manager contract. + * @author Edge & Node + * @notice This contract holds all the storage variables for the Allocation Manager contract * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/subgraph-service/contracts/utilities/AttestationManager.sol b/packages/subgraph-service/contracts/utilities/AttestationManager.sol index f385beeb5..2c45fad3a 100644 --- a/packages/subgraph-service/contracts/utilities/AttestationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AttestationManager.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-small-strings + import { IAttestation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAttestation.sol"; import { AttestationManagerV1Storage } from "./AttestationManagerStorage.sol"; @@ -10,7 +13,8 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I /** * @title AttestationManager contract - * @notice A helper contract implementing attestation verification. + * @author Edge & Node + * @notice A helper contract implementing attestation verification * Uses a custom implementation of EIP712 for backwards compatibility with attestations. * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. @@ -34,18 +38,18 @@ abstract contract AttestationManager is Initializable, AttestationManagerV1Stora bytes32 private constant DOMAIN_SALT = 0xa070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2; /** - * @dev Initialize the AttestationManager contract and parent contracts + * @notice Initialize the AttestationManager contract and parent contracts */ - // solhint-disable-next-line func-name-mixedcase function __AttestationManager_init() internal onlyInitializing { + // solhint-disable-previous-line func-name-mixedcase __AttestationManager_init_unchained(); } /** - * @dev Initialize the AttestationManager contract + * @notice Initialize the AttestationManager contract */ - // solhint-disable-next-line func-name-mixedcase function __AttestationManager_init_unchained() internal onlyInitializing { + // solhint-disable-previous-line func-name-mixedcase _domainSeparator = keccak256( abi.encode( DOMAIN_TYPE_HASH, @@ -59,7 +63,7 @@ abstract contract AttestationManager is Initializable, AttestationManagerV1Stora } /** - * @dev Recover the signer address of the `_attestation`. + * @notice Recover the signer address of the `_attestation` * @param _attestation The attestation struct * @return Signer address */ diff --git a/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol index 1c720ec8c..1559a52fa 100644 --- a/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol +++ b/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol @@ -3,7 +3,8 @@ pragma solidity 0.8.27; /** * @title AttestationManagerStorage - * @notice This contract holds all the storage variables for the Attestation Manager contract. + * @author Edge & Node + * @notice This contract holds all the storage variables for the Attestation Manager contract * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol index 6b96616f4..4bfc1daa0 100644 --- a/packages/subgraph-service/contracts/utilities/Directory.sol +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events + import { IDisputeManager } from "@graphprotocol/interfaces/contracts/subgraph-service/IDisputeManager.sol"; import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-service/ISubgraphService.sol"; import { IGraphTallyCollector } from "@graphprotocol/interfaces/contracts/horizon/IGraphTallyCollector.sol"; @@ -8,7 +11,8 @@ import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curatio /** * @title Directory contract - * @notice This contract is meant to be inherited by {SubgraphService} contract. + * @author Edge & Node + * @notice This contract is meant to be inherited by {SubgraphService} contract * It contains the addresses of the contracts that the contract interacts with. * Uses immutable variables to minimize gas costs. * @custom:security-contact Please email security+contracts@thegraph.com if you find any diff --git a/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol b/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol index b3724daae..8286f2570 100644 --- a/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol +++ b/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol @@ -71,6 +71,12 @@ contract MockRewardsManager is IRewardsManager { function calcRewards(uint256, uint256) external pure returns (uint256) {} + function getRewardsIssuancePerBlock() external view returns (uint256) {} + + // -- Setters -- + + function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external {} + // -- Updates -- function updateAccRewardsPerSignal() external returns (uint256) {} diff --git a/packages/token-distribution/contracts/GraphTokenDistributor.sol b/packages/token-distribution/contracts/GraphTokenDistributor.sol index a7cd88137..79c2dd399 100644 --- a/packages/token-distribution/contracts/GraphTokenDistributor.sol +++ b/packages/token-distribution/contracts/GraphTokenDistributor.sol @@ -2,10 +2,14 @@ pragma solidity ^0.7.3; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec, gas-indexed-events, gas-increment-by-one +// solhint-disable named-parameters-mapping + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; /** * @title GraphTokenDistributor diff --git a/packages/token-distribution/contracts/GraphTokenLock.sol b/packages/token-distribution/contracts/GraphTokenLock.sol index 03cfbca43..545b25413 100644 --- a/packages/token-distribution/contracts/GraphTokenLock.sol +++ b/packages/token-distribution/contracts/GraphTokenLock.sol @@ -2,13 +2,16 @@ pragma solidity ^0.7.3; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec, gas-indexed-events, gas-strict-inequalities, gas-small-strings + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; import { Ownable as OwnableInitializable } from "./Ownable.sol"; -import "./MathUtils.sol"; -import "./IGraphTokenLock.sol"; +import { MathUtils } from "./MathUtils.sol"; +import { IGraphTokenLock } from "./IGraphTokenLock.sol"; /** * @title GraphTokenLock @@ -54,7 +57,7 @@ abstract contract GraphTokenLock is OwnableInitializable, IGraphTokenLock { // A cliff set a date to which a beneficiary needs to get to vest // all preceding periods uint256 public vestingCliffTime; - Revocability public revocable; // Whether to use vesting for locked funds + IGraphTokenLock.Revocability public revocable; // Whether to use vesting for locked funds // State @@ -103,7 +106,7 @@ abstract contract GraphTokenLock is OwnableInitializable, IGraphTokenLock { uint256 _periods, uint256 _releaseStartTime, uint256 _vestingCliffTime, - Revocability _revocable + IGraphTokenLock.Revocability _revocable ) internal { require(!isInitialized, "Already initialized"); require(_owner != address(0), "Owner cannot be zero"); @@ -113,7 +116,7 @@ abstract contract GraphTokenLock is OwnableInitializable, IGraphTokenLock { require(_startTime != 0, "Start time must be set"); require(_startTime < _endTime, "Start time > end time"); require(_periods >= MIN_PERIOD, "Periods cannot be below minimum"); - require(_revocable != Revocability.NotSet, "Must set a revocability option"); + require(_revocable != IGraphTokenLock.Revocability.NotSet, "Must set a revocability option"); require(_releaseStartTime < _endTime, "Release start time must be before end time"); require(_vestingCliffTime < _endTime, "Cliff time must be before end time"); @@ -271,7 +274,7 @@ abstract contract GraphTokenLock is OwnableInitializable, IGraphTokenLock { */ function vestedAmount() public view override returns (uint256) { // If non-revocable it is fully vested - if (revocable == Revocability.Disabled) { + if (revocable == IGraphTokenLock.Revocability.Disabled) { return managedAmount; } @@ -298,7 +301,11 @@ abstract contract GraphTokenLock is OwnableInitializable, IGraphTokenLock { // Vesting cliff is activated and it has not passed means nothing is vested yet // so funds cannot be released - if (revocable == Revocability.Enabled && vestingCliffTime > 0 && currentTime() < vestingCliffTime) { + if ( + revocable == IGraphTokenLock.Revocability.Enabled && + vestingCliffTime > 0 && + currentTime() < vestingCliffTime + ) { return 0; } @@ -368,7 +375,7 @@ abstract contract GraphTokenLock is OwnableInitializable, IGraphTokenLock { * @dev Vesting schedule is always calculated based on managed tokens */ function revoke() external override onlyOwner { - require(revocable == Revocability.Enabled, "Contract is non-revocable"); + require(revocable == IGraphTokenLock.Revocability.Enabled, "Contract is non-revocable"); require(isRevoked == false, "Already revoked"); uint256 unvestedAmount = managedAmount.sub(vestedAmount()); diff --git a/packages/token-distribution/contracts/GraphTokenLockManager.sol b/packages/token-distribution/contracts/GraphTokenLockManager.sol index 2ec96887e..8eb719685 100644 --- a/packages/token-distribution/contracts/GraphTokenLockManager.sol +++ b/packages/token-distribution/contracts/GraphTokenLockManager.sol @@ -3,14 +3,19 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin/contracts/utils/EnumerableSet.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec, gas-indexed-events, gas-strict-inequalities, gas-increment-by-one +// solhint-disable named-parameters-mapping + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/EnumerableSet.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import "./MinimalProxyFactory.sol"; -import "./IGraphTokenLockManager.sol"; +import { MinimalProxyFactory } from "./MinimalProxyFactory.sol"; +import { IGraphTokenLockManager } from "./IGraphTokenLockManager.sol"; +import { IGraphTokenLock } from "./IGraphTokenLock.sol"; import { GraphTokenLockWallet } from "./GraphTokenLockWallet.sol"; /** diff --git a/packages/token-distribution/contracts/GraphTokenLockSimple.sol b/packages/token-distribution/contracts/GraphTokenLockSimple.sol index e5b00a3fa..9f0ae48a9 100644 --- a/packages/token-distribution/contracts/GraphTokenLockSimple.sol +++ b/packages/token-distribution/contracts/GraphTokenLockSimple.sol @@ -2,7 +2,12 @@ pragma solidity ^0.7.3; -import "./GraphTokenLock.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + +import { GraphTokenLock } from "./GraphTokenLock.sol"; +import { IGraphTokenLock } from "./IGraphTokenLock.sol"; +import { Ownable as OwnableInitializable } from "./Ownable.sol"; /** * @title GraphTokenLockSimple @@ -29,7 +34,7 @@ contract GraphTokenLockSimple is GraphTokenLock { uint256 _periods, uint256 _releaseStartTime, uint256 _vestingCliffTime, - Revocability _revocable + IGraphTokenLock.Revocability _revocable ) external onlyOwner { _initialize( _owner, diff --git a/packages/token-distribution/contracts/GraphTokenLockWallet.sol b/packages/token-distribution/contracts/GraphTokenLockWallet.sol index bf47f2f88..3c240396e 100644 --- a/packages/token-distribution/contracts/GraphTokenLockWallet.sol +++ b/packages/token-distribution/contracts/GraphTokenLockWallet.sol @@ -3,12 +3,15 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec, gas-increment-by-one, gas-strict-inequalities, gas-small-strings -import "./GraphTokenLock.sol"; -import "./IGraphTokenLockManager.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; + +import { GraphTokenLock, MathUtils } from "./GraphTokenLock.sol"; +import { IGraphTokenLock } from "./IGraphTokenLock.sol"; +import { IGraphTokenLockManager } from "./IGraphTokenLockManager.sol"; /** * @title GraphTokenLockWallet @@ -54,7 +57,7 @@ contract GraphTokenLockWallet is GraphTokenLock { uint256 _periods, uint256 _releaseStartTime, uint256 _vestingCliffTime, - Revocability _revocable + IGraphTokenLock.Revocability _revocable ) external { _initialize( _owner, diff --git a/packages/token-distribution/contracts/ICallhookReceiver.sol b/packages/token-distribution/contracts/ICallhookReceiver.sol index f8f01d56f..8aab3a1e8 100644 --- a/packages/token-distribution/contracts/ICallhookReceiver.sol +++ b/packages/token-distribution/contracts/ICallhookReceiver.sol @@ -10,6 +10,9 @@ */ pragma solidity ^0.7.3; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + interface ICallhookReceiver { /** * @notice Receive tokens with a callhook from the bridge diff --git a/packages/token-distribution/contracts/IGraphTokenLock.sol b/packages/token-distribution/contracts/IGraphTokenLock.sol index eac89f414..ccdaa004f 100644 --- a/packages/token-distribution/contracts/IGraphTokenLock.sol +++ b/packages/token-distribution/contracts/IGraphTokenLock.sol @@ -3,7 +3,8 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec interface IGraphTokenLock { enum Revocability { diff --git a/packages/token-distribution/contracts/IGraphTokenLockManager.sol b/packages/token-distribution/contracts/IGraphTokenLockManager.sol index c646e5e16..0897f4869 100644 --- a/packages/token-distribution/contracts/IGraphTokenLockManager.sol +++ b/packages/token-distribution/contracts/IGraphTokenLockManager.sol @@ -3,9 +3,12 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec -import "./IGraphTokenLock.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IGraphTokenLock } from "./IGraphTokenLock.sol"; interface IGraphTokenLockManager { // -- Factory -- diff --git a/packages/token-distribution/contracts/L1GraphTokenLockTransferTool.sol b/packages/token-distribution/contracts/L1GraphTokenLockTransferTool.sol index 962cf54ee..5e8bbf325 100644 --- a/packages/token-distribution/contracts/L1GraphTokenLockTransferTool.sol +++ b/packages/token-distribution/contracts/L1GraphTokenLockTransferTool.sol @@ -3,6 +3,10 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable function-max-lines, gas-indexed-events, gas-strict-inequalities, use-natspec +// solhint-disable named-parameters-mapping + import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import { ITokenGateway } from "./arbitrum/ITokenGateway.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -38,12 +42,16 @@ contract L1GraphTokenLockTransferTool is OwnableInitializable, Initializable, Mi using SafeMathUpgradeable for uint256; /// Address of the L1 GRT token contract + // solhint-disable-next-line immutable-vars-naming IERC20 public immutable graphToken; /// Address of the L2GraphTokenLockWallet implementation in L2, used to compute L2 wallet addresses + // solhint-disable-next-line immutable-vars-naming address public immutable l2Implementation; /// Address of the L1GraphTokenGateway contract + // solhint-disable-next-line immutable-vars-naming ITokenGateway public immutable l1Gateway; /// Address of the Staking contract, used to pull ETH for L2 ticket gas + // solhint-disable-next-line immutable-vars-naming address payable public immutable staking; /// L2 lock manager for each L1 lock manager. /// L1 GraphTokenLockManager => L2GraphTokenLockManager diff --git a/packages/token-distribution/contracts/L2GraphTokenLockManager.sol b/packages/token-distribution/contracts/L2GraphTokenLockManager.sol index ee1c30a59..f7609cb03 100644 --- a/packages/token-distribution/contracts/L2GraphTokenLockManager.sol +++ b/packages/token-distribution/contracts/L2GraphTokenLockManager.sol @@ -3,6 +3,10 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-indexed-events, use-natspec +// solhint-disable named-parameters-mapping + import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; @@ -42,8 +46,10 @@ contract L2GraphTokenLockManager is GraphTokenLockManager, ICallhookReceiver { } /// Address of the L2GraphTokenGateway + // solhint-disable-next-line immutable-vars-naming address public immutable l2Gateway; /// Address of the L1 transfer tool contract (in L1, no aliasing) + // solhint-disable-next-line immutable-vars-naming address public immutable l1TransferTool; /// Mapping of each L1 wallet to its L2 wallet counterpart (populated when each wallet is received) /// L1 address => L2 address diff --git a/packages/token-distribution/contracts/L2GraphTokenLockTransferTool.sol b/packages/token-distribution/contracts/L2GraphTokenLockTransferTool.sol index 01010a3a0..81f15c960 100644 --- a/packages/token-distribution/contracts/L2GraphTokenLockTransferTool.sol +++ b/packages/token-distribution/contracts/L2GraphTokenLockTransferTool.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-strict-inequalities, use-natspec + import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { L2GraphTokenLockManager } from "./L2GraphTokenLockManager.sol"; @@ -16,10 +19,13 @@ import { ITokenGateway } from "./arbitrum/ITokenGateway.sol"; */ contract L2GraphTokenLockTransferTool { /// Address of the L2 GRT token + // solhint-disable-next-line immutable-vars-naming IERC20 public immutable graphToken; /// Address of the L2GraphTokenGateway + // solhint-disable-next-line immutable-vars-naming ITokenGateway public immutable l2Gateway; /// Address of the L1 GRT token (in L1, no aliasing) + // solhint-disable-next-line immutable-vars-naming address public immutable l1GraphToken; /// @dev Emitted when GRT is sent to L1 from a token lock diff --git a/packages/token-distribution/contracts/L2GraphTokenLockWallet.sol b/packages/token-distribution/contracts/L2GraphTokenLockWallet.sol index 905bee460..3fcba490a 100644 --- a/packages/token-distribution/contracts/L2GraphTokenLockWallet.sol +++ b/packages/token-distribution/contracts/L2GraphTokenLockWallet.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { GraphTokenLockWallet } from "./GraphTokenLockWallet.sol"; diff --git a/packages/token-distribution/contracts/MathUtils.sol b/packages/token-distribution/contracts/MathUtils.sol index 742c52c37..22853c809 100644 --- a/packages/token-distribution/contracts/MathUtils.sol +++ b/packages/token-distribution/contracts/MathUtils.sol @@ -2,6 +2,9 @@ pragma solidity ^0.7.3; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + library MathUtils { function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; diff --git a/packages/token-distribution/contracts/MinimalProxyFactory.sol b/packages/token-distribution/contracts/MinimalProxyFactory.sol index ca1f03ee1..9da66d95e 100644 --- a/packages/token-distribution/contracts/MinimalProxyFactory.sol +++ b/packages/token-distribution/contracts/MinimalProxyFactory.sol @@ -2,6 +2,9 @@ pragma solidity ^0.7.3; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; diff --git a/packages/token-distribution/contracts/Ownable.sol b/packages/token-distribution/contracts/Ownable.sol index 73ec22821..4e45fcd25 100644 --- a/packages/token-distribution/contracts/Ownable.sol +++ b/packages/token-distribution/contracts/Ownable.sol @@ -2,6 +2,9 @@ pragma solidity ^0.7.3; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec, gas-small-strings + /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to diff --git a/packages/token-distribution/contracts/arbitrum/ITokenGateway.sol b/packages/token-distribution/contracts/arbitrum/ITokenGateway.sol index bf2968309..06ec7be0d 100644 --- a/packages/token-distribution/contracts/arbitrum/ITokenGateway.sol +++ b/packages/token-distribution/contracts/arbitrum/ITokenGateway.sol @@ -26,6 +26,9 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + interface ITokenGateway { /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated // event OutboundTransferInitiated( diff --git a/packages/token-distribution/contracts/tests/BridgeMock.sol b/packages/token-distribution/contracts/tests/BridgeMock.sol index 643a20428..48bfbbec3 100644 --- a/packages/token-distribution/contracts/tests/BridgeMock.sol +++ b/packages/token-distribution/contracts/tests/BridgeMock.sol @@ -2,7 +2,10 @@ pragma solidity ^0.7.3; -import "./arbitrum/IBridge.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable gas-increment-by-one, use-natspec + +import { IBridge } from "./arbitrum/IBridge.sol"; /** * @title Arbitrum Bridge mock contract diff --git a/packages/token-distribution/contracts/tests/GraphTokenMock.sol b/packages/token-distribution/contracts/tests/GraphTokenMock.sol index bc52b9456..f513c92d6 100644 --- a/packages/token-distribution/contracts/tests/GraphTokenMock.sol +++ b/packages/token-distribution/contracts/tests/GraphTokenMock.sol @@ -2,8 +2,11 @@ pragma solidity ^0.7.3; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; /** * @title Graph Token Mock contract. diff --git a/packages/token-distribution/contracts/tests/InboxMock.sol b/packages/token-distribution/contracts/tests/InboxMock.sol index 9c16ee4ab..fa6b28c82 100644 --- a/packages/token-distribution/contracts/tests/InboxMock.sol +++ b/packages/token-distribution/contracts/tests/InboxMock.sol @@ -2,8 +2,12 @@ pragma solidity ^0.7.3; -import "./arbitrum/IInbox.sol"; -import "./arbitrum/AddressAliasHelper.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + +import { IInbox } from "./arbitrum/IInbox.sol"; +import { AddressAliasHelper } from "./arbitrum/AddressAliasHelper.sol"; +import { IBridge } from "./arbitrum/IBridge.sol"; /** * @title Arbitrum Inbox mock contract diff --git a/packages/token-distribution/contracts/tests/L1TokenGatewayMock.sol b/packages/token-distribution/contracts/tests/L1TokenGatewayMock.sol index 679bcb0ad..8e2fbd11a 100644 --- a/packages/token-distribution/contracts/tests/L1TokenGatewayMock.sol +++ b/packages/token-distribution/contracts/tests/L1TokenGatewayMock.sol @@ -2,6 +2,8 @@ pragma solidity ^0.7.3; +// solhint-disable gas-increment-by-one, gas-indexed-events, gas-strict-inequalities, use-natspec + import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; diff --git a/packages/token-distribution/contracts/tests/L2TokenGatewayMock.sol b/packages/token-distribution/contracts/tests/L2TokenGatewayMock.sol index c9e12dd74..c7794d2b5 100644 --- a/packages/token-distribution/contracts/tests/L2TokenGatewayMock.sol +++ b/packages/token-distribution/contracts/tests/L2TokenGatewayMock.sol @@ -2,6 +2,8 @@ pragma solidity ^0.7.3; +// solhint-disable gas-increment-by-one, gas-indexed-events, use-natspec + import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ITokenGateway } from "../arbitrum//ITokenGateway.sol"; import { GraphTokenMock } from "./GraphTokenMock.sol"; @@ -13,8 +15,10 @@ import { ICallhookReceiver } from "../ICallhookReceiver.sol"; */ contract L2TokenGatewayMock is Ownable { /// Address of the L1 GRT contract + // solhint-disable-next-line immutable-vars-naming address public immutable l1Token; /// Address of the L2 GRT contract + // solhint-disable-next-line immutable-vars-naming address public immutable l2Token; /// Next ID to return when sending an outboundTransfer uint256 public nextId; diff --git a/packages/token-distribution/contracts/tests/Stakes.sol b/packages/token-distribution/contracts/tests/Stakes.sol index bf140aa8f..81d5480fa 100644 --- a/packages/token-distribution/contracts/tests/Stakes.sol +++ b/packages/token-distribution/contracts/tests/Stakes.sol @@ -3,7 +3,10 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts/math/SafeMath.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; /** * @title A collection of data structures and functions to manage the Indexer Stake state. diff --git a/packages/token-distribution/contracts/tests/StakingMock.sol b/packages/token-distribution/contracts/tests/StakingMock.sol index 8c5fffc80..b4c1871ed 100644 --- a/packages/token-distribution/contracts/tests/StakingMock.sol +++ b/packages/token-distribution/contracts/tests/StakingMock.sol @@ -3,48 +3,71 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// solhint-disable named-parameters-mapping +// solhint-disable gas-strict-inequalities -import "./Stakes.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { Stakes } from "./Stakes.sol"; + +/** + * @title StakingMock contract + * @author Edge & Node + * @notice A mock contract for testing staking functionality + */ contract StakingMock { using SafeMath for uint256; using Stakes for Stakes.Indexer; // -- State -- + /// @notice Minimum stake required for indexers uint256 public minimumIndexerStake = 100e18; + /// @notice Thawing period in blocks uint256 public thawingPeriod = 10; // 10 blocks + /// @notice The token contract IERC20 public token; - // Indexer stakes : indexer => Stake + /// @notice Indexer stakes mapping mapping(address => Stakes.Indexer) public stakes; /** - * @dev Emitted when `indexer` stake `tokens` amount. + * @notice Emitted when indexer stakes tokens + * @param indexer The indexer address + * @param tokens The amount of tokens staked */ - event StakeDeposited(address indexed indexer, uint256 tokens); + event StakeDeposited(address indexed indexer, uint256 indexed tokens); /** - * @dev Emitted when `indexer` unstaked and locked `tokens` amount `until` block. + * @notice Emitted when indexer unstakes and locks tokens + * @param indexer The indexer address + * @param tokens The amount of tokens locked + * @param until The block number until which tokens are locked */ - event StakeLocked(address indexed indexer, uint256 tokens, uint256 until); + event StakeLocked(address indexed indexer, uint256 indexed tokens, uint256 indexed until); /** - * @dev Emitted when `indexer` withdrew `tokens` staked. + * @notice Emitted when indexer withdraws staked tokens + * @param indexer The indexer address + * @param tokens The amount of tokens withdrawn */ - event StakeWithdrawn(address indexed indexer, uint256 tokens); + event StakeWithdrawn(address indexed indexer, uint256 indexed tokens); - // Contract constructor. + /** + * @notice Contract constructor + * @param _token The token contract address + */ constructor(IERC20 _token) { require(address(_token) != address(0), "!token"); token = _token; } + /// @notice Receive function to accept ETH receive() external payable {} /** - * @dev Deposit tokens on the indexer stake. + * @notice Deposit tokens on the indexer stake * @param _tokens Amount of tokens to stake */ function stake(uint256 _tokens) external { @@ -52,7 +75,7 @@ contract StakingMock { } /** - * @dev Deposit tokens on the indexer stake. + * @notice Deposit tokens on the indexer stake * @param _indexer Address of the indexer * @param _tokens Amount of tokens to stake */ @@ -70,7 +93,7 @@ contract StakingMock { } /** - * @dev Unstake tokens from the indexer stake, lock them until thawing period expires. + * @notice Unstake tokens from the indexer stake, lock them until thawing period expires * @param _tokens Amount of tokens to unstake */ function unstake(uint256 _tokens) external { @@ -97,12 +120,17 @@ contract StakingMock { } /** - * @dev Withdraw indexer tokens once the thawing period has passed. + * @notice Withdraw indexer tokens once the thawing period has passed */ function withdraw() external { _withdraw(msg.sender); } + /** + * @notice Internal function to stake tokens for an indexer + * @param _indexer Address of the indexer + * @param _tokens Amount of tokens to stake + */ function _stake(address _indexer, uint256 _tokens) internal { // Deposit tokens into the indexer stake Stakes.Indexer storage indexerStake = stakes[_indexer]; @@ -112,7 +140,7 @@ contract StakingMock { } /** - * @dev Withdraw indexer tokens once the thawing period has passed. + * @notice Withdraw indexer tokens once the thawing period has passed * @param _indexer Address of indexer to withdraw funds from */ function _withdraw(address _indexer) private { diff --git a/packages/token-distribution/contracts/tests/WalletMock.sol b/packages/token-distribution/contracts/tests/WalletMock.sol index 872760d6e..b526e547b 100644 --- a/packages/token-distribution/contracts/tests/WalletMock.sol +++ b/packages/token-distribution/contracts/tests/WalletMock.sol @@ -3,6 +3,9 @@ pragma solidity ^0.7.3; pragma experimental ABIEncoderV2; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + import { Address } from "@openzeppelin/contracts/utils/Address.sol"; /** @@ -14,14 +17,19 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol"; */ contract WalletMock { /// Target contract for the fallback function (usually a transfer tool contract) + // solhint-disable-next-line immutable-vars-naming address public immutable target; /// Address of the GRT (mock) token + // solhint-disable-next-line immutable-vars-naming address public immutable token; /// Address of the wallet's manager + // solhint-disable-next-line immutable-vars-naming address public immutable manager; /// Whether the wallet has been initialized + // solhint-disable-next-line immutable-vars-naming bool public immutable isInitialized; /// Whether the beneficiary has accepted the lock + // solhint-disable-next-line immutable-vars-naming bool public immutable isAccepted; /** diff --git a/packages/token-distribution/contracts/tests/arbitrum/AddressAliasHelper.sol b/packages/token-distribution/contracts/tests/arbitrum/AddressAliasHelper.sol index 146c1c876..525e71640 100644 --- a/packages/token-distribution/contracts/tests/arbitrum/AddressAliasHelper.sol +++ b/packages/token-distribution/contracts/tests/arbitrum/AddressAliasHelper.sol @@ -25,8 +25,12 @@ pragma solidity ^0.7.3; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + library AddressAliasHelper { - uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); + // solhint-disable-next-line const-name-snakecase + uint160 internal constant offset = uint160(0x1111000000000000000000000000000000001111); /// @notice Utility function that converts the address in the L1 that submitted a tx to /// the inbox to the msg.sender viewed in the L2 diff --git a/packages/token-distribution/contracts/tests/arbitrum/IBridge.sol b/packages/token-distribution/contracts/tests/arbitrum/IBridge.sol index fdfa34eed..f4ace5767 100644 --- a/packages/token-distribution/contracts/tests/arbitrum/IBridge.sol +++ b/packages/token-distribution/contracts/tests/arbitrum/IBridge.sol @@ -25,6 +25,9 @@ pragma solidity ^0.7.3; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec, gas-indexed-events + interface IBridge { event MessageDelivered( uint256 indexed messageIndex, diff --git a/packages/token-distribution/contracts/tests/arbitrum/IInbox.sol b/packages/token-distribution/contracts/tests/arbitrum/IInbox.sol index 0a6e78dc3..a3b7b096a 100644 --- a/packages/token-distribution/contracts/tests/arbitrum/IInbox.sol +++ b/packages/token-distribution/contracts/tests/arbitrum/IInbox.sol @@ -25,8 +25,11 @@ pragma solidity ^0.7.3; -import "./IBridge.sol"; -import "./IMessageProvider.sol"; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + +import { IBridge } from "./IBridge.sol"; +import { IMessageProvider } from "./IMessageProvider.sol"; interface IInbox is IMessageProvider { function sendL2Message(bytes calldata messageData) external returns (uint256); diff --git a/packages/token-distribution/contracts/tests/arbitrum/IMessageProvider.sol b/packages/token-distribution/contracts/tests/arbitrum/IMessageProvider.sol index cf8446af2..28b3937e8 100644 --- a/packages/token-distribution/contracts/tests/arbitrum/IMessageProvider.sol +++ b/packages/token-distribution/contracts/tests/arbitrum/IMessageProvider.sol @@ -25,6 +25,9 @@ pragma solidity ^0.7.3; +// TODO: Re-enable and fix issues when publishing a new version +// solhint-disable use-natspec + interface IMessageProvider { event InboxMessageDelivered(uint256 indexed messageNum, bytes data); From 04f20cd99ea1591d1d64218f0ce4fc2b4031bc91 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:16:59 +0000 Subject: [PATCH 06/14] refactor: consolidate duplicate interface files to interfaces package - Move 36 duplicate interface files from contracts and token-distribution to interfaces package - Update all import statements across contracts, token-distribution, horizon, and subgraph-service to use @graphprotocol/interfaces - Delete duplicate interface files from source packages - Fix pragma versions for compatibility across compiler configurations - Add @graphprotocol/interfaces dependency to token-distribution All builds and tests passing. --- .../contracts/arbitrum/IArbToken.sol | 57 --- .../contracts/contracts/arbitrum/IBridge.sol | 153 ------ .../contracts/contracts/arbitrum/IInbox.sol | 167 ------- .../contracts/arbitrum/IMessageProvider.sol | 46 -- .../contracts/contracts/arbitrum/IOutbox.sol | 114 ----- .../contracts/arbitrum/ITokenGateway.sol | 97 ---- .../arbitrum/L1ArbitrumMessenger.sol | 6 +- .../contracts/contracts/base/IMulticall.sol | 18 - .../contracts/contracts/base/Multicall.sol | 2 +- .../contracts/contracts/curation/Curation.sol | 4 +- .../contracts/curation/CurationStorage.sol | 4 +- .../contracts/curation/ICuration.sol | 124 ----- .../curation/IGraphCurationToken.sol | 32 -- .../contracts/contracts/discovery/GNS.sol | 6 +- .../contracts/discovery/GNSStorage.sol | 6 +- .../contracts/contracts/discovery/IGNS.sol | 232 --------- .../contracts/discovery/IServiceRegistry.sol | 53 -- .../contracts/discovery/ISubgraphNFT.sol | 67 --- .../discovery/ISubgraphNFTDescriptor.sol | 24 - .../contracts/contracts/discovery/L1GNS.sol | 6 +- .../contracts/discovery/ServiceRegistry.sol | 2 +- .../discovery/ServiceRegistryStorage.sol | 2 +- .../contracts/discovery/SubgraphNFT.sol | 4 +- .../discovery/SubgraphNFTDescriptor.sol | 2 +- .../erc1056/IEthereumDIDRegistry.sol | 26 - .../contracts/disputes/DisputeManager.sol | 4 +- .../disputes/DisputeManagerStorage.sol | 2 +- .../contracts/disputes/IDisputeManager.sol | 203 -------- .../contracts/epochs/EpochManager.sol | 2 +- .../contracts/epochs/IEpochManager.sol | 78 --- .../contracts/gateway/GraphTokenGateway.sol | 2 +- .../contracts/gateway/ICallhookReceiver.sol | 25 - .../contracts/gateway/L1GraphTokenGateway.sol | 10 +- .../contracts/governance/Controller.sol | 4 +- .../contracts/governance/IController.sol | 79 --- .../contracts/governance/IManaged.sol | 32 -- .../contracts/governance/Managed.sol | 16 +- .../contracts/l2/curation/IL2Curation.sol | 48 -- .../contracts/l2/curation/L2Curation.sol | 6 +- .../contracts/l2/discovery/IL2GNS.sol | 67 --- .../contracts/l2/discovery/L2GNS.sol | 6 +- .../contracts/l2/discovery/L2GNSStorage.sol | 2 +- .../l2/gateway/L2GraphTokenGateway.sol | 4 +- .../contracts/l2/staking/IL2Staking.sol | 20 - .../contracts/l2/staking/IL2StakingBase.sol | 24 - .../contracts/l2/staking/IL2StakingTypes.sol | 27 -- .../contracts/l2/staking/L2Staking.sol | 8 +- .../contracts/l2/token/L2GraphToken.sol | 2 +- .../contracts/payments/AllocationExchange.sol | 4 +- .../contracts/rewards/IRewardsIssuer.sol | 42 -- .../contracts/rewards/RewardsManager.sol | 4 +- .../rewards/RewardsManagerStorage.sol | 2 +- .../staking/IL1GraphTokenLockTransferTool.sol | 31 -- .../contracts/staking/IL1Staking.sol | 19 - .../contracts/staking/IL1StakingBase.sol | 171 ------- .../contracts/contracts/staking/IStaking.sol | 19 - .../contracts/staking/IStakingBase.sol | 455 ------------------ .../contracts/staking/IStakingData.sol | 83 ---- .../contracts/staking/IStakingExtension.sol | 322 ------------- .../contracts/contracts/staking/L1Staking.sol | 14 +- .../contracts/staking/L1StakingStorage.sol | 2 +- .../contracts/contracts/staking/Staking.sol | 8 +- .../contracts/staking/StakingExtension.sol | 8 +- .../contracts/staking/StakingStorage.sol | 4 +- .../contracts/staking/libs/IStakes.sol | 18 - .../contracts/staking/libs/Stakes.sol | 2 +- .../contracts/tests/CallhookReceiverMock.sol | 2 +- .../contracts/tests/LegacyGNSMock.sol | 2 +- .../contracts/tests/arbitrum/BridgeMock.sol | 2 +- .../contracts/tests/arbitrum/InboxMock.sol | 4 +- .../contracts/tests/arbitrum/OutboxMock.sol | 4 +- .../contracts/contracts/token/IGraphToken.sol | 107 ---- .../contracts/upgrades/GraphProxy.sol | 2 +- .../contracts/upgrades/GraphProxyAdmin.sol | 2 +- .../contracts/upgrades/GraphUpgradeable.sol | 2 +- .../contracts/upgrades/IGraphProxy.sol | 77 --- .../contracts/contracts/utils/TokenUtils.sol | 2 +- .../horizon/contracts/mocks/MockGRTToken.sol | 2 +- .../contracts/payments/GraphPayments.sol | 2 +- .../contracts/payments/PaymentsEscrow.sol | 2 +- .../contracts/staking/HorizonStaking.sol | 2 +- .../staking/HorizonStakingExtension.sol | 2 +- .../contracts/utilities/GraphDirectory.sol | 2 +- .../GraphDirectoryImplementation.sol | 2 +- .../contracts/arbitrum/ITokenGateway.sol | 2 +- .../contracts/contracts/discovery/IGNS.sol | 14 - .../contracts/gateway/ICallhookReceiver.sol | 2 +- .../contracts/l2/staking/IL2Staking.sol | 2 +- .../contracts/contracts/staking/IStaking.sol | 2 +- .../contracts/staking/IStakingBase.sol | 6 +- .../contracts/toolshed/IL2GNSToolshed.sol | 1 + .../contracts/DisputeManager.sol | 2 +- .../contracts/SubgraphService.sol | 2 +- .../contracts/utilities/AllocationManager.sol | 2 +- .../contracts/ICallhookReceiver.sol | 24 - .../L1GraphTokenLockTransferTool.sol | 2 +- .../contracts/L2GraphTokenLockManager.sol | 2 +- .../L2GraphTokenLockTransferTool.sol | 2 +- .../contracts/arbitrum/ITokenGateway.sol | 78 --- .../contracts/tests/L1TokenGatewayMock.sol | 2 +- .../contracts/tests/L2TokenGatewayMock.sol | 4 +- packages/token-distribution/package.json | 1 + pnpm-lock.yaml | 3 + 103 files changed, 120 insertions(+), 3386 deletions(-) delete mode 100644 packages/contracts/contracts/arbitrum/IArbToken.sol delete mode 100644 packages/contracts/contracts/arbitrum/IBridge.sol delete mode 100644 packages/contracts/contracts/arbitrum/IInbox.sol delete mode 100644 packages/contracts/contracts/arbitrum/IMessageProvider.sol delete mode 100644 packages/contracts/contracts/arbitrum/IOutbox.sol delete mode 100644 packages/contracts/contracts/arbitrum/ITokenGateway.sol delete mode 100644 packages/contracts/contracts/base/IMulticall.sol delete mode 100644 packages/contracts/contracts/curation/ICuration.sol delete mode 100644 packages/contracts/contracts/curation/IGraphCurationToken.sol delete mode 100644 packages/contracts/contracts/discovery/IGNS.sol delete mode 100644 packages/contracts/contracts/discovery/IServiceRegistry.sol delete mode 100644 packages/contracts/contracts/discovery/ISubgraphNFT.sol delete mode 100644 packages/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol delete mode 100644 packages/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol delete mode 100644 packages/contracts/contracts/disputes/IDisputeManager.sol delete mode 100644 packages/contracts/contracts/epochs/IEpochManager.sol delete mode 100644 packages/contracts/contracts/gateway/ICallhookReceiver.sol delete mode 100644 packages/contracts/contracts/governance/IController.sol delete mode 100644 packages/contracts/contracts/governance/IManaged.sol delete mode 100644 packages/contracts/contracts/l2/curation/IL2Curation.sol delete mode 100644 packages/contracts/contracts/l2/discovery/IL2GNS.sol delete mode 100644 packages/contracts/contracts/l2/staking/IL2Staking.sol delete mode 100644 packages/contracts/contracts/l2/staking/IL2StakingBase.sol delete mode 100644 packages/contracts/contracts/l2/staking/IL2StakingTypes.sol delete mode 100644 packages/contracts/contracts/rewards/IRewardsIssuer.sol delete mode 100644 packages/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol delete mode 100644 packages/contracts/contracts/staking/IL1Staking.sol delete mode 100644 packages/contracts/contracts/staking/IL1StakingBase.sol delete mode 100644 packages/contracts/contracts/staking/IStaking.sol delete mode 100644 packages/contracts/contracts/staking/IStakingBase.sol delete mode 100644 packages/contracts/contracts/staking/IStakingData.sol delete mode 100644 packages/contracts/contracts/staking/IStakingExtension.sol delete mode 100644 packages/contracts/contracts/staking/libs/IStakes.sol delete mode 100644 packages/contracts/contracts/token/IGraphToken.sol delete mode 100644 packages/contracts/contracts/upgrades/IGraphProxy.sol delete mode 100644 packages/token-distribution/contracts/ICallhookReceiver.sol delete mode 100644 packages/token-distribution/contracts/arbitrum/ITokenGateway.sol diff --git a/packages/contracts/contracts/arbitrum/IArbToken.sol b/packages/contracts/contracts/arbitrum/IArbToken.sol deleted file mode 100644 index 6517f0c57..000000000 --- a/packages/contracts/contracts/arbitrum/IArbToken.sol +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright 2020, Offchain Labs, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Originally copied from: - * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals - * - * MODIFIED from Offchain Labs' implementation: - * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) - * - */ - -/** - * @title Minimum expected interface for L2 token that interacts with the L2 token bridge (this is the interface necessary - * for a custom token that interacts with the bridge, see TestArbCustomToken.sol for an example implementation). - */ -pragma solidity ^0.7.6; - -/** - * @title Arbitrum Token Interface - * @author Edge & Node - * @notice Interface for tokens that can be minted and burned on Arbitrum L2 - */ -interface IArbToken { - /** - * @notice should increase token supply by amount, and should (probably) only be callable by the L1 bridge. - * @param account Account to mint tokens to - * @param amount Amount of tokens to mint - */ - function bridgeMint(address account, uint256 amount) external; - - /** - * @notice should decrease token supply by amount, and should (probably) only be callable by the L1 bridge. - * @param account Account to burn tokens from - * @param amount Amount of tokens to burn - */ - function bridgeBurn(address account, uint256 amount) external; - - /** - * @notice Get the L1 token address - * @return address of layer 1 token - */ - function l1Address() external view returns (address); -} diff --git a/packages/contracts/contracts/arbitrum/IBridge.sol b/packages/contracts/contracts/arbitrum/IBridge.sol deleted file mode 100644 index 92deee878..000000000 --- a/packages/contracts/contracts/arbitrum/IBridge.sol +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright 2021, Offchain Labs, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Originally copied from: - * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth - * - * MODIFIED from Offchain Labs' implementation: - * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) - * - */ - -pragma solidity ^0.7.6; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - -/** - * @title Bridge Interface - * @author Edge & Node - * @notice Interface for the Arbitrum Bridge contract - */ -interface IBridge { - /** - * @notice Emitted when a message is delivered to the inbox - * @param messageIndex Index of the message - * @param beforeInboxAcc Inbox accumulator before this message - * @param inbox Address of the inbox - * @param kind Type of the message - * @param sender Address that sent the message - * @param messageDataHash Hash of the message data - */ - event MessageDelivered( - uint256 indexed messageIndex, - bytes32 indexed beforeInboxAcc, - address inbox, - uint8 kind, - address sender, - bytes32 messageDataHash - ); - - /** - * @notice Emitted when a bridge call is triggered - * @param outbox Address of the outbox - * @param destAddr Destination address for the call - * @param amount ETH amount sent with the call - * @param data Calldata for the function call - */ - event BridgeCallTriggered(address indexed outbox, address indexed destAddr, uint256 amount, bytes data); - - /** - * @notice Emitted when an inbox is enabled or disabled - * @param inbox Address of the inbox - * @param enabled Whether the inbox is enabled - */ - event InboxToggle(address indexed inbox, bool enabled); - - /** - * @notice Emitted when an outbox is enabled or disabled - * @param outbox Address of the outbox - * @param enabled Whether the outbox is enabled - */ - event OutboxToggle(address indexed outbox, bool enabled); - - /** - * @notice Deliver a message to the inbox - * @param kind Type of the message - * @param sender Address that is sending the message - * @param messageDataHash keccak256 hash of the message data - * @return The message index - */ - function deliverMessageToInbox( - uint8 kind, - address sender, - bytes32 messageDataHash - ) external payable returns (uint256); - - /** - * @notice Execute a call from L2 to L1 - * @param destAddr Contract to call - * @param amount ETH value to send - * @param data Calldata for the function call - * @return success True if the call was successful, false otherwise - * @return returnData Return data from the call - */ - function executeCall( - address destAddr, - uint256 amount, - bytes calldata data - ) external returns (bool success, bytes memory returnData); - - /** - * @notice Set the address of an inbox - * @param inbox Address of the inbox - * @param enabled Whether to enable the inbox - */ - function setInbox(address inbox, bool enabled) external; - - /** - * @notice Set the address of an outbox - * @param inbox Address of the outbox - * @param enabled Whether to enable the outbox - */ - function setOutbox(address inbox, bool enabled) external; - - // View functions - - /** - * @notice Get the active outbox address - * @return The active outbox address - */ - function activeOutbox() external view returns (address); - - /** - * @notice Check if an address is an allowed inbox - * @param inbox Address to check - * @return True if the address is an allowed inbox, false otherwise - */ - function allowedInboxes(address inbox) external view returns (bool); - - /** - * @notice Check if an address is an allowed outbox - * @param outbox Address to check - * @return True if the address is an allowed outbox, false otherwise - */ - function allowedOutboxes(address outbox) external view returns (bool); - - /** - * @notice Get the inbox accumulator at a specific index - * @param index Index to query - * @return The inbox accumulator at the given index - */ - function inboxAccs(uint256 index) external view returns (bytes32); - - /** - * @notice Get the count of messages in the inbox - * @return Number of messages in the inbox - */ - function messageCount() external view returns (uint256); -} diff --git a/packages/contracts/contracts/arbitrum/IInbox.sol b/packages/contracts/contracts/arbitrum/IInbox.sol deleted file mode 100644 index 8ded1c1bb..000000000 --- a/packages/contracts/contracts/arbitrum/IInbox.sol +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright 2021, Offchain Labs, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Originally copied from: - * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth - * - * MODIFIED from Offchain Labs' implementation: - * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) - * - */ - -pragma solidity ^0.7.6; - -import { IBridge } from "./IBridge.sol"; -import { IMessageProvider } from "./IMessageProvider.sol"; - -/** - * @title Inbox Interface - * @author Edge & Node - * @notice Interface for the Arbitrum Inbox contract - */ -interface IInbox is IMessageProvider { - /** - * @notice Send a message to L2 - * @param messageData Encoded data to send in the message - * @return Message number returned by the inbox - */ - function sendL2Message(bytes calldata messageData) external returns (uint256); - - /** - * @notice Send an unsigned transaction to L2 - * @param maxGas Maximum gas for the L2 transaction - * @param gasPriceBid Gas price bid for the L2 transaction - * @param nonce Nonce for the transaction - * @param destAddr Destination address on L2 - * @param amount Amount of ETH to send - * @param data Transaction data - * @return Message number returned by the inbox - */ - function sendUnsignedTransaction( - uint256 maxGas, - uint256 gasPriceBid, - uint256 nonce, - address destAddr, - uint256 amount, - bytes calldata data - ) external returns (uint256); - - /** - * @notice Send a contract transaction to L2 - * @param maxGas Maximum gas for the L2 transaction - * @param gasPriceBid Gas price bid for the L2 transaction - * @param destAddr Destination address on L2 - * @param amount Amount of ETH to send - * @param data Transaction data - * @return Message number returned by the inbox - */ - function sendContractTransaction( - uint256 maxGas, - uint256 gasPriceBid, - address destAddr, - uint256 amount, - bytes calldata data - ) external returns (uint256); - - /** - * @notice Send an L1-funded unsigned transaction to L2 - * @param maxGas Maximum gas for the L2 transaction - * @param gasPriceBid Gas price bid for the L2 transaction - * @param nonce Nonce for the transaction - * @param destAddr Destination address on L2 - * @param data Transaction data - * @return Message number returned by the inbox - */ - function sendL1FundedUnsignedTransaction( - uint256 maxGas, - uint256 gasPriceBid, - uint256 nonce, - address destAddr, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Send an L1-funded contract transaction to L2 - * @param maxGas Maximum gas for the L2 transaction - * @param gasPriceBid Gas price bid for the L2 transaction - * @param destAddr Destination address on L2 - * @param data Transaction data - * @return Message number returned by the inbox - */ - function sendL1FundedContractTransaction( - uint256 maxGas, - uint256 gasPriceBid, - address destAddr, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Create a retryable ticket for an L2 transaction - * @param destAddr Destination address on L2 - * @param arbTxCallValue Call value for the L2 transaction - * @param maxSubmissionCost Maximum cost for submitting the ticket - * @param submissionRefundAddress Address to refund submission cost to - * @param valueRefundAddress Address to refund excess value to - * @param maxGas Maximum gas for the L2 transaction - * @param gasPriceBid Gas price bid for the L2 transaction - * @param data Transaction data - * @return Message number returned by the inbox - */ - function createRetryableTicket( - address destAddr, - uint256 arbTxCallValue, - uint256 maxSubmissionCost, - address submissionRefundAddress, - address valueRefundAddress, - uint256 maxGas, - uint256 gasPriceBid, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Deposit ETH to L2 - * @param maxSubmissionCost Maximum cost for submitting the deposit - * @return Message number returned by the inbox - */ - function depositEth(uint256 maxSubmissionCost) external payable returns (uint256); - - /** - * @notice Get the bridge contract - * @return The bridge contract address - */ - function bridge() external view returns (IBridge); - - /** - * @notice Pause the creation of retryable tickets - */ - function pauseCreateRetryables() external; - - /** - * @notice Unpause the creation of retryable tickets - */ - function unpauseCreateRetryables() external; - - /** - * @notice Start rewriting addresses - */ - function startRewriteAddress() external; - - /** - * @notice Stop rewriting addresses - */ - function stopRewriteAddress() external; -} diff --git a/packages/contracts/contracts/arbitrum/IMessageProvider.sol b/packages/contracts/contracts/arbitrum/IMessageProvider.sol deleted file mode 100644 index ce5822358..000000000 --- a/packages/contracts/contracts/arbitrum/IMessageProvider.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright 2021, Offchain Labs, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Originally copied from: - * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth - * - * MODIFIED from Offchain Labs' implementation: - * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) - * - */ - -pragma solidity ^0.7.6; - -/** - * @title Message Provider Interface - * @author Edge & Node - * @notice Interface for Arbitrum message providers - */ -interface IMessageProvider { - /** - * @notice Emitted when a message is delivered to the inbox - * @param messageNum Message number - * @param data Message data - */ - event InboxMessageDelivered(uint256 indexed messageNum, bytes data); - - /** - * @notice Emitted when a message is delivered from origin - * @param messageNum Message number - */ - event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); -} diff --git a/packages/contracts/contracts/arbitrum/IOutbox.sol b/packages/contracts/contracts/arbitrum/IOutbox.sol deleted file mode 100644 index 658a6c233..000000000 --- a/packages/contracts/contracts/arbitrum/IOutbox.sol +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright 2021, Offchain Labs, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Originally copied from: - * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-eth - * - * MODIFIED from Offchain Labs' implementation: - * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) - * - */ - -pragma solidity ^0.7.6; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - -/** - * @title Arbitrum Outbox Interface - * @author Edge & Node - * @notice Interface for the Arbitrum outbox contract - */ -interface IOutbox { - /** - * @notice Emitted when an outbox entry is created - * @param batchNum Batch number - * @param outboxEntryIndex Index of the outbox entry - * @param outputRoot Output root hash - * @param numInBatch Number of messages in the batch - */ - event OutboxEntryCreated( - uint256 indexed batchNum, - uint256 outboxEntryIndex, - bytes32 outputRoot, - uint256 numInBatch - ); - - /** - * @notice Emitted when an outbox transaction is executed - * @param destAddr Destination address - * @param l2Sender L2 sender address - * @param outboxEntryIndex Index of the outbox entry - * @param transactionIndex Index of the transaction - */ - event OutBoxTransactionExecuted( - address indexed destAddr, - address indexed l2Sender, - uint256 indexed outboxEntryIndex, - uint256 transactionIndex - ); - - /** - * @notice Get the L2 to L1 sender address - * @return The sender address - */ - function l2ToL1Sender() external view returns (address); - - /** - * @notice Get the L2 to L1 block number - * @return The block number - */ - function l2ToL1Block() external view returns (uint256); - - /** - * @notice Get the L2 to L1 Ethereum block number - * @return The Ethereum block number - */ - function l2ToL1EthBlock() external view returns (uint256); - - /** - * @notice Get the L2 to L1 timestamp - * @return The timestamp - */ - function l2ToL1Timestamp() external view returns (uint256); - - /** - * @notice Get the L2 to L1 batch number - * @return The batch number - */ - function l2ToL1BatchNum() external view returns (uint256); - - /** - * @notice Get the L2 to L1 output ID - * @return The output ID - */ - function l2ToL1OutputId() external view returns (bytes32); - - /** - * @notice Process outgoing messages - * @param sendsData Encoded message data - * @param sendLengths Array of message lengths - */ - function processOutgoingMessages(bytes calldata sendsData, uint256[] calldata sendLengths) external; - - /** - * @notice Check if an outbox entry exists - * @param batchNum Batch number to check - * @return True if the entry exists - */ - function outboxEntryExists(uint256 batchNum) external view returns (bool); -} diff --git a/packages/contracts/contracts/arbitrum/ITokenGateway.sol b/packages/contracts/contracts/arbitrum/ITokenGateway.sol deleted file mode 100644 index 2a0d1a2f3..000000000 --- a/packages/contracts/contracts/arbitrum/ITokenGateway.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright 2020, Offchain Labs, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Originally copied from: - * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals - * - * MODIFIED from Offchain Labs' implementation: - * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) - * - */ - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Token Gateway Interface - * @author Edge & Node - * @notice Interface for token gateways that handle cross-chain token transfers - */ -interface ITokenGateway { - /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated - // event OutboundTransferInitiated( - // address token, - // address indexed _from, - // address indexed _to, - // uint256 indexed _transferId, - // uint256 _amount, - // bytes _data - // ); - - /// @notice event deprecated in favor of DepositFinalized and WithdrawalFinalized - // event InboundTransferFinalized( - // address token, - // address indexed _from, - // address indexed _to, - // uint256 indexed _transferId, - // uint256 _amount, - // bytes _data - // ); - - /** - * @notice Transfer tokens from L1 to L2 or L2 to L1 - * @param token Address of the token being transferred - * @param to Recipient address on the destination chain - * @param amount Amount of tokens to transfer - * @param maxGas Maximum gas for the transaction - * @param gasPriceBid Gas price bid for the transaction - * @param data Additional data for the transfer - * @return Transaction data - */ - function outboundTransfer( - address token, - address to, - uint256 amount, - uint256 maxGas, - uint256 gasPriceBid, - bytes calldata data - ) external payable returns (bytes memory); - - /** - * @notice Finalize an inbound token transfer - * @param token Address of the token being transferred - * @param from Sender address on the source chain - * @param to Recipient address on the destination chain - * @param amount Amount of tokens being transferred - * @param data Additional data for the transfer - */ - function finalizeInboundTransfer( - address token, - address from, - address to, - uint256 amount, - bytes calldata data - ) external payable; - - /** - * @notice Calculate the address used when bridging an ERC20 token - * @dev the L1 and L2 address oracles may not always be in sync. - * For example, a custom token may have been registered but not deployed or the contract self destructed. - * @param l1ERC20 address of L1 token - * @return L2 address of a bridged ERC20 token - */ - function calculateL2TokenAddress(address l1ERC20) external view returns (address); -} diff --git a/packages/contracts/contracts/arbitrum/L1ArbitrumMessenger.sol b/packages/contracts/contracts/arbitrum/L1ArbitrumMessenger.sol index 9613294ad..0428e9b87 100644 --- a/packages/contracts/contracts/arbitrum/L1ArbitrumMessenger.sol +++ b/packages/contracts/contracts/arbitrum/L1ArbitrumMessenger.sol @@ -25,9 +25,9 @@ pragma solidity ^0.7.6; -import { IInbox } from "./IInbox.sol"; -import { IOutbox } from "./IOutbox.sol"; -import { IBridge } from "./IBridge.sol"; +import { IInbox } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IInbox.sol"; +import { IOutbox } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IOutbox.sol"; +import { IBridge } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IBridge.sol"; /** * @title L1 Arbitrum Messenger diff --git a/packages/contracts/contracts/base/IMulticall.sol b/packages/contracts/contracts/base/IMulticall.sol deleted file mode 100644 index 07f40ea36..000000000 --- a/packages/contracts/contracts/base/IMulticall.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; -pragma abicoder v2; - -/** - * @title Multicall interface - * @author Edge & Node - * @notice Enables calling multiple methods in a single call to the contract - */ -interface IMulticall { - /** - * @notice Call multiple functions in the current contract and return the data from all of them if they all succeed - * @param data The encoded function data for each of the calls to make to this contract - * @return results The results from each of the calls passed in via data - */ - function multicall(bytes[] calldata data) external returns (bytes[] memory results); -} diff --git a/packages/contracts/contracts/base/Multicall.sol b/packages/contracts/contracts/base/Multicall.sol index 9d9d48d34..808f7695f 100644 --- a/packages/contracts/contracts/base/Multicall.sol +++ b/packages/contracts/contracts/base/Multicall.sol @@ -6,7 +6,7 @@ pragma abicoder v2; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-increment-by-one -import { IMulticall } from "./IMulticall.sol"; +import { IMulticall } from "@graphprotocol/interfaces/contracts/contracts/base/IMulticall.sol"; // Inspired by https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/base/Multicall.sol // Note: Removed payable from the multicall diff --git a/packages/contracts/contracts/curation/Curation.sol b/packages/contracts/contracts/curation/Curation.sol index acbbc5df9..e7aac2cc2 100644 --- a/packages/contracts/contracts/curation/Curation.sol +++ b/packages/contracts/contracts/curation/Curation.sol @@ -15,9 +15,9 @@ import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; import { TokenUtils } from "../utils/TokenUtils.sol"; import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { Managed } from "../governance/Managed.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { CurationV2Storage } from "./CurationStorage.sol"; -import { IGraphCurationToken } from "./IGraphCurationToken.sol"; +import { IGraphCurationToken } from "@graphprotocol/interfaces/contracts/contracts/curation/IGraphCurationToken.sol"; /** * @title Curation contract diff --git a/packages/contracts/contracts/curation/CurationStorage.sol b/packages/contracts/contracts/curation/CurationStorage.sol index 92e0f7843..67b302bfe 100644 --- a/packages/contracts/contracts/curation/CurationStorage.sol +++ b/packages/contracts/contracts/curation/CurationStorage.sol @@ -8,8 +8,8 @@ pragma solidity ^0.7.6; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; -import { ICuration } from "./ICuration.sol"; -import { IGraphCurationToken } from "./IGraphCurationToken.sol"; +import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; +import { IGraphCurationToken } from "@graphprotocol/interfaces/contracts/contracts/curation/IGraphCurationToken.sol"; import { Managed } from "../governance/Managed.sol"; /** diff --git a/packages/contracts/contracts/curation/ICuration.sol b/packages/contracts/contracts/curation/ICuration.sol deleted file mode 100644 index b71273c25..000000000 --- a/packages/contracts/contracts/curation/ICuration.sol +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Curation Interface - * @author Edge & Node - * @notice Interface for the Curation contract (and L2Curation too) - */ -interface ICuration { - // -- Configuration -- - - /** - * @notice Update the default reserve ratio to `_defaultReserveRatio` - * @param _defaultReserveRatio Reserve ratio (in PPM) - */ - function setDefaultReserveRatio(uint32 _defaultReserveRatio) external; - - /** - * @notice Update the minimum deposit amount needed to intialize a new subgraph - * @param _minimumCurationDeposit Minimum amount of tokens required deposit - */ - function setMinimumCurationDeposit(uint256 _minimumCurationDeposit) external; - - /** - * @notice Set the curation tax percentage to charge when a curator deposits GRT tokens. - * @param _percentage Curation tax percentage charged when depositing GRT tokens - */ - function setCurationTaxPercentage(uint32 _percentage) external; - - /** - * @notice Set the master copy to use as clones for the curation token. - * @param _curationTokenMaster Address of implementation contract to use for curation tokens - */ - function setCurationTokenMaster(address _curationTokenMaster) external; - - // -- Curation -- - - /** - * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. - * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal - * @param _tokensIn Amount of Graph Tokens to deposit - * @param _signalOutMin Expected minimum amount of signal to receive - * @return Amount of signal minted - * @return Amount of curation tax burned - */ - function mint( - bytes32 _subgraphDeploymentID, - uint256 _tokensIn, - uint256 _signalOutMin - ) external returns (uint256, uint256); - - /** - * @notice Burn _signal from the SubgraphDeployment curation pool - * @param _subgraphDeploymentID SubgraphDeployment the curator is returning signal - * @param _signalIn Amount of signal to return - * @param _tokensOutMin Expected minimum amount of tokens to receive - * @return Tokens returned - */ - function burn(bytes32 _subgraphDeploymentID, uint256 _signalIn, uint256 _tokensOutMin) external returns (uint256); - - /** - * @notice Assign Graph Tokens collected as curation fees to the curation pool reserve. - * @param _subgraphDeploymentID SubgraphDeployment where funds should be allocated as reserves - * @param _tokens Amount of Graph Tokens to add to reserves - */ - function collect(bytes32 _subgraphDeploymentID, uint256 _tokens) external; - - // -- Getters -- - - /** - * @notice Check if any GRT tokens are deposited for a SubgraphDeployment. - * @param _subgraphDeploymentID SubgraphDeployment to check if curated - * @return True if curated, false otherwise - */ - function isCurated(bytes32 _subgraphDeploymentID) external view returns (bool); - - /** - * @notice Get the amount of signal a curator has in a curation pool. - * @param _curator Curator owning the signal tokens - * @param _subgraphDeploymentID Subgraph deployment curation pool - * @return Amount of signal owned by a curator for the subgraph deployment - */ - function getCuratorSignal(address _curator, bytes32 _subgraphDeploymentID) external view returns (uint256); - - /** - * @notice Get the amount of signal in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation pool - * @return Amount of signal minted for the subgraph deployment - */ - function getCurationPoolSignal(bytes32 _subgraphDeploymentID) external view returns (uint256); - - /** - * @notice Get the amount of token reserves in a curation pool. - * @param _subgraphDeploymentID Subgraph deployment curation pool - * @return Amount of token reserves in the curation pool - */ - function getCurationPoolTokens(bytes32 _subgraphDeploymentID) external view returns (uint256); - - /** - * @notice Calculate amount of signal that can be bought with tokens in a curation pool. - * This function considers and excludes the deposit tax. - * @param _subgraphDeploymentID Subgraph deployment to mint signal - * @param _tokensIn Amount of tokens used to mint signal - * @return Amount of signal that can be bought - * @return Amount of tokens that will be burned as curation tax - */ - function tokensToSignal(bytes32 _subgraphDeploymentID, uint256 _tokensIn) external view returns (uint256, uint256); - - /** - * @notice Calculate number of tokens to get when burning signal from a curation pool. - * @param _subgraphDeploymentID Subgraph deployment to burn signal - * @param _signalIn Amount of signal to burn - * @return Amount of tokens to get for the specified amount of signal - */ - function signalToTokens(bytes32 _subgraphDeploymentID, uint256 _signalIn) external view returns (uint256); - - /** - * @notice Tax charged when curators deposit funds. - * Parts per million. (Allows for 4 decimal points, 999,999 = 99.9999%) - * @return Curation tax percentage expressed in PPM - */ - function curationTaxPercentage() external view returns (uint32); -} diff --git a/packages/contracts/contracts/curation/IGraphCurationToken.sol b/packages/contracts/contracts/curation/IGraphCurationToken.sol deleted file mode 100644 index 10dda6dcf..000000000 --- a/packages/contracts/contracts/curation/IGraphCurationToken.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; - -/** - * @title Graph Curation Token Interface - * @author Edge & Node - * @notice Interface for curation tokens that represent shares in subgraph curation pools - */ -interface IGraphCurationToken is IERC20Upgradeable { - /** - * @notice Graph Curation Token Contract initializer. - * @param _owner Address of the contract issuing this token - */ - function initialize(address _owner) external; - - /** - * @notice Burn tokens from an address. - * @param _account Address from where tokens will be burned - * @param _amount Amount of tokens to burn - */ - function burnFrom(address _account, uint256 _amount) external; - - /** - * @notice Mint new tokens. - * @param _to Address to send the newly minted tokens - * @param _amount Amount of tokens to mint - */ - function mint(address _to, uint256 _amount) external; -} diff --git a/packages/contracts/contracts/discovery/GNS.sol b/packages/contracts/contracts/discovery/GNS.sol index 27310242c..384bd3b66 100644 --- a/packages/contracts/contracts/discovery/GNS.sol +++ b/packages/contracts/contracts/discovery/GNS.sol @@ -12,11 +12,11 @@ import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/Ad import { Multicall } from "../base/Multicall.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; import { TokenUtils } from "../utils/TokenUtils.sol"; -import { ICuration } from "../curation/ICuration.sol"; +import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; import { Managed } from "../governance/Managed.sol"; -import { ISubgraphNFT } from "./ISubgraphNFT.sol"; +import { ISubgraphNFT } from "@graphprotocol/interfaces/contracts/contracts/discovery/ISubgraphNFT.sol"; -import { IGNS } from "./IGNS.sol"; +import { IGNS } from "@graphprotocol/interfaces/contracts/contracts/discovery/IGNS.sol"; import { GNSV3Storage } from "./GNSStorage.sol"; /** diff --git a/packages/contracts/contracts/discovery/GNSStorage.sol b/packages/contracts/contracts/discovery/GNSStorage.sol index 696546cab..ca746ec29 100644 --- a/packages/contracts/contracts/discovery/GNSStorage.sol +++ b/packages/contracts/contracts/discovery/GNSStorage.sol @@ -10,9 +10,9 @@ pragma abicoder v2; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; import { Managed } from "../governance/Managed.sol"; -import { IEthereumDIDRegistry } from "./erc1056/IEthereumDIDRegistry.sol"; -import { IGNS } from "./IGNS.sol"; -import { ISubgraphNFT } from "./ISubgraphNFT.sol"; +import { IEthereumDIDRegistry } from "@graphprotocol/interfaces/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol"; +import { IGNS } from "@graphprotocol/interfaces/contracts/contracts/discovery/IGNS.sol"; +import { ISubgraphNFT } from "@graphprotocol/interfaces/contracts/contracts/discovery/ISubgraphNFT.sol"; /** * @title GNSV1Storage diff --git a/packages/contracts/contracts/discovery/IGNS.sol b/packages/contracts/contracts/discovery/IGNS.sol deleted file mode 100644 index 98f4b339e..000000000 --- a/packages/contracts/contracts/discovery/IGNS.sol +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Interface for GNS - * @author Edge & Node - * @notice Interface for the Graph Name System (GNS) contract - */ -interface IGNS { - // -- Pool -- - - /** - * @dev The SubgraphData struct holds information about subgraphs - * and their signal; both nSignal (i.e. name signal at the GNS level) - * and vSignal (i.e. version signal at the Curation contract level) - * @param vSignal The token of the subgraph-deployment bonding curve - * @param nSignal The token of the subgraph bonding curve - * @param curatorNSignal Mapping of curator addresses to their name signal amounts - * @param subgraphDeploymentID The deployment ID this subgraph points to - * @param __DEPRECATED_reserveRatio Deprecated reserve ratio field - * @param disabled Whether the subgraph is disabled/deprecated - * @param withdrawableGRT Amount of GRT available for withdrawal after deprecation - */ - struct SubgraphData { - uint256 vSignal; // The token of the subgraph-deployment bonding curve - uint256 nSignal; // The token of the subgraph bonding curve - mapping(address => uint256) curatorNSignal; - bytes32 subgraphDeploymentID; - uint32 __DEPRECATED_reserveRatio; // solhint-disable-line var-name-mixedcase - bool disabled; - uint256 withdrawableGRT; - } - - /** - * @dev The LegacySubgraphKey struct holds the account and sequence ID - * used to generate subgraph IDs in legacy subgraphs. - * @param account The account that created the legacy subgraph - * @param accountSeqID The sequence ID for the account's subgraphs - */ - struct LegacySubgraphKey { - address account; - uint256 accountSeqID; - } - - // -- Configuration -- - - /** - * @notice Approve curation contract to pull funds. - */ - function approveAll() external; - - /** - * @notice Set the owner fee percentage. This is used to prevent a subgraph owner to drain all - * the name curators tokens while upgrading or deprecating and is configurable in parts per million. - * @param _ownerTaxPercentage Owner tax percentage - */ - function setOwnerTaxPercentage(uint32 _ownerTaxPercentage) external; - - // -- Publishing -- - - /** - * @notice Allows a graph account to set a default name - * @param _graphAccount Account that is setting its name - * @param _nameSystem Name system account already has ownership of a name in - * @param _nameIdentifier The unique identifier that is used to identify the name in the system - * @param _name The name being set as default - */ - function setDefaultName( - address _graphAccount, - uint8 _nameSystem, - bytes32 _nameIdentifier, - string calldata _name - ) external; - - /** - * @notice Allows a subgraph owner to update the metadata of a subgraph they have published - * @param _subgraphID Subgraph ID - * @param _subgraphMetadata IPFS hash for the subgraph metadata - */ - function updateSubgraphMetadata(uint256 _subgraphID, bytes32 _subgraphMetadata) external; - - /** - * @notice Publish a new subgraph. - * @param _subgraphDeploymentID Subgraph deployment for the subgraph - * @param _versionMetadata IPFS hash for the subgraph version metadata - * @param _subgraphMetadata IPFS hash for the subgraph metadata - */ - function publishNewSubgraph( - bytes32 _subgraphDeploymentID, - bytes32 _versionMetadata, - bytes32 _subgraphMetadata - ) external; - - /** - * @notice Publish a new version of an existing subgraph. - * @param _subgraphID Subgraph ID - * @param _subgraphDeploymentID Subgraph deployment ID of the new version - * @param _versionMetadata IPFS hash for the subgraph version metadata - */ - function publishNewVersion(uint256 _subgraphID, bytes32 _subgraphDeploymentID, bytes32 _versionMetadata) external; - - /** - * @notice Deprecate a subgraph. The bonding curve is destroyed, the vSignal is burned, and the GNS - * contract holds the GRT from burning the vSignal, which all curators can withdraw manually. - * Can only be done by the subgraph owner. - * @param _subgraphID Subgraph ID - */ - function deprecateSubgraph(uint256 _subgraphID) external; - - // -- Curation -- - - /** - * @notice Deposit GRT into a subgraph and mint signal. - * @param _subgraphID Subgraph ID - * @param _tokensIn The amount of tokens the nameCurator wants to deposit - * @param _nSignalOutMin Expected minimum amount of name signal to receive - */ - function mintSignal(uint256 _subgraphID, uint256 _tokensIn, uint256 _nSignalOutMin) external; - - /** - * @notice Burn signal for a subgraph and return the GRT. - * @param _subgraphID Subgraph ID - * @param _nSignal The amount of nSignal the nameCurator wants to burn - * @param _tokensOutMin Expected minimum amount of tokens to receive - */ - function burnSignal(uint256 _subgraphID, uint256 _nSignal, uint256 _tokensOutMin) external; - - /** - * @notice Move subgraph signal from sender to `_recipient` - * @param _subgraphID Subgraph ID - * @param _recipient Address to send the signal to - * @param _amount The amount of nSignal to transfer - */ - function transferSignal(uint256 _subgraphID, address _recipient, uint256 _amount) external; - - /** - * @notice Withdraw tokens from a deprecated subgraph. - * When the subgraph is deprecated, any curator can call this function and - * withdraw the GRT they are entitled for its original deposit - * @param _subgraphID Subgraph ID - */ - function withdraw(uint256 _subgraphID) external; - - // -- Getters -- - - /** - * @notice Return the owner of a subgraph. - * @param _tokenID Subgraph ID - * @return Owner address - */ - function ownerOf(uint256 _tokenID) external view returns (address); - - /** - * @notice Return the total signal on the subgraph. - * @param _subgraphID Subgraph ID - * @return Total signal on the subgraph - */ - function subgraphSignal(uint256 _subgraphID) external view returns (uint256); - - /** - * @notice Return the total tokens on the subgraph at current value. - * @param _subgraphID Subgraph ID - * @return Total tokens on the subgraph - */ - function subgraphTokens(uint256 _subgraphID) external view returns (uint256); - - /** - * @notice Calculate subgraph signal to be returned for an amount of tokens. - * @param _subgraphID Subgraph ID - * @param _tokensIn Tokens being exchanged for subgraph signal - * @return Amount of subgraph signal that can be bought - * @return Amount of version signal that can be bought - * @return Amount of curation tax - */ - function tokensToNSignal(uint256 _subgraphID, uint256 _tokensIn) external view returns (uint256, uint256, uint256); - - /** - * @notice Calculate tokens returned for an amount of subgraph signal. - * @param _subgraphID Subgraph ID - * @param _nSignalIn Subgraph signal being exchanged for tokens - * @return Amount of tokens returned for an amount of subgraph signal - * @return Amount of version signal returned - */ - function nSignalToTokens(uint256 _subgraphID, uint256 _nSignalIn) external view returns (uint256, uint256); - - /** - * @notice Calculate subgraph signal to be returned for an amount of subgraph deployment signal. - * @param _subgraphID Subgraph ID - * @param _vSignalIn Amount of subgraph deployment signal to exchange for subgraph signal - * @return Amount of subgraph signal that can be bought - */ - function vSignalToNSignal(uint256 _subgraphID, uint256 _vSignalIn) external view returns (uint256); - - /** - * @notice Calculate subgraph deployment signal to be returned for an amount of subgraph signal. - * @param _subgraphID Subgraph ID - * @param _nSignalIn Subgraph signal being exchanged for subgraph deployment signal - * @return Amount of subgraph deployment signal that can be returned - */ - function nSignalToVSignal(uint256 _subgraphID, uint256 _nSignalIn) external view returns (uint256); - - /** - * @notice Get the amount of subgraph signal a curator has. - * @param _subgraphID Subgraph ID - * @param _curator Curator address - * @return Amount of subgraph signal owned by a curator - */ - function getCuratorSignal(uint256 _subgraphID, address _curator) external view returns (uint256); - - /** - * @notice Return whether a subgraph is published. - * @param _subgraphID Subgraph ID - * @return Return true if subgraph is currently published - */ - function isPublished(uint256 _subgraphID) external view returns (bool); - - /** - * @notice Return whether a subgraph is a legacy subgraph (created before subgraph NFTs). - * @param _subgraphID Subgraph ID - * @return Return true if subgraph is a legacy subgraph - */ - function isLegacySubgraph(uint256 _subgraphID) external view returns (bool); - - /** - * @notice Returns account and sequence ID for a legacy subgraph (created before subgraph NFTs). - * @param _subgraphID Subgraph ID - * @return account Account that created the subgraph (or 0 if it's not a legacy subgraph) - * @return seqID Sequence number for the subgraph - */ - function getLegacySubgraphKey(uint256 _subgraphID) external view returns (address account, uint256 seqID); -} diff --git a/packages/contracts/contracts/discovery/IServiceRegistry.sol b/packages/contracts/contracts/discovery/IServiceRegistry.sol deleted file mode 100644 index 89dafafd3..000000000 --- a/packages/contracts/contracts/discovery/IServiceRegistry.sol +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Service Registry Interface - * @author Edge & Node - * @notice Interface for the Service Registry contract that manages indexer service information - */ -interface IServiceRegistry { - /** - * @dev Indexer service information - * @param url URL of the indexer service - * @param geohash Geohash of the indexer service location - */ - struct IndexerService { - string url; - string geohash; - } - - /** - * @notice Register an indexer service - * @param _url URL of the indexer service - * @param _geohash Geohash of the indexer service location - */ - function register(string calldata _url, string calldata _geohash) external; - - /** - * @notice Register an indexer service - * @param _indexer Address of the indexer - * @param _url URL of the indexer service - * @param _geohash Geohash of the indexer service location - */ - function registerFor(address _indexer, string calldata _url, string calldata _geohash) external; - - /** - * @notice Unregister an indexer service - */ - function unregister() external; - - /** - * @notice Unregister an indexer service - * @param _indexer Address of the indexer - */ - function unregisterFor(address _indexer) external; - - /** - * @notice Return the registration status of an indexer service - * @param _indexer Address of the indexer - * @return True if the indexer service is registered - */ - function isRegistered(address _indexer) external view returns (bool); -} diff --git a/packages/contracts/contracts/discovery/ISubgraphNFT.sol b/packages/contracts/contracts/discovery/ISubgraphNFT.sol deleted file mode 100644 index f735c00f6..000000000 --- a/packages/contracts/contracts/discovery/ISubgraphNFT.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -/** - * @title Subgraph NFT Interface - * @author Edge & Node - * @notice Interface for the Subgraph NFT contract that represents subgraph ownership - */ -interface ISubgraphNFT is IERC721 { - // -- Config -- - - /** - * @notice Set the minter allowed to perform actions on the NFT - * @dev Minter can mint, burn and update the metadata - * @param _minter Address of the allowed minter - */ - function setMinter(address _minter) external; - - /** - * @notice Set the token descriptor contract - * @dev Token descriptor can be zero. If set, it must be a contract - * @param _tokenDescriptor Address of the contract that creates the NFT token URI - */ - function setTokenDescriptor(address _tokenDescriptor) external; - - /** - * @notice Set the base URI - * @dev Can be set to empty - * @param _baseURI Base URI to use to build the token URI - */ - function setBaseURI(string memory _baseURI) external; - - // -- Actions -- - - /** - * @notice Mint `_tokenId` and transfers it to `_to` - * @dev `tokenId` must not exist and `to` cannot be the zero address - * @param _to Address receiving the minted NFT - * @param _tokenId ID of the NFT - */ - function mint(address _to, uint256 _tokenId) external; - - /** - * @notice Burn `_tokenId` - * @dev The approval is cleared when the token is burned - * @param _tokenId ID of the NFT - */ - function burn(uint256 _tokenId) external; - - /** - * @notice Set the metadata for a subgraph represented by `_tokenId` - * @dev `_tokenId` must exist - * @param _tokenId ID of the NFT - * @param _subgraphMetadata IPFS hash for the metadata - */ - function setSubgraphMetadata(uint256 _tokenId, bytes32 _subgraphMetadata) external; - - /** - * @notice Returns the Uniform Resource Identifier (URI) for `_tokenId` token - * @param _tokenId ID of the NFT - * @return The URI for the token - */ - function tokenURI(uint256 _tokenId) external view returns (string memory); -} diff --git a/packages/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol b/packages/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol deleted file mode 100644 index 5a6893234..000000000 --- a/packages/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -/** - * @title Describes subgraph NFT tokens via URI - * @author Edge & Node - * @notice Interface for describing subgraph NFT tokens via URI - */ -interface ISubgraphNFTDescriptor { - /// @notice Produces the URI describing a particular token ID for a Subgraph - /// @dev Note this URI may be data: URI with the JSON contents directly inlined - /// @param _minter Address of the allowed minter - /// @param _tokenId The ID of the subgraph NFT for which to produce a description, which may not be valid - /// @param _baseURI The base URI that could be prefixed to the final URI - /// @param _subgraphMetadata Subgraph metadata set for the subgraph - /// @return The URI of the ERC721-compliant metadata - function tokenURI( - address _minter, - uint256 _tokenId, - string calldata _baseURI, - bytes32 _subgraphMetadata - ) external view returns (string memory); -} diff --git a/packages/contracts/contracts/discovery/L1GNS.sol b/packages/contracts/contracts/discovery/L1GNS.sol index 03167dcd9..3441d05fa 100644 --- a/packages/contracts/contracts/discovery/L1GNS.sol +++ b/packages/contracts/contracts/discovery/L1GNS.sol @@ -7,9 +7,9 @@ import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/Sa import { GNS } from "./GNS.sol"; -import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; -import { IL2GNS } from "../l2/discovery/IL2GNS.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; +import { IL2GNS } from "@graphprotocol/interfaces/contracts/contracts/l2/discovery/IL2GNS.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { L1GNSV1Storage } from "./L1GNSStorage.sol"; /** diff --git a/packages/contracts/contracts/discovery/ServiceRegistry.sol b/packages/contracts/contracts/discovery/ServiceRegistry.sol index 3fa27f452..32db1fe1b 100644 --- a/packages/contracts/contracts/discovery/ServiceRegistry.sol +++ b/packages/contracts/contracts/discovery/ServiceRegistry.sol @@ -7,7 +7,7 @@ import { Managed } from "../governance/Managed.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; import { ServiceRegistryV1Storage } from "./ServiceRegistryStorage.sol"; -import { IServiceRegistry } from "./IServiceRegistry.sol"; +import { IServiceRegistry } from "@graphprotocol/interfaces/contracts/contracts/discovery/IServiceRegistry.sol"; /** * @title ServiceRegistry contract diff --git a/packages/contracts/contracts/discovery/ServiceRegistryStorage.sol b/packages/contracts/contracts/discovery/ServiceRegistryStorage.sol index 23b0cf63d..4ad8a7359 100644 --- a/packages/contracts/contracts/discovery/ServiceRegistryStorage.sol +++ b/packages/contracts/contracts/discovery/ServiceRegistryStorage.sol @@ -7,7 +7,7 @@ pragma solidity ^0.7.6; import { Managed } from "../governance/Managed.sol"; -import { IServiceRegistry } from "./IServiceRegistry.sol"; +import { IServiceRegistry } from "@graphprotocol/interfaces/contracts/contracts/discovery/IServiceRegistry.sol"; /** * @title Service Registry Storage V1 diff --git a/packages/contracts/contracts/discovery/SubgraphNFT.sol b/packages/contracts/contracts/discovery/SubgraphNFT.sol index f33766d02..22fc307c0 100644 --- a/packages/contracts/contracts/discovery/SubgraphNFT.sol +++ b/packages/contracts/contracts/discovery/SubgraphNFT.sol @@ -11,8 +11,8 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Governed } from "../governance/Governed.sol"; import { HexStrings } from "../libraries/HexStrings.sol"; -import { ISubgraphNFT } from "./ISubgraphNFT.sol"; -import { ISubgraphNFTDescriptor } from "./ISubgraphNFTDescriptor.sol"; +import { ISubgraphNFT } from "@graphprotocol/interfaces/contracts/contracts/discovery/ISubgraphNFT.sol"; +import { ISubgraphNFTDescriptor } from "@graphprotocol/interfaces/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol"; /** * @title NFT that represents ownership of a Subgraph diff --git a/packages/contracts/contracts/discovery/SubgraphNFTDescriptor.sol b/packages/contracts/contracts/discovery/SubgraphNFTDescriptor.sol index a21cadfb7..a4f5a5080 100644 --- a/packages/contracts/contracts/discovery/SubgraphNFTDescriptor.sol +++ b/packages/contracts/contracts/discovery/SubgraphNFTDescriptor.sol @@ -3,7 +3,7 @@ pragma solidity ^0.7.6; import { Base58Encoder } from "../libraries/Base58Encoder.sol"; -import { ISubgraphNFTDescriptor } from "./ISubgraphNFTDescriptor.sol"; +import { ISubgraphNFTDescriptor } from "@graphprotocol/interfaces/contracts/contracts/discovery/ISubgraphNFTDescriptor.sol"; /** * @title Describes subgraph NFT tokens via URI diff --git a/packages/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol b/packages/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol deleted file mode 100644 index 0be104968..000000000 --- a/packages/contracts/contracts/discovery/erc1056/IEthereumDIDRegistry.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.7.6; - -/** - * @title Ethereum DID Registry Interface - * @author Edge & Node - * @notice Interface for the Ethereum DID Registry contract - */ -interface IEthereumDIDRegistry { - /** - * @notice Get the owner of an identity - * @param identity The identity address - * @return The address of the identity owner - */ - function identityOwner(address identity) external view returns (address); - - /** - * @notice Set an attribute for an identity - * @param identity The identity address - * @param name The attribute name - * @param value The attribute value - * @param validity The validity period in seconds - */ - function setAttribute(address identity, bytes32 name, bytes calldata value, uint256 validity) external; -} diff --git a/packages/contracts/contracts/disputes/DisputeManager.sol b/packages/contracts/contracts/disputes/DisputeManager.sol index 51c28e5db..9ef426453 100644 --- a/packages/contracts/contracts/disputes/DisputeManager.sol +++ b/packages/contracts/contracts/disputes/DisputeManager.sol @@ -12,10 +12,10 @@ import { ECDSA } from "@openzeppelin/contracts/cryptography/ECDSA.sol"; import { Managed } from "../governance/Managed.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; import { TokenUtils } from "../utils/TokenUtils.sol"; -import { IStaking } from "../staking/IStaking.sol"; +import { IStaking } from "@graphprotocol/interfaces/contracts/contracts/staking/IStaking.sol"; import { DisputeManagerV1Storage } from "./DisputeManagerStorage.sol"; -import { IDisputeManager } from "./IDisputeManager.sol"; +import { IDisputeManager } from "@graphprotocol/interfaces/contracts/contracts/disputes/IDisputeManager.sol"; /** * @title DisputeManager diff --git a/packages/contracts/contracts/disputes/DisputeManagerStorage.sol b/packages/contracts/contracts/disputes/DisputeManagerStorage.sol index f82cff337..f2ad2b7c3 100644 --- a/packages/contracts/contracts/disputes/DisputeManagerStorage.sol +++ b/packages/contracts/contracts/disputes/DisputeManagerStorage.sol @@ -7,7 +7,7 @@ pragma solidity ^0.7.6; import { Managed } from "../governance/Managed.sol"; -import { IDisputeManager } from "./IDisputeManager.sol"; +import { IDisputeManager } from "@graphprotocol/interfaces/contracts/contracts/disputes/IDisputeManager.sol"; /** * @title Dispute Manager Storage V1 diff --git a/packages/contracts/contracts/disputes/IDisputeManager.sol b/packages/contracts/contracts/disputes/IDisputeManager.sol deleted file mode 100644 index 0db94e290..000000000 --- a/packages/contracts/contracts/disputes/IDisputeManager.sol +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; -pragma abicoder v2; - -/** - * @title Dispute Manager Interface - * @author Edge & Node - * @notice Interface for the Dispute Manager contract that handles indexing and query disputes - */ -interface IDisputeManager { - // -- Dispute -- - - /** - * @dev Types of disputes that can be created - */ - enum DisputeType { - Null, - IndexingDispute, - QueryDispute - } - - /** - * @dev Status of a dispute - */ - enum DisputeStatus { - Null, - Accepted, - Rejected, - Drawn, - Pending - } - - /** - * @dev Disputes contain info necessary for the Arbitrator to verify and resolve - * @param indexer Address of the indexer being disputed - * @param fisherman Address of the challenger creating the dispute - * @param deposit Amount of tokens staked as deposit - * @param relatedDisputeID ID of related dispute (for conflicting attestations) - * @param disputeType Type of dispute (Query or Indexing) - * @param status Current status of the dispute - */ - struct Dispute { - address indexer; - address fisherman; - uint256 deposit; - bytes32 relatedDisputeID; - DisputeType disputeType; - DisputeStatus status; - } - - // -- Attestation -- - - /** - * @dev Receipt content sent from indexer in response to request - * @param requestCID Content ID of the request - * @param responseCID Content ID of the response - * @param subgraphDeploymentID ID of the subgraph deployment - */ - struct Receipt { - bytes32 requestCID; - bytes32 responseCID; - bytes32 subgraphDeploymentID; - } - - /** - * @dev Attestation sent from indexer in response to a request - * @param requestCID Content ID of the request - * @param responseCID Content ID of the response - * @param subgraphDeploymentID ID of the subgraph deployment - * @param r R component of the signature - * @param s S component of the signature - * @param v Recovery ID of the signature - */ - struct Attestation { - bytes32 requestCID; - bytes32 responseCID; - bytes32 subgraphDeploymentID; - bytes32 r; - bytes32 s; - uint8 v; - } - - // -- Configuration -- - - /** - * @dev Set the arbitrator address. - * @notice Update the arbitrator to `_arbitrator` - * @param _arbitrator The address of the arbitration contract or party - */ - function setArbitrator(address _arbitrator) external; - - /** - * @dev Set the minimum deposit required to create a dispute. - * @notice Update the minimum deposit to `_minimumDeposit` Graph Tokens - * @param _minimumDeposit The minimum deposit in Graph Tokens - */ - function setMinimumDeposit(uint256 _minimumDeposit) external; - - /** - * @dev Set the percent reward that the fisherman gets when slashing occurs. - * @notice Update the reward percentage to `_percentage` - * @param _percentage Reward as a percentage of indexer stake - */ - function setFishermanRewardPercentage(uint32 _percentage) external; - - /** - * @notice Set the percentage used for slashing indexers. - * @param _qryPercentage Percentage slashing for query disputes - * @param _idxPercentage Percentage slashing for indexing disputes - */ - function setSlashingPercentage(uint32 _qryPercentage, uint32 _idxPercentage) external; - - // -- Getters -- - - /** - * @notice Check if a dispute has been created - * @param _disputeID Dispute identifier - * @return True if the dispute exists - */ - function isDisputeCreated(bytes32 _disputeID) external view returns (bool); - - /** - * @notice Encode a receipt into a hash for EIP-712 signature verification - * @param _receipt The receipt to encode - * @return The encoded hash - */ - function encodeHashReceipt(Receipt memory _receipt) external view returns (bytes32); - - /** - * @notice Check if two attestations are conflicting - * @param _attestation1 First attestation - * @param _attestation2 Second attestation - * @return True if attestations are conflicting - */ - function areConflictingAttestations( - Attestation memory _attestation1, - Attestation memory _attestation2 - ) external pure returns (bool); - - /** - * @notice Get the indexer address from an attestation - * @param _attestation The attestation to extract indexer from - * @return The indexer address - */ - function getAttestationIndexer(Attestation memory _attestation) external view returns (address); - - // -- Dispute -- - - /** - * @notice Create a query dispute for the arbitrator to resolve. - * This function is called by a fisherman that will need to `_deposit` at - * least `minimumDeposit` GRT tokens. - * @param _attestationData Attestation bytes submitted by the fisherman - * @param _deposit Amount of tokens staked as deposit - * @return The dispute ID - */ - function createQueryDispute(bytes calldata _attestationData, uint256 _deposit) external returns (bytes32); - - /** - * @notice Create query disputes for two conflicting attestations. - * A conflicting attestation is a proof presented by two different indexers - * where for the same request on a subgraph the response is different. - * For this type of dispute the submitter is not required to present a deposit - * as one of the attestation is considered to be right. - * Two linked disputes will be created and if the arbitrator resolve one, the other - * one will be automatically resolved. - * @param _attestationData1 First attestation data submitted - * @param _attestationData2 Second attestation data submitted - * @return First dispute ID - * @return Second dispute ID - */ - function createQueryDisputeConflict( - bytes calldata _attestationData1, - bytes calldata _attestationData2 - ) external returns (bytes32, bytes32); - - /** - * @notice Create an indexing dispute - * @param _allocationID Allocation ID being disputed - * @param _deposit Deposit amount for the dispute - * @return The dispute ID - */ - function createIndexingDispute(address _allocationID, uint256 _deposit) external returns (bytes32); - - /** - * @notice Accept a dispute (arbitrator only) - * @param _disputeID ID of the dispute to accept - */ - function acceptDispute(bytes32 _disputeID) external; - - /** - * @notice Reject a dispute (arbitrator only) - * @param _disputeID ID of the dispute to reject - */ - function rejectDispute(bytes32 _disputeID) external; - - /** - * @notice Draw a dispute (arbitrator only) - * @param _disputeID ID of the dispute to draw - */ - function drawDispute(bytes32 _disputeID) external; -} diff --git a/packages/contracts/contracts/epochs/EpochManager.sol b/packages/contracts/contracts/epochs/EpochManager.sol index 440f3d1cb..d69002794 100644 --- a/packages/contracts/contracts/epochs/EpochManager.sol +++ b/packages/contracts/contracts/epochs/EpochManager.sol @@ -11,7 +11,7 @@ import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; import { Managed } from "../governance/Managed.sol"; import { EpochManagerV1Storage } from "./EpochManagerStorage.sol"; -import { IEpochManager } from "./IEpochManager.sol"; +import { IEpochManager } from "@graphprotocol/interfaces/contracts/contracts/epochs/IEpochManager.sol"; /** * @title EpochManager contract diff --git a/packages/contracts/contracts/epochs/IEpochManager.sol b/packages/contracts/contracts/epochs/IEpochManager.sol deleted file mode 100644 index 24759f603..000000000 --- a/packages/contracts/contracts/epochs/IEpochManager.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Epoch Manager Interface - * @author Edge & Node - * @notice Interface for the Epoch Manager contract that handles protocol epochs - */ -interface IEpochManager { - // -- Configuration -- - - /** - * @notice Set epoch length to `_epochLength` blocks - * @param _epochLength Epoch length in blocks - */ - function setEpochLength(uint256 _epochLength) external; - - // -- Epochs - - /** - * @dev Run a new epoch, should be called once at the start of any epoch. - * @notice Perform state changes for the current epoch - */ - function runEpoch() external; - - // -- Getters -- - - /** - * @notice Check if the current epoch has been run - * @return True if current epoch has been run, false otherwise - */ - function isCurrentEpochRun() external view returns (bool); - - /** - * @notice Get the current block number - * @return Current block number - */ - function blockNum() external view returns (uint256); - - /** - * @notice Get the hash of a specific block - * @param _block Block number to get hash for - * @return Block hash - */ - function blockHash(uint256 _block) external view returns (bytes32); - - /** - * @notice Get the current epoch number - * @return Current epoch number - */ - function currentEpoch() external view returns (uint256); - - /** - * @notice Get the block number when the current epoch started - * @return Block number of current epoch start - */ - function currentEpochBlock() external view returns (uint256); - - /** - * @notice Get the number of blocks since the current epoch started - * @return Number of blocks since current epoch start - */ - function currentEpochBlockSinceStart() external view returns (uint256); - - /** - * @notice Get the number of epochs since a given epoch - * @param _epoch Epoch to calculate from - * @return Number of epochs since the given epoch - */ - function epochsSince(uint256 _epoch) external view returns (uint256); - - /** - * @notice Get the number of epochs since the last epoch length update - * @return Number of epochs since last update - */ - function epochsSinceUpdate() external view returns (uint256); -} diff --git a/packages/contracts/contracts/gateway/GraphTokenGateway.sol b/packages/contracts/contracts/gateway/GraphTokenGateway.sol index f11f52f7d..81edb9922 100644 --- a/packages/contracts/contracts/gateway/GraphTokenGateway.sol +++ b/packages/contracts/contracts/gateway/GraphTokenGateway.sol @@ -3,7 +3,7 @@ pragma solidity ^0.7.6; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; -import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; import { Pausable } from "../governance/Pausable.sol"; import { Managed } from "../governance/Managed.sol"; diff --git a/packages/contracts/contracts/gateway/ICallhookReceiver.sol b/packages/contracts/contracts/gateway/ICallhookReceiver.sol deleted file mode 100644 index d3b674bcc..000000000 --- a/packages/contracts/contracts/gateway/ICallhookReceiver.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -/** - * @title Interface for contracts that can receive callhooks through the Arbitrum GRT bridge - * @author Edge & Node - * @notice Any contract that can receive a callhook on L2, sent through the bridge from L1, must - * be allowlisted by the governor, but also implement this interface that contains - * the function that will actually be called by the L2GraphTokenGateway. - */ -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Callhook Receiver Interface - * @author Edge & Node - * @notice Interface for contracts that can receive tokens with callhook from the bridge - */ -interface ICallhookReceiver { - /** - * @notice Receive tokens with a callhook from the bridge - * @param _from Token sender in L1 - * @param _amount Amount of tokens that were transferred - * @param _data ABI-encoded callhook data - */ - function onTokenTransfer(address _from, uint256 _amount, bytes calldata _data) external; -} diff --git a/packages/contracts/contracts/gateway/L1GraphTokenGateway.sol b/packages/contracts/contracts/gateway/L1GraphTokenGateway.sol index 20c25d2e6..d9216b956 100644 --- a/packages/contracts/contracts/gateway/L1GraphTokenGateway.sol +++ b/packages/contracts/contracts/gateway/L1GraphTokenGateway.sol @@ -12,13 +12,13 @@ import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/Ad import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; import { L1ArbitrumMessenger } from "../arbitrum/L1ArbitrumMessenger.sol"; -import { IBridge } from "../arbitrum/IBridge.sol"; -import { IInbox } from "../arbitrum/IInbox.sol"; -import { IOutbox } from "../arbitrum/IOutbox.sol"; -import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; +import { IBridge } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IBridge.sol"; +import { IInbox } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IInbox.sol"; +import { IOutbox } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IOutbox.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; import { Managed } from "../governance/Managed.sol"; import { GraphTokenGateway } from "./GraphTokenGateway.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; /** * @title L1 Graph Token Gateway Contract diff --git a/packages/contracts/contracts/governance/Controller.sol b/packages/contracts/contracts/governance/Controller.sol index 3bd3c77cb..c850542ab 100644 --- a/packages/contracts/contracts/governance/Controller.sol +++ b/packages/contracts/contracts/governance/Controller.sol @@ -8,8 +8,8 @@ pragma solidity ^0.7.6 || 0.8.27; /* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 -import { IController } from "./IController.sol"; -import { IManaged } from "./IManaged.sol"; +import { IController } from "@graphprotocol/interfaces/contracts/contracts/governance/IController.sol"; +import { IManaged } from "@graphprotocol/interfaces/contracts/contracts/governance/IManaged.sol"; import { Governed } from "./Governed.sol"; import { Pausable } from "./Pausable.sol"; diff --git a/packages/contracts/contracts/governance/IController.sol b/packages/contracts/contracts/governance/IController.sol deleted file mode 100644 index af1c43a0e..000000000 --- a/packages/contracts/contracts/governance/IController.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Controller Interface - * @author Edge & Node - * @notice Interface for the Controller contract that manages protocol governance and contract registry - */ -interface IController { - /** - * @notice Return the governor address - * @return The governor address - */ - function getGovernor() external view returns (address); - - // -- Registry -- - - /** - * @notice Register contract id and mapped address - * @param _id Contract id (keccak256 hash of contract name) - * @param _contractAddress Contract address - */ - function setContractProxy(bytes32 _id, address _contractAddress) external; - - /** - * @notice Unregister a contract address - * @param _id Contract id (keccak256 hash of contract name) - */ - function unsetContractProxy(bytes32 _id) external; - - /** - * @notice Update contract's controller - * @param _id Contract id (keccak256 hash of contract name) - * @param _controller Controller address - */ - function updateController(bytes32 _id, address _controller) external; - - /** - * @notice Get contract proxy address by its id - * @param _id Contract id - * @return Address of the proxy contract for the provided id - */ - function getContractProxy(bytes32 _id) external view returns (address); - - // -- Pausing -- - - /** - * @notice Change the partial paused state of the contract - * Partial pause is intended as a partial pause of the protocol - * @param _partialPaused True if the contracts should be (partially) paused, false otherwise - */ - function setPartialPaused(bool _partialPaused) external; - - /** - * @notice Change the paused state of the contract - * Full pause most of protocol functions - * @param _paused True if the contracts should be paused, false otherwise - */ - function setPaused(bool _paused) external; - - /** - * @notice Change the Pause Guardian - * @param _newPauseGuardian The address of the new Pause Guardian - */ - function setPauseGuardian(address _newPauseGuardian) external; - - /** - * @notice Return whether the protocol is paused - * @return True if the protocol is paused - */ - function paused() external view returns (bool); - - /** - * @notice Return whether the protocol is partially paused - * @return True if the protocol is partially paused - */ - function partialPaused() external view returns (bool); -} diff --git a/packages/contracts/contracts/governance/IManaged.sol b/packages/contracts/contracts/governance/IManaged.sol deleted file mode 100644 index 8bfe2ae0b..000000000 --- a/packages/contracts/contracts/governance/IManaged.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -import { IController } from "./IController.sol"; - -/** - * @title Managed Interface - * @author Edge & Node - * @notice Interface for contracts that can be managed by a controller. - */ -interface IManaged { - /** - * @notice Set Controller. Only callable by current controller. - * @param _controller Controller contract address - */ - function setController(address _controller) external; - - /** - * @notice Sync protocol contract addresses from the Controller registry - * @dev This function will cache all the contracts using the latest addresses - * Anyone can call the function whenever a Proxy contract change in the - * controller to ensure the protocol is using the latest version - */ - function syncAllContracts() external; - - /** - * @notice Get the Controller that manages this contract - * @return The Controller as an IController interface - */ - function controller() external view returns (IController); -} diff --git a/packages/contracts/contracts/governance/Managed.sol b/packages/contracts/contracts/governance/Managed.sol index 596dee0f7..c4718a1e6 100644 --- a/packages/contracts/contracts/governance/Managed.sol +++ b/packages/contracts/contracts/governance/Managed.sol @@ -6,17 +6,17 @@ pragma solidity ^0.7.6; // solhint-disable gas-indexed-events // solhint-disable named-parameters-mapping -import { IController } from "./IController.sol"; +import { IController } from "@graphprotocol/interfaces/contracts/contracts/governance/IController.sol"; -import { ICuration } from "../curation/ICuration.sol"; -import { IEpochManager } from "../epochs/IEpochManager.sol"; +import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; +import { IEpochManager } from "@graphprotocol/interfaces/contracts/contracts/epochs/IEpochManager.sol"; import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; -import { IStaking } from "../staking/IStaking.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; -import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; -import { IGNS } from "../discovery/IGNS.sol"; +import { IStaking } from "@graphprotocol/interfaces/contracts/contracts/staking/IStaking.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; +import { IGNS } from "@graphprotocol/interfaces/contracts/contracts/discovery/IGNS.sol"; -import { IManaged } from "./IManaged.sol"; +import { IManaged } from "@graphprotocol/interfaces/contracts/contracts/governance/IManaged.sol"; /** * @title Graph Managed contract diff --git a/packages/contracts/contracts/l2/curation/IL2Curation.sol b/packages/contracts/contracts/l2/curation/IL2Curation.sol deleted file mode 100644 index 3b39ac7a6..000000000 --- a/packages/contracts/contracts/l2/curation/IL2Curation.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Interface of the L2 Curation contract. - * @author Edge & Node - * @notice Interface for the L2 Curation contract that handles curation on Layer 2 - */ -interface IL2Curation { - /** - * @notice Set the subgraph service address. - * @param _subgraphService Address of the SubgraphService contract - */ - function setSubgraphService(address _subgraphService) external; - - /** - * @notice Deposit Graph Tokens in exchange for signal of a SubgraphDeployment curation pool. - * @dev This function charges no tax and can only be called by GNS in specific scenarios (for now - * only during an L1-L2 transfer). - * @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal - * @param _tokensIn Amount of Graph Tokens to deposit - * @return Signal minted - */ - function mintTaxFree(bytes32 _subgraphDeploymentID, uint256 _tokensIn) external returns (uint256); - - /** - * @notice Calculate amount of signal that can be bought with tokens in a curation pool, - * without accounting for curation tax. - * @param _subgraphDeploymentID Subgraph deployment for which to mint signal - * @param _tokensIn Amount of tokens used to mint signal - * @return Amount of signal that can be bought - */ - function tokensToSignalNoTax(bytes32 _subgraphDeploymentID, uint256 _tokensIn) external view returns (uint256); - - /** - * @notice Calculate the amount of tokens that would be recovered if minting signal with - * the input tokens and then burning it. This can be used to compute rounding error. - * This function does not account for curation tax. - * @param _subgraphDeploymentID Subgraph deployment for which to mint signal - * @param _tokensIn Amount of tokens used to mint signal - * @return Amount of tokens that would be recovered after minting and burning signal - */ - function tokensToSignalToTokensNoTax( - bytes32 _subgraphDeploymentID, - uint256 _tokensIn - ) external view returns (uint256); -} diff --git a/packages/contracts/contracts/l2/curation/L2Curation.sol b/packages/contracts/contracts/l2/curation/L2Curation.sol index 9a5f6a4e6..56e83c13a 100644 --- a/packages/contracts/contracts/l2/curation/L2Curation.sol +++ b/packages/contracts/contracts/l2/curation/L2Curation.sol @@ -14,10 +14,10 @@ import { GraphUpgradeable } from "../../upgrades/GraphUpgradeable.sol"; import { TokenUtils } from "../../utils/TokenUtils.sol"; import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { Managed } from "../../governance/Managed.sol"; -import { IGraphToken } from "../../token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { CurationV3Storage } from "../../curation/CurationStorage.sol"; -import { IGraphCurationToken } from "../../curation/IGraphCurationToken.sol"; -import { IL2Curation } from "./IL2Curation.sol"; +import { IGraphCurationToken } from "@graphprotocol/interfaces/contracts/contracts/curation/IGraphCurationToken.sol"; +import { IL2Curation } from "@graphprotocol/interfaces/contracts/contracts/l2/curation/IL2Curation.sol"; /** * @title L2Curation contract diff --git a/packages/contracts/contracts/l2/discovery/IL2GNS.sol b/packages/contracts/contracts/l2/discovery/IL2GNS.sol deleted file mode 100644 index 9b3a26152..000000000 --- a/packages/contracts/contracts/l2/discovery/IL2GNS.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; - -/** - * @title Interface for the L2GNS contract. - * @author Edge & Node - * @notice Interface for the L2 Graph Name System (GNS) contract - */ -interface IL2GNS is ICallhookReceiver { - /** - * @dev Message codes for L1 to L2 communication - * @param RECEIVE_SUBGRAPH_CODE Code for receiving subgraph transfers - * @param RECEIVE_CURATOR_BALANCE_CODE Code for receiving curator balance transfers - */ - enum L1MessageCodes { - RECEIVE_SUBGRAPH_CODE, - RECEIVE_CURATOR_BALANCE_CODE - } - - /** - * @dev The SubgraphL2TransferData struct holds information - * about a subgraph related to its transfer from L1 to L2. - * @param tokens GRT that will be sent to L2 to mint signal - * @param curatorBalanceClaimed True for curators whose balance has been claimed in L2 - * @param l2Done Transfer finished on L2 side - * @param subgraphReceivedOnL2BlockNumber Block number when the subgraph was received on L2 - */ - struct SubgraphL2TransferData { - uint256 tokens; // GRT that will be sent to L2 to mint signal - mapping(address => bool) curatorBalanceClaimed; // True for curators whose balance has been claimed in L2 - bool l2Done; // Transfer finished on L2 side - uint256 subgraphReceivedOnL2BlockNumber; // Block number when the subgraph was received on L2 - } - - /** - * @notice Finish a subgraph transfer from L1. - * The subgraph must have been previously sent through the bridge - * using the sendSubgraphToL2 function on L1GNS. - * @param _l2SubgraphID Subgraph ID in L2 (aliased from the L1 subgraph ID) - * @param _subgraphDeploymentID Latest subgraph deployment to assign to the subgraph - * @param _subgraphMetadata IPFS hash of the subgraph metadata - * @param _versionMetadata IPFS hash of the version metadata - */ - function finishSubgraphTransferFromL1( - uint256 _l2SubgraphID, - bytes32 _subgraphDeploymentID, - bytes32 _subgraphMetadata, - bytes32 _versionMetadata - ) external; - - /** - * @notice Return the aliased L2 subgraph ID from a transferred L1 subgraph ID - * @param _l1SubgraphID L1 subgraph ID - * @return L2 subgraph ID - */ - function getAliasedL2SubgraphID(uint256 _l1SubgraphID) external pure returns (uint256); - - /** - * @notice Return the unaliased L1 subgraph ID from a transferred L2 subgraph ID - * @param _l2SubgraphID L2 subgraph ID - * @return L1subgraph ID - */ - function getUnaliasedL1SubgraphID(uint256 _l2SubgraphID) external pure returns (uint256); -} diff --git a/packages/contracts/contracts/l2/discovery/L2GNS.sol b/packages/contracts/contracts/l2/discovery/L2GNS.sol index cf5528953..bd176dbcf 100644 --- a/packages/contracts/contracts/l2/discovery/L2GNS.sol +++ b/packages/contracts/contracts/l2/discovery/L2GNS.sol @@ -9,11 +9,11 @@ pragma abicoder v2; import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; import { GNS } from "../../discovery/GNS.sol"; -import { ICuration } from "../../curation/ICuration.sol"; -import { IL2GNS } from "./IL2GNS.sol"; +import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; +import { IL2GNS } from "@graphprotocol/interfaces/contracts/contracts/l2/discovery/IL2GNS.sol"; import { L2GNSV1Storage } from "./L2GNSStorage.sol"; -import { IL2Curation } from "../curation/IL2Curation.sol"; +import { IL2Curation } from "@graphprotocol/interfaces/contracts/contracts/l2/curation/IL2Curation.sol"; /** * @title L2GNS diff --git a/packages/contracts/contracts/l2/discovery/L2GNSStorage.sol b/packages/contracts/contracts/l2/discovery/L2GNSStorage.sol index 26e7bf1e0..d464ea891 100644 --- a/packages/contracts/contracts/l2/discovery/L2GNSStorage.sol +++ b/packages/contracts/contracts/l2/discovery/L2GNSStorage.sol @@ -6,7 +6,7 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import { IL2GNS } from "./IL2GNS.sol"; +import { IL2GNS } from "@graphprotocol/interfaces/contracts/contracts/l2/discovery/IL2GNS.sol"; /** * @title L2GNSV1Storage diff --git a/packages/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol b/packages/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol index 3b4c1c0ed..aa8868c49 100644 --- a/packages/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol +++ b/packages/contracts/contracts/l2/gateway/L2GraphTokenGateway.sol @@ -11,10 +11,10 @@ import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/Sa import { L2ArbitrumMessenger } from "../../arbitrum/L2ArbitrumMessenger.sol"; import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; -import { ITokenGateway } from "../../arbitrum/ITokenGateway.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; import { Managed } from "../../governance/Managed.sol"; import { GraphTokenGateway } from "../../gateway/GraphTokenGateway.sol"; -import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; +import { ICallhookReceiver } from "@graphprotocol/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol"; import { L2GraphToken } from "../token/L2GraphToken.sol"; /** diff --git a/packages/contracts/contracts/l2/staking/IL2Staking.sol b/packages/contracts/contracts/l2/staking/IL2Staking.sol deleted file mode 100644 index 522a9ca12..000000000 --- a/packages/contracts/contracts/l2/staking/IL2Staking.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.6.12 <0.8.0; -pragma abicoder v2; - -import { IStaking } from "../../staking/IStaking.sol"; -import { IL2StakingBase } from "./IL2StakingBase.sol"; -import { IL2StakingTypes } from "./IL2StakingTypes.sol"; - -/** - * @title Interface for the L2 Staking contract - * @author Edge & Node - * @notice This is the interface that should be used when interacting with the L2 Staking contract. - * It extends the IStaking interface with the functions that are specific to L2, adding the callhook receiver - * to receive transferred stake and delegation from L1. - * @dev Note that L2Staking doesn't actually inherit this interface. This is because of - * the custom setup of the Staking contract where part of the functionality is implemented - * in a separate contract (StakingExtension) to which calls are delegated through the fallback function. - */ -interface IL2Staking is IStaking, IL2StakingBase, IL2StakingTypes {} diff --git a/packages/contracts/contracts/l2/staking/IL2StakingBase.sol b/packages/contracts/contracts/l2/staking/IL2StakingBase.sol deleted file mode 100644 index 7f861db39..000000000 --- a/packages/contracts/contracts/l2/staking/IL2StakingBase.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - -import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; - -/** - * @title Base interface for the L2Staking contract. - * @author Edge & Node - * @notice This interface is used to define the callhook receiver interface that is implemented by L2Staking. - * @dev Note it includes only the L2-specific functionality, not the full IStaking interface. - */ -interface IL2StakingBase is ICallhookReceiver { - /** - * @notice Emitted when transferred delegation is returned to a delegator - * @param indexer Address of the indexer - * @param delegator Address of the delegator - * @param amount Amount of delegation returned - */ - event TransferredDelegationReturnedToDelegator(address indexed indexer, address indexed delegator, uint256 amount); -} diff --git a/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol b/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol deleted file mode 100644 index 722a2dbf4..000000000 --- a/packages/contracts/contracts/l2/staking/IL2StakingTypes.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title IL2StakingTypes - * @author Edge & Node - * @notice Interface defining types and enums used by L2 staking contracts - */ -interface IL2StakingTypes { - /// @dev Message codes for the L1 -> L2 bridge callhook - enum L1MessageCodes { - RECEIVE_INDEXER_STAKE_CODE, - RECEIVE_DELEGATION_CODE - } - - /// @dev Encoded message struct when receiving indexer stake through the bridge - struct ReceiveIndexerStakeData { - address indexer; - } - - /// @dev Encoded message struct when receiving delegation through the bridge - struct ReceiveDelegationData { - address indexer; - address delegator; - } -} diff --git a/packages/contracts/contracts/l2/staking/L2Staking.sol b/packages/contracts/contracts/l2/staking/L2Staking.sol index fb3784456..305747801 100644 --- a/packages/contracts/contracts/l2/staking/L2Staking.sol +++ b/packages/contracts/contracts/l2/staking/L2Staking.sol @@ -8,13 +8,13 @@ pragma abicoder v2; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { Staking } from "../../staking/Staking.sol"; -import { IL2StakingBase } from "./IL2StakingBase.sol"; +import { IL2StakingBase } from "@graphprotocol/interfaces/contracts/contracts/l2/staking/IL2StakingBase.sol"; import { Stakes } from "../../staking/libs/Stakes.sol"; -import { IStakes } from "../../staking/libs/IStakes.sol"; -import { IL2StakingTypes } from "./IL2StakingTypes.sol"; +import { IStakes } from "@graphprotocol/interfaces/contracts/contracts/staking/libs/IStakes.sol"; +import { IL2StakingTypes } from "@graphprotocol/interfaces/contracts/contracts/l2/staking/IL2StakingTypes.sol"; // solhint-disable-next-line no-unused-import -import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol"; // Used by @inheritdoc +import { ICallhookReceiver } from "@graphprotocol/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol"; // Used by @inheritdoc /** * @title L2Staking contract diff --git a/packages/contracts/contracts/l2/token/L2GraphToken.sol b/packages/contracts/contracts/l2/token/L2GraphToken.sol index c16f0164c..d37731f53 100644 --- a/packages/contracts/contracts/l2/token/L2GraphToken.sol +++ b/packages/contracts/contracts/l2/token/L2GraphToken.sol @@ -6,7 +6,7 @@ pragma solidity ^0.7.6; // solhint-disable gas-indexed-events import { GraphTokenUpgradeable } from "./GraphTokenUpgradeable.sol"; -import { IArbToken } from "../../arbitrum/IArbToken.sol"; +import { IArbToken } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IArbToken.sol"; /** * @title L2 Graph Token Contract diff --git a/packages/contracts/contracts/payments/AllocationExchange.sol b/packages/contracts/contracts/payments/AllocationExchange.sol index 9bbd983a1..288bdda32 100644 --- a/packages/contracts/contracts/payments/AllocationExchange.sol +++ b/packages/contracts/contracts/payments/AllocationExchange.sol @@ -10,8 +10,8 @@ pragma abicoder v2; import { ECDSA } from "@openzeppelin/contracts/cryptography/ECDSA.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Governed } from "../governance/Governed.sol"; -import { IStaking } from "../staking/IStaking.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; +import { IStaking } from "@graphprotocol/interfaces/contracts/contracts/staking/IStaking.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; /** * @title Allocation Exchange diff --git a/packages/contracts/contracts/rewards/IRewardsIssuer.sol b/packages/contracts/contracts/rewards/IRewardsIssuer.sol deleted file mode 100644 index 614dbf40e..000000000 --- a/packages/contracts/contracts/rewards/IRewardsIssuer.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Rewards Issuer Interface - * @author Edge & Node - * @notice Interface for contracts that issue rewards based on allocation data - */ -interface IRewardsIssuer { - /** - * @notice Get allocation data to calculate rewards issuance - * - * @param allocationId The allocation Id - * @return isActive Whether the allocation is active or not - * @return indexer The indexer address - * @return subgraphDeploymentId Subgraph deployment id for the allocation - * @return tokens Amount of allocated tokens - * @return accRewardsPerAllocatedToken Rewards snapshot - * @return accRewardsPending Snapshot of accumulated rewards from previous allocation resizing, pending to be claimed - */ - function getAllocationData( - address allocationId - ) - external - view - returns ( - bool isActive, - address indexer, - bytes32 subgraphDeploymentId, - uint256 tokens, - uint256 accRewardsPerAllocatedToken, - uint256 accRewardsPending - ); - - /** - * @notice Return the total amount of tokens allocated to subgraph. - * @param _subgraphDeploymentId Deployment Id for the subgraph - * @return Total tokens allocated to subgraph - */ - function getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentId) external view returns (uint256); -} diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 083908b3b..767449026 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -11,10 +11,10 @@ import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; import { Managed } from "../governance/Managed.sol"; import { MathUtils } from "../staking/libs/MathUtils.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { RewardsManagerV5Storage } from "./RewardsManagerStorage.sol"; -import { IRewardsIssuer } from "./IRewardsIssuer.sol"; +import { IRewardsIssuer } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol"; import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; /** diff --git a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol index 5002d7890..d78eb81ef 100644 --- a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol +++ b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol @@ -7,7 +7,7 @@ pragma solidity ^0.7.6 || 0.8.27; -import { IRewardsIssuer } from "./IRewardsIssuer.sol"; +import { IRewardsIssuer } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol"; import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { Managed } from "../governance/Managed.sol"; diff --git a/packages/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol b/packages/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol deleted file mode 100644 index 9a2224048..000000000 --- a/packages/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.6.12 <0.8.0; -pragma abicoder v2; - -/** - * @title Interface for the L1GraphTokenLockTransferTool contract - * @author Edge & Node - * @notice This interface defines the function to get the L2 wallet address for a given L1 token lock wallet. - * The Transfer Tool contract is implemented in the token-distribution repo: https://github.com/graphprotocol/token-distribution/pull/64 - * and is only included here to provide support in L1Staking for the transfer of stake and delegation - * owned by token lock contracts. See GIP-0046 for details: https://forum.thegraph.com/t/4023 - */ -interface IL1GraphTokenLockTransferTool { - /** - * @notice Pulls ETH from an L1 wallet's account to use for L2 ticket gas. - * @dev This function is only callable by the staking contract. - * @param _l1Wallet Address of the L1 token lock wallet - * @param _amount Amount of ETH to pull from the transfer tool contract - */ - function pullETH(address _l1Wallet, uint256 _amount) external; - - /** - * @notice Get the L2 token lock wallet address for a given L1 token lock wallet - * @dev In the actual L1GraphTokenLockTransferTool contract, this is simply the default getter for a public mapping variable. - * @param _l1Wallet Address of the L1 token lock wallet - * @return Address of the L2 token lock wallet if the wallet has an L2 counterpart, or address zero if - * the wallet doesn't have an L2 counterpart (or is not known to be a token lock wallet). - */ - function l2WalletAddress(address _l1Wallet) external view returns (address); -} diff --git a/packages/contracts/contracts/staking/IL1Staking.sol b/packages/contracts/contracts/staking/IL1Staking.sol deleted file mode 100644 index d64372f0a..000000000 --- a/packages/contracts/contracts/staking/IL1Staking.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.6.12 <0.8.0; -pragma abicoder v2; - -import { IStaking } from "./IStaking.sol"; -import { IL1StakingBase } from "./IL1StakingBase.sol"; - -/** - * @title Interface for the L1 Staking contract - * @author Edge & Node - * @notice This is the interface that should be used when interacting with the L1 Staking contract. - * It extends the IStaking interface with the functions that are specific to L1, adding the transfer tools - * to send stake and delegation to L2. - * @dev Note that L1Staking doesn't actually inherit this interface. This is because of - * the custom setup of the Staking contract where part of the functionality is implemented - * in a separate contract (StakingExtension) to which calls are delegated through the fallback function. - */ -interface IL1Staking is IStaking, IL1StakingBase {} diff --git a/packages/contracts/contracts/staking/IL1StakingBase.sol b/packages/contracts/contracts/staking/IL1StakingBase.sol deleted file mode 100644 index 09b2c3cca..000000000 --- a/packages/contracts/contracts/staking/IL1StakingBase.sol +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.6.12 <0.8.0; -pragma abicoder v2; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - -import { IL1GraphTokenLockTransferTool } from "./IL1GraphTokenLockTransferTool.sol"; - -/** - * @title Base interface for the L1Staking contract. - * @author Edge & Node - * @notice This interface is used to define the transfer tools that are implemented in L1Staking. - * @dev Note it includes only the L1-specific functionality, not the full IStaking interface. - */ -interface IL1StakingBase { - /** - * @notice Emitted when an indexer transfers their stake to L2. - * This can happen several times as indexers can transfer partial stake. - * @param indexer Address of the indexer on L1 - * @param l2Indexer Address of the indexer on L2 - * @param transferredStakeTokens Amount of stake tokens transferred - */ - event IndexerStakeTransferredToL2( - address indexed indexer, - address indexed l2Indexer, - uint256 transferredStakeTokens - ); - - /** - * @notice Emitted when a delegator transfers their delegation to L2 - * @param delegator Address of the delegator on L1 - * @param l2Delegator Address of the delegator on L2 - * @param indexer Address of the indexer on L1 - * @param l2Indexer Address of the indexer on L2 - * @param transferredDelegationTokens Amount of delegation tokens transferred - */ - event DelegationTransferredToL2( - address indexed delegator, - address indexed l2Delegator, - address indexed indexer, - address l2Indexer, - uint256 transferredDelegationTokens - ); - - /** - * @notice Emitted when the L1GraphTokenLockTransferTool is set - * @param l1GraphTokenLockTransferTool Address of the L1GraphTokenLockTransferTool contract - */ - event L1GraphTokenLockTransferToolSet(address l1GraphTokenLockTransferTool); - - /** - * @notice Emitted when a delegator unlocks their tokens ahead of time because the indexer has transferred to L2 - * @param indexer Address of the indexer that transferred to L2 - * @param delegator Address of the delegator unlocking their tokens - */ - event StakeDelegatedUnlockedDueToL2Transfer(address indexed indexer, address indexed delegator); - - /** - * @notice Set the L1GraphTokenLockTransferTool contract address - * @dev This function can only be called by the governor. - * @param _l1GraphTokenLockTransferTool Address of the L1GraphTokenLockTransferTool contract - */ - function setL1GraphTokenLockTransferTool(IL1GraphTokenLockTransferTool _l1GraphTokenLockTransferTool) external; - - /** - * @notice Send an indexer's stake to L2. - * @dev This function can only be called by the indexer (not an operator). - * It will validate that the remaining stake is sufficient to cover all the allocated - * stake, so the indexer might have to close some allocations before transferring. - * It will also check that the indexer's stake is not locked for withdrawal. - * Since the indexer address might be an L1-only contract, the function takes a beneficiary - * address that will be the indexer's address in L2. - * The caller must provide an amount of ETH to use for the L2 retryable ticket, that - * must be at least `_maxSubmissionCost + _gasPriceBid * _maxGas`. - * @param _l2Beneficiary Address of the indexer in L2. If the indexer has previously transferred stake, this must match the previously-used value. - * @param _amount Amount of stake GRT to transfer to L2 - * @param _maxGas Max gas to use for the L2 retryable ticket - * @param _gasPriceBid Gas price bid for the L2 retryable ticket - * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket - */ - function transferStakeToL2( - address _l2Beneficiary, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - uint256 _maxSubmissionCost - ) external payable; - - /** - * @notice Send an indexer's stake to L2, from a GraphTokenLockWallet vesting contract. - * @dev This function can only be called by the indexer (not an operator). - * It will validate that the remaining stake is sufficient to cover all the allocated - * stake, so the indexer might have to close some allocations before transferring. - * It will also check that the indexer's stake is not locked for withdrawal. - * The L2 beneficiary for the stake will be determined by calling the L1GraphTokenLockTransferTool contract, - * so the caller must have previously transferred tokens through that first - * (see GIP-0046 for details: https://forum.thegraph.com/t/4023). - * The ETH for the L2 gas will be pulled from the L1GraphTokenLockTransferTool, so the owner of - * the GraphTokenLockWallet must have previously deposited at least `_maxSubmissionCost + _gasPriceBid * _maxGas` - * ETH into the L1GraphTokenLockTransferTool contract (using its depositETH function). - * @param _amount Amount of stake GRT to transfer to L2 - * @param _maxGas Max gas to use for the L2 retryable ticket - * @param _gasPriceBid Gas price bid for the L2 retryable ticket - * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket - */ - function transferLockedStakeToL2( - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - uint256 _maxSubmissionCost - ) external; - - /** - * @notice Send a delegator's delegated tokens to L2 - * @dev This function can only be called by the delegator. - * This function will validate that the indexer has transferred their stake using transferStakeToL2, - * and that the delegation is not locked for undelegation. - * Since the delegator's address might be an L1-only contract, the function takes a beneficiary - * address that will be the delegator's address in L2. - * The caller must provide an amount of ETH to use for the L2 retryable ticket, that - * must be at least `_maxSubmissionCost + _gasPriceBid * _maxGas`. - * @param _indexer Address of the indexer (in L1, before transferring to L2) - * @param _l2Beneficiary Address of the delegator in L2 - * @param _maxGas Max gas to use for the L2 retryable ticket - * @param _gasPriceBid Gas price bid for the L2 retryable ticket - * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket - */ - function transferDelegationToL2( - address _indexer, - address _l2Beneficiary, - uint256 _maxGas, - uint256 _gasPriceBid, - uint256 _maxSubmissionCost - ) external payable; - - /** - * @notice Send a delegator's delegated tokens to L2, for a GraphTokenLockWallet vesting contract - * @dev This function can only be called by the delegator. - * This function will validate that the indexer has transferred their stake using transferStakeToL2, - * and that the delegation is not locked for undelegation. - * The L2 beneficiary for the delegation will be determined by calling the L1GraphTokenLockTransferTool contract, - * so the caller must have previously transferred tokens through that first - * (see GIP-0046 for details: https://forum.thegraph.com/t/4023). - * The ETH for the L2 gas will be pulled from the L1GraphTokenLockTransferTool, so the owner of - * the GraphTokenLockWallet must have previously deposited at least `_maxSubmissionCost + _gasPriceBid * _maxGas` - * ETH into the L1GraphTokenLockTransferTool contract (using its depositETH function). - * @param _indexer Address of the indexer (in L1, before transferring to L2) - * @param _maxGas Max gas to use for the L2 retryable ticket - * @param _gasPriceBid Gas price bid for the L2 retryable ticket - * @param _maxSubmissionCost Max submission cost for the L2 retryable ticket - */ - function transferLockedDelegationToL2( - address _indexer, - uint256 _maxGas, - uint256 _gasPriceBid, - uint256 _maxSubmissionCost - ) external; - - /** - * @notice Unlock a delegator's delegated tokens, if the indexer has transferred to L2 - * @dev This function can only be called by the delegator. - * This function will validate that the indexer has transferred their stake using transferStakeToL2, - * and that the indexer has no remaining stake in L1. - * The tokens must previously be locked for undelegation by calling `undelegate()`, - * and can be withdrawn with `withdrawDelegated()` immediately after calling this. - * @param _indexer Address of the indexer (in L1, before transferring to L2) - */ - function unlockDelegationToTransferredIndexer(address _indexer) external; -} diff --git a/packages/contracts/contracts/staking/IStaking.sol b/packages/contracts/contracts/staking/IStaking.sol deleted file mode 100644 index 16c1db02d..000000000 --- a/packages/contracts/contracts/staking/IStaking.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.6.12 <0.8.0 || 0.8.27; -pragma abicoder v2; - -import { IStakingBase } from "./IStakingBase.sol"; -import { IStakingExtension } from "./IStakingExtension.sol"; -import { IMulticall } from "../base/IMulticall.sol"; -import { IManaged } from "../governance/IManaged.sol"; - -/** - * @title Interface for the Staking contract - * @author Edge & Node - * @notice This is the interface that should be used when interacting with the Staking contract. - * @dev Note that Staking doesn't actually inherit this interface. This is because of - * the custom setup of the Staking contract where part of the functionality is implemented - * in a separate contract (StakingExtension) to which calls are delegated through the fallback function. - */ -interface IStaking is IStakingBase, IStakingExtension, IMulticall, IManaged {} diff --git a/packages/contracts/contracts/staking/IStakingBase.sol b/packages/contracts/contracts/staking/IStakingBase.sol deleted file mode 100644 index 23b867dd4..000000000 --- a/packages/contracts/contracts/staking/IStakingBase.sol +++ /dev/null @@ -1,455 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.6.12 <0.8.0 || 0.8.27; -pragma abicoder v2; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - -import { IStakingData } from "./IStakingData.sol"; - -/** - * @title Base interface for the Staking contract. - * @author Edge & Node - * @notice Base interface for the Staking contract. - * @dev This interface includes only what's implemented in the base Staking contract. - * It does not include the L1 and L2 specific functionality. It also does not include - * several functions that are implemented in the StakingExtension contract, and are called - * via delegatecall through the fallback function. See IStaking.sol for an interface - * that includes the full functionality. - */ -interface IStakingBase is IStakingData { - /** - * @notice Emitted when `indexer` stakes `tokens` amount. - * @param indexer Address of the indexer - * @param tokens Amount of tokens staked - */ - event StakeDeposited(address indexed indexer, uint256 tokens); - - /** - * @notice Emitted when `indexer` unstaked and locked `tokens` amount until `until` block. - * @param indexer Address of the indexer - * @param tokens Amount of tokens locked - * @param until Block number until which tokens are locked - */ - event StakeLocked(address indexed indexer, uint256 tokens, uint256 until); - - /** - * @notice Emitted when `indexer` withdrew `tokens` staked. - * @param indexer Address of the indexer - * @param tokens Amount of tokens withdrawn - */ - event StakeWithdrawn(address indexed indexer, uint256 tokens); - - /** - * @notice Emitted when `indexer` allocated `tokens` amount to `subgraphDeploymentID` - * during `epoch`. - * `allocationID` indexer derived address used to identify the allocation. - * `metadata` additional information related to the allocation. - * @param indexer Address of the indexer - * @param subgraphDeploymentID Subgraph deployment ID - * @param epoch Epoch when allocation was created - * @param tokens Amount of tokens allocated - * @param allocationID Allocation identifier - * @param metadata IPFS hash for additional allocation information - */ - event AllocationCreated( - address indexed indexer, - bytes32 indexed subgraphDeploymentID, - uint256 epoch, - uint256 tokens, - address indexed allocationID, - bytes32 metadata - ); - - /** - * @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 Address of the indexer - * @param subgraphDeploymentID Subgraph deployment ID - * @param epoch Epoch when allocation was closed - * @param tokens Amount of tokens unallocated - * @param allocationID Allocation identifier - * @param sender Address that closed the allocation - * @param poi Proof of indexing submitted - * @param isPublic True if closed by someone other than the indexer - */ - 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 Address providing the rebate tokens - * @param indexer Address of the indexer collecting the rebate - * @param subgraphDeploymentID Subgraph deployment ID - * @param allocationID Allocation identifier - * @param epoch Epoch when rebate was collected - * @param tokens Total amount of tokens in the rebate - * @param protocolTax Amount burned as protocol tax - * @param curationFees Amount distributed to curators - * @param queryFees Amount available for rebate after fees - * @param queryRebates Amount distributed to the indexer - * @param delegationRewards Amount distributed to delegators - */ - 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` update the delegation parameters for its delegation pool. - * @param indexer Address of the indexer - * @param indexingRewardCut Percentage of indexing rewards left for the indexer - * @param queryFeeCut Percentage of query fees left for the indexer - * @param __DEPRECATED_cooldownBlocks Deprecated parameter (no longer used) - */ - event DelegationParametersUpdated( - address indexed indexer, - uint32 indexingRewardCut, - uint32 queryFeeCut, - uint32 __DEPRECATED_cooldownBlocks // solhint-disable-line var-name-mixedcase - ); - - /** - * @notice Emitted when `indexer` set `operator` access. - * @param indexer Address of the indexer - * @param operator Address of the operator - * @param allowed Whether the operator is authorized - */ - event SetOperator(address indexed indexer, address indexed operator, bool allowed); - - /** - * @notice Emitted when `indexer` set an address to receive rewards. - * @param indexer Address of the indexer - * @param destination Address to receive rewards - */ - event SetRewardsDestination(address indexed indexer, address indexed destination); - - /** - * @notice Emitted when `extensionImpl` was set as the address of the StakingExtension contract - * to which extended functionality is delegated. - * @param extensionImpl Address of the StakingExtension implementation - */ - event ExtensionImplementationSet(address indexed extensionImpl); - - /** - * @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 Initialize this contract. - * @param _controller Address of the controller that manages this contract - * @param _minimumIndexerStake Minimum amount of tokens that an indexer must stake - * @param _thawingPeriod Number of blocks that tokens get locked after unstaking - * @param _protocolPercentage Percentage of query fees that are burned as protocol fee (in PPM) - * @param _curationPercentage Percentage of query fees that are given to curators (in PPM) - * @param _maxAllocationEpochs The maximum number of epochs that an allocation can be active - * @param _delegationUnbondingPeriod The period in epochs that tokens get locked after undelegating - * @param _delegationRatio The ratio between an indexer's own stake and the delegation they can use - * @param _rebatesParameters Alpha and lambda parameters for rebates function - * @param _extensionImpl Address of the StakingExtension implementation - */ - function initialize( - address _controller, - uint256 _minimumIndexerStake, - uint32 _thawingPeriod, - uint32 _protocolPercentage, - uint32 _curationPercentage, - uint32 _maxAllocationEpochs, - uint32 _delegationUnbondingPeriod, - uint32 _delegationRatio, - RebatesParameters calldata _rebatesParameters, - address _extensionImpl - ) external; - - /** - * @notice Set the address of the StakingExtension implementation. - * @dev This function can only be called by the governor. - * @param _extensionImpl Address of the StakingExtension implementation - */ - function setExtensionImpl(address _extensionImpl) external; - - /** - * @notice Set the address of the counterpart (L1 or L2) staking contract. - * @dev This function can only be called by the governor. - * @param _counterpart Address of the counterpart staking contract in the other chain, without any aliasing. - */ - function setCounterpartStakingAddress(address _counterpart) external; - - /** - * @notice Set the minimum stake needed to be an Indexer - * @dev This function can only be called by the governor. - * @param _minimumIndexerStake Minimum amount of tokens that an indexer must stake - */ - function setMinimumIndexerStake(uint256 _minimumIndexerStake) external; - - /** - * @notice Set the number of blocks that tokens get locked after unstaking - * @dev This function can only be called by the governor. - * @param _thawingPeriod Number of blocks that tokens get locked after unstaking - */ - function setThawingPeriod(uint32 _thawingPeriod) external; - - /** - * @notice Set the curation percentage of query fees sent to curators. - * @dev This function can only be called by the governor. - * @param _percentage Percentage of query fees sent to curators - */ - function setCurationPercentage(uint32 _percentage) external; - - /** - * @notice Set a protocol percentage to burn when collecting query fees. - * @dev This function can only be called by the governor. - * @param _percentage Percentage of query fees to burn as protocol fee - */ - function setProtocolPercentage(uint32 _percentage) external; - - /** - * @notice Set the max time allowed for indexers to allocate on a subgraph - * before others are allowed to close the allocation. - * @dev This function can only be called by the governor. - * @param _maxAllocationEpochs Allocation duration limit in epochs - */ - function setMaxAllocationEpochs(uint32 _maxAllocationEpochs) external; - - /** - * @notice Set the rebate parameters - * @dev This function can only be called by the governor. - * @param _alphaNumerator Numerator of `alpha` - * @param _alphaDenominator Denominator of `alpha` - * @param _lambdaNumerator Numerator of `lambda` - * @param _lambdaDenominator Denominator of `lambda` - */ - function setRebateParameters( - uint32 _alphaNumerator, - uint32 _alphaDenominator, - uint32 _lambdaNumerator, - uint32 _lambdaDenominator - ) external; - - /** - * @notice Authorize or unauthorize an address to be an operator for the caller. - * @param _operator Address to authorize or unauthorize - * @param _allowed Whether the operator is authorized or not - */ - function setOperator(address _operator, bool _allowed) external; - - /** - * @notice Deposit tokens on the indexer's stake. - * The amount staked must be over the minimumIndexerStake. - * @param _tokens Amount of tokens to stake - */ - function stake(uint256 _tokens) external; - - /** - * @notice Deposit tokens on the Indexer stake, on behalf of the Indexer. - * The amount staked must be over the minimumIndexerStake. - * @param _indexer Address of the indexer - * @param _tokens Amount of tokens to stake - */ - function stakeTo(address _indexer, uint256 _tokens) external; - - /** - * @notice Unstake tokens from the indexer stake, lock them until the thawing period expires. - * @dev NOTE: The function accepts an amount greater than the currently staked tokens. - * If that happens, it will try to unstake the max amount of tokens it can. - * The reason for this behaviour is to avoid time conditions while the transaction - * is in flight. - * @param _tokens Amount of tokens to unstake - */ - function unstake(uint256 _tokens) external; - - /** - * @notice Withdraw indexer tokens once the thawing period has passed. - */ - function withdraw() external; - - /** - * @notice Set the destination where to send rewards for an indexer. - * @param _destination Rewards destination address. If set to zero, rewards will be restaked - */ - function setRewardsDestination(address _destination) external; - - /** - * @notice Set the delegation parameters for the caller. - * @param _indexingRewardCut Percentage of indexing rewards left for the indexer - * @param _queryFeeCut Percentage of query fees left for the indexer - * @param _cooldownBlocks Deprecated cooldown blocks parameter (no longer used) - */ - function setDelegationParameters(uint32 _indexingRewardCut, uint32 _queryFeeCut, uint32 _cooldownBlocks) external; - - /** - * @notice Allocate available tokens to a subgraph deployment. - * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated - * @param _tokens Amount of tokens to allocate - * @param _allocationID The allocation identifier - * @param _metadata IPFS hash for additional information about the allocation - * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` - */ - function allocate( - bytes32 _subgraphDeploymentID, - uint256 _tokens, - address _allocationID, - bytes32 _metadata, - bytes calldata _proof - ) external; - - /** - * @notice Allocate available tokens to a subgraph deployment from and indexer's stake. - * The caller must be the indexer or the indexer's operator. - * @param _indexer Indexer address to allocate funds from. - * @param _subgraphDeploymentID ID of the SubgraphDeployment where tokens will be allocated - * @param _tokens Amount of tokens to allocate - * @param _allocationID The allocation identifier - * @param _metadata IPFS hash for additional information about the allocation - * @param _proof A 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationID)` - */ - function allocateFrom( - address _indexer, - bytes32 _subgraphDeploymentID, - uint256 _tokens, - address _allocationID, - bytes32 _metadata, - bytes calldata _proof - ) external; - - /** - * @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 query fees from state channels and assign them to an allocation. - * Funds received are only accepted from a valid sender. - * @dev To avoid reverting on the withdrawal from channel flow this function will: - * 1) Accept calls with zero tokens. - * 2) Accept calls after an allocation passed the dispute period, in that case, all - * the received tokens are burned. - * @param _tokens Amount of tokens to collect - * @param _allocationID Allocation where the tokens will be assigned - */ - function collect(uint256 _tokens, address _allocationID) external; - - /** - * @notice Return true if operator is allowed for indexer. - * @param _operator Address of the operator - * @param _indexer Address of the indexer - * @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 Get the total amount of tokens available to use in allocations. - * This considers the indexer stake and delegated tokens according to delegation ratio - * @param _indexer Address of the indexer - * @return Amount of tokens available to allocate including delegation - */ - function getIndexerCapacity(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 Get the allocation data for the rewards manager - * @dev New function to get the allocation data for the rewards manager - * @dev Note that this is only to make tests pass, as the staking contract with - * this changes will never get deployed. HorizonStaking is taking it's place. - * @param _allocationID The allocation identifier - * @return isActive Whether the allocation is active - * @return indexer Address of the indexer - * @return subgraphDeploymentID Subgraph deployment ID - * @return tokens Amount of tokens allocated - * @return createdAtEpoch Epoch when allocation was created - * @return closedAtEpoch Epoch when allocation was closed (0 if still active) - */ - function getAllocationData( - address _allocationID - ) external view returns (bool, address, bytes32, uint256, uint256, uint256); - - /** - * @notice Get the allocation active status for the rewards manager - * @dev New function to get the allocation active status for the rewards manager - * @dev Note that this is only to make tests pass, as the staking contract with - * this changes will never get deployed. HorizonStaking is taking it's place. - * @param _allocationID The allocation identifier - * @return True if the allocation is active, false otherwise - */ - function isActiveAllocation(address _allocationID) external view returns (bool); - - /** - * @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 total amount of tokens allocated to subgraph. - * @param _subgraphDeploymentID Deployment ID for the subgraph - * @return Total tokens allocated to subgraph - */ - function getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentID) external view returns (uint256); -} diff --git a/packages/contracts/contracts/staking/IStakingData.sol b/packages/contracts/contracts/staking/IStakingData.sol deleted file mode 100644 index edac435c7..000000000 --- a/packages/contracts/contracts/staking/IStakingData.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.6.12 <0.8.0 || 0.8.27; - -/** - * @title Staking Data interface - * @author Edge & Node - * @notice This interface defines some structures used by the Staking contract. - */ -interface IStakingData { - /** - * @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 Address of the indexer that owns the allocation - * @param subgraphDeploymentID Subgraph deployment ID being allocated to - * @param tokens Tokens allocated to a SubgraphDeployment - * @param createdAtEpoch Epoch when it was created - * @param closedAtEpoch Epoch when it was closed - * @param collectedFees Collected fees for the allocation - * @param __DEPRECATED_effectiveAllocation Deprecated field for effective allocation - * @param accRewardsPerAllocatedToken Snapshot used for reward calc - * @param distributedRebates Collected rebates that have been rebated - */ - struct Allocation { - address indexer; - bytes32 subgraphDeploymentID; - uint256 tokens; // Tokens allocated to a SubgraphDeployment - uint256 createdAtEpoch; // Epoch when it was created - uint256 closedAtEpoch; // Epoch when it was closed - uint256 collectedFees; // Collected fees for the allocation - uint256 __DEPRECATED_effectiveAllocation; // solhint-disable-line var-name-mixedcase - uint256 accRewardsPerAllocatedToken; // Snapshot used for reward calc - uint256 distributedRebates; // Collected rebates that have been rebated - } - - // -- Delegation Data -- - - /** - * @dev Delegation pool information. One per indexer. - * @param __DEPRECATED_cooldownBlocks Deprecated field for cooldown blocks - * @param indexingRewardCut Indexing reward cut in PPM - * @param queryFeeCut Query fee cut in PPM - * @param updatedAtBlock Block when the pool was last updated - * @param tokens Total tokens as pool reserves - * @param shares Total shares minted in the pool - * @param delegators Mapping of delegator => Delegation - */ - struct DelegationPool { - uint32 __DEPRECATED_cooldownBlocks; // solhint-disable-line var-name-mixedcase - uint32 indexingRewardCut; // in PPM - uint32 queryFeeCut; // in PPM - uint256 updatedAtBlock; // Block when the pool was last updated - uint256 tokens; // Total tokens as pool reserves - uint256 shares; // Total shares minted in the pool - mapping(address => Delegation) delegators; // Mapping of delegator => Delegation - } - - /** - * @dev Individual delegation data of a delegator in a pool. - * @param shares Shares owned by a delegator in the pool - * @param tokensLocked Tokens locked for undelegation - * @param tokensLockedUntil Epoch when locked tokens can be withdrawn - */ - struct Delegation { - uint256 shares; // Shares owned by a delegator in the pool - uint256 tokensLocked; // Tokens locked for undelegation - uint256 tokensLockedUntil; // Epoch when locked tokens can be withdrawn - } - - /** - * @dev Rebates parameters. Used to avoid stack too deep errors in Staking initialize function. - * @param alphaNumerator Alpha parameter numerator for rebate calculation - * @param alphaDenominator Alpha parameter denominator for rebate calculation - * @param lambdaNumerator Lambda parameter numerator for rebate calculation - * @param lambdaDenominator Lambda parameter denominator for rebate calculation - */ - struct RebatesParameters { - uint32 alphaNumerator; - uint32 alphaDenominator; - uint32 lambdaNumerator; - uint32 lambdaDenominator; - } -} diff --git a/packages/contracts/contracts/staking/IStakingExtension.sol b/packages/contracts/contracts/staking/IStakingExtension.sol deleted file mode 100644 index 3053e7acf..000000000 --- a/packages/contracts/contracts/staking/IStakingExtension.sol +++ /dev/null @@ -1,322 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity >=0.6.12 <0.8.0 || 0.8.27; -pragma abicoder v2; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - -import { IStakingData } from "./IStakingData.sol"; -import { IStakes } from "./libs/IStakes.sol"; - -/** - * @title Interface for the StakingExtension contract - * @author Edge & Node - * @notice This interface defines the events and functions implemented - * in the StakingExtension contract, which is used to extend the functionality - * of the Staking contract while keeping it within the 24kB mainnet size limit. - * In particular, this interface includes delegation functions and various storage - * getters. - */ -interface IStakingExtension is IStakingData { - /** - * @dev DelegationPool struct as returned by delegationPools(), since - * the original DelegationPool in IStakingData.sol contains a nested mapping. - * @param __DEPRECATED_cooldownBlocks Deprecated field for cooldown blocks - * @param indexingRewardCut Indexing reward cut in PPM - * @param queryFeeCut Query fee cut in PPM - * @param updatedAtBlock Block when the pool was last updated - * @param tokens Total tokens as pool reserves - * @param shares Total shares minted in the pool - */ - struct DelegationPoolReturn { - uint32 __DEPRECATED_cooldownBlocks; // solhint-disable-line var-name-mixedcase - uint32 indexingRewardCut; // in PPM - uint32 queryFeeCut; // in PPM - uint256 updatedAtBlock; // Block when the pool was last updated - uint256 tokens; // Total tokens as pool reserves - uint256 shares; // Total shares minted in the pool - } - - /** - * @notice Emitted when `delegator` delegated `tokens` to the `indexer`, the delegator - * gets `shares` for the delegation pool proportionally to the tokens staked. - * @param indexer Address of the indexer receiving the delegation - * @param delegator Address of the delegator - * @param tokens Amount of tokens delegated - * @param shares Amount of shares issued to the delegator - */ - event StakeDelegated(address indexed indexer, address indexed delegator, uint256 tokens, uint256 shares); - - /** - * @notice Emitted when `delegator` undelegated `tokens` from `indexer`. - * Tokens get locked for withdrawal after a period of time. - * @param indexer Address of the indexer from which tokens are undelegated - * @param delegator Address of the delegator - * @param tokens Amount of tokens undelegated - * @param shares Amount of shares returned - * @param until Epoch until which tokens are locked - */ - event StakeDelegatedLocked( - address indexed indexer, - address indexed delegator, - uint256 tokens, - uint256 shares, - uint256 until - ); - - /** - * @notice Emitted when `delegator` withdrew delegated `tokens` from `indexer`. - * @param indexer Address of the indexer from which tokens are withdrawn - * @param delegator Address of the delegator - * @param tokens Amount of tokens withdrawn - */ - event StakeDelegatedWithdrawn(address indexed indexer, address indexed delegator, uint256 tokens); - - /** - * @notice Emitted when `indexer` was slashed for a total of `tokens` amount. - * Tracks `reward` amount of tokens given to `beneficiary`. - * @param indexer Address of the indexer that was slashed - * @param tokens Total amount of tokens slashed - * @param reward Amount of tokens given as reward - * @param beneficiary Address receiving the reward - */ - event StakeSlashed(address indexed indexer, uint256 tokens, uint256 reward, address beneficiary); - - /** - * @notice Emitted when `caller` set `slasher` address as `allowed` to slash stakes. - * @param caller Address that updated the slasher status - * @param slasher Address of the slasher - * @param allowed Whether the slasher is allowed to slash - */ - event SlasherUpdate(address indexed caller, address indexed slasher, bool allowed); - - /** - * @notice Set the delegation ratio. - * If set to 10 it means the indexer can use up to 10x the indexer staked amount - * from their delegated tokens - * @dev This function is only callable by the governor - * @param _delegationRatio Delegation capacity multiplier - */ - function setDelegationRatio(uint32 _delegationRatio) external; - - /** - * @notice Set the time, in epochs, a Delegator needs to wait to withdraw tokens after undelegating. - * @dev This function is only callable by the governor - * @param _delegationUnbondingPeriod Period in epochs to wait for token withdrawals after undelegating - */ - function setDelegationUnbondingPeriod(uint32 _delegationUnbondingPeriod) external; - - /** - * @notice Set a delegation tax percentage to burn when delegated funds are deposited. - * @dev This function is only callable by the governor - * @param _percentage Percentage of delegated tokens to burn as delegation tax, expressed in parts per million - */ - function setDelegationTaxPercentage(uint32 _percentage) external; - - /** - * @notice Set or unset an address as allowed slasher. - * @dev This function can only be called by the governor. - * @param _slasher Address of the party allowed to slash indexers - * @param _allowed True if slasher is allowed - */ - function setSlasher(address _slasher, bool _allowed) external; - - /** - * @notice Delegate tokens to an indexer. - * @param _indexer Address of the indexer to which tokens are delegated - * @param _tokens Amount of tokens to delegate - * @return Amount of shares issued from the delegation pool - */ - function delegate(address _indexer, uint256 _tokens) external returns (uint256); - - /** - * @notice Undelegate tokens from an indexer. Tokens will be locked for the unbonding period. - * @param _indexer Address of the indexer to which tokens had been delegated - * @param _shares Amount of shares to return and undelegate tokens - * @return Amount of tokens returned for the shares of the delegation pool - */ - function undelegate(address _indexer, uint256 _shares) external returns (uint256); - - /** - * @notice Withdraw undelegated tokens once the unbonding period has passed, and optionally - * re-delegate to a new indexer. - * @param _indexer Withdraw available tokens delegated to indexer - * @param _newIndexer Re-delegate to indexer address if non-zero, withdraw if zero address - * @return Amount of tokens withdrawn - */ - function withdrawDelegated(address _indexer, address _newIndexer) external returns (uint256); - - /** - * @notice Slash the indexer stake. Delegated tokens are not subject to slashing. - * @dev Can only be called by the slasher role. - * @param _indexer Address of indexer to slash - * @param _tokens Amount of tokens to slash from the indexer stake - * @param _reward Amount of reward tokens to send to a beneficiary - * @param _beneficiary Address of a beneficiary to receive a reward for the slashing - */ - function slash(address _indexer, uint256 _tokens, uint256 _reward, address _beneficiary) external; - - /** - * @notice Return the delegation from a delegator to an indexer. - * @param _indexer Address of the indexer where funds have been delegated - * @param _delegator Address of the delegator - * @return Delegation data - */ - function getDelegation(address _indexer, address _delegator) external view returns (Delegation memory); - - /** - * @notice Return whether the delegator has delegated to the indexer. - * @param _indexer Address of the indexer where funds have been delegated - * @param _delegator Address of the delegator - * @return True if delegator has tokens delegated to the indexer - */ - function isDelegator(address _indexer, address _delegator) external view returns (bool); - - /** - * @notice Returns amount of delegated tokens ready to be withdrawn after unbonding period. - * @param _delegation Delegation of tokens from delegator to indexer - * @return Amount of tokens to withdraw - */ - function getWithdraweableDelegatedTokens(Delegation memory _delegation) external view returns (uint256); - - /** - * @notice Getter for the delegationRatio, i.e. the delegation capacity multiplier: - * If delegation ratio is 100, and an Indexer has staked 5 GRT, - * then they can use up to 500 GRT from the delegated stake - * @return Delegation ratio - */ - function delegationRatio() external view returns (uint32); - - /** - * @notice Getter for delegationUnbondingPeriod: - * Time in epochs a delegator needs to wait to withdraw delegated stake - * @return Delegation unbonding period in epochs - */ - function delegationUnbondingPeriod() external view returns (uint32); - - /** - * @notice Getter for delegationTaxPercentage: - * Percentage of tokens to tax a delegation deposit, expressed in parts per million - * @return Delegation tax percentage in parts per million - */ - function delegationTaxPercentage() external view returns (uint32); - - /** - * @notice Getter for delegationPools[_indexer]: - * gets the delegation pool structure for a particular indexer. - * @param _indexer Address of the indexer for which to query the delegation pool - * @return Delegation pool as a DelegationPoolReturn struct - */ - function delegationPools(address _indexer) external view returns (DelegationPoolReturn memory); - - /** - * @notice Getter for operatorAuth[_indexer][_maybeOperator]: - * returns true if the operator is authorized to operate on behalf of the indexer. - * @param _indexer The indexer address for which to query authorization - * @param _maybeOperator The address that may or may not be an operator - * @return True if the operator is authorized to operate on behalf of the indexer - */ - function operatorAuth(address _indexer, address _maybeOperator) external view returns (bool); - - /** - * @notice Getter for rewardsDestination[_indexer]: - * returns the address where the indexer's rewards are sent. - * @param _indexer The indexer address for which to query the rewards destination - * @return The address where the indexer's rewards are sent, zero if none is set in which case rewards are re-staked - */ - function rewardsDestination(address _indexer) external view returns (address); - - /** - * @notice Getter for subgraphAllocations[_subgraphDeploymentId]: - * returns the amount of tokens allocated to a subgraph deployment. - * @param _subgraphDeploymentId The subgraph deployment for which to query the allocations - * @return The amount of tokens allocated to the subgraph deployment - */ - function subgraphAllocations(bytes32 _subgraphDeploymentId) external view returns (uint256); - - /** - * @notice Getter for slashers[_maybeSlasher]: - * returns true if the address is a slasher, i.e. an entity that can slash indexers - * @param _maybeSlasher Address for which to check the slasher role - * @return True if the address is a slasher - */ - function slashers(address _maybeSlasher) external view returns (bool); - - /** - * @notice Getter for minimumIndexerStake: the minimum - * amount of GRT that an indexer needs to stake. - * @return Minimum indexer stake in GRT - */ - function minimumIndexerStake() external view returns (uint256); - - /** - * @notice Getter for thawingPeriod: the time in blocks an - * indexer needs to wait to unstake tokens. - * @return Thawing period in blocks - */ - function thawingPeriod() external view returns (uint32); - - /** - * @notice Getter for curationPercentage: the percentage of - * query fees that are distributed to curators. - * @return Curation percentage in parts per million - */ - function curationPercentage() external view returns (uint32); - - /** - * @notice Getter for protocolPercentage: the percentage of - * query fees that are burned as protocol fees. - * @return Protocol percentage in parts per million - */ - function protocolPercentage() external view returns (uint32); - - /** - * @notice Getter for maxAllocationEpochs: the maximum time in epochs - * that an allocation can be open before anyone is allowed to close it. This - * also caps the effective allocation when sending the allocation's query fees - * to the rebate pool. - * @return Maximum allocation period in epochs - */ - function maxAllocationEpochs() external view returns (uint32); - - /** - * @notice Getter for the numerator of the rebates alpha parameter - * @return Alpha numerator - */ - function alphaNumerator() external view returns (uint32); - - /** - * @notice Getter for the denominator of the rebates alpha parameter - * @return Alpha denominator - */ - function alphaDenominator() external view returns (uint32); - - /** - * @notice Getter for the numerator of the rebates lambda parameter - * @return Lambda numerator - */ - function lambdaNumerator() external view returns (uint32); - - /** - * @notice Getter for the denominator of the rebates lambda parameter - * @return Lambda denominator - */ - function lambdaDenominator() external view returns (uint32); - - /** - * @notice Getter for stakes[_indexer]: - * gets the stake information for an indexer as a IStakes.Indexer struct. - * @param _indexer Indexer address for which to query the stake information - * @return Stake information for the specified indexer, as a IStakes.Indexer struct - */ - function stakes(address _indexer) external view returns (IStakes.Indexer memory); - - /** - * @notice Getter for allocations[_allocationID]: - * gets an allocation's information as an IStakingData.Allocation struct. - * @param _allocationID Allocation ID for which to query the allocation information - * @return The specified allocation, as an IStakingData.Allocation struct - */ - function allocations(address _allocationID) external view returns (IStakingData.Allocation memory); -} diff --git a/packages/contracts/contracts/staking/L1Staking.sol b/packages/contracts/contracts/staking/L1Staking.sol index 2ffc3b62a..b2a00c57e 100644 --- a/packages/contracts/contracts/staking/L1Staking.sol +++ b/packages/contracts/contracts/staking/L1Staking.sol @@ -8,17 +8,17 @@ pragma abicoder v2; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; import { Staking } from "./Staking.sol"; import { Stakes } from "./libs/Stakes.sol"; -import { IStakes } from "./libs/IStakes.sol"; -import { IStakingData } from "./IStakingData.sol"; +import { IStakes } from "@graphprotocol/interfaces/contracts/contracts/staking/libs/IStakes.sol"; +import { IStakingData } from "@graphprotocol/interfaces/contracts/contracts/staking/IStakingData.sol"; import { L1StakingV1Storage } from "./L1StakingStorage.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; -import { IL1StakingBase } from "./IL1StakingBase.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; +import { IL1StakingBase } from "@graphprotocol/interfaces/contracts/contracts/staking/IL1StakingBase.sol"; import { MathUtils } from "./libs/MathUtils.sol"; -import { IL1GraphTokenLockTransferTool } from "./IL1GraphTokenLockTransferTool.sol"; -import { IL2StakingTypes } from "../l2/staking/IL2StakingTypes.sol"; +import { IL1GraphTokenLockTransferTool } from "@graphprotocol/interfaces/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol"; +import { IL2StakingTypes } from "@graphprotocol/interfaces/contracts/contracts/l2/staking/IL2StakingTypes.sol"; /** * @title L1Staking contract diff --git a/packages/contracts/contracts/staking/L1StakingStorage.sol b/packages/contracts/contracts/staking/L1StakingStorage.sol index b07ffa5e1..5ee76f633 100644 --- a/packages/contracts/contracts/staking/L1StakingStorage.sol +++ b/packages/contracts/contracts/staking/L1StakingStorage.sol @@ -6,7 +6,7 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import { IL1GraphTokenLockTransferTool } from "./IL1GraphTokenLockTransferTool.sol"; +import { IL1GraphTokenLockTransferTool } from "@graphprotocol/interfaces/contracts/contracts/staking/IL1GraphTokenLockTransferTool.sol"; /** * @title L1StakingV1Storage diff --git a/packages/contracts/contracts/staking/Staking.sol b/packages/contracts/contracts/staking/Staking.sol index 4f6107bd0..4b7551d02 100644 --- a/packages/contracts/contracts/staking/Staking.sol +++ b/packages/contracts/contracts/staking/Staking.sol @@ -12,14 +12,14 @@ import { ECDSA } from "@openzeppelin/contracts/cryptography/ECDSA.sol"; import { Multicall } from "../base/Multicall.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; import { TokenUtils } from "../utils/TokenUtils.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; -import { IStakingBase } from "./IStakingBase.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; +import { IStakingBase } from "@graphprotocol/interfaces/contracts/contracts/staking/IStakingBase.sol"; import { StakingV4Storage } from "./StakingStorage.sol"; import { MathUtils } from "./libs/MathUtils.sol"; import { Stakes } from "./libs/Stakes.sol"; -import { IStakes } from "./libs/IStakes.sol"; +import { IStakes } from "@graphprotocol/interfaces/contracts/contracts/staking/libs/IStakes.sol"; import { Managed } from "../governance/Managed.sol"; -import { ICuration } from "../curation/ICuration.sol"; +import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; import { StakingExtension } from "./StakingExtension.sol"; import { LibExponential } from "./libs/Exponential.sol"; diff --git a/packages/contracts/contracts/staking/StakingExtension.sol b/packages/contracts/contracts/staking/StakingExtension.sol index fb8dea3e8..8bde14add 100644 --- a/packages/contracts/contracts/staking/StakingExtension.sol +++ b/packages/contracts/contracts/staking/StakingExtension.sol @@ -8,13 +8,13 @@ pragma abicoder v2; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { StakingV4Storage } from "./StakingStorage.sol"; -import { IStakingExtension } from "./IStakingExtension.sol"; +import { IStakingExtension } from "@graphprotocol/interfaces/contracts/contracts/staking/IStakingExtension.sol"; import { TokenUtils } from "../utils/TokenUtils.sol"; -import { IGraphToken } from "../token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol"; -import { IStakes } from "./libs/IStakes.sol"; +import { IStakes } from "@graphprotocol/interfaces/contracts/contracts/staking/libs/IStakes.sol"; import { Stakes } from "./libs/Stakes.sol"; -import { IStakingData } from "./IStakingData.sol"; +import { IStakingData } from "@graphprotocol/interfaces/contracts/contracts/staking/IStakingData.sol"; import { MathUtils } from "./libs/MathUtils.sol"; /** diff --git a/packages/contracts/contracts/staking/StakingStorage.sol b/packages/contracts/contracts/staking/StakingStorage.sol index 221359b50..24f46009f 100644 --- a/packages/contracts/contracts/staking/StakingStorage.sol +++ b/packages/contracts/contracts/staking/StakingStorage.sol @@ -9,8 +9,8 @@ pragma solidity ^0.7.6; import { Managed } from "../governance/Managed.sol"; -import { IStakingData } from "./IStakingData.sol"; -import { IStakes } from "./libs/IStakes.sol"; +import { IStakingData } from "@graphprotocol/interfaces/contracts/contracts/staking/IStakingData.sol"; +import { IStakes } from "@graphprotocol/interfaces/contracts/contracts/staking/libs/IStakes.sol"; /** * @title StakingV1Storage diff --git a/packages/contracts/contracts/staking/libs/IStakes.sol b/packages/contracts/contracts/staking/libs/IStakes.sol deleted file mode 100644 index 10364ebad..000000000 --- a/packages/contracts/contracts/staking/libs/IStakes.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; -pragma abicoder v2; - -/** - * @title Interface for staking data structures - * @author Edge & Node - * @notice Defines the data structures used for indexer staking - */ -interface IStakes { - struct Indexer { - uint256 tokensStaked; // Tokens on the indexer stake (staked by the indexer) - uint256 tokensAllocated; // Tokens used in allocations - uint256 tokensLocked; // Tokens locked for withdrawal subject to thawing period - uint256 tokensLockedUntil; // Block when locked tokens can be withdrawn - } -} diff --git a/packages/contracts/contracts/staking/libs/Stakes.sol b/packages/contracts/contracts/staking/libs/Stakes.sol index 175e0bc21..459a82996 100644 --- a/packages/contracts/contracts/staking/libs/Stakes.sol +++ b/packages/contracts/contracts/staking/libs/Stakes.sol @@ -6,7 +6,7 @@ pragma abicoder v2; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { MathUtils } from "./MathUtils.sol"; -import { IStakes } from "./IStakes.sol"; +import { IStakes } from "@graphprotocol/interfaces/contracts/contracts/staking/libs/IStakes.sol"; /** * @title A collection of data structures and functions to manage the Indexer Stake state. diff --git a/packages/contracts/contracts/tests/CallhookReceiverMock.sol b/packages/contracts/contracts/tests/CallhookReceiverMock.sol index b87d57cf0..f6b9d130e 100644 --- a/packages/contracts/contracts/tests/CallhookReceiverMock.sol +++ b/packages/contracts/contracts/tests/CallhookReceiverMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.7.6; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-indexed-events, use-natspec -import { ICallhookReceiver } from "../gateway/ICallhookReceiver.sol"; +import { ICallhookReceiver } from "@graphprotocol/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol"; /** * @title CallhookReceiverMock contract diff --git a/packages/contracts/contracts/tests/LegacyGNSMock.sol b/packages/contracts/contracts/tests/LegacyGNSMock.sol index 30e619e6e..30f98ea31 100644 --- a/packages/contracts/contracts/tests/LegacyGNSMock.sol +++ b/packages/contracts/contracts/tests/LegacyGNSMock.sol @@ -7,7 +7,7 @@ pragma abicoder v2; // solhint-disable use-natspec import { L1GNS } from "../discovery/L1GNS.sol"; -import { IGNS } from "../discovery/IGNS.sol"; +import { IGNS } from "@graphprotocol/interfaces/contracts/contracts/discovery/IGNS.sol"; /** * @title LegacyGNSMock contract diff --git a/packages/contracts/contracts/tests/arbitrum/BridgeMock.sol b/packages/contracts/contracts/tests/arbitrum/BridgeMock.sol index 141cf2dda..9bcc71982 100644 --- a/packages/contracts/contracts/tests/arbitrum/BridgeMock.sol +++ b/packages/contracts/contracts/tests/arbitrum/BridgeMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.7.6; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-increment-by-one, use-natspec -import { IBridge } from "../../arbitrum/IBridge.sol"; +import { IBridge } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IBridge.sol"; /** * @title Arbitrum Bridge mock contract diff --git a/packages/contracts/contracts/tests/arbitrum/InboxMock.sol b/packages/contracts/contracts/tests/arbitrum/InboxMock.sol index c920ea314..38cee3b44 100644 --- a/packages/contracts/contracts/tests/arbitrum/InboxMock.sol +++ b/packages/contracts/contracts/tests/arbitrum/InboxMock.sol @@ -2,9 +2,9 @@ pragma solidity ^0.7.6; -import { IInbox } from "../../arbitrum/IInbox.sol"; +import { IInbox } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IInbox.sol"; import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; -import { IBridge } from "../../arbitrum/IBridge.sol"; +import { IBridge } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IBridge.sol"; /** * @title Arbitrum Inbox mock contract diff --git a/packages/contracts/contracts/tests/arbitrum/OutboxMock.sol b/packages/contracts/contracts/tests/arbitrum/OutboxMock.sol index 4191e9e0a..7a8e9932b 100644 --- a/packages/contracts/contracts/tests/arbitrum/OutboxMock.sol +++ b/packages/contracts/contracts/tests/arbitrum/OutboxMock.sol @@ -5,8 +5,8 @@ pragma solidity ^0.7.6; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable use-natspec -import { IOutbox } from "../../arbitrum/IOutbox.sol"; -import { IBridge } from "../../arbitrum/IBridge.sol"; +import { IOutbox } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IOutbox.sol"; +import { IBridge } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/IBridge.sol"; /** * @title Arbitrum Outbox mock contract diff --git a/packages/contracts/contracts/token/IGraphToken.sol b/packages/contracts/contracts/token/IGraphToken.sol deleted file mode 100644 index 924183e46..000000000 --- a/packages/contracts/contracts/token/IGraphToken.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || ^0.8.0; - -// Solhint linting fails for 0.8.0. -// solhint-disable-next-line import-path-check -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -/** - * @title IGraphToken - * @author Edge & Node - * @notice Interface for the Graph Token contract - * @dev Extends IERC20 with additional functionality for minting, burning, and permit - */ -interface IGraphToken is IERC20 { - // -- Mint and Burn -- - - /** - * @notice Burns tokens from the caller's account - * @param amount The amount of tokens to burn - */ - function burn(uint256 amount) external; - - /** - * @notice Burns tokens from a specified account (requires allowance) - * @param _from The account to burn tokens from - * @param amount The amount of tokens to burn - */ - function burnFrom(address _from, uint256 amount) external; - - /** - * @notice Mints new tokens to a specified account - * @dev Only callable by accounts with minter role - * @param _to The account to mint tokens to - * @param _amount The amount of tokens to mint - */ - function mint(address _to, uint256 _amount) external; - - // -- Mint Admin -- - - /** - * @notice Adds a new minter account - * @dev Only callable by accounts with appropriate permissions - * @param _account The account to grant minter role to - */ - function addMinter(address _account) external; - - /** - * @notice Removes minter role from an account - * @dev Only callable by accounts with appropriate permissions - * @param _account The account to revoke minter role from - */ - function removeMinter(address _account) external; - - /** - * @notice Renounces minter role for the caller - * @dev Allows a minter to voluntarily give up their minting privileges - */ - function renounceMinter() external; - - /** - * @notice Checks if an account has minter role - * @param _account The account to check - * @return True if the account is a minter, false otherwise - */ - function isMinter(address _account) external view returns (bool); - - // -- Permit -- - - /** - * @notice Allows approval via signature (EIP-2612) - * @param _owner The token owner's address - * @param _spender The spender's address - * @param _value The allowance amount - * @param _deadline The deadline timestamp for the permit - * @param _v The recovery byte of the signature - * @param _r Half of the ECDSA signature pair - * @param _s Half of the ECDSA signature pair - */ - function permit( - address _owner, - address _spender, - uint256 _value, - uint256 _deadline, - uint8 _v, - bytes32 _r, - bytes32 _s - ) external; - - // -- Allowance -- - - /** - * @notice Increases the allowance granted to a spender - * @param spender The account whose allowance will be increased - * @param addedValue The amount to increase the allowance by - * @return True if the operation succeeded - */ - function increaseAllowance(address spender, uint256 addedValue) external returns (bool); - - /** - * @notice Decreases the allowance granted to a spender - * @param spender The account whose allowance will be decreased - * @param subtractedValue The amount to decrease the allowance by - * @return True if the operation succeeded - */ - function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); -} diff --git a/packages/contracts/contracts/upgrades/GraphProxy.sol b/packages/contracts/contracts/upgrades/GraphProxy.sol index 733e3c0be..b787b476a 100644 --- a/packages/contracts/contracts/upgrades/GraphProxy.sol +++ b/packages/contracts/contracts/upgrades/GraphProxy.sol @@ -9,7 +9,7 @@ pragma solidity ^0.7.6 || 0.8.27; import { GraphProxyStorage } from "./GraphProxyStorage.sol"; -import { IGraphProxy } from "./IGraphProxy.sol"; +import { IGraphProxy } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxy.sol"; /** * @title Graph Proxy diff --git a/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol b/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol index 83550a3e5..97f0b2e11 100644 --- a/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol +++ b/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol @@ -6,7 +6,7 @@ pragma solidity ^0.7.6 || 0.8.27; import { Governed } from "../governance/Governed.sol"; -import { IGraphProxy } from "./IGraphProxy.sol"; +import { IGraphProxy } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxy.sol"; import { GraphUpgradeable } from "./GraphUpgradeable.sol"; /** diff --git a/packages/contracts/contracts/upgrades/GraphUpgradeable.sol b/packages/contracts/contracts/upgrades/GraphUpgradeable.sol index ada2b04fd..827082213 100644 --- a/packages/contracts/contracts/upgrades/GraphUpgradeable.sol +++ b/packages/contracts/contracts/upgrades/GraphUpgradeable.sol @@ -4,7 +4,7 @@ pragma solidity ^0.7.6 || 0.8.27; /* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 -import { IGraphProxy } from "./IGraphProxy.sol"; +import { IGraphProxy } from "@graphprotocol/interfaces/contracts/contracts/upgrades/IGraphProxy.sol"; /** * @title Graph Upgradeable diff --git a/packages/contracts/contracts/upgrades/IGraphProxy.sol b/packages/contracts/contracts/upgrades/IGraphProxy.sol deleted file mode 100644 index a3f2fdd8e..000000000 --- a/packages/contracts/contracts/upgrades/IGraphProxy.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6 || 0.8.27; - -/** - * @title Graph Proxy Interface - * @author Edge & Node - * @notice Interface for the Graph Proxy contract that handles upgradeable proxy functionality - */ -interface IGraphProxy { - /** - * @notice Get the current admin. - * - * @dev NOTE: Only the admin can call this function. - * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the - * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. - * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` - * - * @return adminAddress The address of the current admin - */ - function admin() external returns (address); - - /** - * @notice Change the admin of the proxy. - * - * @dev NOTE: Only the admin can call this function. - * - * @param _newAdmin Address of the new admin - */ - function setAdmin(address _newAdmin) external; - - /** - * @notice Get the current implementation. - * - * @dev NOTE: Only the admin can call this function. - * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the - * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. - * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` - * - * @return implementationAddress The address of the current implementation for this proxy - */ - function implementation() external returns (address); - - /** - * @notice Get the current pending implementation. - * - * @dev NOTE: Only the admin can call this function. - * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the - * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. - * `0x9e5eddc59e0b171f57125ab86bee043d9128098c3a6b9adb4f2e86333c2f6f8c` - * - * @return pendingImplementationAddress The address of the current pending implementation for this proxy - */ - function pendingImplementation() external returns (address); - - /** - * @notice Upgrades to a new implementation contract. - * @dev NOTE: Only the admin can call this function. - * @param _newImplementation Address of implementation contract - */ - function upgradeTo(address _newImplementation) external; - - /** - * @notice Admin function for new implementation to accept its role as implementation. - */ - function acceptUpgrade() external; - - /** - * @notice Admin function for new implementation to accept its role as implementation, - * calling a function on the new implementation. - * @param data Calldata (including selector) for the function to delegatecall into the implementation - */ - function acceptUpgradeAndCall(bytes calldata data) external; -} diff --git a/packages/contracts/contracts/utils/TokenUtils.sol b/packages/contracts/contracts/utils/TokenUtils.sol index 50ad277b4..b1c2290f6 100644 --- a/packages/contracts/contracts/utils/TokenUtils.sol +++ b/packages/contracts/contracts/utils/TokenUtils.sol @@ -4,7 +4,7 @@ pragma solidity ^0.7.6 || 0.8.27; /* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 -import { IGraphToken } from "../token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; /** * @title TokenUtils library diff --git a/packages/horizon/contracts/mocks/MockGRTToken.sol b/packages/horizon/contracts/mocks/MockGRTToken.sol index 385b3b4e2..3186aeb1c 100644 --- a/packages/horizon/contracts/mocks/MockGRTToken.sol +++ b/packages/horizon/contracts/mocks/MockGRTToken.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.27; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; /** * @title MockGRTToken diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol index f8268efca..144a2daa1 100644 --- a/packages/horizon/contracts/payments/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable function-max-lines -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; diff --git a/packages/horizon/contracts/payments/PaymentsEscrow.sol b/packages/horizon/contracts/payments/PaymentsEscrow.sol index c36b9bd73..50a5386c9 100644 --- a/packages/horizon/contracts/payments/PaymentsEscrow.sol +++ b/packages/horizon/contracts/payments/PaymentsEscrow.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IPaymentsEscrow } from "@graphprotocol/interfaces/contracts/horizon/IPaymentsEscrow.sol"; diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index 1c3e2e8af..73f48c354 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -7,7 +7,7 @@ pragma solidity 0.8.27; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +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"; diff --git a/packages/horizon/contracts/staking/HorizonStakingExtension.sol b/packages/horizon/contracts/staking/HorizonStakingExtension.sol index 867a01f23..b1adcde0d 100644 --- a/packages/horizon/contracts/staking/HorizonStakingExtension.sol +++ b/packages/horizon/contracts/staking/HorizonStakingExtension.sol @@ -6,7 +6,7 @@ pragma solidity 0.8.27; // solhint-disable function-max-lines, gas-strict-inequalities import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.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"; diff --git a/packages/horizon/contracts/utilities/GraphDirectory.sol b/packages/horizon/contracts/utilities/GraphDirectory.sol index 1071605d9..6e657c6d7 100644 --- a/packages/horizon/contracts/utilities/GraphDirectory.sol +++ b/packages/horizon/contracts/utilities/GraphDirectory.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.27; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IPaymentsEscrow } from "@graphprotocol/interfaces/contracts/horizon/IPaymentsEscrow.sol"; diff --git a/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol b/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol index f62576084..4a88bf0cd 100644 --- a/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol +++ b/packages/horizon/test/unit/utilities/GraphDirectoryImplementation.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.27; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IPaymentsEscrow } from "@graphprotocol/interfaces/contracts/horizon/IPaymentsEscrow.sol"; diff --git a/packages/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol b/packages/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol index 2a0d1a2f3..c1b582b29 100644 --- a/packages/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol +++ b/packages/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol @@ -23,7 +23,7 @@ * */ -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.3 || 0.8.27; /** * @title Token Gateway Interface diff --git a/packages/interfaces/contracts/contracts/discovery/IGNS.sol b/packages/interfaces/contracts/contracts/discovery/IGNS.sol index c502cd7af..219f21edd 100644 --- a/packages/interfaces/contracts/contracts/discovery/IGNS.sol +++ b/packages/interfaces/contracts/contracts/discovery/IGNS.sol @@ -43,20 +43,6 @@ interface IGNS { uint256 accountSeqID; } - // -- Events -- - - /** - * @notice Emitted when a subgraph version is updated. - * @param subgraphID The subgraph ID - * @param subgraphDeploymentID The subgraph deployment ID - * @param versionMetadata The version metadata - */ - event SubgraphVersionUpdated( - uint256 indexed subgraphID, - bytes32 indexed subgraphDeploymentID, - bytes32 versionMetadata - ); - // -- Configuration -- /** diff --git a/packages/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol b/packages/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol index f96672a49..3b9c361cc 100644 --- a/packages/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol +++ b/packages/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol @@ -7,7 +7,7 @@ * be allowlisted by the governor, but also implement this interface that contains * the function that will actually be called by the L2GraphTokenGateway. */ -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.3 || 0.8.27; /** * @title Callhook Receiver Interface diff --git a/packages/interfaces/contracts/contracts/l2/staking/IL2Staking.sol b/packages/interfaces/contracts/contracts/l2/staking/IL2Staking.sol index 4290e5842..4fa87e294 100644 --- a/packages/interfaces/contracts/contracts/l2/staking/IL2Staking.sol +++ b/packages/interfaces/contracts/contracts/l2/staking/IL2Staking.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || ^0.8.0; pragma abicoder v2; import { IStaking } from "../../staking/IStaking.sol"; diff --git a/packages/interfaces/contracts/contracts/staking/IStaking.sol b/packages/interfaces/contracts/contracts/staking/IStaking.sol index 9b9a6fc5c..c644009b6 100644 --- a/packages/interfaces/contracts/contracts/staking/IStaking.sol +++ b/packages/interfaces/contracts/contracts/staking/IStaking.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || ^0.8.0; pragma abicoder v2; import { IStakingBase } from "./IStakingBase.sol"; diff --git a/packages/interfaces/contracts/contracts/staking/IStakingBase.sol b/packages/interfaces/contracts/contracts/staking/IStakingBase.sol index 25d643c6f..4c9b62023 100644 --- a/packages/interfaces/contracts/contracts/staking/IStakingBase.sol +++ b/packages/interfaces/contracts/contracts/staking/IStakingBase.sol @@ -407,6 +407,7 @@ interface IStakingBase is IStakingData { /** * @notice Get the allocation data for the rewards manager + * @dev New function to get the allocation data for the rewards manager * @dev Note that this is only to make tests pass, as the staking contract with * this changes will never get deployed. HorizonStaking is taking it's place. * @param allocationID The allocation ID @@ -423,10 +424,11 @@ interface IStakingBase is IStakingData { /** * @notice Get the allocation active status for the rewards manager + * @dev New function to get the allocation active status for the rewards manager * @dev Note that this is only to make tests pass, as the staking contract with * this changes will never get deployed. HorizonStaking is taking it's place. - * @param allocationID The allocation ID - * @return Whether the allocation is active + * @param allocationID The allocation identifier + * @return True if the allocation is active, false otherwise */ function isActiveAllocation(address allocationID) external view returns (bool); diff --git a/packages/interfaces/contracts/toolshed/IL2GNSToolshed.sol b/packages/interfaces/contracts/toolshed/IL2GNSToolshed.sol index 573f79292..744227278 100644 --- a/packages/interfaces/contracts/toolshed/IL2GNSToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IL2GNSToolshed.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || 0.8.27; +pragma abicoder v2; // solhint-disable use-natspec diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index a581f1c2c..6f73b2c5d 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -4,7 +4,7 @@ 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 { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { IDisputeManager } from "@graphprotocol/interfaces/contracts/subgraph-service/IDisputeManager.sol"; import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-service/ISubgraphService.sol"; diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 37eb8f28e..0ba0b3035 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -6,7 +6,7 @@ pragma solidity 0.8.27; // solhint-disable function-max-lines import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IGraphTallyCollector } from "@graphprotocol/interfaces/contracts/horizon/IGraphTallyCollector.sol"; import { IRewardsIssuer } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol"; import { IDataService } from "@graphprotocol/interfaces/contracts/data-service/IDataService.sol"; diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 746c31f14..08608d8b4 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -7,7 +7,7 @@ pragma solidity 0.8.27; // solhint-disable function-max-lines import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; -import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; import { IAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocation.sol"; import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol"; diff --git a/packages/token-distribution/contracts/ICallhookReceiver.sol b/packages/token-distribution/contracts/ICallhookReceiver.sol deleted file mode 100644 index 8aab3a1e8..000000000 --- a/packages/token-distribution/contracts/ICallhookReceiver.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -// Copied from graphprotocol/contracts, changed solidity version to 0.7.3 - -/** - * @title Interface for contracts that can receive callhooks through the Arbitrum GRT bridge - * @dev Any contract that can receive a callhook on L2, sent through the bridge from L1, must - * be allowlisted by the governor, but also implement this interface that contains - * the function that will actually be called by the L2GraphTokenGateway. - */ -pragma solidity ^0.7.3; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable use-natspec - -interface ICallhookReceiver { - /** - * @notice Receive tokens with a callhook from the bridge - * @param _from Token sender in L1 - * @param _amount Amount of tokens that were transferred - * @param _data ABI-encoded callhook data - */ - function onTokenTransfer(address _from, uint256 _amount, bytes calldata _data) external; -} diff --git a/packages/token-distribution/contracts/L1GraphTokenLockTransferTool.sol b/packages/token-distribution/contracts/L1GraphTokenLockTransferTool.sol index 5e8bbf325..c6d7ffda8 100644 --- a/packages/token-distribution/contracts/L1GraphTokenLockTransferTool.sol +++ b/packages/token-distribution/contracts/L1GraphTokenLockTransferTool.sol @@ -8,7 +8,7 @@ pragma experimental ABIEncoderV2; // solhint-disable named-parameters-mapping import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import { ITokenGateway } from "./arbitrum/ITokenGateway.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { L2GraphTokenLockManager } from "./L2GraphTokenLockManager.sol"; import { GraphTokenLockWallet } from "./GraphTokenLockWallet.sol"; diff --git a/packages/token-distribution/contracts/L2GraphTokenLockManager.sol b/packages/token-distribution/contracts/L2GraphTokenLockManager.sol index f7609cb03..75a2da68c 100644 --- a/packages/token-distribution/contracts/L2GraphTokenLockManager.sol +++ b/packages/token-distribution/contracts/L2GraphTokenLockManager.sol @@ -10,7 +10,7 @@ pragma experimental ABIEncoderV2; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; -import { ICallhookReceiver } from "./ICallhookReceiver.sol"; +import { ICallhookReceiver } from "@graphprotocol/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol"; import { GraphTokenLockManager } from "./GraphTokenLockManager.sol"; import { L2GraphTokenLockWallet } from "./L2GraphTokenLockWallet.sol"; diff --git a/packages/token-distribution/contracts/L2GraphTokenLockTransferTool.sol b/packages/token-distribution/contracts/L2GraphTokenLockTransferTool.sol index 81f15c960..0ef769549 100644 --- a/packages/token-distribution/contracts/L2GraphTokenLockTransferTool.sol +++ b/packages/token-distribution/contracts/L2GraphTokenLockTransferTool.sol @@ -10,7 +10,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { L2GraphTokenLockManager } from "./L2GraphTokenLockManager.sol"; import { L2GraphTokenLockWallet } from "./L2GraphTokenLockWallet.sol"; -import { ITokenGateway } from "./arbitrum/ITokenGateway.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; /** * @title L2GraphTokenLockTransferTool contract diff --git a/packages/token-distribution/contracts/arbitrum/ITokenGateway.sol b/packages/token-distribution/contracts/arbitrum/ITokenGateway.sol deleted file mode 100644 index 06ec7be0d..000000000 --- a/packages/token-distribution/contracts/arbitrum/ITokenGateway.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -/* - * Copyright 2020, Offchain Labs, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Originally copied from: - * https://github.com/OffchainLabs/arbitrum/tree/e3a6307ad8a2dc2cad35728a2a9908cfd8dd8ef9/packages/arb-bridge-peripherals - * - * MODIFIED from Offchain Labs' implementation: - * - Changed solidity version to 0.7.6 (pablo@edgeandnode.com) - * - */ - -pragma solidity ^0.7.3; -pragma experimental ABIEncoderV2; - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable use-natspec - -interface ITokenGateway { - /// @notice event deprecated in favor of DepositInitiated and WithdrawalInitiated - // event OutboundTransferInitiated( - // address token, - // address indexed _from, - // address indexed _to, - // uint256 indexed _transferId, - // uint256 _amount, - // bytes _data - // ); - - /// @notice event deprecated in favor of DepositFinalized and WithdrawalFinalized - // event InboundTransferFinalized( - // address token, - // address indexed _from, - // address indexed _to, - // uint256 indexed _transferId, - // uint256 _amount, - // bytes _data - // ); - - function outboundTransfer( - address _token, - address _to, - uint256 _amount, - uint256 _maxGas, - uint256 _gasPriceBid, - bytes calldata _data - ) external payable returns (bytes memory); - - function finalizeInboundTransfer( - address _token, - address _from, - address _to, - uint256 _amount, - bytes calldata _data - ) external payable; - - /** - * @notice Calculate the address used when bridging an ERC20 token - * @dev the L1 and L2 address oracles may not always be in sync. - * For example, a custom token may have been registered but not deployed or the contract self destructed. - * @param l1ERC20 address of L1 token - * @return L2 address of a bridged ERC20 token - */ - function calculateL2TokenAddress(address l1ERC20) external view returns (address); -} diff --git a/packages/token-distribution/contracts/tests/L1TokenGatewayMock.sol b/packages/token-distribution/contracts/tests/L1TokenGatewayMock.sol index 8e2fbd11a..fdbab7bcb 100644 --- a/packages/token-distribution/contracts/tests/L1TokenGatewayMock.sol +++ b/packages/token-distribution/contracts/tests/L1TokenGatewayMock.sol @@ -7,7 +7,7 @@ pragma solidity ^0.7.3; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import { ITokenGateway } from "../arbitrum//ITokenGateway.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; /** * @title L1 Token Gateway mock contract diff --git a/packages/token-distribution/contracts/tests/L2TokenGatewayMock.sol b/packages/token-distribution/contracts/tests/L2TokenGatewayMock.sol index c7794d2b5..02a5383e4 100644 --- a/packages/token-distribution/contracts/tests/L2TokenGatewayMock.sol +++ b/packages/token-distribution/contracts/tests/L2TokenGatewayMock.sol @@ -5,9 +5,9 @@ pragma solidity ^0.7.3; // solhint-disable gas-increment-by-one, gas-indexed-events, use-natspec import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { ITokenGateway } from "../arbitrum//ITokenGateway.sol"; +import { ITokenGateway } from "@graphprotocol/interfaces/contracts/contracts/arbitrum/ITokenGateway.sol"; import { GraphTokenMock } from "./GraphTokenMock.sol"; -import { ICallhookReceiver } from "../ICallhookReceiver.sol"; +import { ICallhookReceiver } from "@graphprotocol/interfaces/contracts/contracts/gateway/ICallhookReceiver.sol"; /** * @title L2 Token Gateway mock contract diff --git a/packages/token-distribution/package.json b/packages/token-distribution/package.json index f02699c4b..21019d6e6 100644 --- a/packages/token-distribution/package.json +++ b/packages/token-distribution/package.json @@ -46,6 +46,7 @@ "@ethersproject/providers": "^5.7.0", "@graphprotocol/client-cli": "^2.2.22", "@graphprotocol/contracts": "workspace:^", + "@graphprotocol/interfaces": "workspace:^", "@graphql-yoga/plugin-persisted-operations": "^3.13.5", "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-etherscan": "^3.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99a5dc71c..1da271388 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1109,6 +1109,9 @@ importers: '@graphprotocol/contracts': specifier: workspace:^ version: link:../contracts + '@graphprotocol/interfaces': + specifier: workspace:^ + version: link:../interfaces '@graphql-yoga/plugin-persisted-operations': specifier: ^3.13.5 version: 3.15.2(graphql-yoga@5.15.2(graphql@16.11.0))(graphql@16.11.0) From 4a1c24a8f892b31fef6842163d9b94a8269b9574 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Mon, 9 Mar 2026 09:50:36 -0300 Subject: [PATCH 07/14] fix: security advisory revocable vesting contracts linting errors --- .../token-distribution/contracts/GraphTokenLockWallet.sol | 2 +- packages/token-distribution/test/tokenLockWallet.test.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/token-distribution/contracts/GraphTokenLockWallet.sol b/packages/token-distribution/contracts/GraphTokenLockWallet.sol index 3c240396e..724031edf 100644 --- a/packages/token-distribution/contracts/GraphTokenLockWallet.sol +++ b/packages/token-distribution/contracts/GraphTokenLockWallet.sol @@ -9,7 +9,7 @@ pragma experimental ABIEncoderV2; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import { GraphTokenLock, MathUtils } from "./GraphTokenLock.sol"; +import { GraphTokenLock } from "./GraphTokenLock.sol"; import { IGraphTokenLock } from "./IGraphTokenLock.sol"; import { IGraphTokenLockManager } from "./IGraphTokenLockManager.sol"; diff --git a/packages/token-distribution/test/tokenLockWallet.test.ts b/packages/token-distribution/test/tokenLockWallet.test.ts index 362aabfe5..00146a03f 100644 --- a/packages/token-distribution/test/tokenLockWallet.test.ts +++ b/packages/token-distribution/test/tokenLockWallet.test.ts @@ -9,16 +9,12 @@ import { DeployOptions } from 'hardhat-deploy/types' import { GraphTokenLockManager, GraphTokenLockWallet, GraphTokenMock, StakingMock } from '../types' import { defaultInitArgs, Revocability, TokenLockParameters } from './config' -import { Account, advanceBlocks, advanceTimeAndBlock, getAccounts, getContract, randomHexBytes, toGRT } from './network' +import { Account, advanceTimeAndBlock, getAccounts, getContract, randomHexBytes, toGRT } from './network' const { AddressZero, MaxUint256 } = constants // -- Time utils -- -const advancePeriods = async (tokenLock: GraphTokenLockWallet, n = 1) => { - const periodDuration = await tokenLock.periodDuration() - return advanceTimeAndBlock(periodDuration.mul(n).toNumber()) // advance N period -} const advanceToStart = async (tokenLock: GraphTokenLockWallet) => moveToTime(tokenLock, await tokenLock.startTime(), 60) const moveToTime = async (tokenLock: GraphTokenLockWallet, target: BigNumber, buffer: number) => { const ts = await tokenLock.currentTime() From 4a6d052d4ed382ee5aa52e398f5d0f95cae65e47 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:29:59 +0000 Subject: [PATCH 08/14] feat: non-Solidy changes --- .dockerignore | 40 + .github/actions/setup/action.yml | 8 +- .github/workflows/build-test.yml | 9 +- .github/workflows/lint.yml | 370 +- .github/workflows/publish-image.yml | 148 + .github/workflows/publish.yml | 51 +- .github/workflows/require-audit-label.yml | 56 + .github/workflows/verifydeployed.yml | 8 +- .gitignore | 36 +- .markdownlint.json | 3 +- .npmrc | 1 + .nvmrc | 1 + DEPLOYMENT.md | 218 + Dockerfile | 27 + README.md | 176 +- docker-compose.yml | 13 + docs/ForgeLintSymlinkIssue.md | 150 + docs/IGraphProxyAdminInterfaceFix.md | 201 + docs/Linting.md | 210 + docs/PaymentsTrustModel.md | 176 + docs/RewardAccountingSafety.md | 177 + docs/RewardConditions.md | 249 + docs/RewardsBehaviourChanges.md | 175 + docs/archive/CompilerUpgrade0833.md | 151 + eslint.config.mjs | 2 +- justfile | 8 + package.json | 34 +- packages/address-book/CHANGELOG.md | 12 + packages/address-book/docs/PublishingGuide.md | 108 + packages/address-book/package.json | 10 +- .../scripts/copy-addresses-for-publish.js | 65 +- .../address-book/scripts/restore-symlinks.js | 48 +- packages/address-book/scripts/sources.js | 4 + .../address-book/src/issuance/addresses.json | 1 + .../test => contracts-test}/.solcover.js | 2 +- .../test => contracts-test}/CHANGELOG.md | 0 .../config/graph.arbitrum-goerli.yml | 0 .../config/graph.arbitrum-hardhat.yml | 0 .../config/graph.arbitrum-localhost.yml | 0 .../config/graph.arbitrum-one.yml | 0 .../config/graph.arbitrum-sepolia.yml | 0 .../config/graph.goerli.yml | 0 .../config/graph.hardhat.yml | 0 .../config/graph.localhost.yml | 0 .../config/graph.mainnet.yml | 0 .../config/graph.sepolia.yml | 0 packages/contracts-test/contracts | 1 + .../test => contracts-test}/hardhat.config.ts | 19 +- .../test => contracts-test}/package.json | 1 + packages/contracts-test/prettier.config.cjs | 5 + .../test => contracts-test}/scripts/coverage | 0 .../test => contracts-test}/scripts/e2e | 0 .../test => contracts-test}/scripts/evm | 0 .../scripts/setup-symlinks | 4 +- .../test => contracts-test}/scripts/test | 6 +- .../scripts/test-coverage-file | 0 .../tasks/migrate/nitro.ts | 2 +- .../tasks/test-upgrade.ts | 0 .../tests/unit/curation/configuration.test.ts | 0 .../tests/unit/curation/curation.test.ts | 0 .../tests/unit/disputes/common.ts | 0 .../tests/unit/disputes/configuration.test.ts | 0 .../tests/unit/disputes/poi.test.ts | 8 +- .../tests/unit/disputes/query.test.ts | 8 +- .../tests/unit/epochs.test.ts | 0 .../tests/unit/gateway/bridgeEscrow.test.ts | 0 .../unit/gateway/l1GraphTokenGateway.test.ts | 0 .../tests/unit/gns.test.ts | 0 .../tests/unit/governance/controller.test.ts | 0 .../tests/unit/governance/governed.test.ts | 0 .../tests/unit/governance/pausing.test.ts | 0 .../tests/unit/graphToken.test.ts | 0 .../tests/unit/l2/l2ArbitrumMessengerMock.ts | 0 .../tests/unit/l2/l2Curation.test.ts | 27 +- .../tests/unit/l2/l2GNS.test.ts | 106 +- .../tests/unit/l2/l2GraphToken.test.ts | 0 .../tests/unit/l2/l2GraphTokenGateway.test.ts | 0 .../tests/unit/l2/l2Staking.test.ts | 8 +- .../tests/unit/lib/fixtures.ts | 2 +- .../tests/unit/lib/gnsUtils.ts | 0 .../tests/unit/lib/graphTokenTests.ts | 0 .../unit/payments/allocationExchange.test.ts | 0 .../unit/rewards/rewards-calculations.test.ts | 425 + .../tests/unit/rewards/rewards-config.test.ts | 320 + .../unit/rewards/rewards-distribution.test.ts | 745 ++ .../rewards-eligibility-oracle.test.ts | 700 ++ .../unit/rewards/rewards-interface.test.ts | 144 + .../rewards-issuance-allocator.test.ts | 420 + .../unit/rewards/rewards-reclaim.test.ts | 1115 ++ .../rewards-signal-allocation-update.test.ts | 564 + .../rewards-snapshot-inversion.test.ts | 444 + .../rewards/rewards-subgraph-service.test.ts | 483 + .../tests/unit/rewards/rewards.test.ts | 409 +- .../unit/rewards/subgraphAvailability.test.ts | 2 +- .../tests/unit/serviceRegisty.test.ts | 0 .../tests/unit/staking/allocation.test.ts | 4 + .../tests/unit/staking/configuration.test.ts | 0 .../tests/unit/staking/delegation.test.ts | 8 +- .../tests/unit/staking/l2Transfer.test.ts | 0 .../tests/unit/staking/rebate.test.ts | 0 .../tests/unit/staking/staking.test.ts | 0 .../tests/unit/upgrade/admin.test.ts | 0 .../test => contracts-test}/tsconfig.json | 0 .../test => contracts-test}/utils/coverage.ts | 0 packages/contracts/.solhint.json | 2 +- packages/contracts/hardhat.config.ts | 12 +- packages/contracts/package.json | 14 +- packages/contracts/task/hardhat.config.ts | 10 - packages/contracts/task/package.json | 2 +- .../contracts/task/tasks/bridge/deposits.ts | 3 - .../task/tasks/bridge/withdrawals.ts | 3 - .../contracts/task/tasks/verify/verify.ts | 3 - packages/contracts/test/contracts | 1 - packages/contracts/test/prettier.config.cjs | 5 - packages/data-edge/.solhint.json | 2 +- packages/data-edge/hardhat.config.ts | 43 +- packages/data-edge/package.json | 45 +- packages/data-edge/tasks/craft-calldata.ts | 6 +- packages/data-edge/tasks/deploy.ts | 22 +- packages/data-edge/tasks/post-calldata.ts | 18 +- packages/data-edge/test/dataedge.test.ts | 40 +- .../data-edge/test/eventful-dataedge.test.ts | 50 +- packages/deployment/.gitignore | 4 + packages/deployment/.markdownlint.json | 3 + packages/deployment/CLAUDE.md | 25 + packages/deployment/README.md | 84 + packages/deployment/config/arbitrumOne.json5 | 12 + .../deployment/config/arbitrumSepolia.json5 | 12 + packages/deployment/config/localNetwork.json5 | 11 + .../deploy/agreement/manager/01_deploy.ts | 16 + .../deploy/agreement/manager/02_upgrade.ts | 4 + .../deploy/agreement/manager/04_configure.ts | 225 + .../manager/05_transfer_governance.ts | 60 + .../deploy/agreement/manager/09_end.ts | 4 + .../deploy/agreement/manager/10_status.ts | 4 + .../deploy/allocate/allocator/01_deploy.ts | 12 + .../deploy/allocate/allocator/02_upgrade.ts | 4 + .../deploy/allocate/allocator/04_configure.ts | 168 + .../allocator/06_transfer_governance.ts | 61 + .../deploy/allocate/allocator/09_end.ts | 4 + .../deploy/allocate/allocator/10_status.ts | 4 + .../deploy/allocate/default/01_deploy.ts | 39 + .../deploy/allocate/default/02_upgrade.ts | 27 + .../deploy/allocate/default/04_configure.ts | 119 + .../default/05_transfer_governance.ts | 51 + .../deploy/allocate/default/09_end.ts | 4 + .../deploy/allocate/default/10_status.ts | 10 + .../deploy/allocate/direct/01_impl.ts | 74 + packages/deployment/deploy/common/00_sync.ts | 26 + packages/deployment/deploy/gip/0088/09_end.ts | 114 + .../deployment/deploy/gip/0088/10_status.ts | 179 + .../deploy/gip/0088/eligibility_integrate.ts | 74 + .../deploy/gip/0088/issuance_allocate.ts | 193 + .../deploy/gip/0088/issuance_close_guard.ts | 81 + .../deploy/gip/0088/issuance_connect.ts | 247 + .../deploy/gip/0088/upgrade/01_deploy.ts | 47 + .../deploy/gip/0088/upgrade/02_configure.ts | 40 + .../deploy/gip/0088/upgrade/03_transfer.ts | 39 + .../deploy/gip/0088/upgrade/04_upgrade.ts | 447 + .../deploy/gip/0088/upgrade/10_status.ts | 331 + .../deploy/horizon/curation/01_deploy.ts | 4 + .../deploy/horizon/curation/02_upgrade.ts | 4 + .../deploy/horizon/curation/09_end.ts | 4 + .../deploy/horizon/curation/10_status.ts | 4 + .../horizon/payments-escrow/01_deploy.ts | 58 + .../horizon/payments-escrow/02_upgrade.ts | 4 + .../deploy/horizon/payments-escrow/09_end.ts | 4 + .../horizon/payments-escrow/10_status.ts | 4 + .../horizon/recurring-collector/01_deploy.ts | 48 + .../horizon/recurring-collector/02_upgrade.ts | 4 + .../recurring-collector/04_configure.ts | 62 + .../05_transfer_governance.ts | 69 + .../horizon/recurring-collector/09_end.ts | 4 + .../horizon/recurring-collector/10_status.ts | 4 + .../deploy/horizon/staking/01_deploy.ts | 15 + .../deploy/horizon/staking/02_upgrade.ts | 4 + .../deploy/horizon/staking/09_end.ts | 4 + .../deploy/horizon/staking/10_status.ts | 4 + .../deploy/rewards/eligibility/a/01_deploy.ts | 12 + .../rewards/eligibility/a/02_upgrade.ts | 4 + .../rewards/eligibility/a/04_configure.ts | 39 + .../eligibility/a/05_transfer_governance.ts | 45 + .../deploy/rewards/eligibility/a/09_end.ts | 4 + .../deploy/rewards/eligibility/a/10_status.ts | 4 + .../deploy/rewards/eligibility/b/01_deploy.ts | 12 + .../rewards/eligibility/b/02_upgrade.ts | 4 + .../rewards/eligibility/b/04_configure.ts | 39 + .../eligibility/b/05_transfer_governance.ts | 45 + .../deploy/rewards/eligibility/b/09_end.ts | 4 + .../deploy/rewards/eligibility/b/10_status.ts | 4 + .../rewards/eligibility/mock/01_deploy.ts | 12 + .../rewards/eligibility/mock/02_upgrade.ts | 4 + .../mock/05_transfer_governance.ts | 39 + .../rewards/eligibility/mock/06_integrate.ts | 39 + .../deploy/rewards/eligibility/mock/09_end.ts | 4 + .../rewards/eligibility/mock/10_status.ts | 4 + .../deploy/rewards/manager/01_deploy.ts | 4 + .../deploy/rewards/manager/02_upgrade.ts | 4 + .../deploy/rewards/manager/09_end.ts | 4 + .../deploy/rewards/manager/10_status.ts | 4 + .../deploy/rewards/reclaim/01_deploy.ts | 45 + .../deploy/rewards/reclaim/02_upgrade.ts | 36 + .../deploy/rewards/reclaim/04_configure.ts | 144 + .../rewards/reclaim/05_transfer_governance.ts | 56 + .../deploy/rewards/reclaim/09_end.ts | 4 + .../deploy/rewards/reclaim/10_status.ts | 14 + .../deploy/service/dispute/01_deploy.ts | 12 + .../deploy/service/dispute/02_upgrade.ts | 4 + .../deploy/service/dispute/09_end.ts | 4 + .../deploy/service/dispute/10_status.ts | 4 + .../deploy/service/subgraph/01_deploy.ts | 146 + .../deploy/service/subgraph/02_upgrade.ts | 4 + .../deploy/service/subgraph/04_configure.ts | 22 + .../deploy/service/subgraph/09_end.ts | 4 + .../deploy/service/subgraph/10_status.ts | 4 + packages/deployment/docs/Architecture.md | 61 + packages/deployment/docs/DeploymentSetup.md | 224 + packages/deployment/docs/Design.md | 244 + packages/deployment/docs/Gip0088.md | 241 + .../deployment/docs/GovernanceWorkflow.md | 373 + packages/deployment/docs/LocalForkTesting.md | 170 + .../docs/SyncBytecodeDetectionFix.md | 149 + packages/deployment/docs/SyncSpecification.md | 285 + .../docs/address-book/LayerAnalysis.md | 47 + .../deployment/docs/address-book/README.md | 58 + .../docs/deploy/ImplementationPrinciples.md | 610 + .../deploy/IssuanceAllocatorDeployment.md | 82 + .../RewardsEligibilityOracleDeployment.md | 105 + .../docs/plans/AddressBookEnhancement.md | 448 + packages/deployment/hardhat.config.ts | 321 + packages/deployment/lib/abis.ts | 150 + packages/deployment/lib/address-book-ops.ts | 554 + packages/deployment/lib/address-book-utils.ts | 461 + .../deployment/lib/apply-configuration.ts | 164 + packages/deployment/lib/artifact-loaders.ts | 215 + packages/deployment/lib/bytecode-utils.ts | 149 + packages/deployment/lib/contract-checks.ts | 969 ++ packages/deployment/lib/contract-registry.ts | 437 + packages/deployment/lib/controller-utils.ts | 90 + .../deployment/lib/deploy-implementation.ts | 437 + packages/deployment/lib/deploy-standalone.ts | 71 + packages/deployment/lib/deployment-config.ts | 131 + .../deployment/lib/deployment-metadata.ts | 59 + packages/deployment/lib/deployment-tags.ts | 150 + .../deployment/lib/deployment-validation.ts | 312 + packages/deployment/lib/execute-governance.ts | 494 + packages/deployment/lib/format.ts | 10 + .../deployment/lib/issuance-deploy-utils.ts | 646 + packages/deployment/lib/keystore-utils.ts | 49 + packages/deployment/lib/oz-proxy-verify.ts | 247 + packages/deployment/lib/preconditions.ts | 380 + packages/deployment/lib/script-factories.ts | 384 + packages/deployment/lib/status-detail.ts | 1135 ++ packages/deployment/lib/sync-utils.ts | 1441 +++ packages/deployment/lib/task-utils.ts | 134 + .../deployment/lib/tx-builder-template.json | 14 + packages/deployment/lib/tx-builder.ts | 181 + packages/deployment/lib/tx-executor.ts | 134 + .../deployment/lib/upgrade-implementation.ts | 319 + packages/deployment/package.json | 82 + packages/deployment/prettier.config.cjs | 5 + packages/deployment/rocketh/config.ts | 92 + packages/deployment/rocketh/deploy.ts | 168 + packages/deployment/scripts/check-bytecode.ts | 54 + .../scripts/check-rocketh-bytecode.ts | 34 + .../deployment/scripts/debug-deploy-state.ts | 27 + packages/deployment/scripts/generate-abis.ts | 264 + packages/deployment/scripts/tag-deployment.sh | 337 + packages/deployment/tasks/check-deployer.ts | 80 + .../deployment/tasks/deployment-status.ts | 423 + packages/deployment/tasks/eth-tasks.ts | 208 + .../deployment/tasks/execute-governance.ts | 90 + packages/deployment/tasks/grant-role.ts | 239 + packages/deployment/tasks/grt-tasks.ts | 449 + .../tasks/list-pending-implementations.ts | 141 + packages/deployment/tasks/list-roles.ts | 171 + packages/deployment/tasks/reo-tasks.ts | 597 + packages/deployment/tasks/reset-fork.ts | 67 + packages/deployment/tasks/revoke-role.ts | 239 + packages/deployment/tasks/ss-tasks.ts | 306 + packages/deployment/tasks/sync.ts | 37 + packages/deployment/tasks/verify-contract.ts | 662 + .../test/bytecode-comparison.test.ts | 242 + .../test/chain-id-resolution.test.ts | 354 + .../test/config-reconciliation.test.ts | 231 + .../test/contract-registry-mapping.test.ts | 177 + .../test/deployment-metadata.test.ts | 410 + .../test/interface-id-stability.test.ts | 34 + .../test/should-seed-rocketh.test.ts | 126 + packages/deployment/test/tx-builder.test.ts | 172 + packages/deployment/tsconfig.json | 10 + packages/deployment/types/rocketh.d.ts | 24 + packages/hardhat-graph-protocol/src/config.ts | 2 +- packages/horizon/.solhint.json | 2 +- packages/horizon/CHANGELOG.md | 12 + packages/horizon/addresses.json | 73 +- .../audits/2025-06-Indexing-Payments.pdf | Bin 0 -> 622268 bytes .../collectors/MaxSecondsPerCollectionCap.md | 68 + .../collectors/RecurringCollector.todo.md | 23 + packages/horizon/foundry.toml | 27 +- packages/horizon/hardhat.config.ts | 19 - .../configs/migrate.arbitrumOne.json5 | 71 +- .../configs/migrate.arbitrumSepolia.json5 | 73 +- .../ignition/configs/migrate.default.json5 | 71 +- .../configs/migrate.integration.json5 | 71 +- .../configs/migrate.localNetwork.json5 | 71 +- .../ignition/configs/protocol.default.json5 | 54 +- .../configs/protocol.localNetwork.json5 | 54 +- .../ignition/modules/core/GraphPayments.ts | 2 +- .../ignition/modules/core/HorizonStaking.ts | 41 +- .../ignition/modules/core/PaymentsEscrow.ts | 2 +- .../modules/core/RecurringCollector.ts | 54 + .../horizon/ignition/modules/core/core.ts | 8 +- packages/horizon/ignition/modules/deploy.ts | 10 + .../ignition/modules/periphery/Controller.ts | 2 +- .../horizon/ignition/modules/periphery/GNS.ts | 2 +- .../modules/periphery/GraphProxyAdmin.ts | 2 +- .../ignition/modules/periphery/GraphToken.ts | 4 + .../proxy/TransparentUpgradeableProxy.ts | 6 +- .../horizon/ignition/modules/proxy/utils.ts | 5 +- packages/horizon/package.json | 12 +- packages/horizon/remappings.txt | 9 +- packages/horizon/scripts/integration | 6 - .../verify-debug/decode-creation-args.ts | 23 +- .../read-immutables-from-event.ts | 2 +- packages/horizon/tasks/tenderly.ts | 28 + packages/horizon/tasks/test/integration.ts | 11 +- .../tasks/transitions/thawing-period.ts | 22 - packages/horizon/tenderly.config.json | 24 + .../test/deployment/HorizonStaking.test.ts | 12 +- .../RecurringCollectorCallbackGas.test.ts | 172 + .../delegator.test.ts | 143 - .../multicall.test.ts | 114 - .../during-transition-period/operator.test.ts | 99 - .../permissionless.test.ts | 66 - .../service-provider.test.ts | 521 - .../during-transition-period/slasher.test.ts | 88 - .../horizon/types/hardhat-graph-protocol.d.ts | 1 + packages/interfaces/.solhint.json | 2 +- packages/interfaces/CHANGELOG.md | 12 + packages/interfaces/README.md | 26 +- .../internal/IHorizonStakingTypes.todo.md | 21 + packages/interfaces/hardhat.config.ts | 2 +- packages/interfaces/package.json | 13 +- packages/interfaces/src/index.ts | 1 + packages/interfaces/src/types/horizon.ts | 8 +- packages/interfaces/src/types/issuance.ts | 29 + .../interfaces/src/types/subgraph-service.ts | 2 +- packages/issuance/.markdownlint.json | 3 + packages/issuance/.solhint.json | 3 + packages/issuance/README.md | 63 + packages/issuance/addresses.json | 172 + .../2025-11-17_Graph_PR1242_1243_v02.pdf | Bin 0 -> 392349 bytes ...2025-12-13_Graph_EligibilityOracle_v02.pdf | Bin 0 -> 507237 bytes ...2025-12-28_Graph_IssuanceAllocator_v03.pdf | Bin 0 -> 570295 bytes .../audits/2026-02-15_Graph_PR1279_v02.pdf | Bin 0 -> 516878 bytes .../audits/2026-05-09_Graph_PR1334_v05.pdf | Bin 0 -> 676182 bytes .../audits/2026-06-05_Graph_PR1342_v06.pdf | Bin 0 -> 666683 bytes .../agreement/RecurringAgreementManager.md | 175 + .../contracts/allocate/IssuanceAllocator.md | 252 + .../allocate/IssuanceAllocator.todo.md | 14 + .../contracts/common/BaseUpgradeable.todo.md | 11 + .../eligibility/RewardsEligibilityOracle.md | 378 + .../docs/testing/reo/BaselineTestPlan.md | 811 ++ .../docs/testing/reo/IndexerTestGuide.md | 542 + .../docs/testing/reo/MainnetDetails.md | 38 + packages/issuance/docs/testing/reo/README.md | 156 + .../issuance/docs/testing/reo/ReoTestPlan.md | 1103 ++ .../testing/reo/RewardsConditionsTestPlan.md | 781 ++ .../testing/reo/SubgraphDenialTestPlan.md | 680 + .../docs/testing/reo/TestnetDetails.md | 65 + .../reo/support/IssuanceAllocatorTestPlan.md | 98 + .../docs/testing/reo/support/NotionSetup.md | 70 + .../testing/reo/support/NotionTracker.csv | 77 + .../testing/reo/support/indexer-status.sh | 75 + packages/issuance/foundry.toml | 36 + packages/issuance/hardhat.base.config.ts | 90 + packages/issuance/hardhat.config.ts | 26 + packages/issuance/package.json | 82 + packages/issuance/prettier.config.cjs | 5 + packages/issuance/remappings.txt | 2 + packages/issuance/tsconfig.json | 9 + packages/issuance/tsconfig.typechain.json | 14 + packages/subgraph-service/.solhint.json | 2 +- packages/subgraph-service/CHANGELOG.md | 12 + packages/subgraph-service/addresses.json | 26 +- .../libraries/AllocationHandler.todo.md | 14 + packages/subgraph-service/foundry.toml | 25 +- packages/subgraph-service/hardhat.config.ts | 10 +- .../configs/migrate.arbitrumOne.json5 | 58 +- .../configs/migrate.arbitrumSepolia.json5 | 58 +- .../ignition/configs/migrate.default.json5 | 58 +- .../configs/migrate.integration.json5 | 58 +- .../configs/migrate.localNetwork.json5 | 58 +- .../ignition/configs/protocol.default.json5 | 58 +- .../configs/protocol.localNetwork.json5 | 58 +- .../ignition/modules/SubgraphService.ts | 37 +- packages/subgraph-service/package.json | 12 +- packages/subgraph-service/remappings.txt | 10 +- packages/subgraph-service/scripts/integration | 7 - packages/subgraph-service/tasks/deploy.ts | 1 + packages/subgraph-service/tasks/tenderly.ts | 28 + .../tasks/test/integration.ts | 11 +- .../subgraph-service/tenderly.config.json | 14 + .../subgraph-service/indexer.test.ts | 58 +- .../subgraph-service/permisionless.test.ts | 6 +- .../dispute-manager.test.ts | 157 - .../governance.test.ts | 76 - .../during-transition-period/indexer.test.ts | 100 - .../legacy-dispute-manager.test.ts | 256 - .../types/hardhat-graph-protocol.d.ts | 1 + packages/testing/foundry.toml | 41 + packages/testing/package.json | 24 + .../.graphclient-extracted/index.d.ts | 1126 +- .../.graphclient-extracted/index.js | 156 +- packages/token-distribution/.solhint.json | 2 +- packages/token-distribution/CHANGELOG.md | 12 + packages/token-distribution/README.md | 26 +- packages/token-distribution/hardhat.config.ts | 10 - packages/token-distribution/package.json | 8 +- packages/token-distribution/scripts/build.js | 30 +- .../scripts/extract-graphclient.js | 64 +- packages/toolshed/CHANGELOG.md | 31 + packages/toolshed/package.json | 8 +- packages/toolshed/src/core/custom-errors.ts | 31 + packages/toolshed/src/core/index.ts | 2 + .../toolshed/src/core/recurring-collector.ts | 123 + .../toolshed/src/core/subgraph-service.ts | 15 + .../toolshed/src/deployments/address-book.ts | 60 +- .../src/deployments/horizon/actions.ts | 21 - .../src/deployments/horizon/contracts.ts | 3 + .../toolshed/src/deployments/horizon/index.ts | 8 +- packages/toolshed/src/deployments/index.ts | 4 +- .../src/deployments/issuance/address-book.ts | 34 + .../src/deployments/issuance/contracts.ts | 35 + .../src/deployments/issuance/index.ts | 32 + .../src/deployments/subgraph-service/index.ts | 8 +- .../src/hardhat/hardhat.base.config.ts | 22 +- packages/toolshed/src/hardhat/index.ts | 19 +- packages/toolshed/src/hardhat/tenderly.ts | 597 + packages/toolshed/tsconfig.json | 3 +- patches/rocketh@0.17.13.patch | 33 + pnpm-lock.yaml | 10217 +++++----------- pnpm-workspace.yaml | 33 +- scripts/lint-staged-run.sh | 13 +- 445 files changed, 44385 insertions(+), 10960 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/publish-image.yml create mode 100644 .github/workflows/require-audit-label.yml create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 DEPLOYMENT.md create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 docs/ForgeLintSymlinkIssue.md create mode 100644 docs/IGraphProxyAdminInterfaceFix.md create mode 100644 docs/Linting.md create mode 100644 docs/PaymentsTrustModel.md create mode 100644 docs/RewardAccountingSafety.md create mode 100644 docs/RewardConditions.md create mode 100644 docs/RewardsBehaviourChanges.md create mode 100644 docs/archive/CompilerUpgrade0833.md create mode 100644 justfile create mode 100644 packages/address-book/docs/PublishingGuide.md create mode 100644 packages/address-book/scripts/sources.js create mode 120000 packages/address-book/src/issuance/addresses.json rename packages/{contracts/test => contracts-test}/.solcover.js (96%) rename packages/{contracts/test => contracts-test}/CHANGELOG.md (100%) rename packages/{contracts/test => contracts-test}/config/graph.arbitrum-goerli.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.arbitrum-hardhat.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.arbitrum-localhost.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.arbitrum-one.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.arbitrum-sepolia.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.goerli.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.hardhat.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.localhost.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.mainnet.yml (100%) rename packages/{contracts/test => contracts-test}/config/graph.sepolia.yml (100%) create mode 120000 packages/contracts-test/contracts rename packages/{contracts/test => contracts-test}/hardhat.config.ts (75%) rename packages/{contracts/test => contracts-test}/package.json (98%) create mode 100644 packages/contracts-test/prettier.config.cjs rename packages/{contracts/test => contracts-test}/scripts/coverage (100%) rename packages/{contracts/test => contracts-test}/scripts/e2e (100%) rename packages/{contracts/test => contracts-test}/scripts/evm (100%) rename packages/{contracts/test => contracts-test}/scripts/setup-symlinks (89%) rename packages/{contracts/test => contracts-test}/scripts/test (72%) rename packages/{contracts/test => contracts-test}/scripts/test-coverage-file (100%) rename packages/{contracts/test => contracts-test}/tasks/migrate/nitro.ts (98%) rename packages/{contracts/test => contracts-test}/tasks/test-upgrade.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/curation/configuration.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/curation/curation.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/disputes/common.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/disputes/configuration.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/disputes/poi.test.ts (95%) rename packages/{contracts/test => contracts-test}/tests/unit/disputes/query.test.ts (98%) rename packages/{contracts/test => contracts-test}/tests/unit/epochs.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/gateway/bridgeEscrow.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/gateway/l1GraphTokenGateway.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/gns.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/governance/controller.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/governance/governed.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/governance/pausing.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/graphToken.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/l2/l2ArbitrumMessengerMock.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/l2/l2Curation.test.ts (97%) rename packages/{contracts/test => contracts-test}/tests/unit/l2/l2GNS.test.ts (85%) rename packages/{contracts/test => contracts-test}/tests/unit/l2/l2GraphToken.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/l2/l2GraphTokenGateway.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/l2/l2Staking.test.ts (97%) rename packages/{contracts/test => contracts-test}/tests/unit/lib/fixtures.ts (99%) rename packages/{contracts/test => contracts-test}/tests/unit/lib/gnsUtils.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/lib/graphTokenTests.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/payments/allocationExchange.test.ts (100%) create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-config.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-distribution.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-snapshot-inversion.test.ts create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts rename packages/{contracts/test => contracts-test}/tests/unit/rewards/rewards.test.ts (72%) rename packages/{contracts/test => contracts-test}/tests/unit/rewards/subgraphAvailability.test.ts (99%) rename packages/{contracts/test => contracts-test}/tests/unit/serviceRegisty.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/staking/allocation.test.ts (99%) rename packages/{contracts/test => contracts-test}/tests/unit/staking/configuration.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/staking/delegation.test.ts (98%) rename packages/{contracts/test => contracts-test}/tests/unit/staking/l2Transfer.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/staking/rebate.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/staking/staking.test.ts (100%) rename packages/{contracts/test => contracts-test}/tests/unit/upgrade/admin.test.ts (100%) rename packages/{contracts/test => contracts-test}/tsconfig.json (100%) rename packages/{contracts/test => contracts-test}/utils/coverage.ts (100%) delete mode 120000 packages/contracts/test/contracts delete mode 100644 packages/contracts/test/prettier.config.cjs create mode 100644 packages/deployment/.gitignore create mode 100644 packages/deployment/.markdownlint.json create mode 100644 packages/deployment/CLAUDE.md create mode 100644 packages/deployment/README.md create mode 100644 packages/deployment/config/arbitrumOne.json5 create mode 100644 packages/deployment/config/arbitrumSepolia.json5 create mode 100644 packages/deployment/config/localNetwork.json5 create mode 100644 packages/deployment/deploy/agreement/manager/01_deploy.ts create mode 100644 packages/deployment/deploy/agreement/manager/02_upgrade.ts create mode 100644 packages/deployment/deploy/agreement/manager/04_configure.ts create mode 100644 packages/deployment/deploy/agreement/manager/05_transfer_governance.ts create mode 100644 packages/deployment/deploy/agreement/manager/09_end.ts create mode 100644 packages/deployment/deploy/agreement/manager/10_status.ts create mode 100644 packages/deployment/deploy/allocate/allocator/01_deploy.ts create mode 100644 packages/deployment/deploy/allocate/allocator/02_upgrade.ts create mode 100644 packages/deployment/deploy/allocate/allocator/04_configure.ts create mode 100644 packages/deployment/deploy/allocate/allocator/06_transfer_governance.ts create mode 100644 packages/deployment/deploy/allocate/allocator/09_end.ts create mode 100644 packages/deployment/deploy/allocate/allocator/10_status.ts create mode 100644 packages/deployment/deploy/allocate/default/01_deploy.ts create mode 100644 packages/deployment/deploy/allocate/default/02_upgrade.ts create mode 100644 packages/deployment/deploy/allocate/default/04_configure.ts create mode 100644 packages/deployment/deploy/allocate/default/05_transfer_governance.ts create mode 100644 packages/deployment/deploy/allocate/default/09_end.ts create mode 100644 packages/deployment/deploy/allocate/default/10_status.ts create mode 100644 packages/deployment/deploy/allocate/direct/01_impl.ts create mode 100644 packages/deployment/deploy/common/00_sync.ts create mode 100644 packages/deployment/deploy/gip/0088/09_end.ts create mode 100644 packages/deployment/deploy/gip/0088/10_status.ts create mode 100644 packages/deployment/deploy/gip/0088/eligibility_integrate.ts create mode 100644 packages/deployment/deploy/gip/0088/issuance_allocate.ts create mode 100644 packages/deployment/deploy/gip/0088/issuance_close_guard.ts create mode 100644 packages/deployment/deploy/gip/0088/issuance_connect.ts create mode 100644 packages/deployment/deploy/gip/0088/upgrade/01_deploy.ts create mode 100644 packages/deployment/deploy/gip/0088/upgrade/02_configure.ts create mode 100644 packages/deployment/deploy/gip/0088/upgrade/03_transfer.ts create mode 100644 packages/deployment/deploy/gip/0088/upgrade/04_upgrade.ts create mode 100644 packages/deployment/deploy/gip/0088/upgrade/10_status.ts create mode 100644 packages/deployment/deploy/horizon/curation/01_deploy.ts create mode 100644 packages/deployment/deploy/horizon/curation/02_upgrade.ts create mode 100644 packages/deployment/deploy/horizon/curation/09_end.ts create mode 100644 packages/deployment/deploy/horizon/curation/10_status.ts create mode 100644 packages/deployment/deploy/horizon/payments-escrow/01_deploy.ts create mode 100644 packages/deployment/deploy/horizon/payments-escrow/02_upgrade.ts create mode 100644 packages/deployment/deploy/horizon/payments-escrow/09_end.ts create mode 100644 packages/deployment/deploy/horizon/payments-escrow/10_status.ts create mode 100644 packages/deployment/deploy/horizon/recurring-collector/01_deploy.ts create mode 100644 packages/deployment/deploy/horizon/recurring-collector/02_upgrade.ts create mode 100644 packages/deployment/deploy/horizon/recurring-collector/04_configure.ts create mode 100644 packages/deployment/deploy/horizon/recurring-collector/05_transfer_governance.ts create mode 100644 packages/deployment/deploy/horizon/recurring-collector/09_end.ts create mode 100644 packages/deployment/deploy/horizon/recurring-collector/10_status.ts create mode 100644 packages/deployment/deploy/horizon/staking/01_deploy.ts create mode 100644 packages/deployment/deploy/horizon/staking/02_upgrade.ts create mode 100644 packages/deployment/deploy/horizon/staking/09_end.ts create mode 100644 packages/deployment/deploy/horizon/staking/10_status.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/a/01_deploy.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/a/02_upgrade.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/a/04_configure.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/a/05_transfer_governance.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/a/09_end.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/a/10_status.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/b/01_deploy.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/b/02_upgrade.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/b/04_configure.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/b/05_transfer_governance.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/b/09_end.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/b/10_status.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/mock/01_deploy.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/mock/02_upgrade.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/mock/05_transfer_governance.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/mock/06_integrate.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/mock/09_end.ts create mode 100644 packages/deployment/deploy/rewards/eligibility/mock/10_status.ts create mode 100644 packages/deployment/deploy/rewards/manager/01_deploy.ts create mode 100644 packages/deployment/deploy/rewards/manager/02_upgrade.ts create mode 100644 packages/deployment/deploy/rewards/manager/09_end.ts create mode 100644 packages/deployment/deploy/rewards/manager/10_status.ts create mode 100644 packages/deployment/deploy/rewards/reclaim/01_deploy.ts create mode 100644 packages/deployment/deploy/rewards/reclaim/02_upgrade.ts create mode 100644 packages/deployment/deploy/rewards/reclaim/04_configure.ts create mode 100644 packages/deployment/deploy/rewards/reclaim/05_transfer_governance.ts create mode 100644 packages/deployment/deploy/rewards/reclaim/09_end.ts create mode 100644 packages/deployment/deploy/rewards/reclaim/10_status.ts create mode 100644 packages/deployment/deploy/service/dispute/01_deploy.ts create mode 100644 packages/deployment/deploy/service/dispute/02_upgrade.ts create mode 100644 packages/deployment/deploy/service/dispute/09_end.ts create mode 100644 packages/deployment/deploy/service/dispute/10_status.ts create mode 100644 packages/deployment/deploy/service/subgraph/01_deploy.ts create mode 100644 packages/deployment/deploy/service/subgraph/02_upgrade.ts create mode 100644 packages/deployment/deploy/service/subgraph/04_configure.ts create mode 100644 packages/deployment/deploy/service/subgraph/09_end.ts create mode 100644 packages/deployment/deploy/service/subgraph/10_status.ts create mode 100644 packages/deployment/docs/Architecture.md create mode 100644 packages/deployment/docs/DeploymentSetup.md create mode 100644 packages/deployment/docs/Design.md create mode 100644 packages/deployment/docs/Gip0088.md create mode 100644 packages/deployment/docs/GovernanceWorkflow.md create mode 100644 packages/deployment/docs/LocalForkTesting.md create mode 100644 packages/deployment/docs/SyncBytecodeDetectionFix.md create mode 100644 packages/deployment/docs/SyncSpecification.md create mode 100644 packages/deployment/docs/address-book/LayerAnalysis.md create mode 100644 packages/deployment/docs/address-book/README.md create mode 100644 packages/deployment/docs/deploy/ImplementationPrinciples.md create mode 100644 packages/deployment/docs/deploy/IssuanceAllocatorDeployment.md create mode 100644 packages/deployment/docs/deploy/RewardsEligibilityOracleDeployment.md create mode 100644 packages/deployment/docs/plans/AddressBookEnhancement.md create mode 100644 packages/deployment/hardhat.config.ts create mode 100644 packages/deployment/lib/abis.ts create mode 100644 packages/deployment/lib/address-book-ops.ts create mode 100644 packages/deployment/lib/address-book-utils.ts create mode 100644 packages/deployment/lib/apply-configuration.ts create mode 100644 packages/deployment/lib/artifact-loaders.ts create mode 100644 packages/deployment/lib/bytecode-utils.ts create mode 100644 packages/deployment/lib/contract-checks.ts create mode 100644 packages/deployment/lib/contract-registry.ts create mode 100644 packages/deployment/lib/controller-utils.ts create mode 100644 packages/deployment/lib/deploy-implementation.ts create mode 100644 packages/deployment/lib/deploy-standalone.ts create mode 100644 packages/deployment/lib/deployment-config.ts create mode 100644 packages/deployment/lib/deployment-metadata.ts create mode 100644 packages/deployment/lib/deployment-tags.ts create mode 100644 packages/deployment/lib/deployment-validation.ts create mode 100644 packages/deployment/lib/execute-governance.ts create mode 100644 packages/deployment/lib/format.ts create mode 100644 packages/deployment/lib/issuance-deploy-utils.ts create mode 100644 packages/deployment/lib/keystore-utils.ts create mode 100644 packages/deployment/lib/oz-proxy-verify.ts create mode 100644 packages/deployment/lib/preconditions.ts create mode 100644 packages/deployment/lib/script-factories.ts create mode 100644 packages/deployment/lib/status-detail.ts create mode 100644 packages/deployment/lib/sync-utils.ts create mode 100644 packages/deployment/lib/task-utils.ts create mode 100644 packages/deployment/lib/tx-builder-template.json create mode 100644 packages/deployment/lib/tx-builder.ts create mode 100644 packages/deployment/lib/tx-executor.ts create mode 100644 packages/deployment/lib/upgrade-implementation.ts create mode 100644 packages/deployment/package.json create mode 100644 packages/deployment/prettier.config.cjs create mode 100644 packages/deployment/rocketh/config.ts create mode 100644 packages/deployment/rocketh/deploy.ts create mode 100644 packages/deployment/scripts/check-bytecode.ts create mode 100644 packages/deployment/scripts/check-rocketh-bytecode.ts create mode 100644 packages/deployment/scripts/debug-deploy-state.ts create mode 100644 packages/deployment/scripts/generate-abis.ts create mode 100755 packages/deployment/scripts/tag-deployment.sh create mode 100644 packages/deployment/tasks/check-deployer.ts create mode 100644 packages/deployment/tasks/deployment-status.ts create mode 100644 packages/deployment/tasks/eth-tasks.ts create mode 100644 packages/deployment/tasks/execute-governance.ts create mode 100644 packages/deployment/tasks/grant-role.ts create mode 100644 packages/deployment/tasks/grt-tasks.ts create mode 100644 packages/deployment/tasks/list-pending-implementations.ts create mode 100644 packages/deployment/tasks/list-roles.ts create mode 100644 packages/deployment/tasks/reo-tasks.ts create mode 100644 packages/deployment/tasks/reset-fork.ts create mode 100644 packages/deployment/tasks/revoke-role.ts create mode 100644 packages/deployment/tasks/ss-tasks.ts create mode 100644 packages/deployment/tasks/sync.ts create mode 100644 packages/deployment/tasks/verify-contract.ts create mode 100644 packages/deployment/test/bytecode-comparison.test.ts create mode 100644 packages/deployment/test/chain-id-resolution.test.ts create mode 100644 packages/deployment/test/config-reconciliation.test.ts create mode 100644 packages/deployment/test/contract-registry-mapping.test.ts create mode 100644 packages/deployment/test/deployment-metadata.test.ts create mode 100644 packages/deployment/test/interface-id-stability.test.ts create mode 100644 packages/deployment/test/should-seed-rocketh.test.ts create mode 100644 packages/deployment/test/tx-builder.test.ts create mode 100644 packages/deployment/tsconfig.json create mode 100644 packages/deployment/types/rocketh.d.ts create mode 100644 packages/horizon/audits/2025-06-Indexing-Payments.pdf create mode 100644 packages/horizon/contracts/payments/collectors/MaxSecondsPerCollectionCap.md create mode 100644 packages/horizon/contracts/payments/collectors/RecurringCollector.todo.md create mode 100644 packages/horizon/ignition/modules/core/RecurringCollector.ts create mode 100644 packages/horizon/tasks/tenderly.ts delete mode 100644 packages/horizon/tasks/transitions/thawing-period.ts create mode 100644 packages/horizon/tenderly.config.json create mode 100644 packages/horizon/test/deployment/RecurringCollectorCallbackGas.test.ts delete mode 100644 packages/horizon/test/integration/during-transition-period/delegator.test.ts delete mode 100644 packages/horizon/test/integration/during-transition-period/multicall.test.ts delete mode 100644 packages/horizon/test/integration/during-transition-period/operator.test.ts delete mode 100644 packages/horizon/test/integration/during-transition-period/permissionless.test.ts delete mode 100644 packages/horizon/test/integration/during-transition-period/service-provider.test.ts delete mode 100644 packages/horizon/test/integration/during-transition-period/slasher.test.ts create mode 100644 packages/interfaces/contracts/horizon/internal/IHorizonStakingTypes.todo.md create mode 100644 packages/interfaces/src/types/issuance.ts create mode 100644 packages/issuance/.markdownlint.json create mode 100644 packages/issuance/.solhint.json create mode 100644 packages/issuance/README.md create mode 100644 packages/issuance/addresses.json create mode 100644 packages/issuance/audits/2025-11-17_Graph_PR1242_1243_v02.pdf create mode 100644 packages/issuance/audits/2025-12-13_Graph_EligibilityOracle_v02.pdf create mode 100644 packages/issuance/audits/2025-12-28_Graph_IssuanceAllocator_v03.pdf create mode 100644 packages/issuance/audits/2026-02-15_Graph_PR1279_v02.pdf create mode 100644 packages/issuance/audits/2026-05-09_Graph_PR1334_v05.pdf create mode 100644 packages/issuance/audits/2026-06-05_Graph_PR1342_v06.pdf create mode 100644 packages/issuance/contracts/agreement/RecurringAgreementManager.md create mode 100644 packages/issuance/contracts/allocate/IssuanceAllocator.md create mode 100644 packages/issuance/contracts/allocate/IssuanceAllocator.todo.md create mode 100644 packages/issuance/contracts/common/BaseUpgradeable.todo.md create mode 100644 packages/issuance/contracts/eligibility/RewardsEligibilityOracle.md create mode 100644 packages/issuance/docs/testing/reo/BaselineTestPlan.md create mode 100644 packages/issuance/docs/testing/reo/IndexerTestGuide.md create mode 100644 packages/issuance/docs/testing/reo/MainnetDetails.md create mode 100644 packages/issuance/docs/testing/reo/README.md create mode 100644 packages/issuance/docs/testing/reo/ReoTestPlan.md create mode 100644 packages/issuance/docs/testing/reo/RewardsConditionsTestPlan.md create mode 100644 packages/issuance/docs/testing/reo/SubgraphDenialTestPlan.md create mode 100644 packages/issuance/docs/testing/reo/TestnetDetails.md create mode 100644 packages/issuance/docs/testing/reo/support/IssuanceAllocatorTestPlan.md create mode 100644 packages/issuance/docs/testing/reo/support/NotionSetup.md create mode 100644 packages/issuance/docs/testing/reo/support/NotionTracker.csv create mode 100755 packages/issuance/docs/testing/reo/support/indexer-status.sh create mode 100644 packages/issuance/foundry.toml create mode 100644 packages/issuance/hardhat.base.config.ts create mode 100644 packages/issuance/hardhat.config.ts create mode 100644 packages/issuance/package.json create mode 100644 packages/issuance/prettier.config.cjs create mode 100644 packages/issuance/remappings.txt create mode 100644 packages/issuance/tsconfig.json create mode 100644 packages/issuance/tsconfig.typechain.json create mode 100644 packages/subgraph-service/contracts/libraries/AllocationHandler.todo.md create mode 100644 packages/subgraph-service/tasks/tenderly.ts create mode 100644 packages/subgraph-service/tenderly.config.json delete mode 100644 packages/subgraph-service/test/integration/during-transition-period/dispute-manager.test.ts delete mode 100644 packages/subgraph-service/test/integration/during-transition-period/governance.test.ts delete mode 100644 packages/subgraph-service/test/integration/during-transition-period/indexer.test.ts delete mode 100644 packages/subgraph-service/test/integration/during-transition-period/legacy-dispute-manager.test.ts create mode 100644 packages/testing/foundry.toml create mode 100644 packages/testing/package.json create mode 100644 packages/toolshed/src/core/custom-errors.ts create mode 100644 packages/toolshed/src/core/recurring-collector.ts create mode 100644 packages/toolshed/src/deployments/issuance/address-book.ts create mode 100644 packages/toolshed/src/deployments/issuance/contracts.ts create mode 100644 packages/toolshed/src/deployments/issuance/index.ts create mode 100644 packages/toolshed/src/hardhat/tenderly.ts create mode 100644 patches/rocketh@0.17.13.patch diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..4cef3c944 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,40 @@ +# Patterns are path-anchored under packages/ rather than `**//` because +# BuildKit treats `/` as matching files too, so `**/build/` would also +# hide the per-package `scripts/build` entry scripts. + +.git +.github +**/node_modules +.pnpm-store + +# Build outputs (regenerated by `pnpm build`). +packages/*/artifacts +packages/*/cache +packages/*/cache_forge +packages/*/forge-artifacts +packages/*/out +packages/*/dist +packages/*/build +packages/*/typechain-types +packages/*/coverage +packages/*/types + +# Forge crash dumps (also gitignored). Match only the per-package `core` file at the +# package root — NOT `**/core`, which would also exclude `packages/toolshed/src/core/` +# and other directories named `core` deeper in the tree. +packages/*/core + +# Editor / OS noise +.vscode +.idea +.DS_Store + +# Local env / addresses (must not leak into a published image) +.env +**/.env +**/addresses-local*.json +**/localNetwork.json +**/.keystore + +# Docs and audit PDFs aren't needed at runtime +docs diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 23c24f1be..5a7def0ac 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -1,4 +1,5 @@ name: Setup +description: Install system deps, Foundry, Node.js, pnpm, and the workspace's dependencies. runs: using: composite @@ -15,13 +16,10 @@ runs: shell: bash run: corepack enable - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version-file: '.nvmrc' cache: 'pnpm' - - name: Set up pnpm via Corepack - shell: bash - run: corepack prepare pnpm@10.17.0 --activate - name: Install dependencies shell: bash run: pnpm install --frozen-lockfile diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9f7c1a7a7..bc35bc4f2 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -6,7 +6,6 @@ env: on: pull_request: - branches: '*' workflow_dispatch: jobs: @@ -15,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive @@ -34,14 +33,14 @@ jobs: - name: Find coverage files id: coverage_files run: | - # Find all coverage-final.json files - COVERAGE_FILES=$(find ./packages -name "coverage-final.json" -path "*/coverage/*" | tr '\n' ',' | sed 's/,$//') + # Find coverage files: Istanbul JSON (Hardhat) and lcov (Forge) + COVERAGE_FILES=$(find ./packages \( -name "coverage-final.json" -o -name "lcov.info" \) -path "*/coverage/*" | tr '\n' ',' | sed 's/,$//') echo "files=$COVERAGE_FILES" >> $GITHUB_OUTPUT echo "Found coverage files: $COVERAGE_FILES" - name: Upload coverage reports if: steps.coverage_files.outputs.files != '' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} files: ${{ steps.coverage_files.outputs.files }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7746988af..729e38f6c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,14 +2,13 @@ name: Lint # This workflow runs linting on files in the repository # It can be configured to run on all files or just changed files -# It will fail the build if there are errors, but only report warnings +# It will fail the build if any linter fails env: CI: true on: pull_request: - branches: '*' workflow_dispatch: inputs: lint_mode: @@ -27,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # Needed to get all history for comparing changes @@ -65,7 +64,7 @@ jobs: # Get changed files, filtering out deleted files and files in ignored directories CHANGED_TS_JS=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.(js|ts|jsx|tsx|cjs|mjs)$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true) - CHANGED_SOL=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.sol$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true) + CHANGED_SOL=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.sol$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts|/test/)' || true) CHANGED_MD=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.md$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true) CHANGED_JSON=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.json$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true) CHANGED_YAML=$(git diff --name-only --diff-filter=d $BASE_SHA $HEAD_SHA | grep -E '\.(yml|yaml)$' | grep -v -E '(node_modules|dist|build|cache|reports|lib|coverage|artifacts|typechain|hardhat-cache|ignition/deployments|ignition/modules/artifacts)' || true) @@ -99,278 +98,179 @@ jobs: echo "- JSON: $JSON_COUNT" echo "- YAML: $YAML_COUNT" - - name: Lint TypeScript/JavaScript files (ESLint) - id: lint_ts_eslint + - name: Lint Solidity files + id: lint_sol continue-on-error: true run: | if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Running ESLint on all TypeScript/JavaScript files..." - npx eslint --max-warnings=0 '**/*.{js,ts,cjs,mjs,jsx,tsx}' - echo "ts_eslint_exit_code=$?" >> $GITHUB_OUTPUT - elif [ "${{ steps.changed_files.outputs.ts_js_count }}" -gt "0" ]; then - echo "Running ESLint on changed TypeScript/JavaScript files..." - cat changed_ts_js.txt | xargs npx eslint --max-warnings=0 - echo "ts_eslint_exit_code=$?" >> $GITHUB_OUTPUT - else - echo "No TypeScript/JavaScript files to lint with ESLint." - echo "ts_eslint_exit_code=0" >> $GITHUB_OUTPUT - fi - - - name: Lint TypeScript/JavaScript files (Prettier) - id: lint_ts_prettier - continue-on-error: true - run: | - if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Checking all TypeScript/JavaScript files with Prettier..." - npx prettier --check --cache --log-level warn '**/*.{js,ts,cjs,mjs,jsx,tsx}' - echo "ts_prettier_exit_code=$?" >> $GITHUB_OUTPUT - elif [ "${{ steps.changed_files.outputs.ts_js_count }}" -gt "0" ]; then - echo "Checking changed TypeScript/JavaScript files with Prettier..." - cat changed_ts_js.txt | xargs npx prettier --check --cache --log-level warn - echo "ts_prettier_exit_code=$?" >> $GITHUB_OUTPUT - else - echo "No TypeScript/JavaScript files to check with Prettier." - echo "ts_prettier_exit_code=0" >> $GITHUB_OUTPUT - fi - - - name: Lint Solidity files (Solhint) - id: lint_sol_solhint - continue-on-error: true - run: | - if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Running Solhint on all Solidity files..." - npx solhint --max-warnings=0 --noPrompt --noPoster 'packages/*/contracts/**/*.sol' - echo "sol_solhint_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting all Solidity files in each package..." + # Run solhint in each workspace package that has contracts (check mode) + pnpm -r exec bash -c 'if [ -d "contracts" ]; then npx solhint --max-warnings=0 --noPrompt --noPoster "contracts/**/*.sol" 2>/dev/null || exit 1; fi' + # Check formatting with prettier + SOL_FILES=$(git ls-files '*.sol') + if [ -n "$SOL_FILES" ]; then + echo "$SOL_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn + else + echo "No Solidity files found" + fi elif [ "${{ steps.changed_files.outputs.sol_count }}" -gt "0" ]; then - echo "Running Solhint on changed Solidity files..." - cat changed_sol.txt | xargs npx solhint --max-warnings=0 --noPrompt --noPoster - echo "sol_solhint_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting changed Solidity files..." + # Lint each changed file from its package directory + for file in $(cat changed_sol.txt); do + # Walk up to find package.json + dir="$file" + found=false + while [ "$dir" != "." ]; do + dir=$(dirname "$dir") + if [ -f "$dir/package.json" ]; then + relative_file="${file#$dir/}" + echo " Checking $file" + (cd "$dir" && npx solhint --max-warnings=0 --noPrompt --noPoster "$relative_file") + found=true + break + fi + done + if [ "$found" = false ]; then + echo "::error::No package.json found for $file - workflow needs fixing" + exit 1 + fi + done + + # Check formatting with prettier + cat changed_sol.txt | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn else - echo "No Solidity files to lint with Solhint." - echo "sol_solhint_exit_code=0" >> $GITHUB_OUTPUT + echo "No Solidity files to lint" fi - - name: Lint Solidity files (Prettier) - id: lint_sol_prettier + - name: Lint TypeScript/JavaScript files + id: lint_ts continue-on-error: true run: | if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Checking all Solidity files with Prettier..." - npx prettier --check --cache --log-level warn '**/*.sol' - echo "sol_prettier_exit_code=$?" >> $GITHUB_OUTPUT - elif [ "${{ steps.changed_files.outputs.sol_count }}" -gt "0" ]; then - echo "Checking changed Solidity files with Prettier..." - cat changed_sol.txt | xargs npx prettier --check --cache --log-level warn - echo "sol_prettier_exit_code=$?" >> $GITHUB_OUTPUT - else - echo "No Solidity files to check with Prettier." - echo "sol_prettier_exit_code=0" >> $GITHUB_OUTPUT - fi - - - name: Lint Markdown files (Markdownlint) - id: lint_md_markdownlint - continue-on-error: true - run: | - if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Running Markdownlint on all Markdown files..." - npx markdownlint --ignore-path .gitignore --ignore-path .markdownlintignore '**/*.md' - echo "md_markdownlint_exit_code=$?" >> $GITHUB_OUTPUT - elif [ "${{ steps.changed_files.outputs.md_count }}" -gt "0" ]; then - echo "Running Markdownlint on changed Markdown files..." - cat changed_md.txt | xargs npx markdownlint --ignore-path .gitignore --ignore-path .markdownlintignore - echo "md_markdownlint_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting all TypeScript/JavaScript files..." + TS_FILES=$(git ls-files '*.js' '*.ts' '*.cjs' '*.mjs' '*.jsx' '*.tsx') + if [ -n "$TS_FILES" ]; then + echo "$TS_FILES" | tr '\n' '\0' | xargs -0 npx eslint --max-warnings=0 --no-warn-ignored + echo "$TS_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern + else + echo "No TypeScript/JavaScript files found" + fi + elif [ "${{ steps.changed_files.outputs.ts_js_count }}" -gt "0" ]; then + echo "Linting changed TypeScript/JavaScript files..." + cat changed_ts_js.txt | tr '\n' '\0' | xargs -0 npx eslint --max-warnings=0 --no-warn-ignored + cat changed_ts_js.txt | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern else - echo "No Markdown files to lint with Markdownlint." - echo "md_markdownlint_exit_code=0" >> $GITHUB_OUTPUT + echo "No TypeScript/JavaScript files to lint" fi - - name: Lint Markdown files (Prettier) - id: lint_md_prettier + - name: Lint Markdown files + id: lint_md continue-on-error: true run: | if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Checking all Markdown files with Prettier..." - npx prettier --check --cache --log-level warn '**/*.md' - echo "md_prettier_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting all Markdown files..." + MD_FILES=$(git ls-files '*.md') + if [ -n "$MD_FILES" ]; then + echo "$MD_FILES" | tr '\n' '\0' | xargs -0 npx markdownlint --ignore-path .gitignore --ignore-path .markdownlintignore + echo "$MD_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern + else + echo "No Markdown files found" + fi elif [ "${{ steps.changed_files.outputs.md_count }}" -gt "0" ]; then - echo "Checking changed Markdown files with Prettier..." - cat changed_md.txt | xargs npx prettier --check --cache --log-level warn - echo "md_prettier_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting changed Markdown files..." + cat changed_md.txt | tr '\n' '\0' | xargs -0 npx markdownlint --ignore-path .gitignore --ignore-path .markdownlintignore + cat changed_md.txt | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern else - echo "No Markdown files to check with Prettier." - echo "md_prettier_exit_code=0" >> $GITHUB_OUTPUT + echo "No Markdown files to lint" fi - - name: Lint JSON files (Prettier) - id: lint_json_prettier + - name: Lint JSON files + id: lint_json continue-on-error: true run: | if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Checking all JSON files with Prettier..." - npx prettier --check --cache --log-level warn '**/*.json' - echo "json_prettier_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting all JSON files..." + # Exclude Ignition deployment artifacts and other build artifacts + JSON_FILES=$(git ls-files '*.json' | { grep -v -E '(ignition/deployments/.*/artifacts/|ignition/deployments/.*/build-info/|/\.openzeppelin/|deployments/.*/solcInputs/)' || true; }) + if [ -n "$JSON_FILES" ]; then + echo "$JSON_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern + else + echo "No JSON files found" + fi elif [ "${{ steps.changed_files.outputs.json_count }}" -gt "0" ]; then - echo "Checking changed JSON files with Prettier..." - cat changed_json.txt | xargs npx prettier --check --cache --log-level warn - echo "json_prettier_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting changed JSON files..." + JSON_FILES=$(cat changed_json.txt | { grep -v -E '(ignition/deployments/.*/artifacts/|ignition/deployments/.*/build-info/|/\.openzeppelin/|deployments/.*/solcInputs/)' || true; }) + if [ -n "$JSON_FILES" ]; then + echo "$JSON_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern + else + echo "No JSON files to lint (after filtering build artifacts)" + fi else - echo "No JSON files to check with Prettier." - echo "json_prettier_exit_code=0" >> $GITHUB_OUTPUT + echo "No JSON files to lint" fi - - name: Lint YAML files (yaml-lint) - id: lint_yaml_yamllint + - name: Lint YAML files + id: lint_yaml continue-on-error: true run: | if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Running yaml-lint on all YAML files..." - npx yaml-lint .github/**/*.{yml,yaml} - echo "yaml_yamllint_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting all YAML files..." + YAML_FILES=$(git ls-files '*.yml' '*.yaml') + if [ -n "$YAML_FILES" ]; then + echo "$YAML_FILES" | tr '\n' '\0' | xargs -0 npx yaml-lint + echo "$YAML_FILES" | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern + else + echo "No YAML files found" + fi elif [ "${{ steps.changed_files.outputs.yaml_count }}" -gt "0" ]; then - echo "Running yaml-lint on changed YAML files..." - cat changed_yaml.txt | xargs npx yaml-lint - echo "yaml_yamllint_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting changed YAML files..." + cat changed_yaml.txt | tr '\n' '\0' | xargs -0 npx yaml-lint + cat changed_yaml.txt | tr '\n' '\0' | xargs -0 npx prettier --check --cache --log-level warn --no-error-on-unmatched-pattern else - echo "No YAML files to lint with yaml-lint." - echo "yaml_yamllint_exit_code=0" >> $GITHUB_OUTPUT + echo "No YAML files to lint" fi - - name: Lint YAML files (Prettier) - id: lint_yaml_prettier + - name: Lint Forge files + id: lint_forge continue-on-error: true run: | + # Find packages with lint:forge script defined + FORGE_PACKAGES=$(find packages -name "package.json" -exec grep -l '"lint:forge"' {} \; | xargs -I{} dirname {} | sort) + if [ -z "$FORGE_PACKAGES" ]; then + echo "No packages with lint:forge script found" + exit 0 + fi + echo "Packages with lint:forge: $FORGE_PACKAGES" + if [ "${{ steps.lint_mode.outputs.mode }}" = "all" ]; then - echo "Checking all YAML files with Prettier..." - npx prettier --check --cache --log-level warn '**/*.{yml,yaml}' - echo "yaml_prettier_exit_code=$?" >> $GITHUB_OUTPUT - elif [ "${{ steps.changed_files.outputs.yaml_count }}" -gt "0" ]; then - echo "Checking changed YAML files with Prettier..." - cat changed_yaml.txt | xargs npx prettier --check --cache --log-level warn - echo "yaml_prettier_exit_code=$?" >> $GITHUB_OUTPUT + echo "Linting all Forge files..." + pnpm lint:forge + elif [ "${{ steps.changed_files.outputs.sol_count }}" -gt "0" ]; then + # Build regex pattern from packages with lint:forge + FORGE_PATTERN=$(echo "$FORGE_PACKAGES" | tr '\n' '|' | sed 's/|$//') + FORGE_FILES=$(cat changed_sol.txt | grep -E "^($FORGE_PATTERN)/" || true) + if [ -n "$FORGE_FILES" ]; then + echo "Found Forge-related changes, running forge lint..." + pnpm lint:forge + else + echo "No Forge-related Solidity files changed" + fi else - echo "No YAML files to check with Prettier." - echo "yaml_prettier_exit_code=0" >> $GITHUB_OUTPUT + echo "No Solidity files to lint with Forge" fi - - name: Determine overall status - id: status + - name: Check lint results + if: always() run: | - # Collect all exit codes - TS_ESLINT_EXIT_CODE="${{ steps.lint_ts_eslint.outputs.ts_eslint_exit_code }}" - TS_PRETTIER_EXIT_CODE="${{ steps.lint_ts_prettier.outputs.ts_prettier_exit_code }}" - SOL_SOLHINT_EXIT_CODE="${{ steps.lint_sol_solhint.outputs.sol_solhint_exit_code }}" - SOL_PRETTIER_EXIT_CODE="${{ steps.lint_sol_prettier.outputs.sol_prettier_exit_code }}" - SOL_NATSPEC_EXIT_CODE="${{ steps.lint_sol_natspec.outputs.sol_natspec_exit_code }}" - MD_MARKDOWNLINT_EXIT_CODE="${{ steps.lint_md_markdownlint.outputs.md_markdownlint_exit_code }}" - MD_PRETTIER_EXIT_CODE="${{ steps.lint_md_prettier.outputs.md_prettier_exit_code }}" - JSON_PRETTIER_EXIT_CODE="${{ steps.lint_json_prettier.outputs.json_prettier_exit_code }}" - YAML_YAMLLINT_EXIT_CODE="${{ steps.lint_yaml_yamllint.outputs.yaml_yamllint_exit_code }}" - YAML_PRETTIER_EXIT_CODE="${{ steps.lint_yaml_prettier.outputs.yaml_prettier_exit_code }}" - - # Initialize counters - ERRORS=0 - WARNINGS=0 - - # Check each exit code - # Exit code 1 typically indicates errors - # Exit code 2 or higher might indicate warnings or other issues - - # TypeScript/JavaScript - ESLint - if [ "$TS_ESLINT_EXIT_CODE" = "1" ]; then - echo "::error::ESLint found errors in TypeScript/JavaScript files" - ERRORS=$((ERRORS+1)) - elif [ "$TS_ESLINT_EXIT_CODE" != "0" ]; then - echo "::warning::ESLint found warnings in TypeScript/JavaScript files" - WARNINGS=$((WARNINGS+1)) - fi - - # TypeScript/JavaScript - Prettier - if [ "$TS_PRETTIER_EXIT_CODE" = "1" ]; then - echo "::error::Prettier found formatting issues in TypeScript/JavaScript files" - ERRORS=$((ERRORS+1)) - elif [ "$TS_PRETTIER_EXIT_CODE" != "0" ]; then - echo "::warning::Prettier found warnings in TypeScript/JavaScript files" - WARNINGS=$((WARNINGS+1)) - fi - - # Solidity - Solhint - if [ "$SOL_SOLHINT_EXIT_CODE" = "1" ]; then - echo "::error::Solhint found errors in Solidity files" - ERRORS=$((ERRORS+1)) - elif [ "$SOL_SOLHINT_EXIT_CODE" != "0" ]; then - echo "::warning::Solhint found warnings in Solidity files" - WARNINGS=$((WARNINGS+1)) - fi - - # Solidity - Prettier - if [ "$SOL_PRETTIER_EXIT_CODE" = "1" ]; then - echo "::error::Prettier found formatting issues in Solidity files" - ERRORS=$((ERRORS+1)) - elif [ "$SOL_PRETTIER_EXIT_CODE" != "0" ]; then - echo "::warning::Prettier found warnings in Solidity files" - WARNINGS=$((WARNINGS+1)) - fi - - # Markdown - Markdownlint - if [ "$MD_MARKDOWNLINT_EXIT_CODE" = "1" ]; then - echo "::error::Markdownlint found errors in Markdown files" - ERRORS=$((ERRORS+1)) - elif [ "$MD_MARKDOWNLINT_EXIT_CODE" != "0" ]; then - echo "::warning::Markdownlint found warnings in Markdown files" - WARNINGS=$((WARNINGS+1)) - fi - - # Markdown - Prettier - if [ "$MD_PRETTIER_EXIT_CODE" = "1" ]; then - echo "::error::Prettier found formatting issues in Markdown files" - ERRORS=$((ERRORS+1)) - elif [ "$MD_PRETTIER_EXIT_CODE" != "0" ]; then - echo "::warning::Prettier found warnings in Markdown files" - WARNINGS=$((WARNINGS+1)) - fi - - # JSON - Prettier - if [ "$JSON_PRETTIER_EXIT_CODE" = "1" ]; then - echo "::error::Prettier found formatting issues in JSON files" - ERRORS=$((ERRORS+1)) - elif [ "$JSON_PRETTIER_EXIT_CODE" != "0" ]; then - echo "::warning::Prettier found warnings in JSON files" - WARNINGS=$((WARNINGS+1)) - fi - - # YAML - yaml-lint - if [ "$YAML_YAMLLINT_EXIT_CODE" = "1" ]; then - echo "::error::yaml-lint found errors in YAML files" - ERRORS=$((ERRORS+1)) - elif [ "$YAML_YAMLLINT_EXIT_CODE" != "0" ]; then - echo "::warning::yaml-lint found warnings in YAML files" - WARNINGS=$((WARNINGS+1)) - fi - - # YAML - Prettier - if [ "$YAML_PRETTIER_EXIT_CODE" = "1" ]; then - echo "::error::Prettier found formatting issues in YAML files" - ERRORS=$((ERRORS+1)) - elif [ "$YAML_PRETTIER_EXIT_CODE" != "0" ]; then - echo "::warning::Prettier found warnings in YAML files" - WARNINGS=$((WARNINGS+1)) - fi - - # Create summary - LINT_MODE="${{ steps.lint_mode.outputs.mode }}" - if [ "$ERRORS" -gt 0 ]; then - echo "summary=❌ Linting ($LINT_MODE files) failed with $ERRORS error types and $WARNINGS warning types." >> $GITHUB_OUTPUT - echo "Linting failed with errors. CI build will fail." + # Check if any lint step failed + if [ "${{ steps.lint_sol.outcome }}" = "failure" ] || \ + [ "${{ steps.lint_ts.outcome }}" = "failure" ] || \ + [ "${{ steps.lint_md.outcome }}" = "failure" ] || \ + [ "${{ steps.lint_json.outcome }}" = "failure" ] || \ + [ "${{ steps.lint_yaml.outcome }}" = "failure" ] || \ + [ "${{ steps.lint_forge.outcome }}" = "failure" ]; then + echo "❌ One or more linters failed" exit 1 - elif [ "$WARNINGS" -gt 0 ]; then - echo "summary=⚠️ Linting ($LINT_MODE files) passed with $WARNINGS warning types. CI build will continue." >> $GITHUB_OUTPUT - echo "Linting found warnings but no errors. CI build will continue." - exit 0 else - echo "summary=✅ All linters ($LINT_MODE files) passed successfully with no errors or warnings." >> $GITHUB_OUTPUT - echo "All linters passed successfully." - exit 0 + echo "✅ All linters passed" fi - - - name: Post Summary - run: echo "${{ steps.status.outputs.summary }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 000000000..9304aa390 --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,148 @@ +# Builds the workspace image multi-arch (linux/amd64 + linux/arm64) and pushes +# to ghcr.io/graphprotocol/contracts. Consumed by local-network's +# graph-contracts wrapper via CONTRACTS_VERSION (pin a `:sha-` for +# reproducibility). +# +# Native runner per platform (no QEMU), per-platform digest push, manifest +# merge in a separate job. Runs independently of build-test.yml — they share +# `pnpm install + pnpm build` but stay decoupled so test feedback isn't tied +# to release packaging. + +name: Publish container image + +permissions: + contents: read + +on: + workflow_dispatch: + push: + branches: + - main + - 'deployment/**' + tags: + - 'v*' + +env: + IMAGE: ghcr.io/graphprotocol/contracts + +jobs: + build: + name: Build (${{ matrix.platform }}) + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-24.04 + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Prepare platform pair + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: recursive + + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker labels + id: meta + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + with: + images: ${{ env.IMAGE }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + file: Dockerfile + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + pull: true + cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }} + outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + + - name: Export digest + if: github.event_name != 'pull_request' + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: github.event_name != 'pull_request' + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + name: Merge into multi-arch manifest + needs: build + # Avoids orphan per-platform digest blobs if a `need` is skipped or fails. + if: | + !cancelled() + && needs.build.result == 'success' + && github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker tags + id: meta + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + with: + images: ${{ env.IMAGE }} + tags: | + type=ref,event=tag + # Moving per-branch tag for casual use; pin :sha- for reproducibility. + type=ref,event=branch + # Ensures workflow_dispatch from any branch yields a usable tag. + type=sha,enable=true + + # Glob `*` expands to the digest-named files from the build job. + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.IMAGE }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ea8d80315..aaa0a2152 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,3 +1,24 @@ +# Publishes a single workspace package to npm via OIDC trusted publishing. +# No NPM_TOKEN is used; the job exchanges its GitHub OIDC token for a short-lived +# npm credential and attaches SLSA provenance via `--provenance`. +# +# Prerequisite: each package in the `package` choice list must have a Trusted +# Publisher entry configured on npmjs.com under Settings → Publishing access. +# Use owner=`graphprotocol`, repo=`contracts`, workflow=`publish.yml`, +# environment=blank. Adding a package to the choice list without that npm-side +# entry will 403 at the publish step. +# +# Conventions: +# - `tag=latest` is reserved for the changeset-driven release flow (see README). +# Use a custom dist-tag (`dips`, `sepolia`, `next`, …) for ad-hoc or +# pre-release publishes so the stable channel is never overwritten. +# - `dry_run=true` validates the workflow end-to-end without consuming a +# version or pushing a git tag — useful when adding a new package or +# verifying a fresh Trusted Publisher entry. +# +# Fallback: maintainers with publish rights on `@graphprotocol/*` can still run +# `pnpm publish` locally if OIDC is unavailable. + name: Publish package to NPM on: @@ -8,29 +29,49 @@ on: required: true type: choice options: + - address-book - contracts - - sdk + - interfaces + - toolshed tag: description: 'Tag to publish' required: true type: string default: latest + dry_run: + description: 'Dry-run (validate only, no publish or git tag)' + required: false + type: boolean + default: false jobs: publish: name: Publish package runs-on: ubuntu-latest + permissions: + id-token: write + contents: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive - name: Set up environment uses: ./.github/actions/setup - - name: Set npm token for publishing - run: pnpm config set //registry.npmjs.org/:_authToken ${{ secrets.GRAPHPROTOCOL_NPM_TOKEN }} + - name: Read package info + id: pkg + shell: bash + run: | + PKG_NAME=$(node -p "require('./packages/${{ inputs.package }}/package.json').name") + PKG_VERSION=$(node -p "require('./packages/${{ inputs.package }}/package.json').version") + echo "tag=${PKG_NAME}@${PKG_VERSION}" >> $GITHUB_OUTPUT - name: Publish 🚀 shell: bash run: | pushd packages/${{ inputs.package }} - pnpm publish --tag ${{ inputs.tag }} --access public --no-git-checks + pnpm publish --provenance --tag ${{ inputs.tag }} --access public --no-git-checks ${{ inputs.dry_run && '--dry-run' || '' }} + - name: Tag release + if: ${{ !inputs.dry_run }} + run: | + git tag ${{ steps.pkg.outputs.tag }} + git push origin ${{ steps.pkg.outputs.tag }} diff --git a/.github/workflows/require-audit-label.yml b/.github/workflows/require-audit-label.yml new file mode 100644 index 000000000..826c229ad --- /dev/null +++ b/.github/workflows/require-audit-label.yml @@ -0,0 +1,56 @@ +name: Require Audit Label + +on: + pull_request: + branches: [main] + types: [opened, labeled, unlabeled, synchronize] + +jobs: + check-label: + runs-on: ubuntu-latest + steps: + - name: Get changed files + id: changed + uses: actions/github-script@v9 + with: + script: | + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + per_page: 100 + }); + + // Filter for .sol files, excluding tests + const solFiles = files + .map(f => f.filename) + .filter(f => f.endsWith('.sol')) + .filter(f => !f.includes('/test/')) + .filter(f => !f.includes('/tests/')) + .filter(f => !f.endsWith('.t.sol')); + + console.log('Non-test Solidity files changed:', solFiles); + core.setOutput('has_sol_files', solFiles.length > 0); + core.setOutput('sol_files', solFiles.join('\n')); + + - name: Check for required label + if: steps.changed.outputs.has_sol_files == 'true' + run: | + echo "Solidity files changed (excluding tests):" + echo "${{ steps.changed.outputs.sol_files }}" + echo "" + + LABELS='${{ toJson(github.event.pull_request.labels.*.name) }}' + if echo "$LABELS" | grep -q '"audited"'; then + echo "✓ PR has 'audited' label" + else + echo "::error::This PR modifies Solidity contract files and must have the 'audited' label before merging to main." + echo "" + echo "If this code has been audited, add the 'audited' label to proceed." + exit 1 + fi + + - name: Skip check (no contract changes) + if: steps.changed.outputs.has_sol_files == 'false' + run: | + echo "✓ No non-test Solidity files changed, skipping audit label check" diff --git a/.github/workflows/verifydeployed.yml b/.github/workflows/verifydeployed.yml index ba682fc21..d61ecd95a 100644 --- a/.github/workflows/verifydeployed.yml +++ b/.github/workflows/verifydeployed.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - name: Set up environment @@ -36,7 +36,7 @@ jobs: pnpm build - name: Save build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: contract-artifacts path: | @@ -49,7 +49,7 @@ jobs: needs: build steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Set up environment uses: ./.github/actions/setup - name: Build @@ -57,7 +57,7 @@ jobs: pushd packages/contracts pnpm build || pnpm build - name: Get build artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v8 with: name: contract-artifacts diff --git a/.gitignore b/.gitignore index 73a50607e..ba06116ad 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,6 @@ yarn-debug.log* yarn-error.log* node.log -# Core dumps -core -core.* - # Dependency directories node_modules/ .pnpm-store/ @@ -33,15 +29,22 @@ packages/*/.eslintcache dist/ dist-v5/ build/ +packages/contracts/**/types/ +deployments/hardhat/ +*.js.map +*.d.ts.map + +# Generated types (typechain output) typechain/ +typechain-src/ typechain-types/ -types/ types-v5/ wagmi/ -types/ -deployments/hardhat/ -*.js.map -*.d.ts.map +packages/contracts/types/ +packages/contracts-test/types/ +packages/interfaces/types/ +packages/token-distribution/types/ +packages/issuance/types/ # TypeScript incremental compilation cache **/tsconfig.tsbuildinfo @@ -56,6 +59,9 @@ bin/ .env .DS_Store .vscode +# Forge core dumps +**/core +!**/core/ # Coverage and other reports coverage/ @@ -101,3 +107,15 @@ tx-builder-*.json **/horizon-localNetwork/ **/subgraph-service-localNetwork/ !**/ignition/**/artifacts/ + +# Temporary test working directories +**/testing-coverage/ + +# Claude AI settings +.claude/ + +# Tenderly +.tenderly-artifacts/ + +# NFS stale file handles +.nfs* diff --git a/.markdownlint.json b/.markdownlint.json index 1a6cd5315..6ec4812d2 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -5,5 +5,6 @@ "MD013": false, "MD024": { "siblings_only": true }, "MD029": { "style": "ordered" }, - "MD033": false + "MD033": false, + "MD040": false } diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..b6f27f135 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..a45fd52cc --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 000000000..67210468b --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,218 @@ +# Deployment Strategy + +This document outlines the branching and deployment strategy for Solidity contracts in this repository. + +## Overview + +We use **per-environment deployment branches**. Each deploy to an environment gets its own `deployment//YYYY-MM-DD/` branch, branched from `main`, used to run the deploy and capture artifacts, then fast-forward merged back. Every testnet and mainnet deploy is tagged as a self-contained snapshot. Testnet is staging, not development — see principle #4. + +```mermaid +flowchart LR + main1["main
(always audited)"] + branch["deployment/<env>/YYYY-MM-DD/<name>
branched from main"] + deploy["deploy to <env>
tag: deploy/<env>/YYYY-MM-DD/<name>"] + merge["FF merge back to main
delete branch"] + main2["main"] + + main1 -->|branch| branch + branch --> deploy + deploy --> merge + merge --> main2 +``` + +A release typically flows through environments in sequence — local/scratch for development, then testnet, then mainnet — but each environment uses its own independent branch cut from `main`. There is no single long-lived branch cascading from testnet to mainnet; the audited `main` is the only shared substrate. + +For hotfixes, branch from the tag in production instead of from `main`: + +``` +deploy/mainnet/YYYY-MM-DD/ ──branch──► deployment/mainnet/YYYY-MM-DD/-hotfix + │ + ├─► fix + audit + ├─► deploy ──► tag: deploy/mainnet/YYYY-MM-DD/-hotfix + └──PR──► merge back to main +``` + +## Key Principles + +1. **Work in feat branches.** Development happens in `feat/*` branches, merged to `main` when complete. + +2. **`main` is always audited.** PRs modifying production Solidity require the `audited` label to merge to `main`. + +3. **Deployment branches are per-environment.** Each deploy creates a `deployment//YYYY-MM-DD/` branch (e.g. `deployment/testnet/2026-04-19/rewards-manager-and-subgraph-service`), branched from `main`. For testnet and mainnet the branch is short-lived (hours to days) and carries only artifacts and script tweaks — no contract changes. For scratch it may persist longer and host Solidity iteration, with any changes reaching `main` only via a `feat/*` PR. + +4. **Testnet is staging, not development.** Testnet is a pre-production mirror of mainnet, not a place to iterate on contract design. Redeploying updated contracts to testnet pollutes its historical state and can force custom off-chain handling (e.g. subgraph code for events that were later removed pre-mainnet) that then has to be maintained indefinitely. Iterate on the local network or scratch deployments instead (see [Pre-deployment Testing](#pre-deployment-testing)); graduate to testnet only with high confidence that the same contracts will reach mainnet. + +5. **Hotfix branches are branched from the tag they patch.** A hotfix branches from the `deploy/mainnet/YYYY-MM-DD/` tag currently in production, not from `main`. Keeps the hotfix diff minimal and avoids shipping accumulated but undeployed work on `main`. + +6. **Tag every testnet and mainnet deploy.** Each deploy to testnet or mainnet creates an immutable `deploy//YYYY-MM-DD/` tag reproducing the full state at that moment: source, scripts, and artifacts. The tag is the release record. Tagging in other environments (e.g. scratch) is optional and used at the operator's discretion — useful when a scratch state is worth pinning, unnecessary for throwaway iteration. + +7. **Prefer rebase and FF merge for testnet and mainnet.** Testnet and mainnet branches should FF back to `main` to preserve the audit-hash → deployed-bytes link. If `main` advances during the deploy window, rebase before merging. + +## Branches + +| Branch | Purpose | Lifetime | +| ------------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------- | +| `feat/*` | Active development | Until merged to `main` | +| `main` | Audited, deployment-ready code | Permanent | +| `deployment//YYYY-MM-DD/` | Workspace for one deploy to one environment | Hours to days for testnet/mainnet; may persist for scratch while iterating | + +Environments in active use today are `testnet` (Arbitrum Sepolia) and `mainnet` (Arbitrum One). The scheme accommodates additional environments (e.g. a dedicated pre-release staging chain) by adding further `` tokens — no change to the mechanics. + +## Tags + +Testnet and mainnet deploys are always tagged with an immutable annotated tag. Other environments may be tagged at operator discretion using the same format. + +- `deploy/testnet/YYYY-MM-DD/` — testnet deployment snapshot (Arbitrum Sepolia) +- `deploy/mainnet/YYYY-MM-DD/` — mainnet deployment snapshot (Arbitrum One) + +Including a descriptive `` is recommended. A short hyphenated identifier (e.g. `rewards-manager-and-subgraph-service`, `fix-activation`) makes tags self-describing, gives operators something meaningful to search on, and naturally prevents collisions when multiple deploys happen on the same day. The date segment ensures chronological sort regardless. + +Each tag is self-contained: its tree includes the deployed `.sol` sources, the deployment scripts used, and the resulting artifacts (`addresses.json`, etc.). The annotated tag body additionally records deployer identity and the list of changed contracts. Reproducing a past deploy is `git checkout ` and nothing else. + +### Finding and working with deployed code + +Check out what's currently on mainnet: + +```bash +git checkout "$(git tag -l 'deploy/mainnet/*' | sort | tail -1)" +``` + +Check out what's currently on testnet: + +```bash +git checkout "$(git tag -l 'deploy/testnet/*' | sort | tail -1)" +``` + +List all deployment tags: + +```bash +git tag -l "deploy/*" +``` + +Diff between last mainnet deploy and current main: + +```bash +git diff "$(git tag -l 'deploy/mainnet/*' | sort | tail -1)"..main +``` + +List active deployment branches (per environment): + +```bash +git branch -a --list 'deployment/testnet/*' +git branch -a --list 'deployment/mainnet/*' +``` + +## Workflows + +### Pre-deployment Testing + +Iteration on contract design happens on environments that don't pollute the canonical testnet state: + +- **Local network**: a self-contained network run locally as docker containers, bundling chain node, contracts, and off-chain services. The default for development and integration testing. +- **Scratch deployments**: a fresh, separate protocol instance on Arbitrum Sepolia — same chain as the canonical testnet, but distinct protocol instance. A `deployment/scratch/...` branch may persist across multiple iterations and carry contract changes as development progresses; anything worth keeping lands on `main` via a `feat/*` PR, and the scratch branch can be discarded. + +The deployment scripts are written to be network- and instance-agnostic, so the same code path runs against local, scratch, testnet, and mainnet. A release only graduates to testnet once local and scratch testing give high confidence that no further contract changes are needed. + +Terminology: "testnet" always refers to the canonical Graph Protocol testnet instance on Arbitrum Sepolia. A scratch deployment on Sepolia is not "testnet" — same chain, different protocol instance. + +### Testnet and Mainnet Deployment + +Testnet deploys happen against a release already expected to reach mainnet unchanged (principle #4). Mainnet follows with the same contract source; typical differences between the two deploys are artifact files and operational parameters that vary by environment. + +The two deploys use the same procedure, each on its own branch cut from the current `main` (the mainnet branch is cut after the testnet merge-back, so it already includes testnet artifacts): + +1. **Branch.** From current `main`, create `deployment//YYYY-MM-DD/` and push it. Open a tracking PR back to `main`. +2. **Deploy.** Run the deployment scripts against the target network. Commit artifacts, push. +3. **Tag.** Run `tag-deployment.sh --network --name ...` to create `deploy//YYYY-MM-DD/`. Push the tag. +4. **Merge.** Fast-forward merge the PR back into `main`. Delete the branch. If `main` advanced during the window, rebase before merging. + +Network mapping: testnet → `arbitrumSepolia`, mainnet → `arbitrumOne`. + +### Emergency Hotfix + +For critical mainnet issues: + +1. Branch `deployment/mainnet/YYYY-MM-DD/-hotfix` from the current `deploy/mainnet/YYYY-MM-DD/` tag and push it. +2. Apply the fix. If it touches contract source, it must be audited before deploy. Commit and push; open a PR back to `main` at this point — it stays open for the duration of the hotfix as the review/tracking thread and becomes the merge-back PR. +3. Run the deployment scripts against mainnet. If the fix warrants pre-mainnet verification, run it against the local network or a scratch deployment first (per [Pre-deployment Testing](#pre-deployment-testing)) rather than cutting a separate testnet deploy, which would otherwise race the mainnet hotfix. Commit artifacts and push. +4. Run `tag-deployment.sh --network arbitrumOne --name -hotfix ...` to create the `deploy/mainnet/YYYY-MM-DD/-hotfix` tag. Push the tag. +5. Review and merge the open PR back into `main`. The `audited` label applies to any contract changes in this PR. +6. Delete the hotfix branch. +7. If other deployment branches are active at hotfix time, incorporate the hotfix into them (rebase or cherry-pick) before their deploys. + +## Audit Integrity + +Audits certify that specific files have specific content. The operational question is always: + +> For every file in the audit scope, do its current bytes match the audited version's bytes? + +Principles #2, #3, and #7 preserve this for testnet and mainnet by construction: audited bytes reach `main` via `feat/*` PRs, their deployment branches carry no contract changes, and FF merges keep the audit-hash → deployed-bytes link intact. Scratch branches may hold in-progress contract work, but none of it reaches testnet or mainnet without first landing on audited `main`. + +The audit scope is a transitive closure — a reviewed contract's imports are implicitly in scope even if the PR didn't touch them — and the audit reference is a pinned commit SHA, not a PR number or label. A CI check can back up this cultural preference with a mechanical one: diff the audited paths between the last audit tag and `HEAD`, and require either an empty diff or a fresh audit. See [Appendix A: Audit Integrity CI Check](#appendix-a-audit-integrity-ci-check). + +## Automation + +### Tagging + +Tag creation is a **scripted operator step**, run after the deploy. The script captures context a CI workflow couldn't — which deploy script ran, with what flags, by whom, which contracts changed — baked into an annotated tag body, optionally signed. + +Implementation: [`packages/deployment/scripts/tag-deployment.sh`](packages/deployment/scripts/tag-deployment.sh). It takes `--deployer`, `--network`, `--name` (recommended), and `--base`; diffs each address book (`packages/horizon/addresses.json`, `packages/subgraph-service/addresses.json`, `packages/issuance/addresses.json`) against the base ref to enumerate new / updated / removed contracts; and creates the annotated tag in the `deploy//YYYY-MM-DD/` format defined above (or the bare-date fallback when no name is given). + +Typical invocation after the artifact commit is pushed: + +```bash +packages/deployment/scripts/tag-deployment.sh \ + --deployer "packages/deployment --tags RewardsManager,SubgraphService" \ + --network arbitrumSepolia \ + --name rewards-manager-and-subgraph-service +``` + +The script prints a preview (tag name, commit, annotation body), asks for confirmation, and creates a signed annotated tag. Run `tag-deployment.sh --help` for the full option list (`--dry-run`, `--yes`, `--no-sign`, `--base`, …). + +Then push: + +```bash +git push origin +``` + +The diff against `--base` is what populates the tag body's "contracts" section. The default of the previous deploy tag for the same environment is normally correct. For an initial deploy on an environment (no prior tag exists), pass `--base` explicitly. + +### Audit Label Requirement + +PRs to `main` modifying Solidity contract files require an `audited` label before merging (`.github/workflows/require-audit-label.yml`). + +- **Applies to:** `.sol` files outside of test directories +- **Excludes:** Files in `/test/`, `/tests/`, or ending in `.t.sol` +- **Label:** `audited` + +This enforces principle #2: code in `main` must be audited. + +## Appendix A: Audit Integrity CI Check + +A future workflow to enforce the byte-equality property at CI level rather than relying on the cultural FF-preference. Sketched here; design decisions still to make before implementation. + +### Approach + +1. **Audit tags.** Each completed audit produces an annotated tag of the form `audit/YYYY-MM-DD/` pointing at the commit the auditors signed off on. The tag body records the auditor, the scope (which files/paths), and a link to the audit report. +2. **Scope definition.** The "audit scope" is the set of file paths the auditors reviewed, together with the transitive closure of their Solidity imports. Stored as a path list (or glob) in the audit tag's annotation body so it can be parsed programmatically. +3. **CI check.** On every PR to `main` (or every push to `deployment/*`), resolve the most recent `audit/*` tag that covers each in-scope file and compute `git diff HEAD -- `. If non-empty for any in-scope file, require either: + - The PR to carry the `audited` label (operator asserts the diff has been re-reviewed), or + - A new `audit/*` tag to land that covers the current `HEAD` for those paths. +4. **Empty diff ⇒ automatic pass.** When the audited bytes on `HEAD` match the audit tag's bytes exactly for all in-scope files, no human intervention is needed — the CI proves trivially that `HEAD` still matches what was audited. + +### Open design decisions + +- **Where does "audit scope" live?** Most robust: in the `audit/*` tag body as a path list. Alternative: a checked-in `audits/manifest.json`. The tag-body approach keeps the scope immutable alongside the reference commit; the file approach is easier to edit when scopes overlap or evolve. +- **Multi-audit composition.** Different contracts may be covered by different audits. The CI needs a deterministic "most recent audit covering file X" lookup. Overlapping scopes require conflict resolution (most specific wins? most recent?). +- **Transitive closure computation.** For `.sol` files, the importer graph is machine-derivable. A pre-commit or CI step should expand a human-declared scope (e.g. "the `IssuanceAllocator` contract") into the full transitive closure, so scope drift (an import added after audit) is caught automatically. +- **Path inclusion/exclusion rules.** The current `require-audit-label.yml` excludes `/test/`, `/tests/`, and `*.t.sol`, but there are other helper, mock, and internal-only contracts that aren't audit targets (migration scaffolding, local fixtures, temporary scripts). A robust check needs either an explicit in-scope list or a clearer directory convention. + +### Prerequisite: reorganize non-production Solidity + +The current tree mixes production contracts with helpers, mocks, and internal tooling in the same directories. Before the CI check is meaningful: + +- Move non-production Solidity into clearly-named directories outside any plausible audit scope (e.g. `mocks/`, `helpers/`, `scripts/`, a top-level `non-audit/` tree per package). +- Make audit scope a directory-level property wherever possible ("everything under `packages//contracts/` is audit scope; nothing else is") so that inclusion is inferrable from path rather than requiring a bespoke filter. +- Update `require-audit-label.yml`'s filter in the same pass so its exclusions match the new layout. + +Until this reorganization lands, an audit-integrity CI check is possible but would rely on hand-maintained path lists — fragile and easy to drift from reality. The reorganization is low-risk refactoring but should be done in its own PR (itself audited for scope equivalence), separately from adopting this deployment proposal. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..2d3ce01df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Workspace base image: deps + `pnpm install` + `pnpm build`, no entrypoint. +# Consumers (e.g. local-network's graph-contracts wrapper) layer their own +# deploy script on top, or `docker run` to invoke pnpm/hardhat directly. + +FROM node:24-bookworm-slim + +# libudev-dev / libusb-1.0-0-dev: native deps of hardhat-secure-accounts. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + curl git jq python3 make g++ libudev-dev libusb-1.0-0-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/foundry-rs/foundry:stable \ + /usr/local/bin/forge /usr/local/bin/cast /usr/local/bin/ + +ENV COREPACK_ENABLE_STRICT=1 +RUN corepack enable + +# Husky's postinstall needs .git and is pointless in an image build. +ENV HUSKY=0 + +WORKDIR /opt/contracts + +COPY . . + +RUN pnpm install --frozen-lockfile \ + && pnpm build diff --git a/README.md b/README.md index 2fa5496a2..665592e2c 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Build - - CI-Contracts + + Lint

@@ -153,175 +153,53 @@ git push --follow-tags **Note**: this step is meant to be run on the main branch. -Packages are published and distributed via NPM. To publish a package, run the following command from the root of the repository: +The [`Publish package to NPM`](.github/workflows/publish.yml) workflow is the standard publish path. It uses OIDC trusted publishing — no `NPM_TOKEN` is involved, and SLSA provenance is attached automatically. Anyone with `workflow_dispatch` permission on the repo can run it; no local npm credentials needed. The workflow also creates and pushes the package's git tag after a successful publish (so Step 3 can be skipped when using this path). -```bash -# Publish the packages -pnpm changeset publish +Dispatch from the Actions tab, or via `gh`: -# Alternatively use -pnpm publish --recursive +```bash +gh workflow run publish.yml -f package=interfaces -f tag=latest -f dry_run=false ``` -Alternatively, there is a GitHub action that can be manually triggered to publish a package. - -## Linting Configuration - -This monorepo uses a comprehensive linting setup with multiple tools to ensure code quality and consistency across all packages. - -### Linting Tools Overview - -- **ESLint**: JavaScript/TypeScript code quality and style enforcement -- **Prettier**: Code formatting for JavaScript, TypeScript, JSON, Markdown, YAML, and Solidity -- **Solhint**: Solidity-specific linting for smart contracts -- **Markdownlint**: Markdown formatting and style consistency -- **YAML Lint**: YAML file validation and formatting - -### Configuration Architecture - -The linting configuration follows a hierarchical structure where packages inherit from root-level configurations: - -#### ESLint Configuration - -- **Root Configuration**: `eslint.config.mjs` - Modern flat config format -- **Direct Command**: `npx eslint '**/*.{js,ts,cjs,mjs,jsx,tsx}' --fix` -- **Behavior**: ESLint automatically searches up parent directories to find configuration files -- **Package Inheritance**: Packages automatically inherit the root ESLint configuration without needing local config files -- **Global Ignores**: Configured to exclude autogenerated files (`.graphclient-extracted/`, `lib/`) and build outputs - -#### Prettier Configuration - -- **Root Configuration**: `prettier.config.cjs` - Base formatting rules for all file types -- **Direct Command**: `npx prettier -w --cache '**/*.{js,ts,cjs,mjs,jsx,tsx,json,md,sol,yml,yaml}'` -- **Package Inheritance**: Packages that need Prettier must have a `prettier.config.cjs` file that inherits from the shared config -- **Example Package Config**: - - ```javascript - const baseConfig = require('../../prettier.config.cjs') - module.exports = { ...baseConfig } - ``` - -- **Ignore Files**: `.prettierignore` excludes lock files, build outputs, and third-party dependencies - -#### Solidity Linting (Solhint) +Inputs: -- **Root Configuration**: `.solhint.json` - Base Solidity linting rules extending `solhint:recommended` -- **Direct Command**: `npx solhint 'contracts/**/*.sol'` (add `--fix` for auto-fixing) -- **List Applied Rules**: `npx solhint list-rules` -- **TODO Comment Checking**: `scripts/check-todos.sh` - Blocks commits and linting if TODO/FIXME/XXX/HACK comments are found in changed Solidity files -- **Package Inheritance**: Packages can extend the root config with package-specific rules -- **Configuration Inheritance Limitation**: Solhint has a limitation where nested `extends` don't work properly. When a local config extends a parent config that itself extends `solhint:recommended`, the built-in ruleset is ignored. -- **Recommended Package Extension Pattern**: +- `package` — the workspace package to publish (one of `address-book`, `contracts`, `interfaces`, `toolshed`). +- `tag` — npm dist-tag. Use `latest` for stable releases; use a custom tag (`dips`, `sepolia`, `next`, …) for pre-releases so the stable channel isn't overwritten. +- `dry_run` — when `true`, validates the workflow without consuming a version or pushing a git tag. - ```json - { - "extends": ["solhint:recommended", "./../../.solhint.json"], - "rules": { - "no-console": "off", - "import-path-check": "off" - } - } - ``` +The workflow publishes one package per dispatch; for a multi-package release, dispatch once per package. -#### Markdown Linting (Markdownlint) +**Prerequisite:** each package on the choice list must have a Trusted Publisher entry on npmjs.com (Settings → Publishing access) with owner `graphprotocol`, repo `contracts`, workflow `publish.yml`, environment blank. Adding a new package to the workflow's `package` input without configuring its npm-side entry first will 403 at the publish step. -- **Root Configuration**: `.markdownlint.json` - Markdown formatting and style rules -- **Direct Command**: `npx markdownlint '**/*.md' --fix` -- **Ignore Files**: `.markdownlintignore` automatically picked up by markdownlint CLI -- **Global Application**: Applied to all markdown files across the monorepo +#### Alternative: local publish -### Linting Scripts - -#### Root Level Scripts +For maintainers with publish rights on `@graphprotocol/*` — useful as a fallback if OIDC is unavailable, or for packages not on the workflow's choice list. Run from the root of a clean checkout: ```bash -# Run all linting tools -pnpm lint - -# Individual linting commands -pnpm lint:ts # ESLint + Prettier for TypeScript/JavaScript -pnpm lint:sol # TODO check + Solhint + Prettier for Solidity (runs recursively) -pnpm lint:md # Markdownlint + Prettier for Markdown -pnpm lint:json # Prettier for JSON files -pnpm lint:yaml # YAML linting + Prettier - -# Lint only staged files (useful for manual pre-commit checks) -pnpm lint:staged # Run linting on git-staged files only +# Publish the packages +pnpm changeset publish + +# Alternatively use +pnpm publish --recursive ``` -#### Package Level Scripts +## Linting -Each package can define its own linting scripts that work with the inherited configurations: +This monorepo uses multiple linting tools: ESLint, Prettier, Solhint, Forge Lint, Markdownlint, and YAML Lint. ```bash -# Example from packages/contracts -pnpm lint:sol # Solhint for contracts in this package only -pnpm lint:ts # ESLint for TypeScript files in this package +pnpm lint # Run all linters +pnpm lint:staged # Lint only staged files ``` -### Pre-commit Hooks (lint-staged) - -The repository uses `lint-staged` with Husky to run linting on staged files before commits: - -- **Automatic**: Runs automatically on `git commit` via Husky pre-commit hook -- **Manual**: Run `pnpm lint:staged` to manually check staged files before committing -- **Configuration**: Root `package.json` contains lint-staged configuration -- **Custom Script**: `scripts/lint-staged-run.sh` filters out generated files that shouldn't be linted -- **File Type Handling**: - - `.{js,ts,cjs,mjs,jsx,tsx}`: ESLint + Prettier - - `.sol`: TODO check + Solhint + Prettier - - `.md`: Markdownlint + Prettier - - `.json`: Prettier only - - `.{yml,yaml}`: YAML lint + Prettier - -**Usage**: `pnpm lint:staged` is particularly useful when you want to check what linting changes will be applied to your staged files before actually committing. - -### TODO Comment Enforcement - -The repository enforces TODO comment resolution to maintain code quality: - -- **Scope**: Applies only to Solidity (`.sol`) files -- **Detection**: Finds TODO, FIXME, XXX, and HACK comments (case-insensitive) -- **Triggers**: - - **Pre-commit**: Blocks commits if TODO comments exist in files being committed - - **Regular linting**: Flags TODO comments in locally changed, staged, or untracked Solidity files -- **Script**: `scripts/check-todos.sh` (must be run from repository root) -- **Bypass**: Use `git commit --no-verify` to bypass (not recommended for production) - -### Key Design Principles - -1. **Hierarchical Configuration**: Root configurations provide base rules, packages can extend as needed -2. **Tool-Specific Inheritance**: ESLint searches up automatically, Prettier requires explicit inheritance -3. **Generated File Exclusion**: Multiple layers of exclusion for autogenerated content -4. **Consistent Formatting**: Prettier ensures consistent code formatting across all file types -5. **Fail-Fast Linting**: Pre-commit hooks catch issues before they enter the repository - -### Configuration Files Reference - -| Tool | Root Config | Package Config | Ignore Files | -| ------------ | --------------------- | -------------------------------- | ---------------------------- | -| ESLint | `eslint.config.mjs` | Auto-inherited | Built into config | -| Prettier | `prettier.config.cjs` | `prettier.config.cjs` (inherits) | `.prettierignore` | -| Solhint | `.solhint.json` | `.solhint.json` (array extends) | N/A | -| Markdownlint | `.markdownlint.json` | Auto-inherited | `.markdownlintignore` | -| Lint-staged | `package.json` | N/A | `scripts/lint-staged-run.sh` | - -### Troubleshooting - -- **ESLint not finding config**: ESLint searches up parent directories automatically - no local config needed -- **Prettier not working**: Packages need a `prettier.config.cjs` that inherits from root config -- **Solhint missing rules**: If extending a parent config, use array format: `["solhint:recommended", "./../../.solhint.json"]` to ensure all rules are loaded -- **Solhint inheritance not working**: Nested extends don't work - parent config's `solhint:recommended` won't be inherited with simple string extends -- **Solhint rule reference**: Use `npx solhint list-rules` to see all available rules and their descriptions -- **Generated files being linted**: Check ignore patterns in `.prettierignore`, `.markdownlintignore`, and ESLint config -- **Preview lint changes before commit**: Use `pnpm lint:staged` to see what changes will be applied to staged files -- **Commit blocked by linting**: Fix the linting issues or use `git commit --no-verify` to bypass (not recommended) +See [docs/Linting.md](docs/Linting.md) for detailed configuration, inline suppression syntax, and troubleshooting. ## Documentation -> Coming soon +- [Deployment Strategy](DEPLOYMENT.md) — Branching model and deployment workflow for Solidity contracts +- [Linting](docs/Linting.md) — Linting configuration and troubleshooting -For now, each package has its own README with more specific documentation you can check out. +Each package also has its own README with package-specific documentation. ## Contributing diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..267d0e258 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +# Local build of the workspace image. Pair with local-network's +# CONTRACTS_VERSION=local to consume this build from there. +# +# `docker compose build` (or `just build-image`) produces +# `ghcr.io/graphprotocol/contracts:${CONTRACTS_TAG:-local}`. Override +# CONTRACTS_TAG to distinguish multiple in-flight checkouts. + +services: + contracts: + image: ghcr.io/graphprotocol/contracts:${CONTRACTS_TAG:-local} + build: + context: . + dockerfile: Dockerfile diff --git a/docs/ForgeLintSymlinkIssue.md b/docs/ForgeLintSymlinkIssue.md new file mode 100644 index 000000000..d3309c862 --- /dev/null +++ b/docs/ForgeLintSymlinkIssue.md @@ -0,0 +1,150 @@ +# Foundry Issue Draft: forge lint symlink loop + +**Repository:** + +--- + +## Title + +`forge lint` fails with "too many levels of symbolic links" in pnpm workspaces + +## Component + +`forge-lint` + +## Describe the bug + +`forge lint` fails with OS error 40 ("too many levels of symbolic links") when the project uses pnpm workspaces with circular symlinks. This is a standard pnpm workspace pattern where sub-packages depend on their parent package. + +The `[lint] ignore` configuration does not prevent this - forge appears to traverse the entire filesystem tree before applying the ignore filter, hitting the symlink loop in the process. + +Note: `forge build` and `forge test` work correctly in the same project, suggesting they use different traversal logic that handles or avoids symlink loops. + +## Error message + +``` +Error: attempting to read `/path/to/project/node_modules/@graphprotocol/contracts/testing/node_modules/@graphprotocol/contracts/testing/node_modules/@graphprotocol/contracts/testing/[...repeating...]/contracts/governance` resulted in an error: Too many levels of symbolic links (os error 40) +``` + +## To reproduce + +1. Create a pnpm workspace with package A +2. Package A has a sub-directory (e.g., `testing/`) with its own `package.json` +3. The sub-package lists package A as a dependency +4. pnpm creates a symlink: `A/testing/node_modules/A` → `../../..` (circular) +5. Run `forge lint` + +### Minimal reproduction + +```bash +# Create workspace +mkdir -p workspace/packages/parent/child +cd workspace + +# Root package.json +cat > package.json << 'EOF' +{ + "name": "workspace", + "private": true +} +EOF + +# pnpm workspace config +cat > pnpm-workspace.yaml << 'EOF' +packages: + - 'packages/*' + - 'packages/*/child' +EOF + +# Parent package +cat > packages/parent/package.json << 'EOF' +{ + "name": "@example/parent", + "version": "1.0.0" +} +EOF + +# Child package that depends on parent +cat > packages/parent/child/package.json << 'EOF' +{ + "name": "@example/child", + "version": "1.0.0", + "dependencies": { + "@example/parent": "workspace:^" + } +} +EOF + +# Install - pnpm creates circular symlink +pnpm install + +# Verify circular symlink exists +ls -la packages/parent/child/node_modules/@example/parent +# Shows: parent -> ../../.. + +# Create minimal foundry project +cat > packages/parent/foundry.toml << 'EOF' +[profile.default] +src = 'contracts' +libs = ["node_modules"] + +[lint] +ignore = ["node_modules/**/*"] +EOF + +mkdir -p packages/parent/contracts +cat > packages/parent/contracts/Example.sol << 'EOF' +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +contract Example {} +EOF + +# This fails +cd packages/parent && forge lint +``` + +## Expected behavior + +`forge lint` should either: + +1. Apply the `ignore` configuration during traversal (not after), skipping `node_modules` entirely +2. Detect and handle symlink loops gracefully (track visited inodes) +3. Respect the `libs` configuration to avoid deep traversal into library directories + +## Environment + +- forge version: 1.5.1-stable +- OS: Linux +- Package manager: pnpm 9.x with workspaces + +## Root cause hypothesis + +The traversal logic appears to recursively walk the entire directory tree before applying `ignore` patterns, rather than pruning during traversal. + +Evidence: + +- `node_modules/@graphprotocol/contracts/testing/` is not a standard Solidity directory +- It's not in the package exports +- It's not `src`, `test`, `script`, or `lib` +- Yet forge descends into it (and its nested `node_modules`) + +The fix should apply ignore patterns during traversal (using something like `filter_entry` in walkdir) to prune directories before descending, not filter results after traversal. + +## Additional context + +This pattern is common in monorepos where: + +- A main package exists (e.g., `@graphprotocol/contracts`) +- Sub-packages for testing/tooling exist within it (e.g., `contracts/testing/`) +- Sub-packages depend on the parent for shared code + +pnpm resolves this by creating symlinks back to the parent, which is intentional and works correctly with Node.js module resolution (which has built-in cycle detection). + +The workaround of removing these symlinks would break the workspace, so it's not viable. + +## Workaround (current) + +None that preserves full functionality. Options: + +- Skip `forge lint` and use only `solhint` +- Manually delete circular symlinks before linting (breaks workspace) diff --git a/docs/IGraphProxyAdminInterfaceFix.md b/docs/IGraphProxyAdminInterfaceFix.md new file mode 100644 index 000000000..f17ea388c --- /dev/null +++ b/docs/IGraphProxyAdminInterfaceFix.md @@ -0,0 +1,201 @@ +# IGraphProxyAdmin Interface Signature Fix + +## Issue + +The IGraphProxyAdmin interface in `packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol` had incorrect function signatures that didn't match the actual GraphProxyAdmin contract implementation. + +### Understanding the Two Different `acceptProxy` Methods + +There are **two different contracts** with similar-sounding methods, which can cause confusion: + +1. **GraphUpgradeable** (base class for implementation contracts): + + ```solidity + // Called ON the implementation contract + function acceptProxy(IGraphProxy _proxy) external onlyProxyAdmin(_proxy) { + _proxy.acceptUpgrade(); + } + ``` + + This is inherited by implementation contracts like RewardsManager, Staking, etc. + +2. **GraphProxyAdmin** (admin contract that manages upgrades): + + ```solidity + // Called ON the admin contract, which then calls the implementation + function acceptProxy(GraphUpgradeable _implementation, IGraphProxy _proxy) external onlyGovernor { + _implementation.acceptProxy(_proxy); + } + ``` + + This is the admin contract that orchestrates upgrades. + +**IGraphProxyAdmin represents the second one** - the GraphProxyAdmin admin contract, not the GraphUpgradeable base class. + +### Incorrect Interface (Before) + +The interface mistakenly used the single-parameter signature from GraphUpgradeable: + +```solidity +function acceptProxy(IGraphProxy proxy) external; + +function acceptProxyAndCall(IGraphProxy proxy, bytes calldata data) external; +``` + +### Actual GraphProxyAdmin Implementation + +From `packages/contracts/contracts/upgrades/GraphProxyAdmin.sol`: + +```solidity +function acceptProxy(GraphUpgradeable _implementation, IGraphProxy _proxy) external onlyGovernor { + _implementation.acceptProxy(_proxy); +} + +function acceptProxyAndCall( + GraphUpgradeable _implementation, + IGraphProxy _proxy, + bytes calldata _data +) external onlyGovernor { + _implementation.acceptProxyAndCall(_proxy, _data); +} +``` + +The interface was **missing the first parameter** (`implementation` address) from both functions. It had copied the signature from GraphUpgradeable instead of using the correct GraphProxyAdmin signature. + +## Impact + +### Why This Mattered + +The deployment package (`@graphprotocol/deployment`) needs to call `acceptProxy` with the correct signature to upgrade proxy contracts. The function requires TWO parameters: + +1. The implementation contract address +2. The proxy contract address + +Because the interface was wrong, the deployment code had to work around it by loading the full contract ABI instead of using the cleaner interface ABI: + +```typescript +// packages/deployment/lib/abis.ts (old workaround) +// Note: Load from actual contract, not interface, because IGraphProxyAdmin is outdated +// Interface shows: acceptProxy(IGraphProxy proxy) +// Contract has: acceptProxy(GraphUpgradeable _implementation, IGraphProxy _proxy) +export const GRAPH_PROXY_ADMIN_ABI = loadAbi( + '@graphprotocol/contracts/artifacts/contracts/upgrades/GraphProxyAdmin.sol/GraphProxyAdmin.json', +) +``` + +### Why Horizon is Not Affected + +GraphDirectory in horizon (`packages/horizon/contracts/utilities/GraphDirectory.sol`) imports and uses IGraphProxyAdmin, but **only as a type reference**: + +```solidity +IGraphProxyAdmin private immutable GRAPH_PROXY_ADMIN; + +constructor(address controller) { + GRAPH_PROXY_ADMIN = IGraphProxyAdmin(_getContractFromController("GraphProxyAdmin")); +} + +function _graphProxyAdmin() internal view returns (IGraphProxyAdmin) { + return GRAPH_PROXY_ADMIN; +} +``` + +GraphDirectory: + +- Stores the address as an immutable reference +- Returns it via a getter function +- **Never calls any methods on IGraphProxyAdmin** (like `acceptProxy`) + +Since horizon doesn't call the methods, fixing the interface signature doesn't break horizon. + +## Fix Applied + +### Updated Interface + +```solidity +/** + * @notice Accept ownership of a proxy contract + * @param implementation The implementation contract accepting the proxy + * @param proxy The proxy contract to accept + */ +function acceptProxy(address implementation, IGraphProxy proxy) external; + +/** + * @notice Accept ownership of a proxy contract and call a function + * @param implementation The implementation contract accepting the proxy + * @param proxy The proxy contract to accept + * @param data The calldata to execute after accepting + */ +function acceptProxyAndCall(address implementation, IGraphProxy proxy, bytes calldata data) external; +``` + +**Notes on parameter type choice:** + +- Used `address` instead of `GraphUpgradeable` for the implementation parameter +- This avoids creating a dependency from interfaces package to contracts package +- The actual contract uses `GraphUpgradeable`, but `address` is compatible (Solidity allows passing addresses for contract types) +- The ABI encoding is identical - both produce the same function selector and parameter encoding + +**Call flow for context:** + +``` +Deployer/Governor + → GraphProxyAdmin.acceptProxy(implAddress, proxyAddress) ← IGraphProxyAdmin represents THIS + → implAddress.acceptProxy(proxyAddress) ← GraphUpgradeable provides this + → proxyAddress.acceptUpgrade() +``` + +### Updated Deployment Code + +Removed the workaround comment and switched to using the interface: + +```typescript +// packages/deployment/lib/abis.ts (now clean) +export const GRAPH_PROXY_ADMIN_ABI = loadAbi( + '@graphprotocol/interfaces/artifacts/contracts/contracts/upgrades/IGraphProxyAdmin.sol/IGraphProxyAdmin.json', +) +``` + +## Files Changed + +1. `packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol` + - Fixed `acceptProxy` signature + - Fixed `acceptProxyAndCall` signature + +2. `packages/deployment/lib/abis.ts` + - Removed workaround comment + - Changed to load from interface instead of full contract + +## Testing + +Build verification: + +- ✅ interfaces package builds successfully +- ✅ deployment package dependencies build successfully +- ✅ No TypeScript compilation errors +- ✅ Hardhat compilation successful + +The deployment code in `packages/deployment/lib/upgrade-implementation.ts` already calls acceptProxy with both parameters: + +```typescript +const acceptData = encodeFunctionData({ + abi: GRAPH_PROXY_ADMIN_ABI, + functionName: 'acceptProxy', + args: [pendingImpl as `0x${string}`, proxyAddress as `0x${string}`], +}) +``` + +This call now works with the corrected interface ABI. + +## Recommendation + +This fix should be safe to merge. The interface now accurately reflects the actual contract implementation, and no existing code is broken by the change since: + +1. Deployment already expects the two-parameter signature +2. Horizon only uses the type, never calls the methods +3. The fix aligns the interface with reality, reducing confusion + +## Questions for Team Review + +1. Are there other consumers of IGraphProxyAdmin that might be affected? +2. Should this be considered a breaking change requiring a major version bump of @graphprotocol/interfaces? +3. Is there a reason the interface was historically wrong (legacy compatibility concerns)? diff --git a/docs/Linting.md b/docs/Linting.md new file mode 100644 index 000000000..f77a0c61a --- /dev/null +++ b/docs/Linting.md @@ -0,0 +1,210 @@ +# Linting Configuration + +This monorepo uses a comprehensive linting setup with multiple tools to ensure code quality and consistency across all packages. + +## Linting Tools Overview + +- **ESLint**: JavaScript/TypeScript code quality and style enforcement +- **Prettier**: Code formatting for JavaScript, TypeScript, JSON, Markdown, YAML, and Solidity +- **Solhint**: Solidity-specific linting for smart contracts +- **Forge Lint**: Foundry's Solidity linter (for packages using Forge) +- **TODO Check**: Reports TODO/FIXME/XXX/HACK comments in Solidity files (informational) +- **Markdownlint**: Markdown formatting and style consistency +- **YAML Lint**: YAML file validation and formatting + +## Configuration Architecture + +The linting configuration follows a hierarchical structure where packages inherit from root-level configurations. + +### ESLint Configuration + +- **Root Configuration**: `eslint.config.mjs` - Modern flat config format +- **Direct Command**: `npx eslint '**/*.{js,ts,cjs,mjs,jsx,tsx}' --fix` +- **Behavior**: ESLint automatically searches up parent directories to find configuration files +- **Package Inheritance**: Packages automatically inherit the root ESLint configuration without needing local config files +- **Global Ignores**: Configured to exclude autogenerated files (`.graphclient-extracted/`, `lib/`) and build outputs + +### Prettier Configuration + +- **Root Configuration**: `prettier.config.cjs` - Base formatting rules for all file types +- **Direct Command**: `npx prettier -w --cache '**/*.{js,ts,cjs,mjs,jsx,tsx,json,md,sol,yml,yaml}'` +- **Package Inheritance**: Packages that need Prettier must have a `prettier.config.cjs` file that inherits from the shared config +- **Example Package Config**: + + ```javascript + const baseConfig = require('../../prettier.config.cjs') + module.exports = { ...baseConfig } + ``` + +- **Ignore Files**: `.prettierignore` excludes lock files, build outputs, and third-party dependencies + +### Solidity Linting (Solhint) + +- **Root Configuration**: `.solhint.json` - Base Solidity linting rules extending `solhint:recommended` +- **Direct Command**: `npx solhint 'contracts/**/*.sol'` (add `--fix` for auto-fixing) +- **List Applied Rules**: `npx solhint list-rules` +- **Package Inheritance**: Packages can extend the root config with package-specific rules +- **Configuration Inheritance Limitation**: Solhint has a limitation where nested `extends` don't work properly. When a local config extends a parent config that itself extends `solhint:recommended`, the built-in ruleset is ignored. +- **Recommended Package Extension Pattern**: + + ```json + { + "extends": ["solhint:recommended", "./../../.solhint.json"], + "rules": { + "no-console": "off", + "import-path-check": "off" + } + } + ``` + +### Forge Lint + +Forge lint is Foundry's built-in Solidity linter. Packages using Foundry can add `lint:forge` to their lint scripts. + +- **Package Configuration**: `foundry.toml` with `[lint]` section +- **Direct Command**: `forge lint` or `forge lint contracts/` +- **Available in**: Packages with `lint:forge` script defined (horizon, subgraph-service, issuance) + +### Markdown Linting (Markdownlint) + +- **Root Configuration**: `.markdownlint.json` - Markdown formatting and style rules +- **Direct Command**: `npx markdownlint '**/*.md' --fix` +- **Ignore Files**: `.markdownlintignore` automatically picked up by markdownlint CLI +- **Package Inheritance**: Packages that need Markdownlint must have a `.markdownlint.json` file that extends the root config +- **Example Package Config**: + + ```json + { + "extends": "../../.markdownlint.json" + } + ``` + +## Inline Lint Suppression + +When you need to suppress a lint warning for a specific line or item, use the appropriate comment directive. + +### Solhint Suppression + +```solidity +// Disable for next line (can have intervening comments before target) +// solhint-disable-next-line func-name-mixedcase + +// Disable for previous line +function example() { + // solhint-disable-previous-line no-empty-blocks +} + +// Block disable/enable +// solhint-disable no-console +console.log("debug"); +// solhint-enable no-console +``` + +### Forge Lint Suppression + +```solidity + // Disable for next item (function, struct, etc. - AST-aware) +// forge-lint: disable-next-item(mixed-case-function) + +// Note: forge-lint uses "next-item" not "next-line" +// It applies to the entire syntactic construct, not just the next line +``` + +### Combined Example + +For functions that need both Solhint and Forge lint suppression (e.g., OpenZeppelin-style initializers): + +```solidity +// solhint-disable-next-line func-name-mixedcase +// forge-lint: disable-next-item(mixed-case-function) +/** + * @notice Internal function to initialize the contract + */ +function __ContractName_init(address param) internal { + // initialization code +} +``` + +Note: Place suppression comments before natspec to avoid warnings about comments not directly preceding the function. + +## Linting Scripts + +### Root Level Scripts + +```bash +# Run all linting tools +pnpm lint + +# Individual linting commands +pnpm lint:ts # ESLint + Prettier for TypeScript/JavaScript +pnpm lint:sol # TODO check + Solhint + Prettier for Solidity (runs recursively) +pnpm lint:forge # Forge lint for packages that support it +pnpm lint:md # Markdownlint + Prettier for Markdown +pnpm lint:json # Prettier for JSON files +pnpm lint:yaml # YAML linting + Prettier + +# Lint only staged files (useful for manual pre-commit checks) +pnpm lint:staged # Run linting on git-staged files only +``` + +### Package Level Scripts + +Each package can define its own linting scripts that work with the inherited configurations: + +```bash +# Example from packages/contracts +pnpm lint:sol # Solhint for contracts in this package only +pnpm lint:ts # ESLint for TypeScript files in this package +``` + +## Pre-commit Hooks (lint-staged) + +The repository uses `lint-staged` with Husky to run linting on staged files before commits: + +- **Automatic**: Runs automatically on `git commit` via Husky pre-commit hook +- **Manual**: Run `pnpm lint:staged` to manually check staged files before committing +- **Configuration**: Root `package.json` contains lint-staged configuration +- **Custom Script**: `scripts/lint-staged-run.sh` filters out generated files that shouldn't be linted +- **File Type Handling**: + - `.{js,ts,cjs,mjs,jsx,tsx}`: ESLint + Prettier + - `.sol`: TODO check + Solhint + Prettier + - `.md`: Markdownlint + Prettier + - `.json`: Prettier only + - `.{yml,yaml}`: YAML lint + Prettier + +**Usage**: `pnpm lint:staged` is particularly useful when you want to check what linting changes will be applied to your staged files before actually committing. + +## TODO Comment Checking + +The repository reports TODO comments in Solidity files to help track technical debt: + +- **Scope**: Applies only to Solidity (`.sol`) files +- **Detection**: Finds TODO, FIXME, XXX, and HACK comments (case-insensitive) +- **Behavior**: Informational only - does not block commits or fail linting +- **Included in**: `lint:sol` and `lint:staged` scripts +- **Script**: `scripts/check-todos.sh` (must be run from repository root) + +## Configuration Files Reference + +| Tool | Root Config | Package Config | Ignore Files | +| ------------ | ------------------------ | -------------------------------- | ---------------------------- | +| ESLint | `eslint.config.mjs` | Auto-inherited | Built into config | +| Prettier | `prettier.config.cjs` | `prettier.config.cjs` (inherits) | `.prettierignore` | +| Solhint | `.solhint.json` | `.solhint.json` (array extends) | N/A | +| Forge Lint | N/A | `foundry.toml` `[lint]` section | N/A | +| TODO Check | `scripts/check-todos.sh` | N/A | N/A | +| Markdownlint | `.markdownlint.json` | `.markdownlint.json` (extends) | `.markdownlintignore` | +| Lint-staged | `package.json` | N/A | `scripts/lint-staged-run.sh` | + +## Troubleshooting + +- **ESLint not finding config**: ESLint searches up parent directories automatically - no local config needed +- **Prettier not working**: Packages need a `prettier.config.cjs` that inherits from root config +- **Markdownlint not working**: Packages need a `.markdownlint.json` that extends root config +- **Solhint missing rules**: If extending a parent config, use array format: `["solhint:recommended", "./../../.solhint.json"]` to ensure all rules are loaded +- **Solhint inheritance not working**: Nested extends don't work - parent config's `solhint:recommended` won't be inherited with simple string extends +- **Solhint rule reference**: Use `npx solhint list-rules` to see all available rules and their descriptions +- **Generated files being linted**: Check ignore patterns in `.prettierignore`, `.markdownlintignore`, and ESLint config +- **Preview lint changes before commit**: Use `pnpm lint:staged` to see what changes will be applied to staged files +- **Commit blocked by linting**: Fix the linting issues or use `git commit --no-verify` to bypass (not recommended) +- **Forge lint symlink errors**: Forge follows symlinks when scanning for files, which can cause "Too many levels of symbolic links" errors in packages with nested workspace dependencies. If a package has a `test/` subproject with workspace symlinks that create loops, rename the directory (e.g., to `testing/`) so forge doesn't scan it by default. diff --git a/docs/PaymentsTrustModel.md b/docs/PaymentsTrustModel.md new file mode 100644 index 000000000..07bff2468 --- /dev/null +++ b/docs/PaymentsTrustModel.md @@ -0,0 +1,176 @@ +# Payments Trust Model + +This document describes the trust assumptions between the five core actors in the Graph Horizon payments protocol: **payer**, **collector**, **data service**, **receiver**, and **escrow**. The general model is described first, followed by specifics of the current implementation (RecurringCollector, SubgraphService, RAM). + +## Trust Summary + +| Relationship | Trust | Mitigation | +| --------------------------- | ----------------------------------------- | ------------------------------------------------ | +| Payer → Collector | Enforces agreed caps | Protocol-deployed; escrow caps absolute exposure | +| Payer → Receiver | Claimed work is honest | Post-hoc disputes + stake locking | +| Receiver → Payer (EOA) | Escrow stays funded | Thaw period; on-chain visibility | +| Receiver → Payer (contract) | Escrow stays funded; not block collection | RecurringAgreementManager: protocol-deployed | +| Receiver → Collector | Correctly caps and forwards payment | Protocol-deployed; code is transparent | +| Receiver → Data Service | Correct computation; not paused | Protocol-deployed; code is transparent | +| Receiver → Escrow | Releases funds on valid collection | Stateless; no discretionary logic | +| Data Service ↔ Collector | Each trusts the other's domain | Two-layer capping; independent validation | + +## Actors + +| Actor | Role | Examples | +| ---------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| **Payer** | Funds escrow; authorizes collector contracts | RecurringAgreementManager (protocol-managed), external payer (ECDSA-signed) | +| **Collector** | Validates payment requests; enforces per-agreement caps | RecurringCollector | +| **Data service** | Entry point for collection; computes amounts earned | SubgraphService | +| **Receiver** | Service provider receiving payment | Indexer | +| **Escrow** | Holds GRT per (payer, collector, receiver) tuple; enforces thaw periods | PaymentsEscrow | + +## Payment Flow (General Model) + +``` +│ Receiver +└─> Data Service.collect(work done) + └─> Collector.collect(tokens earned) + │ validates payment terms, caps amount + └─> PaymentsEscrow.collect(tokens to collect) + └─> GraphPayments.collect(tokens collected) + │ distributes to: protocol (burned), data service, delegation pool, receiver + <───┘ + <───┘ + <───┘ +<───┘ +``` + +Any data service and collector can plug into this flow. The PaymentsEscrow and GraphPayments layers are fixed protocol infrastructure. The data service computes its own token amount; the collector independently caps it; the actual payment is `min(tokens earned, agreement cap)`, and escrow reverts if balance is insufficient. + +### RecurringCollector Extensions + +RecurringCollector adds payer callbacks when the payer is a contract: + +``` +│ Receiver +└─> Data Service.collect(work done) + └─> RecurringCollector.collect(tokens earned) + │ validates agreement terms, caps amount + │ validates receiver has active provision with data service + │ if 0 < tokensToCollect AND payer is contract: + │ if implements IProviderEligibility: + │ require payer.isEligible(receiver) ← can BLOCK + │ try payer.beforeCollection(id, tokens) (can't block) + └─> PaymentsEscrow.collect(tokens to collect) + └─> GraphPayments.collect(tokens collected) + │ distributes to: protocol (burned), data service, delegation pool, receiver + <───┘ + <───┘ + │ if payer is contract: (even if tokensToCollect == 0) + │ try payer.afterCollection(id, tokens) (can't block) + <───┘ +<───┘ +``` + +- **`isEligible`**: fail-open gate — only an explicit return of `0` blocks collection; call failures (reverts, malformed data) are ignored to prevent a buggy payer from griefing the receiver. Only called when `0 < tokensToCollect`. +- **`beforeCollection`**: try-catch — allows payer to top up escrow (RAM uses this for JIT deposits), but cannot block (though a malicious contract payer could consume excessive gas). Only called when `0 < tokensToCollect`. +- **`afterCollection`**: try-catch — allows payer to reconcile state post-collection, cannot block (same gas exhaustion caveat). Called even when `tokensToCollect == 0` (zero-token collections still trigger reconciliation). + +## Trust Relationships + +### Payer → Collector + +**Trust required**: The payer authorizes the collector contract and trusts it to enforce payment terms; that it will not collect more than the agreed-upon amounts per collection period. + +**Mitigation**: The collector is a protocol-deployed contract with fixed logic. The escrow balance provides an absolute ceiling — the collector cannot extract more than the deposited balance. + +> _RecurringCollector_: enforces per-agreement caps of `maxOngoingTokensPerSecond × maxSecondsPerCollection` (plus `maxInitialTokens` on first collection) per collection window. The payer's exposure is bounded by the agreement terms they signed or authorized. + +### Payer → Receiver + +**Trust required**: The receiver is paid immediately when collecting based on claimed work done. The payer relies on post-hoc enforcement rather than on-chain validation of the receiver's claims. + +**Mitigation**: The payment protocol itself is agnostic to what evidence the receiver provides — that is the data service's domain. + +> _SubgraphService_: the receiver submits a POI (Proof of Indexing) which is emitted in events but not validated on-chain. Payment proceeds regardless of POI correctness. The dispute system provides post-hoc enforcement: fishermen can challenge invalid POIs, and the indexer's locked stake (`tokensCollected × stakeToFeesRatio`) serves as economic collateral during the dispute period. +> +> _RAM as payer_: the payer is the protocol itself, and if configured, an eligibility oracle gates the receiver's ability to collect (checked by RecurringCollector via `IProviderEligibility`). + +### Receiver → Payer + +**Trust minimised by escrow**: The escrow is the primary trust-minimisation mechanism — to avoid trust in the payer, the receiver should bound uncollected work to what the escrow guarantees rather than relying on the payer to top up. + +Caveats on effective escrow (contract payers introduce additional trust requirements — see caveat 3): + +1. **Thawing reduces effective balance** — a payer can initiate a thaw; once the thaw period completes, those tokens are withdrawable. The receiver should account for the thawing period and any in-progress thaws when assessing available escrow. +2. **Cancellation freezes the collection window** at `canceledAt` — the receiver can still collect for the period up to cancellation (with `minSecondsPerCollection` bypassed), but no further. +3. **Contract payers can block** — if the payer is a contract that implements `IProviderEligibility`, it can deny collection via `isEligible` (see [RecurringCollector Extensions](#recurringcollector-extensions)). + +**Mitigation**: The thawing period provides a window for the receiver to collect before funds are withdrawn. The escrow balance and thaw state are publicly visible on-chain. + +> _RAM as payer_: RAM automates escrow maintenance (Full/OnDemand/JIT modes). When not operating in Full escrow mode, the receiver also depends on RAM's ability to fund at collection time. Mitigation: RAM is a protocol-deployed contract — its funding logic is transparent and predictable, with no adversarial incentive to deny payment. + +### Receiver → Data Service + +**Trust required**: The receiver (or their operator) calls the data service's `collect()` directly. The receiver trusts it to: + +1. **Compute amounts correctly** — the data service determines its claim of what is earned +2. **Not be paused** — the data service may have a pause mechanism that would block collection + +**Mitigation**: The data service is a protocol-deployed contract. Token amounts are capped by the collector independently, so data service overstatement is bounded. + +> _SubgraphService_: `_tokensToCollect` computes the amount earned. The `enforceService` modifier requires the caller to be authorized by the receiver (indexer) for their provision. + +### Receiver → Escrow + +**Trust required**: The receiver trusts escrow to release funds when a valid collection is presented. The receiver has no direct access to escrow — funds can only flow through the authorized collection path (data service → collector → escrow → GraphPayments → receiver). + +**Mitigation**: Escrow is a stateless intermediary — it debits the payer's balance and forwards to GraphPayments. No discretionary logic. The failure modes are insufficient balance or protocol-wide pause (escrow's `collect` has a `notPaused` modifier). + +### Data Service → Collector + +**Trust required**: The data service trusts the collector to faithfully enforce temporal and amount-based caps. The data service provides its own token calculation, but the collector applies `min(requested, cap)` — the data service relies on this capping being correct. + +**Mitigation**: Both are protocol-deployed contracts. The two-layer capping model means neither layer alone determines the payout — the minimum of both applies. + +### Collector → Data Service + +**Trust required**: The collector trusts the data service to call `collect()` only with valid, legitimate payment requests. The collector validates payment terms but relies on the data service to verify service delivery. + +**Mitigation**: The collector validates its own domain (agreement existence, temporal bounds, amount caps) independently. + +> _RecurringCollector + SubgraphService_: the collector validates RCA terms; the data service verifies allocation status and emits POIs for dispute. + +## Who Can Block Collection? + +Which actors can prevent a collection from succeeding, and how: + +| Actor | Can block? | How (general model) | +| ------------ | ---------- | ---------------------------------------------- | +| Payer | Yes | Contract payer only, via `isEligible` | +| Collector | Yes | Reject payment request based on its own rules | +| Data service | Yes | Pause mechanism; code-level revert conditions | +| Receiver | No | Can only initiate, not block | +| Escrow | Yes | Insufficient balance; also protocol-wide pause | + +### Implementation-Specific Notes + +**ECDSA-signed agreements** (external payer): the payer is an EOA and has no on-chain blocking mechanism. The receiver's trust is bounded by the current escrow balance (minus any thawing amount). + +**RAM-managed agreements** (protocol payer): the payer (RAM) has no adversarial incentive to block. If an eligibility oracle is configured, blocking trust effectively transfers to the oracle (see [RecurringCollector Extensions](#recurringcollector-extensions)). + +## Trust Reduction Mechanisms + +| Mechanism | What it bounds | Actor protected | Scope | +| --------------------------------------------------------------- | ------------------------------------------------------------------ | --------------- | ------------------------ | +| Escrow deposit + thaw period | Payer can't instantly withdraw | Receiver | General | +| Two-layer token capping | Neither data service nor collector alone sets amount | Payer | General | +| Collector-enforced agreement terms | Per-collection exposure | Payer | General | +| Cancellation still allows final collection | Receiver collects accrued amount | Receiver | General | +| Dispute system + stake locking | Invalid POIs are challengeable | Payer / network | SubgraphService | +| Eligibility oracle | Ineligible receivers denied | Payer | RecurringCollector + RAM | +| `lastCollectionAt` advancing only through validated collections | No fake liveness signals (advances even on zero-token collections) | All | RecurringCollector | + +## Related Documents + +- [MaxSecondsPerCollectionCap.md](../packages/horizon/contracts/payments/collectors/MaxSecondsPerCollectionCap.md) — Two-layer capping semantics +- [RecurringAgreementManager.md](../packages/issuance/contracts/agreement/RecurringAgreementManager.md) — RAM escrow management +- [RewardsEligibilityOracle.md](../packages/issuance/contracts/eligibility/RewardsEligibilityOracle.md) — Oracle trust model and failsafe +- [RewardAccountingSafety.md](./RewardAccountingSafety.md) — Reward accounting invariants +- [RewardConditions.md](./RewardConditions.md) — Reclaim conditions diff --git a/docs/RewardAccountingSafety.md b/docs/RewardAccountingSafety.md new file mode 100644 index 000000000..23270a345 --- /dev/null +++ b/docs/RewardAccountingSafety.md @@ -0,0 +1,177 @@ +# Reward Accounting Safety + +This document describes the mechanisms that prevent reward mis-accounting (double-counting or unintentional loss). + +## Two-Level Accumulation Model + +Rewards flow through two levels before reaching allocations: + +``` +Global Issuance + │ + ▼ (proportional to signal) +┌──────────────────────────────────────────────┐ +│ Level 1: Signal → Subgraph │ +│ accRewardsPerSignal → accRewardsForSubgraph │ +└──────────────────────────────────────────────┘ + │ + ▼ (proportional to allocated tokens) +┌─────────────────────────────────────────┐ +│ Level 2: Subgraph → Allocation │ +│ accRewardsPerAllocatedToken → claim │ +└─────────────────────────────────────────┘ +``` + +Each level uses the same pattern: an accumulator increases over time, and participants snapshot their starting point to calculate their share. + +## Core Safety Mechanism: Snapshots + +**Principle**: Rewards = (current_accumulator - snapshot) × tokens + +Snapshots prevent double-counting by recording each participant's starting point: + +| Component | Accumulator | Snapshot | Prevents | +| ---------- | ----------------------------- | ----------------------------- | ----------------------------------------------- | +| Subgraph | `accRewardsPerSignal` | `accRewardsPerSignalSnapshot` | Same subgraph counting same reward period twice | +| Allocation | `accRewardsPerAllocatedToken` | Stored in allocation state | Same allocation claiming same rewards twice | + +After any update, snapshot = current accumulator. Next calculation starts from zero delta. + +## Key Invariants + +### 1. Monotonic Accumulators + +All accumulators only increase (never decrease): + +| Accumulator | Behavior When Not Claimable | +| ----------------------------- | ----------------------------------------------------------- | +| `accRewardsPerSignal` | Always increases | +| `accRewardsForSubgraph` | Stops increasing (rewards reclaimed, not accumulated) | +| `accRewardsPerAllocatedToken` | Stops increasing (rewards reclaimed instead of distributed) | + +When a subgraph is not claimable (denied or below minimum signal), rewards are reclaimed directly without updating `accRewardsForSubgraph`. This means `accRewardsForSubgraph` only tracks rewards that are actually distributed to allocations. + +**Why it matters**: Decreasing accumulators would cause negative reward calculations or allow re-claiming past rewards. + +### 2. Snapshot Consistency + +After every state update, snapshot equals current accumulator value. + +**Why it matters**: Stale snapshots would allow the same reward period to be counted multiple times. + +### 3. Update-Before-Change + +Accumulators must be updated BEFORE any state change that affects reward distribution: + +- Before `issuancePerBlock` changes → call `updateAccRewardsPerSignal()` +- Before signal changes → call `onSubgraphSignalUpdate()` +- Before allocation changes → call `onSubgraphAllocationUpdate()` + +**Why it matters**: Changing distribution parameters without first crediting accrued rewards would lose or misattribute those rewards. + +## Critical Call Ordering + +### Allocation Creation + +```solidity +// In AllocationManager._allocate(): +_allocationData = _getAllocationData(_subgraphDeploymentId); // ① Calls onSubgraphAllocationUpdate +_allocations.create(...); // ② Creates allocation +_allocations.snapshotRewards(..., onSubgraphAllocationUpdate()); // ③ Updates snapshot +``` + +**Why this order matters**: + +- Step ① with zero allocations → triggers NO_ALLOCATED_TOKENS reclaim for gap period +- Step ② creates allocation → now allocatedTokens > 0 +- Step ③ same block → newRewards ≈ 0, just confirms snapshot + +**If reversed**: Gap-period rewards would be distributed to accumulator but no allocation could claim them (all snapshots would be at/above post-distribution level). + +### Reward Claiming + +```solidity +// In AllocationManager._presentPoi(): +rewards = takeRewards(_allocationId); // ① Mints rewards +snapshotRewards(_allocationId, onSubgraphAllocationUpdate(...)); // ② Updates snapshot +clearPendingRewards(_allocationId); // ③ Clears pending +``` + +**Why this order matters**: + +- Step ① calculates and mints based on current snapshot +- Step ② updates snapshot to current accumulator +- Future claims start from new snapshot (zero delta for same block) + +## Reclaim as Safety Net + +Every reward path that cannot reach an allocation has a reclaim handler: + +| Condition | When Triggered | Reclaim Reason | +| -------------------- | ------------------------------------------------------------ | ------------------------ | +| No global signal | `updateAccRewardsPerSignal()` with signalledTokens = 0 | `NO_SIGNAL` | +| Subgraph denied | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `SUBGRAPH_DENIED` | +| Below minimum signal | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `BELOW_MINIMUM_SIGNAL` | +| No allocations | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `NO_ALLOCATED_TOKENS` | +| Indexer ineligible | `takeRewards()` | `INDEXER_INELIGIBLE` | +| Stale/zero POI | `_presentPoi()` | `STALE_POI` / `ZERO_POI` | +| Allocation close | `_closeAllocation()` | `CLOSE_ALLOCATION` | + +**Reclaim priority**: reason-specific address → defaultReclaimAddress → dropped (no mint) + +## Potential Failure Modes (Mitigated) + +| Failure Mode | How Prevented | +| ---------------------------- | ------------------------------------------------------------------------------------ | +| Double-mint same rewards | Snapshot updated after every claim; same-block calls return ~0 | +| Rewards stuck in accumulator | NO_ALLOCATED_TOKENS reclaim before allocation creation | +| Gap period loss | `_getAllocationData` calls `onSubgraphAllocationUpdate` before allocation exists | +| Denial-period accumulation | `accRewardsForSubgraph` tracks; `accRewardsPerAllocatedToken` frozen; diff reclaimed | +| Signal change mid-period | `onSubgraphSignalUpdate` hook called before signal changes | + +## Division of Responsibility + +RewardsManager and issuers share responsibility for correct reward accounting: + +**RewardsManager** handles what it can observe: + +- Reclaims rewards when subgraph conditions prevent distribution (denied, below minimum, zero allocations) +- Denies rewards at claim time when indexer is ineligible +- Maintains accumulator and snapshot state + +**Issuers** control claim timing and can defer: + +- AllocationManager defers claims for `SUBGRAPH_DENIED` and `ALLOCATION_TOO_YOUNG` by returning early +- This preserves allocation state so rewards remain claimable after conditions change +- RM cannot know issuer intent, so issuers must decide when to attempt claims + +**Example - Subgraph Denial** (see [RewardConditions.md](./RewardConditions.md#subgraph_denied) for full details): + +- RM: Reclaims new rewards; freezes `accRewardsPerAllocatedToken` +- AM: Defers claim; preserves uncollected rewards (no snapshot update) +- After undeny: AM can claim preserved uncollected rewards + +## Issuer Requirements + +RewardsManager relies on issuers to maintain shared state correctly. + +**Required hook**: + +| Hook | When to Call | +| ---------------------------- | ------------------------- | +| `onSubgraphAllocationUpdate` | Before allocation changes | + +Note: If the issuer collects curation fees (`curation.collect()`), it must also call `onSubgraphSignalUpdate` before the collect since that changes signal. SubgraphService does this in `_collectQueryFees`. + +**Allocation snapshot management**: + +Allocation snapshots are stored in issuer contracts, not RewardsManager. After each `takeRewards()` or `reclaimRewards()` call, issuers must update the allocation's snapshot to the current `accRewardsPerAllocatedToken`. Failure to snapshot allows the same rewards to be claimed again. + +**Authorized issuers**: SubgraphService (active), Staking (deprecated, legacy allocations only) + +## Other Hook Callers + +| Hook | Caller | Trigger | +| --------------------------- | ------------------------- | ---------------------------------------------- | +| `updateAccRewardsPerSignal` | RewardsManager (internal) | Before `issuancePerBlock` or allocator changes | +| `onSubgraphSignalUpdate` | Curation | Before mint/burn signal | diff --git a/docs/RewardConditions.md b/docs/RewardConditions.md new file mode 100644 index 000000000..6ccc891df --- /dev/null +++ b/docs/RewardConditions.md @@ -0,0 +1,249 @@ +# Reward Conditions: Collection and Reclaim Reference + +Quick reference for all reward conditions and how they are handled across RewardsManager and AllocationManager. + +## Summary Table + +| Condition | Identifier | Handled By | Action | Rewards Outcome | +| ---------------------- | ----------------------------------- | ----------------- | ------------------------- | -------------------------------------- | +| `NONE` | `bytes32(0)` | — | Normal path | Claimed by indexer | +| `NO_SIGNAL` | `keccak256("NO_SIGNAL")` | RewardsManager | Reclaim | To reclaim address | +| `SUBGRAPH_DENIED` | `keccak256("SUBGRAPH_DENIED")` | Both | Reclaim (RM) / Defer (AM) | New: reclaimed; Uncollected: preserved | +| `BELOW_MINIMUM_SIGNAL` | `keccak256("BELOW_MINIMUM_SIGNAL")` | RewardsManager | Reclaim | To reclaim address | +| `NO_ALLOCATED_TOKENS` | `keccak256("NO_ALLOCATED_TOKENS")` | RewardsManager | Reclaim | To reclaim address | +| `INDEXER_INELIGIBLE` | `keccak256("INDEXER_INELIGIBLE")` | RewardsManager | Reclaim | To reclaim address | +| `STALE_POI` | `keccak256("STALE_POI")` | AllocationManager | Reclaim | To reclaim address | +| `ZERO_POI` | `keccak256("ZERO_POI")` | AllocationManager | Reclaim | To reclaim address | +| `ALLOCATION_TOO_YOUNG` | `keccak256("ALLOCATION_TOO_YOUNG")` | AllocationManager | Defer | Preserved for later | +| `CLOSE_ALLOCATION` | `keccak256("CLOSE_ALLOCATION")` | AllocationManager | Reclaim | To reclaim address | + +## Reward Distribution Levels + +Rewards flow through three levels, with reclaim possible at each: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Level 0: Global Issuance │ +│ ───────────────────────────────────────────────────────────────── │ +│ updateAccRewardsPerSignal() │ +│ │ +│ Reclaim: NO_SIGNAL (when total signalled tokens = 0) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ proportional to signal +┌─────────────────────────────────────────────────────────────────────┐ +│ Level 1: Subgraph │ +│ ───────────────────────────────────────────────────────────────── │ +│ onSubgraphSignalUpdate() / onSubgraphAllocationUpdate() │ +│ │ +│ Reclaim: SUBGRAPH_DENIED, BELOW_MINIMUM_SIGNAL, NO_ALLOCATED_TOKENS │ +│ │ +│ Behavior: │ +│ - accRewardsForSubgraph only increases when claimable │ +│ - accRewardsPerAllocatedToken only increases when claimable │ +│ - Non-claimable rewards are reclaimed immediately, not stored │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ proportional to allocated tokens +┌─────────────────────────────────────────────────────────────────────┐ +│ Level 2: Allocation │ +│ ───────────────────────────────────────────────────────────────── │ +│ takeRewards() / reclaimRewards() / _presentPoi() │ +│ │ +│ Reclaim: INDEXER_INELIGIBLE (at takeRewards) │ +│ STALE_POI, ZERO_POI, CLOSE_ALLOCATION (at _presentPoi) │ +│ │ +│ Defer: SUBGRAPH_DENIED, ALLOCATION_TOO_YOUNG (preserves state) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Condition Details + +### Global Level (RewardsManager.updateAccRewardsPerSignal) + +#### NO_SIGNAL + +- **Trigger**: Total signalled tokens across all subgraphs = 0 +- **Effect**: Issuance cannot be distributed proportionally to signal +- **Handling**: Reclaim to configured address (or drop if unconfigured) + +### Subgraph Level (RewardsManager.onSubgraphAllocationUpdate) + +#### SUBGRAPH_DENIED + +- **Trigger**: `isDenied(subgraphDeploymentId)` returns true +- **Effect**: `accRewardsPerAllocatedToken` stops increasing +- **Handling**: New rewards reclaimed; accumulator frozen (uncollected rewards preserved) +- **Note**: If no SUBGRAPH_DENIED reclaim address AND signal < minimum, reclaims as BELOW_MINIMUM_SIGNAL instead + +**Reward disposition by period:** + +| Period | Disposition | +| ------------- | -------------------------------------------------------- | +| Before denial | Claimable after undeny | +| During denial | Reclaimed to protocol (or dropped if no reclaim address) | +| Post-undeny | Claimable normally | + +**Effect on allocations:** + +- _Existing allocations_: Uncollected rewards preserved (accumulator frozen, snapshot unchanged); cannot claim while denied; claimable after undeny +- _New allocations (created while denied)_: Start with frozen baseline; only earn rewards after undeny +- _POI presentation_: Indexers should continue presenting POIs to prevent staleness (returns 0 but maintains allocation health) + +**Edge cases:** + +| Scenario | Behavior | +| ---------------------------------- | --------------------------------------------------------- | +| All allocations close while denied | Frozen state preserved; new allocations use that baseline | +| Redundant deny/undeny calls | No state change (idempotent) | +| Zero reclaim address | Denial-period rewards dropped (never minted) | + +#### BELOW_MINIMUM_SIGNAL + +- **Trigger**: Subgraph signal < `minimumSubgraphSignal` (and not denied) +- **Effect**: `accRewardsPerAllocatedToken` stops increasing +- **Handling**: Rewards reclaimed to configured address + +#### NO_ALLOCATED_TOKENS + +- **Trigger**: Subgraph has signal but zero allocated tokens +- **Effect**: Rewards cannot be distributed to allocations +- **Handling**: Reclaim to configured address +- **Note**: Triggered when condition is NONE but no allocations exist, or when original condition has no reclaim address + +### Allocation Level (RewardsManager.takeRewards) + +#### INDEXER_INELIGIBLE + +- **Trigger**: `eligibilityOracle.isEligible(indexer)` returns false at claim time +- **Effect**: Indexer cannot claim earned rewards +- **Handling**: Rewards reclaimed to configured address +- **Precedence**: SUBGRAPH_DENIED takes precedence if both apply + +### Allocation Level (AllocationManager.\_presentPoi) + +Conditions checked in order (first match wins): + +#### STALE_POI + +- **Trigger**: `maxPOIStaleness` < Time since last POI +- **Effect**: Allocation locked out due to inactivity +- **Handling**: Rewards reclaimed; allocation snapshotted; pending cleared + +#### ZERO_POI + +- **Trigger**: POI submitted is `bytes32(0)` +- **Effect**: No proof of indexing work provided +- **Handling**: Rewards reclaimed; allocation snapshotted; pending cleared + +#### ALLOCATION_TOO_YOUNG + +- **Trigger**: `currentEpoch <= allocation.createdAtEpoch` +- **Effect**: Allocation hasn't existed for a full epoch +- **Handling**: **Deferred** (returns 0, no snapshot update, rewards preserved) + +#### SUBGRAPH_DENIED (soft deny) + +- **Trigger**: `isDenied(subgraphDeploymentId)` at POI presentation +- **Effect**: Cannot claim while denied +- **Handling**: **Deferred** (returns 0, no snapshot update, uncollected rewards preserved) + +#### CLOSE_ALLOCATION + +- **Trigger**: Allocation being closed (force or normal) +- **Effect**: Uncollected rewards cannot go to indexer +- **Handling**: Rewards reclaimed; allocation snapshotted + +## Action Types + +### Reclaim + +Rewards are minted to a configured reclaim address: + +1. Try reason-specific: `reclaimAddresses[condition]` +2. Fallback: `defaultReclaimAddress` +3. If neither configured: rewards dropped (not minted) + +Emits `RewardsReclaimed(reason, rewards, indexer, allocationId, subgraphDeploymentId)` + +### Defer + +Rewards are preserved for later collection: + +- Returns 0 without modifying allocation state +- No snapshot update (preserves claim position) +- Allows claiming when condition clears + +### Claim (Normal) + +Rewards minted to rewards issuer for distribution: + +- Emits `HorizonRewardsAssigned` +- Allocation snapshotted to prevent double-claim +- Pending rewards cleared + +## Reclaim Address Configuration + +```solidity +// Governor-only functions +setReclaimAddress(bytes32 reason, address newAddress) // Per-condition +setDefaultReclaimAddress(address newAddress) // Fallback + +// Example configuration +reclaimAddresses[SUBGRAPH_DENIED] = treasuryAddress; +reclaimAddresses[INDEXER_INELIGIBLE] = treasuryAddress; +reclaimAddresses[NO_SIGNAL] = treasuryAddress; +defaultReclaimAddress = treasuryAddress; // Catch-all +``` + +**Important**: Changes apply retroactively to all future reclaims. + +## Parameter Changes: minimumSubgraphSignal + +### Retroactive Application Risk + +When `minimumSubgraphSignal` is changed via `setMinimumSubgraphSignal()`, existing subgraphs are NOT automatically updated. When subgraphs are later updated (via signal/allocation changes), the **current** threshold is applied to ALL pending rewards since their last update, regardless of historical threshold values. + +**Impact:** + +| Change Direction | Effect | +| ------------------- | ------------------------------------------------------------------------ | +| Threshold increases | Pending rewards on previously eligible subgraphs are reclaimed | +| Threshold decreases | Previously ineligible subgraphs retroactively accumulate pending rewards | + +### Required Mitigation Process + +To prevent retroactive application to long historical periods: + +1. **Communicate** the planned threshold change with a specific future date +2. **Wait** - notice period allows participants to adjust signal if desired +3. **Identify** affected subgraphs off-chain (those crossing the threshold) +4. **Call** `onSubgraphSignalUpdate()` for all affected subgraphs to accumulate pending rewards under current eligibility rules +5. **Execute** threshold change via `setMinimumSubgraphSignal()` (promptly after step 4, ideally same block) + +**Responsibility:** Governance handles steps 3-5; participants may optionally adjust signal in step 2. + +For implementation details, see NatSpec documentation on `RewardsManager.setMinimumSubgraphSignal()`. + +## Key Behaviors + +### Snapshot Updates + +| Action | Updates Snapshot | Clears Pending | +| ------------ | ---------------- | -------------- | +| Claim (NONE) | Yes | Yes | +| Reclaim | Yes | Yes | +| Defer | No | No | + +### Accumulator Behavior When Not Claimable + +| Field | Behavior | +| ----------------------------- | ---------------------------------------------- | +| `accRewardsForSubgraph` | Does NOT increase (rewards reclaimed directly) | +| `accRewardsPerAllocatedToken` | Does NOT increase (rewards not distributed) | +| New rewards | Reclaimed immediately to configured address | +| Pre-existing stored rewards | Still shown as distributable in view functions | + +## Related Documentation + +- [RewardAccountingSafety.md](./RewardAccountingSafety.md) - Safety mechanisms and invariants diff --git a/docs/RewardsBehaviourChanges.md b/docs/RewardsBehaviourChanges.md new file mode 100644 index 000000000..63c17c4c2 --- /dev/null +++ b/docs/RewardsBehaviourChanges.md @@ -0,0 +1,175 @@ +# Rewards Behaviour Changes + +Functional summary of how reward behaviour changed between the Horizon mainnet baseline and the current issuance upgrade. + +## Activation Overview + +Changes fall into two categories: + +- **Automatic on upgrade:** New logic that activates immediately when the upgraded contracts are deployed behind their proxies. No governance action required. These include: zero-signal detection, zero-allocated-tokens reclaim, POI presentation paths (claim/reclaim/defer), allocation resize staleness check, allocation close reclaim, and the `POIPresented` event. + +- **Governance-gated:** Features that require explicit governance transactions after upgrade. Until configured, the system preserves legacy behaviour (rewards are dropped, not reclaimed). These include: setting the issuance allocator, configuring reclaim addresses (per-condition and default), setting the eligibility oracle, and changing the minimum subgraph signal threshold. + +This two-phase approach allows a safe upgrade with the new infrastructure in place, while governance coordinates separate activation steps for each optional feature. + +## Issuance Rate + +**Before:** A single `issuancePerBlock` storage variable, set by governance via `setIssuancePerBlock()`, determined all reward issuance. + +**After:** An optional `issuanceAllocator` contract can be set by governance. When set, the effective issuance rate comes from the allocator (which can distribute issuance across multiple targets). When unset, the legacy `issuancePerBlock` value is used as a fallback. The allocator calls `beforeIssuanceAllocationChange()` on the RewardsManager before changing rates, ensuring accumulators are snapshotted first. + +**Activates:** Governance-gated — requires `setIssuanceAllocator()`. Until called, the legacy `issuancePerBlock` value continues to apply. + +## Reward Conditions + +A new `RewardsCondition` library defines typed `bytes32` identifiers for every situation where rewards cannot be distributed normally: + +| Condition | Trigger | +| ---------------------- | ---------------------------------------------------- | +| `NO_SIGNAL` | Zero total curation signal globally | +| `SUBGRAPH_DENIED` | Subgraph is on the denylist | +| `BELOW_MINIMUM_SIGNAL` | Subgraph signal below `minimumSubgraphSignal` | +| `NO_ALLOCATED_TOKENS` | Subgraph has signal but zero allocated tokens | +| `INDEXER_INELIGIBLE` | Indexer fails eligibility oracle check at claim time | +| `STALE_POI` | POI presented after staleness deadline | +| `ZERO_POI` | POI is `bytes32(0)` | +| `ALLOCATION_TOO_YOUNG` | Allocation created in the current epoch | +| `CLOSE_ALLOCATION` | Allocation being closed with uncollected rewards | + +**Activates:** Automatic on upgrade — the library and all condition checks are available immediately once the upgraded contracts are deployed. + +## Reclaim System + +**Before:** When rewards could not be distributed (denied subgraph, below-signal subgraph, stale POI, etc.), the tokens were silently lost -- never minted to anyone. + +**After:** Undistributable rewards are _reclaimed_ by minting them to a configurable address. Governance can set a per-condition address via `setReclaimAddress(condition, address)` and a catch-all fallback via `setDefaultReclaimAddress(address)`. If neither is configured for a given condition, rewards are still not minted (preserving the old drop behaviour). Every reclaim emits a `RewardsReclaimed` event with the condition, amount, indexer, allocation, and subgraph. + +**Activates:** Governance-gated — requires `setReclaimAddress()` and/or `setDefaultReclaimAddress()` for each condition. Until configured, rewards are dropped (preserving legacy behaviour). + +## Zero Global Signal + +**Before:** Issuance during periods with zero total curation signal was silently lost. + +**After:** Detected in `updateAccRewardsPerSignal()` and reclaimed as `NO_SIGNAL`. + +**Activates:** Automatic on upgrade — detection is built into the accumulator update. Reclaim requires a configured address for `NO_SIGNAL`. + +## Subgraph-Level Denial + +**Before:** Denial was a binary gate checked only at `takeRewards()` time. When a subgraph was denied, `takeRewards()` returned 0 and emitted `RewardsDenied`. The calling AllocationManager still advanced the allocation's reward snapshot, permanently dropping those rewards. + +**After:** Denial is handled at two levels: + +- **RewardsManager (accumulator level):** When `onSubgraphSignalUpdate` or `onSubgraphAllocationUpdate` is called for a denied subgraph, `accRewardsForSubgraph` and `accRewardsPerAllocatedToken` freeze (stop increasing). New rewards accruing during the denial period are reclaimed immediately rather than accumulated. `setDenied()` now snapshots accumulators before changing denial state so the boundary is clean. + +- **AllocationManager (claim level):** POI presentation for a denied subgraph is _deferred_ -- returns 0 **without advancing the allocation's snapshot**. This preserves uncollected pre-denial rewards. When the subgraph is later un-denied, those preserved rewards become claimable again. + +**Activates:** Automatic on upgrade — the accumulator-level freeze and claim-level deferral apply immediately. Denial state itself is set via `setDenied()` (Governor or SubgraphAvailabilityOracle). + +## Below-Minimum Signal + +**Before:** `getAccRewardsForSubgraph()` silently excluded rewards for subgraphs below `minimumSubgraphSignal`. Those rewards were lost. + +**After:** The same exclusion occurs, but excluded rewards are reclaimed to the `BELOW_MINIMUM_SIGNAL` address instead of being lost. Changes to `minimumSubgraphSignal` apply retroactively to all pending rewards at the next accumulator update, so governance should call `onSubgraphSignalUpdate()` on affected subgraphs before changing the threshold. + +**Activates:** Automatic on upgrade for the reclaim path. Threshold changes via `setMinimumSubgraphSignal()` are retroactive — governance should call `onSubgraphSignalUpdate()` on affected subgraphs before changing the threshold. + +## Zero Allocated Tokens + +**Before:** When a subgraph had signal but no allocations, `getAccRewardsPerAllocatedToken()` returned 0 for per-token rewards. The subgraph-level accumulator still grew, but the rewards were stranded -- distributable to no one. + +**After:** Detected as `NO_ALLOCATED_TOKENS` and reclaimed. When allocations resume, `accRewardsPerAllocatedToken` resumes from its stored value rather than resetting to zero. + +**Activates:** Automatic on upgrade — detection is built into the accumulator update. + +## Indexer Eligibility + +**Before:** No per-indexer eligibility checks existed. + +**After:** An optional `rewardsEligibilityOracle` can be set by governance. When set, `takeRewards()` checks `isEligible(indexer)` at claim time. If the indexer is ineligible, rewards are denied (emitting `RewardsDeniedDueToEligibility`) and reclaimed to the `INDEXER_INELIGIBLE` address. Subgraph denial takes precedence: if a subgraph is denied, eligibility is not checked. + +**Activates:** Governance-gated — requires `setRewardsEligibilityOracle()`. Until called, no eligibility checks are performed. + +## POI Presentation (AllocationManager) + +**Before:** A single conditional expression decided whether `takeRewards()` was called. If any condition failed (stale, zero POI, too young, altruistic), rewards were set to 0. The allocation's reward snapshot always advanced and pending rewards were always cleared, permanently dropping any undistributable rewards. + +**After:** Three distinct paths based on the determined condition: + +1. **Claim** (`NONE`): `takeRewards()` mints tokens, distributed to indexer and delegators. Snapshot advances. +2. **Reclaim** (`STALE_POI`, `ZERO_POI`): `reclaimRewards()` mints tokens to the reclaim address. Snapshot advances and pending rewards are cleared. +3. **Defer** (`ALLOCATION_TOO_YOUNG`, `SUBGRAPH_DENIED`): Returns 0 **without advancing the snapshot or clearing pending rewards**. Rewards are preserved for later collection. Accumulators are still updated via `onSubgraphAllocationUpdate()` to keep reclaim tracking current. + +The POI presentation timestamp is now recorded immediately on entry (before condition evaluation), so the staleness clock resets regardless of reward outcome. Over-delegation force-close is skipped on the deferred path to avoid closing allocations with preserved uncollected rewards. + +**Activates:** Automatic on upgrade — the three-path logic applies to all POI presentations immediately. + +## Allocation Resize + +**Before:** Resizing always accumulated pending rewards for the delta period, regardless of allocation staleness. + +**After:** If the allocation is stale at resize time, pending rewards are reclaimed as `STALE_POI` and cleared. This prevents stale allocations from silently accumulating pending rewards through repeated resizes. + +**Activates:** Automatic on upgrade — applies to all resize operations immediately. + +## Allocation Close + +**Before:** Closing an allocation advanced the snapshot and closed it. Any uncollected rewards were permanently lost. + +**After:** Before closing, `reclaimRewards(CLOSE_ALLOCATION, allocationId)` is called to mint uncollected rewards to the reclaim address. + +**Activates:** Automatic on upgrade — applies to all close operations immediately. + +## Observability + +A new `POIPresented` event is emitted on every POI presentation, including the determined `condition` as a `bytes32` field. This provides off-chain visibility into why a given presentation did or did not result in rewards, which was previously invisible. + +**Activates:** Automatic on upgrade — emitted on every POI presentation immediately. + +## View Functions + +Several view functions were added or changed to expose the new reward state. + +### Accumulator Views Freeze for Non-Claimable Subgraphs + +The existing accumulator view functions now exclude rewards for subgraphs that are not claimable (denied, below minimum signal, or with zero allocated tokens). Previously these accumulators always grew; callers reading them as continuously-increasing counters need to account for the new freeze behaviour. + +**`getAccRewardsForSubgraph()`** — Previously always returned a growing value regardless of subgraph state. Now returns a frozen value when the subgraph is not claimable: the internal helper `_getSubgraphRewardsState()` determines a `RewardsCondition`, and when the condition is anything other than `NONE`, new rewards are excluded from the returned total. The accumulator resumes growing when the subgraph becomes claimable again. + +**`getAccRewardsPerAllocatedToken()`** — Derives from `getAccRewardsForSubgraph()`, so it inherits the freeze. When the subgraph is not claimable, new per-token rewards are zero because the subgraph-level delta is zero. At snapshot points the implementation zeroes `undistributedRewards` and reclaims them instead of adding them to `accRewardsPerAllocatedToken`. + +**`getRewards()`** — Returns the claimable reward estimate for an allocation. Because it reads `getAccRewardsPerAllocatedToken()`, it now returns a frozen value for allocations on non-claimable subgraphs. Pre-existing `accRewardsPending` from prior resizes is still included. Note: indexer eligibility is _not_ checked here (only at `takeRewards()` time), so the view does not reflect eligibility-based denial. + +**`getNewRewardsPerSignal()`** — No visible change in return value. Internally it now separates claimable from unclaimable issuance (zero-signal periods), but the public view still returns only the claimable portion. The unclaimable portion is reclaimed as `NO_SIGNAL` at the next `updateAccRewardsPerSignal()` call. + +### New Getters on IRewardsManager + +| Function | Returns | Purpose | +| ----------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `getIssuanceAllocator()` | `IIssuanceAllocationDistribution` | Current allocator contract (zero if unset) | +| `getReclaimAddress(bytes32 reason)` | `address` | Per-condition reclaim address (zero if unconfigured) | +| `getDefaultReclaimAddress()` | `address` | Fallback reclaim address | +| `getRewardsEligibilityOracle()` | `IRewardsEligibility` | Current eligibility oracle (zero if unset) | +| `getAllocatedIssuancePerBlock()` | `uint256` | Effective issuance rate — returns the allocator rate when set, otherwise falls back to storage. Replaces the legacy `getRewardsIssuancePerBlock()` for callers that need the protocol rate | +| `getRawIssuancePerBlock()` | `uint256` | Raw storage value, ignoring the allocator. Useful for debugging allocator configuration | + +### Changed Return Semantics + +**`getAllocationData()`** (IRewardsIssuer, implemented by SubgraphService) now returns a sixth value, `accRewardsPending`, representing accumulated rewards from allocation resizing that have not yet been claimed. Callers that destructure the return tuple need updating. + +**`IAllocation.State`** struct adds two fields: `accRewardsPending` (pending rewards from resize) and `createdAtEpoch` (epoch when the allocation was created). Both affect the return value of `getAllocation()`. + +## Provenance + +Merge commits into `main` that introduced the changes described above, in chronological order. + +| Date | Merge | PR | Scope | +| ---------- | ----------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2025-12-16 | `ff2f00a62` | #1265 | Eligibility oracle audit doc fixes (TRST-L-1, TRST-L-2) | +| 2025-12-16 | `48be37a20` | #1267 | Issuance allocator audit fix — default allocation, `setReclaimAddress` | +| 2025-12-31 | `89f1321c4` | #1272 | Issuance allocator audit fix v3 — forced reclaim, PPM-to-absolute migration | +| 2026-01-08 | `3d274a4f1` | #1255 | Issuance baseline — RewardsManager extensions, eligibility interface, test suites | +| 2026-01-08 | `363924149` | #1256 | Rewards Eligibility Oracle — full oracle implementation | +| 2026-01-08 | `cdef9b5fd` | #1257 | Issuance Allocator — full allocator, RewardsReclaim library, allocation close reclaim | +| 2026-02-17 | `ada315500` | #1279 | Rewards reclaiming (audited) — RewardsCondition rename, `setDefaultReclaimAddress`, subgraph denial accumulator handling, zero-signal reclaim, POI three-path logic, `POIPresented` event | +| 2026-02-19 | `127b7ef6f` | #1280 | Issuance umbrella merge — all prior work plus stale-allocation-resize reclaim (TRST-R-1) | diff --git a/docs/archive/CompilerUpgrade0833.md b/docs/archive/CompilerUpgrade0833.md new file mode 100644 index 000000000..03a1773c3 --- /dev/null +++ b/docs/archive/CompilerUpgrade0833.md @@ -0,0 +1,151 @@ +# Compiler Upgrade: Solidity 0.8.33 + viaIR + +This document captures the bytecode size changes resulting from the compiler configuration upgrade in the `subgraph-service` and `issuance` packages. + +## Configuration Changes + +### subgraph-service + +| Setting | Before | After | +| ---------------- | ------- | ------- | +| Solidity Version | 0.8.27 | 0.8.33 | +| EVM Version | paris | cancun | +| Optimizer | enabled | enabled | +| Optimizer Runs | 10 | 100 | +| viaIR | false | true | + +### issuance + +| Setting | Before | After | +| ---------------- | ------- | ------- | +| Solidity Version | 0.8.27 | 0.8.33 | +| EVM Version | cancun | cancun | +| Optimizer | enabled | enabled | +| Optimizer Runs | 100 | 100 | +| viaIR | false | true | + +## Subgraph-Service Contract Bytecode Sizes + +All contracts defined in `packages/subgraph-service/contracts/`: + +| Contract | Source File | Before (KiB) | After (KiB) | Change (KiB) | Change (%) | +| --------------------------- | ------------------------------------------------- | ------------ | ----------- | ------------ | ---------- | +| **SubgraphService** | contracts/SubgraphService.sol | 24.455 | 23.110 | **-1.345** | -5.5% | +| **DisputeManager** | contracts/DisputeManager.sol | 13.278 | 10.917 | **-2.361** | -17.8% | +| Allocation | contracts/libraries/Allocation.sol | 0.084 | 0.056 | -0.028 | -33.3% | +| Attestation | contracts/libraries/Attestation.sol | 0.084 | 0.056 | -0.028 | -33.3% | +| LegacyAllocation | contracts/libraries/LegacyAllocation.sol | 0.084 | 0.056 | -0.028 | -33.3% | +| SubgraphServiceV1Storage | contracts/SubgraphServiceStorage.sol | (abstract) | (abstract) | - | - | +| DisputeManagerV1Storage | contracts/DisputeManagerStorage.sol | (abstract) | (abstract) | - | - | +| AllocationManager | contracts/utilities/AllocationManager.sol | (abstract) | (abstract) | - | - | +| AllocationManagerV1Storage | contracts/utilities/AllocationManagerStorage.sol | (abstract) | (abstract) | - | - | +| AttestationManager | contracts/utilities/AttestationManager.sol | (abstract) | (abstract) | - | - | +| AttestationManagerV1Storage | contracts/utilities/AttestationManagerStorage.sol | (abstract) | (abstract) | - | - | +| Directory | contracts/utilities/Directory.sol | (abstract) | (abstract) | - | - | + +### Initcode Size (Subgraph-Service Contracts) + +| Contract | Before (KiB) | After (KiB) | Change (KiB) | +| ------------------- | ------------ | ----------- | ------------ | +| **SubgraphService** | 26.109 | 24.894 | **-1.215** | +| **DisputeManager** | 14.649 | 12.342 | **-2.307** | + +## Issuance Contract Bytecode Sizes + +All contracts defined in `packages/issuance/contracts/`: + +| Contract | Source File | Before (KiB) | After (KiB) | Change (KiB) | Change (%) | +| ---------------------------- | -------------------------------------------------- | ------------ | ----------- | ------------ | ---------- | +| **IssuanceAllocator** | contracts/allocate/IssuanceAllocator.sol | 10.444 | 10.250 | **-0.194** | -1.9% | +| **RewardsEligibilityOracle** | contracts/eligibility/RewardsEligibilityOracle.sol | 4.316 | 4.554 | +0.238 | +5.5% | +| **DirectAllocation** | contracts/allocate/DirectAllocation.sol | 2.978 | 3.393 | +0.415 | +13.9% | +| BaseUpgradeable | contracts/common/BaseUpgradeable.sol | (abstract) | (abstract) | - | - | + +### Initcode Size (Issuance Contracts) + +| Contract | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------------------- | ------------ | ----------- | ------------ | +| **IssuanceAllocator** | 10.817 | 10.601 | **-0.216** | +| **RewardsEligibilityOracle** | 4.666 | 4.881 | +0.215 | +| **DirectAllocation** | 3.330 | 3.723 | +0.393 | + +### Test Contracts (Issuance) + +| Contract | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------------------- | ------------ | ----------- | ------------ | +| IssuanceAllocatorTestHarness | 10.641 | 10.331 | -0.310 | +| MockReentrantTarget | 1.886 | 1.535 | -0.351 | +| MockNotificationTracker | 0.495 | 0.438 | -0.057 | +| MockRevertingTarget | 0.342 | 0.250 | -0.092 | +| MockSimpleTarget | 0.293 | 0.237 | -0.056 | +| MockERC165 | 0.188 | 0.141 | -0.047 | + +## Dependency Library Sizes + +Libraries from horizon and other packages compiled as part of subgraph-service: + +### Horizon Libraries + +| Library | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------- | ------------ | ----------- | ------------ | +| LinkedList | 0.084 | 0.056 | -0.028 | +| TokenUtils | 0.084 | 0.056 | -0.028 | +| UintRange | 0.084 | 0.056 | -0.028 | +| MathUtils | 0.084 | 0.056 | -0.028 | +| PPMMath | 0.084 | 0.056 | -0.028 | +| ProvisionTracker | 0.084 | 0.056 | -0.028 | + +### OpenZeppelin Libraries + +| Library | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------- | ------------ | ----------- | ------------ | +| Address | 0.084 | 0.056 | -0.028 | +| Panic | 0.084 | 0.056 | -0.028 | +| Strings | 0.084 | 0.056 | -0.028 | +| Errors | 0.084 | 0.056 | -0.028 | +| MessageHashUtils | 0.084 | 0.056 | -0.028 | +| SafeCast | 0.084 | 0.056 | -0.028 | +| ECDSA | 0.084 | 0.056 | -0.028 | +| SignedMath | 0.084 | 0.056 | -0.028 | +| Math | 0.084 | 0.056 | -0.028 | + +### Interfaces Package + +| Contract | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------- | ------------ | ----------- | ------------ | +| RewardsCondition | 0.458 | 0.520 | +0.062 | + +## Key Observations + +### subgraph-service + +1. **SubgraphService now fits within mainnet limit**: The 24 KiB contract size limit was exceeded before (24.455 KiB). After the upgrade, it's safely under at 23.110 KiB. + +2. **Significant savings on main contracts**: Despite increasing optimizer runs from 10 to 100 (which typically increases size for runtime gas savings), the viaIR pipeline produced smaller bytecode: + - SubgraphService: -1.345 KiB (-5.5%) + - DisputeManager: -2.361 KiB (-17.8%) + +3. **Abstract contracts have no bytecode**: Storage contracts (e.g., `SubgraphServiceV1Storage`), utility contracts (`AllocationManager`, `AttestationManager`, `Directory`) are inherited by deployable contracts and have no standalone bytecode. + +4. **Library stub sizes reduced**: All library stubs decreased from 0.084 KiB to 0.056 KiB (-33%), indicating more efficient metadata encoding. + +### issuance + +1. **IssuanceAllocator reduced**: The main contract decreased slightly (-0.194 KiB, -1.9%) with viaIR enabled. + +2. **Smaller contracts increased**: DirectAllocation (+13.9%) and RewardsEligibilityOracle (+5.5%) increased in size. This is expected behavior as viaIR optimizations are more effective on larger contracts with complex inheritance patterns. + +3. **Test contracts all decreased**: All mock/test contracts benefited from viaIR, showing -5% to -19% reductions. + +## Why viaIR Reduces Size + +The viaIR (Intermediate Representation) compilation pipeline: + +- Uses Yul as an intermediate language +- Enables more aggressive cross-function optimizations +- Removes redundant code paths more effectively +- Particularly beneficial for large contracts with complex inheritance + +## Date + +Comparison performed: 2026-01-25 diff --git a/eslint.config.mjs b/eslint.config.mjs index 7931af7d0..8613cad3b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -219,7 +219,7 @@ const eslintConfig = [ // Add Mocha globals for test files { - files: ['**/*.test.ts', '**/*.test.js', '**/test/**/*.ts', '**/test/**/*.js'], + files: ['**/*.test.ts', '**/*.test.js', '**/test*/**/*.ts', '**/test*/**/*.js'], languageOptions: { globals: { ...globals.mocha, diff --git a/justfile b/justfile new file mode 100644 index 000000000..8c3362811 --- /dev/null +++ b/justfile @@ -0,0 +1,8 @@ +default: + @just --list + +# Build the workspace image locally as `ghcr.io/graphprotocol/contracts:local`. +# Override the tag with `CONTRACTS_TAG=foo just build-image`. Consumed by +# local-network's graph-contracts wrapper when CONTRACTS_VERSION=local. +build-image: + docker compose build diff --git a/package.json b/package.json index 62f07a03f..c66f0334f 100644 --- a/package.json +++ b/package.json @@ -5,22 +5,27 @@ "license": "GPL-2.0-or-later", "repository": "git@github.com:graphprotocol/contracts.git", "author": "Edge & Node", - "packageManager": "pnpm@10.17.0", + "packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48", + "engines": { + "node": "^24", + "pnpm": "^10.28" + }, "scripts": { "postinstall": "husky", "clean": "pnpm -r run clean", "clean:all": "pnpm clean && rm -rf node_modules packages/*/node_modules packages/*/*/node_modules", "build": "pnpm -r run build:self", "todo": "node scripts/check-todos.mjs", - "lint": "pnpm lint:ts; pnpm lint:sol; pnpm lint:md; pnpm lint:json; pnpm lint:yaml", + "lint": "pnpm lint:ts; pnpm lint:sol; pnpm lint:forge; pnpm lint:md; pnpm lint:json; pnpm lint:yaml", "lint:staged": "lint-staged; pnpm todo", - "lint:ts": "eslint --fix --cache '**/*.{js,ts,cjs,mjs,jsx,tsx}'; prettier -w --cache --log-level warn '**/*.{js,ts,cjs,mjs,jsx,tsx}'", - "lint:sol": "pnpm -r run lint:sol; prettier -w --cache --log-level warn '**/*.sol'; pnpm todo", - "lint:md": "markdownlint --fix --ignore-path .gitignore --ignore-path .markdownlintignore '**/*.md'; prettier -w --cache --log-level warn '**/*.md'", - "lint:json": "prettier -w --cache --log-level warn '**/*.json'", - "lint:yaml": "npx yaml-lint .github/**/*.{yml,yaml} packages/contracts/task/config/*.yml; prettier -w --cache --log-level warn '**/*.{yml,yaml}'", - "format": "prettier -w --cache --log-level warn '**/*.{js,ts,cjs,mjs,jsx,tsx,json,md,yaml,yml}'", + "lint:ts": "eslint --fix --cache 'packages/**/*.{js,ts,cjs,mjs,jsx,tsx}' 'scripts/**/*.{js,ts,cjs,mjs}' '*.{js,ts,cjs,mjs}'; prettier -w --cache --log-level warn 'packages/**/*.{js,ts,cjs,mjs,jsx,tsx}' 'scripts/**/*.{js,ts,cjs,mjs}' '*.{js,ts,cjs,mjs}'", + "lint:sol": "pnpm -r run lint:sol; prettier -w --cache --log-level warn 'packages/**/*.sol'; pnpm todo", + "lint:forge": "pnpm -r run lint:forge", + "lint:md": "markdownlint --fix --ignore-path .gitignore 'packages/**/*.md' 'docs/**/*.md' '*.md'; prettier -w --cache --log-level warn 'packages/**/*.md' 'docs/**/*.md' '*.md'", + "lint:json": "prettier -w --cache --log-level warn 'packages/**/*.{json,json5}' '.changeset/**/*.json' '*.{json,json5}'", + "lint:yaml": "npx yaml-lint .github/**/*.{yml,yaml} packages/contracts/task/config/*.yml; prettier -w --cache --log-level warn 'packages/**/*.{yml,yaml}' '.github/**/*.{yml,yaml}'", "test": "pnpm build && pnpm -r run test:self", + "test:prod": "FOUNDRY_PROFILE=prod pnpm test", "test:coverage": "pnpm build && pnpm -r run build:self:coverage && pnpm -r run test:coverage:self" }, "devDependencies": { @@ -51,8 +56,17 @@ "overrides": { "@types/node": "^20.17.50" }, + "packageExtensions": { + "@nomiclabs/hardhat-waffle@*": { + "dependencies": { + "@ethereum-waffle/chai": "*", + "@ethereum-waffle/provider": "*" + } + } + }, "patchedDependencies": { - "typechain@8.3.2": "patches/typechain@8.3.2.patch" + "typechain@8.3.2": "patches/typechain@8.3.2.patch", + "rocketh@0.17.13": "patches/rocketh@0.17.13.patch" } }, "lint-staged": { @@ -68,7 +82,7 @@ "markdownlint --fix", "prettier -w --cache --log-level warn" ], - "*.json": "prettier -w --cache --log-level warn", + "*.{json,json5}": "prettier -w --cache --log-level warn", "*.{yml,yaml}": [ "npx yamllint", "prettier -w --cache --log-level warn" diff --git a/packages/address-book/CHANGELOG.md b/packages/address-book/CHANGELOG.md index f009dba89..1427d84c2 100644 --- a/packages/address-book/CHANGELOG.md +++ b/packages/address-book/CHANGELOG.md @@ -1,5 +1,17 @@ # @graphprotocol/address-book +## 1.2.0 + +### Minor Changes + +- Upgraded Rewards Manager and Subgraph Service with Rewards Eligibility Oracle and rewards reclaiming. + +## 1.1.0 + +### Minor Changes + +- Graph Horizon phase 3 mainnet deployment + ## 1.0.1 ### Patch Changes diff --git a/packages/address-book/docs/PublishingGuide.md b/packages/address-book/docs/PublishingGuide.md new file mode 100644 index 000000000..d4b021783 --- /dev/null +++ b/packages/address-book/docs/PublishingGuide.md @@ -0,0 +1,108 @@ +# Publishing @graphprotocol/address-book + +Step-by-step guide for releasing a new version of the address-book package and deploying it to the network monitor. + +## Prerequisites + +- npm publish access for the `@graphprotocol` scope +- Write access to the [network-monitor](https://github.com/edgeandnode/network-monitor) repo +- Ability to trigger GitHub Actions workflows in both repos + +## Step 1: Update Address Files + +Update the source address files in the contracts monorepo. These live in: + +- `packages/horizon/addresses.json` +- `packages/subgraph-service/addresses.json` +- `packages/issuance/addresses.json` + +The address-book package symlinks to these files during development, so changes here are automatically reflected locally. + +## Step 2: Create a Changeset + +From the monorepo root: + +```bash +pnpm changeset +``` + +- Select `@graphprotocol/address-book` +- Choose the bump type (patch/minor/major) +- Describe what changed (e.g., "update arbitrumSepolia addresses after deployment") + +## Step 3: Version the Package + +```bash +pnpm changeset version +``` + +This consumes the changeset, bumps the version in `packages/address-book/package.json`, and updates `CHANGELOG.md`. + +## Step 4: Commit and Push + +```bash +git add . +git commit -m "chore: release @graphprotocol/address-book vX.Y.Z" +git push +``` + +## Step 5: Publish to npm + +1. Go to the contracts monorepo → Actions → "Publish package to NPM" +2. Select `address-book` as the package +3. Set tag to `latest` (or a pre-release tag) +4. Run workflow + +The workflow automatically: + +- Publishes to npm (symlinks are converted to real files via `prepublishOnly`) +- Creates and pushes a git tag (`@graphprotocol/address-book@X.Y.Z`) + +## Step 6: Verify on npm + +```bash +npm view @graphprotocol/address-book version +``` + +Confirm the new version is live. + +## Step 7: Update the Network Monitor + +In the [network-monitor](https://github.com/edgeandnode/network-monitor) repo: + +1. Update `package.json` to reference the new version: + + ```json + "@graphprotocol/address-book": "X.Y.Z", + ``` + +2. Run `yarn` to update the lockfile +3. Commit and push + +The network monitor imports addresses from: + +- `@graphprotocol/address-book/horizon/addresses.json` (in `src/env.ts`) +- `@graphprotocol/address-book/subgraph-service/addresses.json` (in `src/env.ts`, `src/tests/contracts.ts`) + +## Step 8: Deploy the Network Monitor + +1. Go to the network-monitor repo → Actions → "Deployment" +2. Choose the target cluster: + - **`network`** → production (mainnet) + - **`testnet`** → testnet +3. Run workflow + +This builds a Docker image, pushes it to `ghcr.io/edgeandnode/network-monitor`, and restarts the StatefulSet on GKE. + +## Quick Reference + +| Step | Action | Where | +| ---- | ------------------------------- | ----------------------------- | +| 1 | Update address files | contracts monorepo | +| 2 | `pnpm changeset` | contracts monorepo | +| 3 | `pnpm changeset version` | contracts monorepo | +| 4 | Commit + push | contracts monorepo | +| 5 | Publish to npm (auto-tags) | contracts monorepo GH Actions | +| 6 | Verify on npm | npmjs.com | +| 7 | Bump version in network-monitor | network-monitor repo | +| 8 | Deploy network monitor | network-monitor GH Actions | diff --git a/packages/address-book/package.json b/packages/address-book/package.json index 512d51a71..f0e85dbd0 100644 --- a/packages/address-book/package.json +++ b/packages/address-book/package.json @@ -1,15 +1,19 @@ { "name": "@graphprotocol/address-book", - "version": "1.0.1", + "version": "1.2.1", "publishConfig": { "access": "public" }, "description": "Contract addresses for The Graph Protocol", "author": "Edge & Node", "license": "GPL-2.0-or-later", + "repository": { + "type": "git", + "url": "git+https://github.com/graphprotocol/contracts", + "directory": "packages/address-book" + }, "exports": { - "./horizon/addresses.json": "./src/horizon/addresses.json", - "./subgraph-service/addresses.json": "./src/subgraph-service/addresses.json" + "./*/addresses.json": "./src/*/addresses.json" }, "files": [ "src/**/*.json", diff --git a/packages/address-book/scripts/copy-addresses-for-publish.js b/packages/address-book/scripts/copy-addresses-for-publish.js index 5fdfdc2c2..0665359d4 100755 --- a/packages/address-book/scripts/copy-addresses-for-publish.js +++ b/packages/address-book/scripts/copy-addresses-for-publish.js @@ -3,63 +3,52 @@ /** * Copy Addresses for Publishing * - * This script copies the actual addresses.json files from horizon and subgraph-service - * packages to replace the symlinks before npm publish. - * - * Why we need this: - * - Development uses symlinks (committed to git) for convenience - * - npm publish doesn't include symlinks in the published package - * - We need actual files in the published package for consumers - * - * The postpublish script will restore the symlinks after publishing. + * Replaces the dev-time symlinks under src//addresses.json with real + * file copies before npm publish — npm does not include symlinks in the + * published tarball. restore-symlinks.js puts the symlinks back afterwards. */ const fs = require('fs') const path = require('path') +const SOURCES = require('./sources') -const FILES_TO_COPY = [ - { - source: '../../../horizon/addresses.json', - target: 'src/horizon/addresses.json', - }, - { - source: '../../../subgraph-service/addresses.json', - target: 'src/subgraph-service/addresses.json', - }, -] +const ROOT = path.resolve(__dirname, '..') +const SRC = path.join(ROOT, 'src') -function copyFileForPublish(source, target) { - const targetPath = path.resolve(__dirname, '..', target) - const sourcePath = path.resolve(path.dirname(targetPath), source) +function copyOne(name) { + const sourcePath = path.resolve(ROOT, '..', name, 'addresses.json') + const targetDir = path.join(SRC, name) + const targetPath = path.join(targetDir, 'addresses.json') - // Ensure source exists if (!fs.existsSync(sourcePath)) { console.error(`❌ Source file ${sourcePath} does not exist`) process.exit(1) } - // Remove existing symlink - if (fs.existsSync(targetPath)) { - fs.unlinkSync(targetPath) - } + fs.mkdirSync(targetDir, { recursive: true }) + fs.rmSync(targetPath, { force: true }) + fs.copyFileSync(sourcePath, targetPath) + console.log(`✅ Copied for publish: src/${name}/addresses.json`) +} - // Copy actual file - try { - fs.copyFileSync(sourcePath, targetPath) - console.log(`✅ Copied for publish: ${target} <- ${source}`) - } catch (error) { - console.error(`❌ Failed to copy ${source} to ${target}:`, error.message) +function checkDrift() { + const dirs = fs + .readdirSync(SRC) + .filter((d) => fs.statSync(path.join(SRC, d)).isDirectory()) + .sort() + const expected = [...SOURCES].sort() + if (JSON.stringify(dirs) !== JSON.stringify(expected)) { + console.error(`❌ Drift between SOURCES and src/`) + console.error(` SOURCES: [${expected.join(', ')}]`) + console.error(` src/ : [${dirs.join(', ')}]`) process.exit(1) } } function main() { console.log('📦 Copying address files for npm publish...') - - for (const { source, target } of FILES_TO_COPY) { - copyFileForPublish(source, target) - } - + for (const name of SOURCES) copyOne(name) + checkDrift() console.log('✅ Address files copied for publish!') } diff --git a/packages/address-book/scripts/restore-symlinks.js b/packages/address-book/scripts/restore-symlinks.js index 05e5ec6f9..eb2f1f5df 100755 --- a/packages/address-book/scripts/restore-symlinks.js +++ b/packages/address-book/scripts/restore-symlinks.js @@ -3,50 +3,32 @@ /** * Restore Symlinks After Publishing * - * This script restores the symlinks after npm publish completes. - * The prepublishOnly script replaces symlinks with actual files for publishing, - * and this script puts the symlinks back for development. + * Restores the dev-time symlinks under src//addresses.json after + * npm publish. copy-addresses-for-publish.js replaces them with real files + * for the publish step; this puts them back. */ const fs = require('fs') const path = require('path') +const SOURCES = require('./sources') -const SYMLINKS_TO_RESTORE = [ - { - target: '../../../horizon/addresses.json', - link: 'src/horizon/addresses.json', - }, - { - target: '../../../subgraph-service/addresses.json', - link: 'src/subgraph-service/addresses.json', - }, -] +const ROOT = path.resolve(__dirname, '..') +const SRC = path.join(ROOT, 'src') -function restoreSymlink(target, link) { - const linkPath = path.resolve(__dirname, '..', link) +function restoreOne(name) { + const linkTarget = `../../../${name}/addresses.json` + const linkDir = path.join(SRC, name) + const linkPath = path.join(linkDir, 'addresses.json') - // Remove the copied file - if (fs.existsSync(linkPath)) { - fs.unlinkSync(linkPath) - } - - // Restore symlink - try { - fs.symlinkSync(target, linkPath) - console.log(`✅ Restored symlink: ${link} -> ${target}`) - } catch (error) { - console.error(`❌ Failed to restore symlink ${link}:`, error.message) - process.exit(1) - } + fs.mkdirSync(linkDir, { recursive: true }) + fs.rmSync(linkPath, { force: true }) + fs.symlinkSync(linkTarget, linkPath) + console.log(`✅ Restored symlink: src/${name}/addresses.json -> ${linkTarget}`) } function main() { console.log('🔗 Restoring symlinks after publish...') - - for (const { target, link } of SYMLINKS_TO_RESTORE) { - restoreSymlink(target, link) - } - + for (const name of SOURCES) restoreOne(name) console.log('✅ Symlinks restored!') } diff --git a/packages/address-book/scripts/sources.js b/packages/address-book/scripts/sources.js new file mode 100644 index 000000000..6885c1a2b --- /dev/null +++ b/packages/address-book/scripts/sources.js @@ -0,0 +1,4 @@ +// Source packages exported from @graphprotocol/address-book. +// Each name corresponds to packages//addresses.json (source of truth) +// and packages/address-book/src//addresses.json (publish symlink). +module.exports = ['horizon', 'issuance', 'subgraph-service'] diff --git a/packages/address-book/src/issuance/addresses.json b/packages/address-book/src/issuance/addresses.json new file mode 120000 index 000000000..b73ad34ff --- /dev/null +++ b/packages/address-book/src/issuance/addresses.json @@ -0,0 +1 @@ +../../../issuance/addresses.json \ No newline at end of file diff --git a/packages/contracts/test/.solcover.js b/packages/contracts-test/.solcover.js similarity index 96% rename from packages/contracts/test/.solcover.js rename to packages/contracts-test/.solcover.js index 7181b78fa..125581cd1 100644 --- a/packages/contracts/test/.solcover.js +++ b/packages/contracts-test/.solcover.js @@ -1,4 +1,4 @@ -const skipFiles = ['bancor', 'ens', 'erc1056', 'arbitrum', 'tests/arbitrum'] +const skipFiles = ['bancor', 'ens', 'erc1056', 'arbitrum', 'tests', '*Mock.sol'] module.exports = { providerOptions: { diff --git a/packages/contracts/test/CHANGELOG.md b/packages/contracts-test/CHANGELOG.md similarity index 100% rename from packages/contracts/test/CHANGELOG.md rename to packages/contracts-test/CHANGELOG.md diff --git a/packages/contracts/test/config/graph.arbitrum-goerli.yml b/packages/contracts-test/config/graph.arbitrum-goerli.yml similarity index 100% rename from packages/contracts/test/config/graph.arbitrum-goerli.yml rename to packages/contracts-test/config/graph.arbitrum-goerli.yml diff --git a/packages/contracts/test/config/graph.arbitrum-hardhat.yml b/packages/contracts-test/config/graph.arbitrum-hardhat.yml similarity index 100% rename from packages/contracts/test/config/graph.arbitrum-hardhat.yml rename to packages/contracts-test/config/graph.arbitrum-hardhat.yml diff --git a/packages/contracts/test/config/graph.arbitrum-localhost.yml b/packages/contracts-test/config/graph.arbitrum-localhost.yml similarity index 100% rename from packages/contracts/test/config/graph.arbitrum-localhost.yml rename to packages/contracts-test/config/graph.arbitrum-localhost.yml diff --git a/packages/contracts/test/config/graph.arbitrum-one.yml b/packages/contracts-test/config/graph.arbitrum-one.yml similarity index 100% rename from packages/contracts/test/config/graph.arbitrum-one.yml rename to packages/contracts-test/config/graph.arbitrum-one.yml diff --git a/packages/contracts/test/config/graph.arbitrum-sepolia.yml b/packages/contracts-test/config/graph.arbitrum-sepolia.yml similarity index 100% rename from packages/contracts/test/config/graph.arbitrum-sepolia.yml rename to packages/contracts-test/config/graph.arbitrum-sepolia.yml diff --git a/packages/contracts/test/config/graph.goerli.yml b/packages/contracts-test/config/graph.goerli.yml similarity index 100% rename from packages/contracts/test/config/graph.goerli.yml rename to packages/contracts-test/config/graph.goerli.yml diff --git a/packages/contracts/test/config/graph.hardhat.yml b/packages/contracts-test/config/graph.hardhat.yml similarity index 100% rename from packages/contracts/test/config/graph.hardhat.yml rename to packages/contracts-test/config/graph.hardhat.yml diff --git a/packages/contracts/test/config/graph.localhost.yml b/packages/contracts-test/config/graph.localhost.yml similarity index 100% rename from packages/contracts/test/config/graph.localhost.yml rename to packages/contracts-test/config/graph.localhost.yml diff --git a/packages/contracts/test/config/graph.mainnet.yml b/packages/contracts-test/config/graph.mainnet.yml similarity index 100% rename from packages/contracts/test/config/graph.mainnet.yml rename to packages/contracts-test/config/graph.mainnet.yml diff --git a/packages/contracts/test/config/graph.sepolia.yml b/packages/contracts-test/config/graph.sepolia.yml similarity index 100% rename from packages/contracts/test/config/graph.sepolia.yml rename to packages/contracts-test/config/graph.sepolia.yml diff --git a/packages/contracts-test/contracts b/packages/contracts-test/contracts new file mode 120000 index 000000000..e741e39c3 --- /dev/null +++ b/packages/contracts-test/contracts @@ -0,0 +1 @@ +../contracts/contracts \ No newline at end of file diff --git a/packages/contracts/test/hardhat.config.ts b/packages/contracts-test/hardhat.config.ts similarity index 75% rename from packages/contracts/test/hardhat.config.ts rename to packages/contracts-test/hardhat.config.ts index 1d8ed1c58..718359730 100644 --- a/packages/contracts/test/hardhat.config.ts +++ b/packages/contracts-test/hardhat.config.ts @@ -5,6 +5,7 @@ import '@nomiclabs/hardhat-waffle' import '@typechain/hardhat' import 'dotenv/config' import 'hardhat-gas-reporter' +import 'hardhat-ignore-warnings' import 'solidity-coverage' // Test-specific tasks import './tasks/migrate/nitro' @@ -38,12 +39,24 @@ const config: HardhatUserConfig = { paths: { tests: './tests/unit', cache: './cache', - graph: '..', + graph: '../contracts', artifacts: './artifacts', }, typechain: { outDir: 'types', }, + warnings: { + // Suppress warnings from legacy OpenZeppelin contracts and external dependencies + 'arbos-precompiles/**/*': { + default: 'off', + }, + '@openzeppelin/contracts/**/*': { + default: 'off', + }, + 'contracts/staking/StakingExtension.sol': { + 5667: 'off', // Unused function parameter + }, + }, defaultNetwork: 'hardhat', networks: { hardhat: { @@ -57,9 +70,10 @@ const config: HardhatUserConfig = { mnemonic: DEFAULT_TEST_MNEMONIC, }, hardfork: 'london', - // Graph Protocol extensions + // Graph Protocol extensions (not in standard Hardhat types) graphConfig: path.join(configDir, 'graph.hardhat.yml'), addressBook: process.env.ADDRESS_BOOK || 'addresses.json', + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, localhost: { chainId: 1337, @@ -74,6 +88,7 @@ const config: HardhatUserConfig = { currency: 'USD', outputFile: 'reports/gas-report.log', }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any export default config diff --git a/packages/contracts/test/package.json b/packages/contracts-test/package.json similarity index 98% rename from packages/contracts/test/package.json rename to packages/contracts-test/package.json index d3e93a843..1ced3bca4 100644 --- a/packages/contracts/test/package.json +++ b/packages/contracts-test/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@graphprotocol/contracts": "workspace:^", + "@graphprotocol/interfaces": "workspace:^", "@graphprotocol/sdk": "0.6.0" }, "devDependencies": { diff --git a/packages/contracts-test/prettier.config.cjs b/packages/contracts-test/prettier.config.cjs new file mode 100644 index 000000000..18006454f --- /dev/null +++ b/packages/contracts-test/prettier.config.cjs @@ -0,0 +1,5 @@ +const baseConfig = require('../contracts/prettier.config.cjs') + +module.exports = { + ...baseConfig, +} diff --git a/packages/contracts/test/scripts/coverage b/packages/contracts-test/scripts/coverage similarity index 100% rename from packages/contracts/test/scripts/coverage rename to packages/contracts-test/scripts/coverage diff --git a/packages/contracts/test/scripts/e2e b/packages/contracts-test/scripts/e2e similarity index 100% rename from packages/contracts/test/scripts/e2e rename to packages/contracts-test/scripts/e2e diff --git a/packages/contracts/test/scripts/evm b/packages/contracts-test/scripts/evm similarity index 100% rename from packages/contracts/test/scripts/evm rename to packages/contracts-test/scripts/evm diff --git a/packages/contracts/test/scripts/setup-symlinks b/packages/contracts-test/scripts/setup-symlinks similarity index 89% rename from packages/contracts/test/scripts/setup-symlinks rename to packages/contracts-test/scripts/setup-symlinks index 357efaa4f..9c7f72949 100755 --- a/packages/contracts/test/scripts/setup-symlinks +++ b/packages/contracts-test/scripts/setup-symlinks @@ -9,9 +9,9 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEST_DIR="$(dirname "$SCRIPT_DIR")" -# Create symbolic link from contracts to ../contracts +# Create symbolic link from contracts to ../contracts/contracts CONTRACTS_LINK="$TEST_DIR/contracts" -CONTRACTS_TARGET="../contracts" +CONTRACTS_TARGET="../contracts/contracts" if [ -L "$CONTRACTS_LINK" ]; then # Check if the link points to the correct target diff --git a/packages/contracts/test/scripts/test b/packages/contracts-test/scripts/test similarity index 72% rename from packages/contracts/test/scripts/test rename to packages/contracts-test/scripts/test index 36888a096..7b5c01372 100755 --- a/packages/contracts/test/scripts/test +++ b/packages/contracts-test/scripts/test @@ -21,11 +21,11 @@ fi ### Main # Init address book -echo {} > ../addresses-local.json +echo {} > ../contracts/addresses-local.json # TODO: fix this! For some reason the resolved package does not have a few required files -echo {} > ../../../node_modules/.pnpm/@graphprotocol+contracts@7.2.1/node_modules/@graphprotocol/contracts/addresses-local.json -cp -r ../config ../../../node_modules/.pnpm/@graphprotocol+contracts@7.2.1/node_modules/@graphprotocol/contracts/config +echo {} > ../../node_modules/.pnpm/@graphprotocol+contracts@7.2.1/node_modules/@graphprotocol/contracts/addresses-local.json +cp -r ../contracts/config ../../node_modules/.pnpm/@graphprotocol+contracts@7.2.1/node_modules/@graphprotocol/contracts/config mkdir -p reports diff --git a/packages/contracts/test/scripts/test-coverage-file b/packages/contracts-test/scripts/test-coverage-file similarity index 100% rename from packages/contracts/test/scripts/test-coverage-file rename to packages/contracts-test/scripts/test-coverage-file diff --git a/packages/contracts/test/tasks/migrate/nitro.ts b/packages/contracts-test/tasks/migrate/nitro.ts similarity index 98% rename from packages/contracts/test/tasks/migrate/nitro.ts rename to packages/contracts-test/tasks/migrate/nitro.ts index 0cd551a18..480830cb4 100644 --- a/packages/contracts/test/tasks/migrate/nitro.ts +++ b/packages/contracts-test/tasks/migrate/nitro.ts @@ -60,5 +60,5 @@ task('migrate:nitro:address-book', 'Write arbitrum addresses to address book') }, } - fs.writeFileSync(taskArgs.arbitrumAddressBook, JSON.stringify(addressBook)) + fs.writeFileSync(taskArgs.arbitrumAddressBook, JSON.stringify(addressBook, null, 2) + '\n') }) diff --git a/packages/contracts/test/tasks/test-upgrade.ts b/packages/contracts-test/tasks/test-upgrade.ts similarity index 100% rename from packages/contracts/test/tasks/test-upgrade.ts rename to packages/contracts-test/tasks/test-upgrade.ts diff --git a/packages/contracts/test/tests/unit/curation/configuration.test.ts b/packages/contracts-test/tests/unit/curation/configuration.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/curation/configuration.test.ts rename to packages/contracts-test/tests/unit/curation/configuration.test.ts diff --git a/packages/contracts/test/tests/unit/curation/curation.test.ts b/packages/contracts-test/tests/unit/curation/curation.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/curation/curation.test.ts rename to packages/contracts-test/tests/unit/curation/curation.test.ts diff --git a/packages/contracts/test/tests/unit/disputes/common.ts b/packages/contracts-test/tests/unit/disputes/common.ts similarity index 100% rename from packages/contracts/test/tests/unit/disputes/common.ts rename to packages/contracts-test/tests/unit/disputes/common.ts diff --git a/packages/contracts/test/tests/unit/disputes/configuration.test.ts b/packages/contracts-test/tests/unit/disputes/configuration.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/disputes/configuration.test.ts rename to packages/contracts-test/tests/unit/disputes/configuration.test.ts diff --git a/packages/contracts/test/tests/unit/disputes/poi.test.ts b/packages/contracts-test/tests/unit/disputes/poi.test.ts similarity index 95% rename from packages/contracts/test/tests/unit/disputes/poi.test.ts rename to 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 similarity index 98% rename from packages/contracts/test/tests/unit/disputes/query.test.ts rename to 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/epochs.test.ts b/packages/contracts-test/tests/unit/epochs.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/epochs.test.ts rename to packages/contracts-test/tests/unit/epochs.test.ts diff --git a/packages/contracts/test/tests/unit/gateway/bridgeEscrow.test.ts b/packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/gateway/bridgeEscrow.test.ts rename to packages/contracts-test/tests/unit/gateway/bridgeEscrow.test.ts diff --git a/packages/contracts/test/tests/unit/gateway/l1GraphTokenGateway.test.ts b/packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/gateway/l1GraphTokenGateway.test.ts rename to packages/contracts-test/tests/unit/gateway/l1GraphTokenGateway.test.ts diff --git a/packages/contracts/test/tests/unit/gns.test.ts b/packages/contracts-test/tests/unit/gns.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/gns.test.ts rename to packages/contracts-test/tests/unit/gns.test.ts diff --git a/packages/contracts/test/tests/unit/governance/controller.test.ts b/packages/contracts-test/tests/unit/governance/controller.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/governance/controller.test.ts rename to packages/contracts-test/tests/unit/governance/controller.test.ts diff --git a/packages/contracts/test/tests/unit/governance/governed.test.ts b/packages/contracts-test/tests/unit/governance/governed.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/governance/governed.test.ts rename to packages/contracts-test/tests/unit/governance/governed.test.ts diff --git a/packages/contracts/test/tests/unit/governance/pausing.test.ts b/packages/contracts-test/tests/unit/governance/pausing.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/governance/pausing.test.ts rename to packages/contracts-test/tests/unit/governance/pausing.test.ts diff --git a/packages/contracts/test/tests/unit/graphToken.test.ts b/packages/contracts-test/tests/unit/graphToken.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/graphToken.test.ts rename to packages/contracts-test/tests/unit/graphToken.test.ts diff --git a/packages/contracts/test/tests/unit/l2/l2ArbitrumMessengerMock.ts b/packages/contracts-test/tests/unit/l2/l2ArbitrumMessengerMock.ts similarity index 100% rename from packages/contracts/test/tests/unit/l2/l2ArbitrumMessengerMock.ts rename to packages/contracts-test/tests/unit/l2/l2ArbitrumMessengerMock.ts diff --git a/packages/contracts/test/tests/unit/l2/l2Curation.test.ts b/packages/contracts-test/tests/unit/l2/l2Curation.test.ts similarity index 97% rename from packages/contracts/test/tests/unit/l2/l2Curation.test.ts rename to packages/contracts-test/tests/unit/l2/l2Curation.test.ts index 6ee8a5cd3..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') }) }) @@ -529,11 +532,11 @@ 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 () { - 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 similarity index 85% rename from packages/contracts/test/tests/unit/l2/l2GNS.test.ts rename to 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: diff --git a/packages/contracts/test/tests/unit/l2/l2GraphToken.test.ts b/packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/l2/l2GraphToken.test.ts rename to packages/contracts-test/tests/unit/l2/l2GraphToken.test.ts diff --git a/packages/contracts/test/tests/unit/l2/l2GraphTokenGateway.test.ts b/packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/l2/l2GraphTokenGateway.test.ts rename to packages/contracts-test/tests/unit/l2/l2GraphTokenGateway.test.ts diff --git a/packages/contracts/test/tests/unit/l2/l2Staking.test.ts b/packages/contracts-test/tests/unit/l2/l2Staking.test.ts similarity index 97% rename from packages/contracts/test/tests/unit/l2/l2Staking.test.ts rename to 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/lib/fixtures.ts b/packages/contracts-test/tests/unit/lib/fixtures.ts similarity index 99% rename from packages/contracts/test/tests/unit/lib/fixtures.ts rename to packages/contracts-test/tests/unit/lib/fixtures.ts index 44ed50faa..2fc370da8 100644 --- a/packages/contracts/test/tests/unit/lib/fixtures.ts +++ b/packages/contracts-test/tests/unit/lib/fixtures.ts @@ -74,7 +74,7 @@ export class NetworkFixture { async load(deployer: SignerWithAddress, l2Deploy?: boolean): Promise { // Use instrumented artifacts when running coverage tests, otherwise use local artifacts - const artifactsDir = isRunningUnderCoverage() ? './artifacts' : '../artifacts' + const artifactsDir = isRunningUnderCoverage() ? './artifacts' : '../contracts/artifacts' const contracts = await deployGraphNetwork( 'addresses-local.json', diff --git a/packages/contracts/test/tests/unit/lib/gnsUtils.ts b/packages/contracts-test/tests/unit/lib/gnsUtils.ts similarity index 100% rename from packages/contracts/test/tests/unit/lib/gnsUtils.ts rename to packages/contracts-test/tests/unit/lib/gnsUtils.ts diff --git a/packages/contracts/test/tests/unit/lib/graphTokenTests.ts b/packages/contracts-test/tests/unit/lib/graphTokenTests.ts similarity index 100% rename from packages/contracts/test/tests/unit/lib/graphTokenTests.ts rename to packages/contracts-test/tests/unit/lib/graphTokenTests.ts diff --git a/packages/contracts/test/tests/unit/payments/allocationExchange.test.ts b/packages/contracts-test/tests/unit/payments/allocationExchange.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/payments/allocationExchange.test.ts rename to packages/contracts-test/tests/unit/payments/allocationExchange.test.ts diff --git a/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts new file mode 100644 index 000000000..168166745 --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts @@ -0,0 +1,425 @@ +import { Curation } from '@graphprotocol/contracts' +import { EpochManager } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { + deriveChannelKey, + formatGRT, + GraphNetworkContracts, + helpers, + randomHexBytes, + toBN, + toGRT, +} from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { BigNumber as BN } from 'bignumber.js' +import { expect } from 'chai' +import { BigNumber, constants } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +const { HashZero, WeiPerEther } = constants + +const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] + +describe('Rewards - Calculations', () => { + const graph = hre.graph() + let governor: SignerWithAddress + let curator1: SignerWithAddress + let curator2: SignerWithAddress + let indexer1: SignerWithAddress + let indexer2: SignerWithAddress + let assetHolder: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let epochManager: EpochManager + let staking: IStaking + let rewardsManager: RewardsManager + + // Derive some channel keys for each indexer used to sign attestations + const channelKey1 = deriveChannelKey() + const channelKey2 = deriveChannelKey() + + const subgraphDeploymentID1 = randomHexBytes() + const subgraphDeploymentID2 = randomHexBytes() + + const allocationID1 = channelKey1.address + const allocationID2 = channelKey2.address + + const metadata = HashZero + + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 800 GRT rewards + const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT every block + + // Core formula that gets accumulated rewards per signal for a period of time + const getRewardsPerSignal = (k: BN, t: BN, s: BN): string => { + if (s.eq(0)) { + return '0' + } + return k.times(t).div(s).toPrecision(18).toString() + } + + // Tracks the accumulated rewards as totalSignalled or supply changes across snapshots + class RewardsTracker { + totalSignalled = BigNumber.from(0) + lastUpdatedBlock = 0 + accumulated = BigNumber.from(0) + + static async create() { + const tracker = new RewardsTracker() + await tracker.snapshot() + return tracker + } + + async snapshot() { + this.accumulated = this.accumulated.add(await this.accrued()) + this.totalSignalled = await grt.balanceOf(curation.address) + this.lastUpdatedBlock = await helpers.latestBlock() + return this + } + + async elapsedBlocks() { + const currentBlock = await helpers.latestBlock() + return currentBlock - this.lastUpdatedBlock + } + + async accrued() { + const nBlocks = await this.elapsedBlocks() + return this.accruedByElapsed(nBlocks) + } + + accruedByElapsed(nBlocks: BigNumber | number) { + const n = getRewardsPerSignal( + new BN(ISSUANCE_PER_BLOCK.toString()), + new BN(nBlocks.toString()), + new BN(this.totalSignalled.toString()), + ) + return toGRT(n) + } + } + + // Test accumulated rewards per signal + const shouldGetNewRewardsPerSignal = async (nBlocks = ISSUANCE_RATE_PERIODS) => { + // -- t0 -- + const tracker = await RewardsTracker.create() + + // Jump + await helpers.mine(nBlocks) + + // -- t1 -- + + // Contract calculation + const contractAccrued = await rewardsManager.getNewRewardsPerSignal() + // Local calculation + const expectedAccrued = await tracker.accrued() + + // Check + expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + return expectedAccrued + } + + before(async function () { + const testAccounts = await graph.getTestAccounts() + ;[indexer1, indexer2, curator1, curator2, assetHolder] = testAccounts + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + epochManager = contracts.EpochManager + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager + + // 200 GRT per block + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + + // Distribute test funds + for (const wallet of [indexer1, indexer2, curator1, curator2, assetHolder]) { + await grt.connect(governor).mint(wallet.address, toGRT('1000000')) + await grt.connect(wallet).approve(staking.address, toGRT('1000000')) + await grt.connect(wallet).approve(curation.address, toGRT('1000000')) + } + + // Set the staking contract as the subgraph service so it can call takeRewards + await rewardsManager.connect(governor).setSubgraphService(staking.address) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + context('issuing rewards', function () { + beforeEach(async function () { + // 5% minute rate (4 blocks) + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + }) + + describe('getNewRewardsPerSignal', function () { + it('accrued per signal when no tokens signalled', async function () { + // When there is no tokens signalled no rewards are accrued + await helpers.mineEpoch(epochManager) + const accrued = await rewardsManager.getNewRewardsPerSignal() + expect(accrued).eq(0) + }) + + it('accrued per signal when tokens signalled', async function () { + // Update total signalled + const tokensToSignal = toGRT('1000') + await curation.connect(curator1).mint(subgraphDeploymentID1, tokensToSignal, 0) + + // Check + await shouldGetNewRewardsPerSignal() + }) + + it('accrued per signal when signalled tokens w/ many subgraphs', async function () { + // Update total signalled + await curation.connect(curator1).mint(subgraphDeploymentID1, toGRT('1000'), 0) + + // Check + await shouldGetNewRewardsPerSignal() + + // Update total signalled + await curation.connect(curator2).mint(subgraphDeploymentID2, toGRT('250'), 0) + + // Check + await shouldGetNewRewardsPerSignal() + }) + }) + + describe('updateAccRewardsPerSignal', function () { + it('update the accumulated rewards per signal state', async function () { + // Update total signalled + await curation.connect(curator1).mint(subgraphDeploymentID1, toGRT('1000'), 0) + // Snapshot + const tracker = await RewardsTracker.create() + + // Update + await rewardsManager.connect(governor).updateAccRewardsPerSignal() + const contractAccrued = await rewardsManager.accRewardsPerSignal() + + // Check + const expectedAccrued = await tracker.accrued() + expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + }) + + it('update the accumulated rewards per signal state after many blocks', async function () { + // Update total signalled + await curation.connect(curator1).mint(subgraphDeploymentID1, toGRT('1000'), 0) + // Snapshot + const tracker = await RewardsTracker.create() + + // Jump + await helpers.mine(ISSUANCE_RATE_PERIODS) + + // Update + await rewardsManager.connect(governor).updateAccRewardsPerSignal() + const contractAccrued = await rewardsManager.accRewardsPerSignal() + + // Check + const expectedAccrued = await tracker.accrued() + expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + }) + }) + + describe('getAccRewardsForSubgraph', function () { + it('accrued for each subgraph', async function () { + // Option B model: rewards only accumulate when allocations exist + const tokensToAllocate = toGRT('12500') + const signalled1 = toGRT('1500') + const signalled2 = toGRT('500') + + // Setup both subgraphs with signal first + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + await curation.connect(curator2).mint(subgraphDeploymentID2, signalled2, 0) + + // Setup both allocations + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + await staking.connect(indexer2).stake(tokensToAllocate) + await staking + .connect(indexer2) + .allocateFrom( + indexer2.address, + subgraphDeploymentID2, + tokensToAllocate, + allocationID2, + metadata, + await channelKey2.generateProof(indexer2.address), + ) + + // Jump to accumulate more rewards + await helpers.mine(ISSUANCE_RATE_PERIODS) + + // Get rewards from contract + const contractRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) + const contractRewardsSG2 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID2) + + // Both subgraphs should have non-zero rewards + expect(contractRewardsSG1).to.be.gt(0) + expect(contractRewardsSG2).to.be.gt(0) + + // SG1 should have more rewards than SG2 (has more signal and allocation was created first) + expect(contractRewardsSG1).to.be.gt(contractRewardsSG2) + }) + + it('should return zero rewards when subgraph signal is below minimum threshold', async function () { + // Set a high minimum signal threshold + const highMinimumSignal = toGRT('2000') + await rewardsManager.connect(governor).setMinimumSubgraphSignal(highMinimumSignal) + + // Signal less than the minimum threshold + const lowSignal = toGRT('1000') + await curation.connect(curator1).mint(subgraphDeploymentID1, lowSignal, 0) + + // Jump some blocks to potentially accrue rewards + await helpers.mine(ISSUANCE_RATE_PERIODS) + + // Check that no rewards are accrued due to minimum signal threshold + const contractRewards = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) + expect(contractRewards).eq(0) + }) + }) + + describe('onSubgraphSignalUpdate', function () { + it('update the accumulated rewards for subgraph state', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate - Option B requires allocation for rewards to accumulate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Snapshot after allocation + const tracker1 = await RewardsTracker.create() + + // Jump + await helpers.mine(ISSUANCE_RATE_PERIODS) + + // Update + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID1) + + // Check + const contractRewardsSG1 = (await rewardsManager.subgraphs(subgraphDeploymentID1)).accRewardsForSubgraph + const rewardsPerSignal1 = await tracker1.accrued() + const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) + expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1)) + + const contractAccrued = await rewardsManager.accRewardsPerSignal() + const expectedAccrued = await tracker1.accrued() + expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + + const contractBlockUpdated = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + const expectedBlockUpdated = await helpers.latestBlock() + expect(expectedBlockUpdated).eq(contractBlockUpdated) + }) + }) + + describe('getAccRewardsPerAllocatedToken', function () { + it('accrued per allocated token', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Jump + await helpers.mine(ISSUANCE_RATE_PERIODS) + + // Check + const sg1 = await rewardsManager.subgraphs(subgraphDeploymentID1) + // We trust this function because it was individually tested in previous test + const accRewardsForSubgraphSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) + const accruedRewardsSG1 = accRewardsForSubgraphSG1.sub(sg1.accRewardsForSubgraphSnapshot) + const expectedRewardsAT1 = accruedRewardsSG1.mul(WeiPerEther).div(tokensToAllocate) + const contractRewardsAT1 = (await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1))[0] + expect(expectedRewardsAT1).eq(contractRewardsAT1) + }) + }) + + describe('onSubgraphAllocationUpdate', function () { + it('update the accumulated rewards for allocated tokens state', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Jump + await helpers.mine(ISSUANCE_RATE_PERIODS) + + // Prepare expected results + // Option B model: accRewardsForSubgraph only tracks distributable rewards + // 2 blocks before allocation = reclaimed (NO_ALLOCATED_TOKENS), 5 blocks after = distributable + const expectedSubgraphRewards = toGRT('1000') // 5 blocks × 200 GRT/block + const expectedRewardsAT = toGRT('0.08') // 1000 GRT / 12500 allocated tokens + + // Update + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Check on demand results saved + const subgraph = await rewardsManager.subgraphs(subgraphDeploymentID1) + const contractSubgraphRewards = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) + const contractRewardsAT = subgraph.accRewardsPerAllocatedToken + + expect(toRound(expectedSubgraphRewards)).eq(toRound(contractSubgraphRewards)) + expect(toRound(expectedRewardsAT.mul(1000))).eq(toRound(contractRewardsAT.mul(1000))) + }) + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts new file mode 100644 index 000000000..bd3b2569a --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts @@ -0,0 +1,320 @@ +import { Curation } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { GraphNetworkContracts, helpers, randomHexBytes, toBN, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT every block + +describe('Rewards - Configuration', () => { + const graph = hre.graph() + let governor: SignerWithAddress + let indexer1: SignerWithAddress + let indexer2: SignerWithAddress + let curator1: SignerWithAddress + let curator2: SignerWithAddress + let oracle: SignerWithAddress + let assetHolder: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let staking: IStaking + let rewardsManager: RewardsManager + + const subgraphDeploymentID1 = randomHexBytes() + + before(async function () { + const testAccounts = await graph.getTestAccounts() + ;[indexer1, indexer2, curator1, curator2, oracle, assetHolder] = testAccounts + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager + + // 200 GRT per block + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + + // Distribute test funds + for (const wallet of [indexer1, indexer2, curator1, curator2, assetHolder]) { + await grt.connect(governor).mint(wallet.address, toGRT('1000000')) + await grt.connect(wallet).approve(staking.address, toGRT('1000000')) + await grt.connect(wallet).approve(curation.address, toGRT('1000000')) + } + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('configuration', function () { + describe('initialize', function () { + it('should revert when called on implementation contract', async function () { + // Try to call initialize on the implementation contract (should revert with onlyImpl) + const tx = rewardsManager.connect(governor).initialize(contracts.Controller.address) + await expect(tx).revertedWith('Only implementation') + }) + }) + + describe('issuance per block update', function () { + it('should reject set issuance per block if unauthorized', async function () { + const tx = rewardsManager.connect(indexer1).setIssuancePerBlock(toGRT('1.025')) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set issuance rate to minimum allowed (0)', async function () { + const newIssuancePerBlock = toGRT('0') + await rewardsManager.connect(governor).setIssuancePerBlock(newIssuancePerBlock) + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) + }) + + it('should set issuance rate', async function () { + const newIssuancePerBlock = toGRT('100.025') + await rewardsManager.connect(governor).setIssuancePerBlock(newIssuancePerBlock) + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) + expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await helpers.latestBlock()) + }) + + it('should update timestamp when transitioning from zero to non-zero issuance', async function () { + // Add some signal so rewards can be calculated + await curation.connect(curator1).mint(subgraphDeploymentID1, toGRT('10000'), 0) + + // Mine some blocks with rewards active + await helpers.mine(10) + + // Set issuance to zero - this updates timestamp correctly + await rewardsManager.connect(governor).setIssuancePerBlock(0) + const blockAfterZeroIssuance = await helpers.latestBlock() + + // Verify timestamp was updated + const timestampAfterZero = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + expect(timestampAfterZero).to.equal(blockAfterZeroIssuance) + + // Mine blocks during zero issuance period + await helpers.mine(10) + + // Set issuance back to non-zero + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + const blockAfterRestore = await helpers.latestBlock() + + // Timestamp should be updated when transitioning from zero issuance + const timestampAfterRestore = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + expect(timestampAfterRestore).to.equal( + blockAfterRestore, + 'Timestamp should be updated when transitioning from zero issuance', + ) + }) + + it('should not over-issue rewards after zero issuance period', async function () { + // Add signal + await curation.connect(curator1).mint(subgraphDeploymentID1, toGRT('10000'), 0) + + // Get signalled tokens for calculation + const signalledTokens = await grt.balanceOf(curation.address) + + // Mine some blocks with rewards active + await helpers.mine(10) + + // Capture rewards and timestamp before zero issuance period + await rewardsManager.connect(governor).updateAccRewardsPerSignal() + const rewardsAfterFirstPeriod = await rewardsManager.accRewardsPerSignal() + + // Set issuance to zero + await rewardsManager.connect(governor).setIssuancePerBlock(0) + const timestampAfterZeroSet = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + + // Mine blocks during zero issuance - NO rewards should accumulate + await helpers.mine(10) + + // Restore issuance - record the block when non-zero issuance starts + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + const timestampAfterRestore = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + + // Mine more blocks with rewards active + await helpers.mine(10) + + // Update and check final rewards + await rewardsManager.connect(governor).updateAccRewardsPerSignal() + const finalRewards = await rewardsManager.accRewardsPerSignal() + const finalTimestamp = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + + // The actual rewards increase from first period to final + const rewardsIncrease = finalRewards.sub(rewardsAfterFirstPeriod) + + // Calculate expected rewards based on ACTUAL blocks where issuance was active + const FIXED_POINT_SCALING_FACTOR = BigNumber.from(10).pow(18) + const activeBlocksAfterRestore = finalTimestamp.sub(timestampAfterRestore) + const expectedIncrease = ISSUANCE_PER_BLOCK.mul(activeBlocksAfterRestore) + .mul(FIXED_POINT_SCALING_FACTOR) + .div(signalledTokens) + + // Key assertion: timestamp should advance during zero issuance period + expect(timestampAfterRestore.toNumber()).to.be.greaterThan( + timestampAfterZeroSet.toNumber(), + 'Timestamp should advance when setting non-zero issuance', + ) + + // Allow some tolerance for block timing (1 block variance) + const tolerance = ISSUANCE_PER_BLOCK.mul(1).mul(FIXED_POINT_SCALING_FACTOR).div(signalledTokens) + + // Rewards should match the active period only + expect(rewardsIncrease).to.be.closeTo(expectedIncrease, tolerance, 'Rewards should match active period only') + }) + }) + + describe('subgraph availability service', function () { + it('should reject set subgraph oracle if unauthorized', async function () { + const tx = rewardsManager.connect(indexer1).setSubgraphAvailabilityOracle(oracle.address) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set subgraph oracle if governor', async function () { + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(oracle.address) + expect(await rewardsManager.subgraphAvailabilityOracle()).eq(oracle.address) + }) + + it('should reject to deny subgraph if not the oracle', async function () { + const tx = rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + await expect(tx).revertedWith('Caller must be the subgraph availability oracle') + }) + + it('should deny subgraph', async function () { + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(oracle.address) + + const tx = rewardsManager.connect(oracle).setDenied(subgraphDeploymentID1, true) + const blockNum = await helpers.latestBlock() + await expect(tx) + .emit(rewardsManager, 'RewardsDenylistUpdated') + .withArgs(subgraphDeploymentID1, blockNum + 1) + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(true) + }) + + it('should allow removing subgraph from denylist', async function () { + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(oracle.address) + + // First deny the subgraph + await rewardsManager.connect(oracle).setDenied(subgraphDeploymentID1, true) + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(true) + + // Then remove from denylist + const tx = rewardsManager.connect(oracle).setDenied(subgraphDeploymentID1, false) + await expect(tx).emit(rewardsManager, 'RewardsDenylistUpdated').withArgs(subgraphDeploymentID1, 0) + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(false) + }) + + it('should be a no-op when denying an already denied subgraph', async function () { + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(oracle.address) + + // Deny the subgraph + await rewardsManager.connect(oracle).setDenied(subgraphDeploymentID1, true) + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(true) + const denyBlockBefore = await rewardsManager.denylist(subgraphDeploymentID1) + + // Deny again - should not emit event or change denylist block number + const tx = rewardsManager.connect(oracle).setDenied(subgraphDeploymentID1, true) + await expect(tx).not.emit(rewardsManager, 'RewardsDenylistUpdated') + + // State should be unchanged + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(true) + const denyBlockAfter = await rewardsManager.denylist(subgraphDeploymentID1) + expect(denyBlockAfter).eq(denyBlockBefore) + }) + + it('should be a no-op when undenying an already not-denied subgraph', async function () { + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(oracle.address) + + // Subgraph is not denied by default + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(false) + + // Undeny should not emit event + const tx = rewardsManager.connect(oracle).setDenied(subgraphDeploymentID1, false) + await expect(tx).not.emit(rewardsManager, 'RewardsDenylistUpdated') + + // State should remain unchanged + expect(await rewardsManager.isDenied(subgraphDeploymentID1)).eq(false) + expect(await rewardsManager.denylist(subgraphDeploymentID1)).eq(0) + }) + + it('should reject setMinimumSubgraphSignal if unauthorized', async function () { + const tx = rewardsManager.connect(indexer1).setMinimumSubgraphSignal(toGRT('1000')) + await expect(tx).revertedWith('Not authorized') + }) + + it('should allow setMinimumSubgraphSignal from subgraph availability oracle', async function () { + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(oracle.address) + + const newMinimumSignal = toGRT('2000') + const tx = rewardsManager.connect(oracle).setMinimumSubgraphSignal(newMinimumSignal) + await expect(tx).emit(rewardsManager, 'ParameterUpdated').withArgs('minimumSubgraphSignal') + + expect(await rewardsManager.minimumSubgraphSignal()).eq(newMinimumSignal) + }) + + it('should allow setMinimumSubgraphSignal from governor', async function () { + const newMinimumSignal = toGRT('3000') + const tx = rewardsManager.connect(governor).setMinimumSubgraphSignal(newMinimumSignal) + await expect(tx).emit(rewardsManager, 'ParameterUpdated').withArgs('minimumSubgraphSignal') + + expect(await rewardsManager.minimumSubgraphSignal()).eq(newMinimumSignal) + }) + }) + + describe('revertOnIneligible', function () { + it('should reject setRevertOnIneligible if unauthorized', async function () { + const tx = rewardsManager.connect(indexer1).setRevertOnIneligible(true) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set revertOnIneligible to true', async function () { + const tx = rewardsManager.connect(governor).setRevertOnIneligible(true) + await expect(tx).emit(rewardsManager, 'ParameterUpdated').withArgs('revertOnIneligible') + expect(await rewardsManager.getRevertOnIneligible()).eq(true) + }) + + it('should set revertOnIneligible to false', async function () { + // First set to true + await rewardsManager.connect(governor).setRevertOnIneligible(true) + + // Then set back to false + const tx = rewardsManager.connect(governor).setRevertOnIneligible(false) + await expect(tx).emit(rewardsManager, 'ParameterUpdated').withArgs('revertOnIneligible') + expect(await rewardsManager.getRevertOnIneligible()).eq(false) + }) + + it('should be a no-op when setting same value (false to false)', async function () { + // Default is false + expect(await rewardsManager.getRevertOnIneligible()).eq(false) + + const tx = rewardsManager.connect(governor).setRevertOnIneligible(false) + await expect(tx).to.not.emit(rewardsManager, 'ParameterUpdated') + + expect(await rewardsManager.getRevertOnIneligible()).eq(false) + }) + + it('should be a no-op when setting same value (true to true)', async function () { + await rewardsManager.connect(governor).setRevertOnIneligible(true) + + const tx = rewardsManager.connect(governor).setRevertOnIneligible(true) + await expect(tx).to.not.emit(rewardsManager, 'ParameterUpdated') + + expect(await rewardsManager.getRevertOnIneligible()).eq(true) + }) + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-distribution.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-distribution.test.ts new file mode 100644 index 000000000..e34ace2fd --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-distribution.test.ts @@ -0,0 +1,745 @@ +import { Curation } from '@graphprotocol/contracts' +import { EpochManager } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { + deriveChannelKey, + formatGRT, + GraphNetworkContracts, + helpers, + randomHexBytes, + toBN, + toGRT, +} from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber, constants } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +const MAX_PPM = 1000000 + +// TODO: Behavior change - HorizonRewardsAssigned is no longer emitted when rewards == 0 +// Set to true if the old behavior is restored (emitting event for zero rewards) +const EMIT_EVENT_FOR_ZERO_REWARDS = false + +const { HashZero, WeiPerEther } = constants + +const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] + +describe('Rewards - Distribution', () => { + const graph = hre.graph() + let delegator: SignerWithAddress + let governor: SignerWithAddress + let curator1: SignerWithAddress + let curator2: SignerWithAddress + let indexer1: SignerWithAddress + let assetHolder: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let epochManager: EpochManager + let staking: IStaking + let rewardsManager: RewardsManager + + // Derive some channel keys for each indexer used to sign attestations + const channelKey1 = deriveChannelKey() + const channelKey2 = deriveChannelKey() + const channelKeyNull = deriveChannelKey() + + const subgraphDeploymentID1 = randomHexBytes() + const subgraphDeploymentID2 = randomHexBytes() + + const allocationID1 = channelKey1.address + const allocationID2 = channelKey2.address + const allocationIDNull = channelKeyNull.address + + const metadata = HashZero + + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 800 GRT rewards + const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT every block + + before(async function () { + ;[delegator, curator1, curator2, indexer1, assetHolder] = await graph.getTestAccounts() + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + epochManager = contracts.EpochManager + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager + + // 200 GRT per block + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + + // Distribute test funds + for (const wallet of [indexer1, curator1, curator2, assetHolder]) { + await grt.connect(governor).mint(wallet.address, toGRT('1000000')) + await grt.connect(wallet).approve(staking.address, toGRT('1000000')) + await grt.connect(wallet).approve(curation.address, toGRT('1000000')) + } + + // Set the staking contract as the subgraph service so it can call takeRewards + await rewardsManager.connect(governor).setSubgraphService(staking.address) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + context('issuing rewards', function () { + beforeEach(async function () { + // 5% minute rate (4 blocks) + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + }) + + describe('getRewards', function () { + it('calculate rewards using the subgraph signalled + allocated tokens', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Jump + await helpers.mine(ISSUANCE_RATE_PERIODS) + + // Rewards + const contractRewards = await rewardsManager.getRewards(staking.address, allocationID1) + + // We trust using this function in the test because we tested it + // standalone in a previous test + const contractRewardsAT1 = (await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1))[0] + + const expectedRewards = contractRewardsAT1.mul(tokensToAllocate).div(WeiPerEther) + expect(expectedRewards).eq(contractRewards) + }) + it('rewards should be zero if the allocation is closed', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Jump + await helpers.mine(ISSUANCE_RATE_PERIODS) + await helpers.mineEpoch(epochManager) + + // Close allocation + await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // Rewards + const contractRewards = await rewardsManager.getRewards(staking.address, allocationID1) + expect(contractRewards).eq(BigNumber.from(0)) + }) + it('rewards should be zero if the allocation does not exist', async function () { + // Rewards + const contractRewards = await rewardsManager.getRewards(staking.address, allocationIDNull) + expect(contractRewards).eq(BigNumber.from(0)) + }) + }) + + describe('takeRewards', function () { + interface DelegationParameters { + indexingRewardCut: BigNumber + queryFeeCut: BigNumber + cooldownBlocks: number + } + + async function setupIndexerAllocation() { + // Setup + await epochManager.connect(governor).setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + async function setupIndexerAllocationSignalingAfter() { + // Setup + await epochManager.connect(governor).setEpochLength(10) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + } + + async function setupIndexerAllocationWithDelegation( + tokensToDelegate: BigNumber, + delegationParams: DelegationParameters, + ) { + const tokensToAllocate = toGRT('12500') + + // Setup + await epochManager.connect(governor).setEpochLength(10) + + // Transfer some funds from the curator, I don't want to mint new tokens + await grt.connect(curator1).transfer(delegator.address, tokensToDelegate) + await grt.connect(delegator).approve(staking.address, tokensToDelegate) + + // Stake and set delegation parameters + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .setDelegationParameters(delegationParams.indexingRewardCut, delegationParams.queryFeeCut, 0) + + // Delegate + await staking.connect(delegator).delegate(indexer1.address, tokensToDelegate) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + it('should distribute rewards on closed allocation and stake', async function () { + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + // Setup + await setupIndexerAllocation() + + // Jump + await helpers.mineEpoch(epochManager) + + // Before state + const beforeTokenSupply = await grt.totalSupply() + const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) + const beforeStakingBalance = await grt.balanceOf(staking.address) + + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted. + // The final snapshot is when we close the allocation, that happens 9 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 7) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('1400') + + // Close allocation. At this point rewards should be collected for that indexer + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + expect(event.indexer).eq(indexer1.address) + expect(event.allocationID).eq(allocationID1) + expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) + + // After state + const afterTokenSupply = await grt.totalSupply() + const afterIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const afterIndexer1Balance = await grt.balanceOf(indexer1.address) + const afterStakingBalance = await grt.balanceOf(staking.address) + + // Check that rewards are put into indexer stake + const expectedIndexerStake = beforeIndexer1Stake.add(expectedIndexingRewards) + const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + // Check stake should have increased with the rewards staked + expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) + // Check indexer balance remains the same + expect(afterIndexer1Balance).eq(beforeIndexer1Balance) + // Check indexing rewards are kept in the staking contract + expect(toRound(afterStakingBalance)).eq(toRound(beforeStakingBalance.add(expectedIndexingRewards))) + // Check that tokens have been minted + expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + }) + + it('does not revert with an underflow if the minimum signal changes', async function () { + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + // Setup + await setupIndexerAllocation() + + await rewardsManager.connect(governor).setMinimumSubgraphSignal(toGRT(14000)) + + // Jump + await helpers.mineEpoch(epochManager) + + // Close allocation. At this point rewards should be collected for that indexer + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } + }) + + it('does not revert with an underflow if the minimum signal changes, and signal came after allocation', async function () { + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + // Setup + await setupIndexerAllocationSignalingAfter() + + await rewardsManager.connect(governor).setMinimumSubgraphSignal(toGRT(14000)) + + // Jump + await helpers.mineEpoch(epochManager) + + // Close allocation. At this point rewards should be collected for that indexer + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } + }) + + it('does not revert if signal was already under minimum', async function () { + await rewardsManager.connect(governor).setMinimumSubgraphSignal(toGRT(2000)) + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + // Setup + await setupIndexerAllocation() + + // Jump + await helpers.mineEpoch(epochManager) + // Close allocation. At this point rewards should be collected for that indexer + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } + }) + + it('should distribute rewards on closed allocation and send to destination', async function () { + const destinationAddress = randomHexBytes(20) + await staking.connect(indexer1).setRewardsDestination(destinationAddress) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + // Setup + await setupIndexerAllocation() + + // Jump + await helpers.mineEpoch(epochManager) + + // Before state + const beforeTokenSupply = await grt.totalSupply() + const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const beforeDestinationBalance = await grt.balanceOf(destinationAddress) + const beforeStakingBalance = await grt.balanceOf(staking.address) + + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted. + // The final snapshot is when we close the allocation, that happens 9 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 7) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('1400') + + // Close allocation. At this point rewards should be collected for that indexer + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + expect(event.indexer).eq(indexer1.address) + expect(event.allocationID).eq(allocationID1) + expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) + + // After state + const afterTokenSupply = await grt.totalSupply() + const afterIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const afterDestinationBalance = await grt.balanceOf(destinationAddress) + const afterStakingBalance = await grt.balanceOf(staking.address) + + // Check that rewards are properly assigned + const expectedIndexerStake = beforeIndexer1Stake + const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + // Check stake should not have changed + expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) + // Check indexing rewards are received by the rewards destination + expect(toRound(afterDestinationBalance)).eq(toRound(beforeDestinationBalance.add(expectedIndexingRewards))) + // Check indexing rewards were not sent to the staking contract + expect(afterStakingBalance).eq(beforeStakingBalance) + // Check that tokens have been minted + expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + }) + + it('should distribute rewards on closed allocation w/delegators', async function () { + // Setup + const delegationParams = { + indexingRewardCut: toBN('823000'), // 82.30% + queryFeeCut: toBN('80000'), // 8% + cooldownBlocks: 0, + } + const tokensToDelegate = toGRT('2000') + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + // Setup the allocation and delegators + await setupIndexerAllocationWithDelegation(tokensToDelegate, delegationParams) + + // Jump + await helpers.mineEpoch(epochManager) + + // Before state + const beforeTokenSupply = await grt.totalSupply() + const beforeDelegationPool = await staking.delegationPools(indexer1.address) + const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + + // Close allocation. At this point rewards should be collected for that indexer + await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // After state + const afterTokenSupply = await grt.totalSupply() + const afterDelegationPool = await staking.delegationPools(indexer1.address) + const afterIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + + // Check that rewards are put into indexer stake (only indexer cut) + // Check that rewards are put into delegators pool accordingly + + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 1 block after the signal is minted. + // The final snapshot is when we close the allocation, that happens 4 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 3) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('600') + // Calculate delegators cut + const indexerRewards = delegationParams.indexingRewardCut.mul(expectedIndexingRewards).div(toBN(MAX_PPM)) + // Calculate indexer cut + const delegatorsRewards = expectedIndexingRewards.sub(indexerRewards) + // Check + const expectedIndexerStake = beforeIndexer1Stake.add(indexerRewards) + const expectedDelegatorsPoolTokens = beforeDelegationPool.tokens.add(delegatorsRewards) + const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) + expect(toRound(afterDelegationPool.tokens)).eq(toRound(expectedDelegatorsPoolTokens)) + // Check that tokens have been minted + expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + }) + + it('should deny rewards if subgraph on denylist', async function () { + // Setup: create allocation BEFORE denying the subgraph + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Create allocation + await setupIndexerAllocation() + + // Jump to earn some rewards + await helpers.mineEpoch(epochManager) + + // Now deny the subgraph - this freezes accRewardsPerAllocatedToken + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Close allocation - pre-denial rewards should be denied + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + }) + + it('should handle zero rewards scenario correctly', async function () { + // Setup allocation with zero issuance to create zero rewards scenario + await rewardsManager.connect(governor).setIssuancePerBlock(0) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Before state + const beforeTokenSupply = await grt.totalSupply() + const beforeStakingBalance = await grt.balanceOf(staking.address) + + // Close allocation. At this point rewards should be zero + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } + + // After state - should be unchanged since no rewards were minted + const afterTokenSupply = await grt.totalSupply() + const afterStakingBalance = await grt.balanceOf(staking.address) + + // Check that no tokens were minted (rewards were 0) + expect(afterTokenSupply).eq(beforeTokenSupply) + expect(afterStakingBalance).eq(beforeStakingBalance) + }) + }) + }) + + describe('edge scenarios', function () { + it('close allocation on a subgraph that no longer have signal', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Jump + await helpers.mineEpoch(epochManager) + + // Remove all signal from the subgraph + const curatorShares = await curation.getCuratorSignal(curator1.address, subgraphDeploymentID1) + await curation.connect(curator1).burn(subgraphDeploymentID1, curatorShares, 0) + + // Close allocation. At this point rewards should be collected for that indexer + await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + }) + }) + + describe('multiple allocations', function () { + it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { + // If rewards are not monotonically increasing, this can trigger + // a subtraction overflow error as seen in mainnet tx: + // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab + await helpers.mineEpoch(epochManager) + + // Setup + await epochManager.connect(governor).setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1).stake(tokensToStake) + + // Allocate simultaneously, burning in the middle + const tokensToAlloc = toGRT('5000') + await helpers.setAutoMine(false) + const tx1 = await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + const tx2 = await grt.connect(indexer1).burn(toGRT(1)) + const tx3 = await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) + + await helpers.mine() + await helpers.setAutoMine(true) + + await expect(tx1).emit(staking, 'AllocationCreated') + await expect(tx2).emit(grt, 'Transfer') + await expect(tx3).emit(staking, 'AllocationCreated') + }) + it('two simultanous-similar allocations should get same amount of rewards', async function () { + await helpers.mineEpoch(epochManager) + + // Setup + await epochManager.connect(governor).setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1).stake(tokensToStake) + + // Allocate simultaneously + const tokensToAlloc = toGRT('5000') + const tx1 = await staking.populateTransaction.allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + const tx2 = await staking.populateTransaction.allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) + await staking.connect(indexer1).multicall([tx1.data, tx2.data]) + + // Jump + await helpers.mineEpoch(epochManager) + + // Close allocations simultaneously + const tx3 = await staking.populateTransaction.closeAllocation(allocationID1, randomHexBytes()) + const tx4 = await staking.populateTransaction.closeAllocation(allocationID2, randomHexBytes()) + const tx5 = await staking.connect(indexer1).multicall([tx3.data, tx4.data]) + + // Both allocations should receive the same amount of rewards + const receipt = await tx5.wait() + const event1 = rewardsManager.interface.parseLog(receipt.logs[1]).args + const event2 = rewardsManager.interface.parseLog(receipt.logs[5]).args + expect(event1.amount).eq(event2.amount) + }) + }) + + describe('rewards progression when collecting query fees', function () { + it('collect query fees with two subgraphs and one allocation', async function () { + async function getRewardsAccrual(subgraphs) { + const [sg1, sg2] = await Promise.all(subgraphs.map((sg) => rewardsManager.getAccRewardsForSubgraph(sg))) + return { + sg1, + sg2, + all: sg1.add(sg2), + } + } + + // set curation percentage + await staking.connect(governor).setCurationPercentage(100000) + + // allow the asset holder + const tokensToCollect = toGRT('10000') + + // signal in two subgraphs in the same block + const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2] + await hre.network.provider.send('evm_setAutomine', [false]) + for (const sub of subgraphs) { + await curation.connect(curator1).mint(sub, toGRT('1500'), 0) + } + await hre.network.provider.send('evm_mine') + await hre.network.provider.send('evm_setAutomine', [true]) + + // allocate + const tokensToAllocate = toGRT('12500') + await staking + .connect(indexer1) + .multicall([ + await staking.populateTransaction.stake(tokensToAllocate).then((tx) => tx.data), + await staking.populateTransaction + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + .then((tx) => tx.data), + ]) + + // snapshot block after allocation (rewards before allocation were reclaimed for subgraph1) + const b1 = await epochManager.blockNum().then((x) => x.toNumber()) + + // move time fwd + await helpers.mineEpoch(epochManager) + + // collect funds into staking for that sub + await staking.connect(assetHolder).collect(tokensToCollect, allocationID1) + + // check rewards diff + await rewardsManager.getRewards(staking.address, allocationID1).then(formatGRT) + + await helpers.mine() + const accrual = await getRewardsAccrual(subgraphs) + const b2 = await epochManager.blockNum().then((x) => x.toNumber()) + + // Only check subgraph1 (with allocation) - subgraph2 has no allocation so its rewards + // are calculated from signal time, not from allocation time + // Each subgraph gets half the issuance (equal signal) + // Small tolerance for fixed-point arithmetic rounding + const expectedSg1Rewards = ISSUANCE_PER_BLOCK.div(2).mul(b2 - b1) + expect(toRound(accrual.sg1.mul(100).div(expectedSg1Rewards))).eq(toRound(BigNumber.from(100))) + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts new file mode 100644 index 000000000..c2137dc64 --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts @@ -0,0 +1,700 @@ +import { Curation } from '@graphprotocol/contracts' +import { EpochManager } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber, constants } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +const { HashZero } = constants + +// Tolerance for fixed-point arithmetic rounding errors (matching Foundry tests) +const REWARDS_TOLERANCE = 20000 + +// Helper to check approximate equality for rewards (allows for rounding errors in fixed-point math) +function expectApproxEq(actual: BigNumber, expected: BigNumber, message: string) { + const diff = actual.sub(expected).abs() + expect( + diff.lte(REWARDS_TOLERANCE), + `${message}: difference ${diff.toString()} exceeds tolerance ${REWARDS_TOLERANCE}`, + ).to.be.true +} + +describe('Rewards - Eligibility Oracle', () => { + const graph = hre.graph() + let curator1: SignerWithAddress + let governor: SignerWithAddress + let indexer1: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let epochManager: EpochManager + let staking: IStaking + let rewardsManager: RewardsManager + + // Derive channel key for indexer used to sign attestations + const channelKey1 = deriveChannelKey() + + const subgraphDeploymentID1 = randomHexBytes() + + const allocationID1 = channelKey1.address + + const metadata = HashZero + + const ISSUANCE_PER_BLOCK = toGRT('200') // 200 GRT every block + + async function setupIndexerAllocation() { + // Setup + await epochManager.connect(governor).setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + before(async function () { + const testAccounts = await graph.getTestAccounts() + curator1 = testAccounts[0] + indexer1 = testAccounts[1] + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + epochManager = contracts.EpochManager + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager + + // 200 GRT per block + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + + // Distribute test funds + for (const wallet of [indexer1, curator1]) { + await grt.connect(governor).mint(wallet.address, toGRT('1000000')) + await grt.connect(wallet).approve(staking.address, toGRT('1000000')) + await grt.connect(wallet).approve(curation.address, toGRT('1000000')) + } + + // Set the staking contract as the subgraph service so it can call takeRewards + await rewardsManager.connect(governor).setSubgraphService(staking.address) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('rewards eligibility oracle', function () { + it('should reject setProviderEligibilityOracle if unauthorized', async function () { + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(true) + await mockOracle.deployed() + const tx = rewardsManager.connect(indexer1).setProviderEligibilityOracle(mockOracle.address) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set rewards eligibility oracle if governor', async function () { + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(true) + await mockOracle.deployed() + + const tx = rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + await expect(tx) + .emit(rewardsManager, 'ProviderEligibilityOracleSet') + .withArgs(constants.AddressZero, mockOracle.address) + + expect(await rewardsManager.getProviderEligibilityOracle()).eq(mockOracle.address) + }) + + it('should allow setting rewards eligibility oracle to zero address', async function () { + // First set an oracle + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(true) + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Then set to zero address to disable + const tx = rewardsManager.connect(governor).setProviderEligibilityOracle(constants.AddressZero) + await expect(tx) + .emit(rewardsManager, 'ProviderEligibilityOracleSet') + .withArgs(mockOracle.address, constants.AddressZero) + + expect(await rewardsManager.getProviderEligibilityOracle()).eq(constants.AddressZero) + }) + + it('should reject setting oracle that does not support interface', async function () { + // Try to set an EOA (externally owned account) as the rewards eligibility oracle + const tx = rewardsManager.connect(governor).setProviderEligibilityOracle(indexer1.address) + // EOA doesn't have code, so the call will revert (error message may vary by ethers version) + await expect(tx).to.be.reverted + }) + + it('should reject setting oracle that does not support IProviderEligibility interface', async function () { + // Deploy a contract that supports ERC165 but not IProviderEligibility + const MockERC165Factory = await hre.ethers.getContractFactory('contracts/tests/MockERC165.sol:MockERC165') + const mockERC165 = await MockERC165Factory.deploy() + await mockERC165.deployed() + + const tx = rewardsManager.connect(governor).setProviderEligibilityOracle(mockERC165.address) + await expect(tx).revertedWith('Contract does not support IProviderEligibility interface') + }) + + it('should not emit event when setting same oracle address', async function () { + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(true) + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Setting the same oracle again should not emit an event + const tx = rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + await expect(tx).to.not.emit(rewardsManager, 'ProviderEligibilityOracleSet') + }) + }) + + describe('rewards eligibility in takeRewards', function () { + it('should deny rewards due to rewards eligibility oracle', async function () { + // Setup rewards eligibility oracle that denies rewards for indexer1 + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Default to deny + await mockOracle.deployed() + + // Set the rewards eligibility oracle + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Calculate expected rewards (for verification in the event) + const expectedIndexingRewards = toGRT('1400') + + // Close allocation. At this point rewards should be denied due to eligibility + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsDeniedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'RewardsDeniedDueToEligibility') + + expect(rewardsDeniedEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + const event = rewardsDeniedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') + }) + + it('should allow rewards when rewards eligibility oracle approves', async function () { + // Setup rewards eligibility oracle that allows rewards for indexer1 + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(true) // Default to allow + await mockOracle.deployed() + + // Set the rewards eligibility oracle + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Calculate expected rewards + const expectedIndexingRewards = toGRT('1400') + + // Close allocation. At this point rewards should be assigned normally + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsAssignedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'HorizonRewardsAssigned') + + expect(rewardsAssignedEvents.length).to.equal(1, 'HorizonRewardsAssigned event not found') + const event = rewardsAssignedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') + }) + }) + + describe('rewards eligibility oracle and denylist interaction', function () { + // Note: With subgraph-level denial, rewards for denied subgraphs are handled via + // onSubgraphAllocationUpdate() at the subgraph level. The allocation-level _deniedRewards() + // path (which checks eligibility) is not reached because rewards = 0 for allocations + // created while denied (frozen accumulator). + + it('should prioritize denylist over REO when both deny', async function () { + // Setup BOTH denial mechanisms + // 1. Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // 2. Setup REO that also denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation (created while denied - accumulator frozen) + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Close allocation - subgraph denial takes precedence (handled at subgraph level) + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // With subgraph-level denial, rewards = 0 (frozen accumulator), so allocation-level + // denial events are not emitted. Rewards are reclaimed at subgraph level. + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenied') + await expect(tx).to.not.emit(rewardsManager, 'RewardsDeniedDueToEligibility') + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + }) + + it('should check REO when denylist allows but indexer ineligible', async function () { + // Setup: Subgraph is allowed (no denylist), but indexer is ineligible + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny indexer + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + const expectedIndexingRewards = toGRT('1400') + + // Close allocation - REO should be checked + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsDeniedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'RewardsDeniedDueToEligibility') + + expect(rewardsDeniedEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + const event = rewardsDeniedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') + }) + + it('should handle indexer becoming ineligible mid-allocation', async function () { + // Setup: Indexer starts eligible + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(true) // Start eligible + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation while indexer is eligible + await setupIndexerAllocation() + + // Jump to next epoch (rewards accrue) + await helpers.mineEpoch(epochManager) + + // Change eligibility AFTER allocation created but BEFORE closing + await mockOracle.setIndexerEligible(indexer1.address, false) + + const expectedIndexingRewards = toGRT('1600') + + // Close allocation - should be denied at close time (not creation time) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsDeniedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'RewardsDeniedDueToEligibility') + + expect(rewardsDeniedEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + const event = rewardsDeniedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') + }) + + it('should handle indexer becoming eligible mid-allocation', async function () { + // Setup: Indexer starts ineligible + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Start ineligible + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation while indexer is ineligible + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Change eligibility before closing + await mockOracle.setIndexerEligible(indexer1.address, true) + + const expectedIndexingRewards = toGRT('1600') + + // Close allocation - should now be allowed + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsAssignedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'HorizonRewardsAssigned') + + expect(rewardsAssignedEvents.length).to.equal(1, 'HorizonRewardsAssigned event not found') + const event = rewardsAssignedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') + }) + + it('should handle denylist being added mid-allocation', async function () { + // Setup: Start with subgraph NOT denied + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation when subgraph is allowed + await setupIndexerAllocation() + + // Jump to next epoch (rewards accrue) + await helpers.mineEpoch(epochManager) + + // Deny the subgraph before closing allocation + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Close allocation - should be denied even though it was created when allowed + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + }) + + it('should handle denylist being removed mid-allocation', async function () { + // Setup: Start with subgraph denied + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation (created while denied - accumulator frozen at this point) + await setupIndexerAllocation() + + // Jump to next epoch (rewards accrue but are reclaimed at subgraph level while denied) + await helpers.mineEpoch(epochManager) + + // Remove from denylist - this snapshots and starts accumulator updating again + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, false) + + // Wait for another epoch to accrue POST-undeny rewards + // Only post-undeny rewards are available (denied-period rewards were reclaimed) + await helpers.mineEpoch(epochManager) + + // Close allocation - should get post-undeny rewards only + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + // Verify rewards are assigned (exact amount depends on blocks since undeny) + await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned') + }) + + it('should allow rewards when REO is zero address (disabled)', async function () { + // Ensure REO is not set (zero address = disabled) + expect(await rewardsManager.getProviderEligibilityOracle()).eq(constants.AddressZero) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + const expectedIndexingRewards = toGRT('1400') + + // Close allocation - should get rewards (no eligibility check when REO is zero) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsAssignedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'HorizonRewardsAssigned') + + expect(rewardsAssignedEvents.length).to.equal(1, 'HorizonRewardsAssigned event not found') + const event = rewardsAssignedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') + }) + + it('should revert for ineligible indexer when revertOnIneligible is true', async function () { + // Setup REO that denies indexer1 + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Enable revert on ineligible + await rewardsManager.connect(governor).setRevertOnIneligible(true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Close allocation - should revert because indexer is ineligible + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx).revertedWith('Indexer not eligible for rewards') + }) + + it('should not revert for eligible indexer when revertOnIneligible is true', async function () { + // Setup REO that allows indexer1 + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(true) // Allow + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Enable revert on ineligible + await rewardsManager.connect(governor).setRevertOnIneligible(true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Close allocation - should succeed (indexer is eligible) + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned') + }) + + it('should reclaim (not revert) for ineligible indexer when revertOnIneligible is false', async function () { + // Setup REO that denies indexer1 + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Ensure revertOnIneligible is false (default) + expect(await rewardsManager.getRevertOnIneligible()).eq(false) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Close allocation - should succeed but deny rewards + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Should emit RewardsDeniedDueToEligibility (not revert) + const rewardsDeniedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'RewardsDeniedDueToEligibility') + + expect(rewardsDeniedEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + }) + + it('should verify event structure differences between denial mechanisms', async function () { + // Test 1: Denylist denial - event WITHOUT amount + // Create allocation FIRST, then deny (so there are pre-denial rewards to deny) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + await helpers.mineEpoch(epochManager) + await setupIndexerAllocation() + await helpers.mineEpoch(epochManager) + + // Deny AFTER allocation created (so rewards have accrued) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + const tx1 = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt1 = await tx1.wait() + + // Find the RewardsDenied event - search in logs as events may be from different contracts + const rewardsDeniedEvent = receipt1.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .find((event) => event?.name === 'RewardsDenied') + + expect(rewardsDeniedEvent).to.not.be.undefined + + // Verify it only has indexer and allocationID (no amount parameter) + expect(rewardsDeniedEvent?.args?.indexer).to.equal(indexer1.address) + expect(rewardsDeniedEvent?.args?.allocationID).to.equal(allocationID1) + // RewardsDenied has only 2 args, amount should not exist + expect(rewardsDeniedEvent?.args?.amount).to.be.undefined + + // Reset for test 2 + await fixture.tearDown() + await fixture.setUp() + + // Test 2: REO denial - event WITH amount + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + await helpers.mineEpoch(epochManager) + await setupIndexerAllocation() + await helpers.mineEpoch(epochManager) + + const tx2 = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt2 = await tx2.wait() + + // Find the RewardsDeniedDueToEligibility event + const eligibilityEvent = receipt2.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .find((event) => event?.name === 'RewardsDeniedDueToEligibility') + + expect(eligibilityEvent).to.not.be.undefined + + // Verify it has indexer, allocationID, AND amount + expect(eligibilityEvent?.args?.indexer).to.equal(indexer1.address) + expect(eligibilityEvent?.args?.allocationID).to.equal(allocationID1) + expect(eligibilityEvent?.args?.amount).to.not.be.undefined + expect(eligibilityEvent?.args?.amount).to.be.gt(0) // Shows what they would have gotten + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts new file mode 100644 index 000000000..db77d5f9b --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts @@ -0,0 +1,144 @@ +import { RewardsManager } from '@graphprotocol/contracts' +import { + IERC165__factory, + IIssuanceTarget__factory, + IProviderEligibilityManagement__factory, + IRewardsManager__factory, +} from '@graphprotocol/interfaces/types' +import { GraphNetworkContracts, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { constants } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +describe('RewardsManager interfaces', () => { + const graph = hre.graph() + let governor: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let rewardsManager: RewardsManager + + before(async function () { + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + rewardsManager = contracts.RewardsManager + + // Set a default issuance per block + await rewardsManager.connect(governor).setIssuancePerBlock(toGRT('200')) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + /** + * Interface ID Stability Tests + * + * These tests verify that interface IDs remain stable across builds. + * Changes to these IDs indicate breaking changes to the interface definitions. + * + * If a test fails: + * 1. Verify the interface change was intentional + * 2. Understand the impact on deployed contracts + * 3. Update the expected ID if the change is correct + * 4. Document the breaking change in release notes + */ + describe('Interface ID Stability', () => { + it('IERC165 should have stable interface ID', () => { + expect(IERC165__factory.interfaceId).to.equal('0x01ffc9a7') + }) + + it('IIssuanceTarget should have stable interface ID', () => { + expect(IIssuanceTarget__factory.interfaceId).to.equal('0x19f6601a') + }) + + it('IRewardsManager should have stable interface ID', () => { + expect(IRewardsManager__factory.interfaceId).to.equal('0x8469b577') + }) + }) + + describe('supportsInterface', function () { + it('should support IIssuanceTarget interface', async function () { + const supports = await rewardsManager.supportsInterface(IIssuanceTarget__factory.interfaceId) + expect(supports).to.be.true + }) + + it('should support IRewardsManager interface', async function () { + const supports = await rewardsManager.supportsInterface(IRewardsManager__factory.interfaceId) + expect(supports).to.be.true + }) + + it('should support IERC165 interface', async function () { + const supports = await rewardsManager.supportsInterface(IERC165__factory.interfaceId) + expect(supports).to.be.true + }) + + it('should support IProviderEligibilityManagement interface', async function () { + const supports = await rewardsManager.supportsInterface(IProviderEligibilityManagement__factory.interfaceId) + expect(supports).to.be.true + }) + + it('should return false for unsupported interfaces', async function () { + // Test with an unknown interface ID + const unknownInterfaceId = '0x12345678' // Random interface ID + const supports = await rewardsManager.supportsInterface(unknownInterfaceId) + expect(supports).to.be.false + }) + }) + + describe('getter functions', function () { + it('should return zero address for issuance allocator when not set', async function () { + const allocator = await rewardsManager.getIssuanceAllocator() + expect(allocator).to.equal(constants.AddressZero) + }) + + it('should return zero address for rewards eligibility oracle when not set', async function () { + const oracle = await rewardsManager.getProviderEligibilityOracle() + expect(oracle).to.equal(constants.AddressZero) + }) + + it('should return zero address for reclaim address when not set', async function () { + const reclaimAddress = await rewardsManager.getReclaimAddress(constants.HashZero) + expect(reclaimAddress).to.equal(constants.AddressZero) + }) + }) + + describe('calcRewards', function () { + it('should calculate rewards correctly', async function () { + const tokens = toGRT('1000') + const accRewardsPerAllocatedToken = toGRT('0.5') + + // Expected: (1000 * 0.5 * 1e18) / 1e18 = 500 GRT + const expectedRewards = toGRT('500') + + const rewards = await rewardsManager.calcRewards(tokens, accRewardsPerAllocatedToken) + expect(rewards).to.equal(expectedRewards) + }) + + it('should return 0 when tokens is 0', async function () { + const tokens = toGRT('0') + const accRewardsPerAllocatedToken = toGRT('0.5') + + const rewards = await rewardsManager.calcRewards(tokens, accRewardsPerAllocatedToken) + expect(rewards).to.equal(0) + }) + + it('should return 0 when accRewardsPerAllocatedToken is 0', async function () { + const tokens = toGRT('1000') + const accRewardsPerAllocatedToken = toGRT('0') + + const rewards = await rewardsManager.calcRewards(tokens, accRewardsPerAllocatedToken) + expect(rewards).to.equal(0) + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts new file mode 100644 index 000000000..8047b8fd6 --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts @@ -0,0 +1,420 @@ +import { Curation } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { GraphNetworkContracts, helpers, randomHexBytes, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { constants } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +describe('Rewards - Issuance Allocator', () => { + const graph = hre.graph() + let curator1: SignerWithAddress + let governor: SignerWithAddress + let indexer1: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let rewardsManager: RewardsManager + + const subgraphDeploymentID1 = randomHexBytes() + + const ISSUANCE_PER_BLOCK = toGRT('200') // 200 GRT every block + + before(async function () { + const testAccounts = await graph.getTestAccounts() + curator1 = testAccounts[0] + indexer1 = testAccounts[1] + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + rewardsManager = contracts.RewardsManager as RewardsManager + + // 200 GRT per block + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + + // Distribute test funds + for (const wallet of [curator1]) { + await grt.connect(governor).mint(wallet.address, toGRT('1000000')) + await grt.connect(wallet).approve(curation.address, toGRT('1000000')) + } + }) + + beforeEach(async function () { + await fixture.setUp() + // Reset issuance allocator to ensure we use direct issuancePerBlock + await rewardsManager.connect(governor).setIssuanceAllocator(constants.AddressZero) + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('setIssuanceAllocator', function () { + describe('ERC-165 validation', function () { + it('should successfully set an issuance allocator that supports the interface', async function () { + // Deploy a mock issuance allocator that supports ERC-165 and IIssuanceAllocationDistribution + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockAllocator.deployed() + + // Should succeed because MockIssuanceAllocator supports IIssuanceAllocationDistribution + await expect(rewardsManager.connect(governor).setIssuanceAllocator(mockAllocator.address)) + .to.emit(rewardsManager, 'IssuanceAllocatorSet') + .withArgs(constants.AddressZero, mockAllocator.address) + + // Verify the allocator was set + expect(await rewardsManager.getIssuanceAllocator()).to.equal(mockAllocator.address) + }) + + it('should revert when setting to EOA address (no contract code)', async function () { + const eoaAddress = indexer1.address + + // Should revert because EOAs don't have contract code to call supportsInterface on + await expect(rewardsManager.connect(governor).setIssuanceAllocator(eoaAddress)).to.be.reverted + }) + + it('should revert when setting to contract that does not support IIssuanceAllocationDistribution', async function () { + // Deploy a contract that supports ERC-165 but not IIssuanceAllocationDistribution + const MockERC165Factory = await hre.ethers.getContractFactory('contracts/tests/MockERC165.sol:MockERC165') + const mockERC165 = await MockERC165Factory.deploy() + await mockERC165.deployed() + + // Should revert because the contract doesn't support IIssuanceAllocationDistribution + await expect(rewardsManager.connect(governor).setIssuanceAllocator(mockERC165.address)).to.be.revertedWith( + 'Contract does not support IIssuanceAllocationDistribution interface', + ) + }) + + it('should validate interface before updating rewards calculation', async function () { + // This test ensures that ERC165 validation happens before updateAccRewardsPerSignal + // Deploy a contract that supports ERC-165 but not IIssuanceAllocationDistribution + const MockERC165Factory = await hre.ethers.getContractFactory('contracts/tests/MockERC165.sol:MockERC165') + const mockERC165 = await MockERC165Factory.deploy() + await mockERC165.deployed() + + // Should revert with interface error, not with any rewards calculation error + await expect(rewardsManager.connect(governor).setIssuanceAllocator(mockERC165.address)).to.be.revertedWith( + 'Contract does not support IIssuanceAllocationDistribution interface', + ) + }) + }) + + describe('access control', function () { + it('should revert when called by non-governor', async function () { + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockAllocator.deployed() + + // Should revert because indexer1 is not the governor + await expect(rewardsManager.connect(indexer1).setIssuanceAllocator(mockAllocator.address)).to.be.revertedWith( + 'Only Controller governor', + ) + }) + }) + + describe('state management', function () { + it('should allow setting issuance allocator to zero address (disable)', async function () { + // First set a valid allocator + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockAllocator.deployed() + + await rewardsManager.connect(governor).setIssuanceAllocator(mockAllocator.address) + expect(await rewardsManager.getIssuanceAllocator()).to.equal(mockAllocator.address) + + // Now disable by setting to zero address + await expect(rewardsManager.connect(governor).setIssuanceAllocator(constants.AddressZero)) + .to.emit(rewardsManager, 'IssuanceAllocatorSet') + .withArgs(mockAllocator.address, constants.AddressZero) + + expect(await rewardsManager.getIssuanceAllocator()).to.equal(constants.AddressZero) + + // Should now use local issuancePerBlock again — both getters agree + expect(await rewardsManager.getAllocatedIssuancePerBlock()).eq(ISSUANCE_PER_BLOCK) + expect(await rewardsManager.getRawIssuancePerBlock()).eq(ISSUANCE_PER_BLOCK) + }) + + it('should emit IssuanceAllocatorSet event when setting allocator', async function () { + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockIssuanceAllocator.deployed() + + const tx = rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) + await expect(tx) + .emit(rewardsManager, 'IssuanceAllocatorSet') + .withArgs(constants.AddressZero, mockIssuanceAllocator.address) + }) + + it('should not emit event when setting to same allocator address', async function () { + // Deploy a mock issuance allocator + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockAllocator.deployed() + + // Set the allocator first time + await rewardsManager.connect(governor).setIssuanceAllocator(mockAllocator.address) + + // Setting to same address should not emit event + const tx = await rewardsManager.connect(governor).setIssuanceAllocator(mockAllocator.address) + const receipt = await tx.wait() + + // Filter for IssuanceAllocatorSet events + const events = receipt.events?.filter((e) => e.event === 'IssuanceAllocatorSet') || [] + expect(events.length).to.equal(0) + }) + + it('should update rewards before changing issuance allocator', async function () { + // This test verifies that updateAccRewardsPerSignal is called when setting allocator + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockIssuanceAllocator.deployed() + + // Setting the allocator should trigger updateAccRewardsPerSignal + // We can't easily test this directly, but we can verify the allocator was set + await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) + expect(await rewardsManager.getIssuanceAllocator()).eq(mockIssuanceAllocator.address) + }) + }) + }) + + describe('getAllocatedIssuancePerBlock', function () { + it('should return issuancePerBlock when no issuanceAllocator is set', async function () { + const expectedIssuance = toGRT('100.025') + await rewardsManager.connect(governor).setIssuancePerBlock(expectedIssuance) + + // Ensure no issuanceAllocator is set + expect(await rewardsManager.getIssuanceAllocator()).eq(constants.AddressZero) + + // Both getters should agree when no allocator is set + expect(await rewardsManager.getAllocatedIssuancePerBlock()).eq(expectedIssuance) + expect(await rewardsManager.getRawIssuancePerBlock()).eq(expectedIssuance) + }) + + it('should return value from issuanceAllocator when set', async function () { + // Create a mock IssuanceAllocator + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockIssuanceAllocator.deployed() + + // Set the mock allocator on RewardsManager + await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) + + // Verify the allocator was set + expect(await rewardsManager.getIssuanceAllocator()).eq(mockIssuanceAllocator.address) + + // Set RewardsManager as a self-minting target with 25 GRT per block + const expectedIssuance = toGRT('25') + await mockIssuanceAllocator['setTargetAllocation(address,uint256,uint256,bool)']( + rewardsManager.address, + 0, // allocator issuance + expectedIssuance, // self issuance + true, + ) + + // Allocated getter returns the allocator value, raw getter still returns storage value + expect(await rewardsManager.getAllocatedIssuancePerBlock()).eq(expectedIssuance) + expect(await rewardsManager.getRawIssuancePerBlock()).eq(ISSUANCE_PER_BLOCK) + }) + + it('should return 0 when issuanceAllocator is set but target not registered as self-minter', async function () { + // Create a mock IssuanceAllocator + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockIssuanceAllocator.deployed() + + // Set the mock allocator on RewardsManager + await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) + + // Set RewardsManager as an allocator-minting target (only allocator issuance) + await mockIssuanceAllocator['setTargetAllocation(address,uint256,uint256,bool)']( + rewardsManager.address, + toGRT('25'), // allocator issuance + 0, // self issuance + false, + ) + + // Allocated returns 0 (not a self-minting target), raw is unchanged + expect(await rewardsManager.getAllocatedIssuancePerBlock()).eq(0) + expect(await rewardsManager.getRawIssuancePerBlock()).eq(ISSUANCE_PER_BLOCK) + }) + }) + + describe('setIssuancePerBlock', function () { + it('should allow setIssuancePerBlock when issuanceAllocator is set', async function () { + // Create and set a mock IssuanceAllocator + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockIssuanceAllocator.deployed() + await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) + + // Should allow setting issuancePerBlock even when allocator is set + const newIssuancePerBlock = toGRT('100') + await rewardsManager.connect(governor).setIssuancePerBlock(newIssuancePerBlock) + + // Both raw getter and storage variable reflect the new value + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) + expect(await rewardsManager.getRawIssuancePerBlock()).eq(newIssuancePerBlock) + + // But the effective (allocated) issuance still comes from the allocator + expect(await rewardsManager.getAllocatedIssuancePerBlock()).not.eq(newIssuancePerBlock) + }) + }) + + describe('beforeIssuanceAllocationChange', function () { + it('should handle beforeIssuanceAllocationChange correctly', async function () { + // Create and set a mock IssuanceAllocator + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockIssuanceAllocator.deployed() + await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) + + // Anyone should be able to call this function + await rewardsManager.connect(governor).beforeIssuanceAllocationChange() + + // Should also succeed when called by the allocator + await mockIssuanceAllocator.callBeforeIssuanceAllocationChange(rewardsManager.address) + }) + }) + + describe('issuance allocator integration', function () { + let mockIssuanceAllocator: any + + beforeEach(async function () { + // Create and setup mock allocator + const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', + ) + mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() + await mockIssuanceAllocator.deployed() + }) + + it('should accumulate rewards using allocator rate over time', async function () { + // Setup: Create signal + const totalSignal = toGRT('1000') + await curation.connect(curator1).mint(subgraphDeploymentID1, totalSignal, 0) + + // Set allocator with specific rate (50 GRT per block, different from local 200 GRT) + const allocatorRate = toGRT('50') + await mockIssuanceAllocator.setTargetAllocation(rewardsManager.address, 0, allocatorRate, false) + await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) + + // Snapshot state after setting allocator + const rewardsAfterSet = await rewardsManager.getAccRewardsPerSignal() + + // Mine blocks to accrue rewards at allocator rate + const blocksToMine = 10 + await helpers.mine(blocksToMine) + + // Get accumulated rewards + const rewardsAfterMining = await rewardsManager.getAccRewardsPerSignal() + const actualAccrued = rewardsAfterMining.sub(rewardsAfterSet) + + // Calculate expected rewards: (rate × blocks) / totalSignal + // Expected = (50 GRT × 10 blocks) / 1000 GRT signal = 0.5 GRT per signal + const expectedAccrued = allocatorRate.mul(blocksToMine).mul(toGRT('1')).div(totalSignal) + + // Verify rewards accumulated at allocator rate (not local rate of 200 GRT/block) + expect(actualAccrued).to.eq(expectedAccrued) + + // Verify NOT using local rate (would be 4x higher: 200 vs 50) + const wrongExpected = ISSUANCE_PER_BLOCK.mul(blocksToMine).mul(toGRT('1')).div(totalSignal) + expect(actualAccrued).to.not.eq(wrongExpected) + }) + + it('should maintain reward consistency when switching between rates', async function () { + // Setup: Create signal + const totalSignal = toGRT('2000') + await curation.connect(curator1).mint(subgraphDeploymentID1, totalSignal, 0) + + // Snapshot initial state + const block0 = await helpers.latestBlock() + const rewards0 = await rewardsManager.getAccRewardsPerSignal() + + // Phase 1: Accrue at local rate (200 GRT/block) + await helpers.mine(5) + const block1 = await helpers.latestBlock() + const rewards1 = await rewardsManager.getAccRewardsPerSignal() + + // Calculate phase 1 accrual + const blocksPhase1 = block1 - block0 + const phase1Accrued = rewards1.sub(rewards0) + const expectedPhase1 = ISSUANCE_PER_BLOCK.mul(blocksPhase1).mul(toGRT('1')).div(totalSignal) + expect(phase1Accrued).to.eq(expectedPhase1) + + // Phase 2: Switch to allocator with different rate (100 GRT/block) + const allocatorRate = toGRT('100') + await mockIssuanceAllocator.setTargetAllocation(rewardsManager.address, 0, allocatorRate, false) + await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) + + const block2 = await helpers.latestBlock() + const rewards2 = await rewardsManager.getAccRewardsPerSignal() + + await helpers.mine(8) + const block3 = await helpers.latestBlock() + const rewards3 = await rewardsManager.getAccRewardsPerSignal() + + // Calculate phase 2 accrual (includes the setIssuanceAllocator block at local rate) + const blocksPhase2 = block3 - block2 + const phase2Accrued = rewards3.sub(rewards2) + const expectedPhase2 = allocatorRate.mul(blocksPhase2).mul(toGRT('1')).div(totalSignal) + expect(phase2Accrued).to.eq(expectedPhase2) + + // Phase 3: Switch back to local rate (200 GRT/block) + await rewardsManager.connect(governor).setIssuanceAllocator(constants.AddressZero) + + const block4 = await helpers.latestBlock() + const rewards4 = await rewardsManager.getAccRewardsPerSignal() + + await helpers.mine(4) + const block5 = await helpers.latestBlock() + const rewards5 = await rewardsManager.getAccRewardsPerSignal() + + // Calculate phase 3 accrual + const blocksPhase3 = block5 - block4 + const phase3Accrued = rewards5.sub(rewards4) + const expectedPhase3 = ISSUANCE_PER_BLOCK.mul(blocksPhase3).mul(toGRT('1')).div(totalSignal) + expect(phase3Accrued).to.eq(expectedPhase3) + + // Verify total consistency: all rewards from start to end must equal sum of all phases + // including the transition blocks (setIssuanceAllocator calls mine blocks too) + const transitionPhase1to2 = rewards2.sub(rewards1) // Block mined by setIssuanceAllocator + const transitionPhase2to3 = rewards4.sub(rewards3) // Block mined by removing allocator + const totalExpected = phase1Accrued + .add(transitionPhase1to2) + .add(phase2Accrued) + .add(transitionPhase2to3) + .add(phase3Accrued) + const totalActual = rewards5.sub(rewards0) + expect(totalActual).to.eq(totalExpected) + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts new file mode 100644 index 000000000..a1a17269a --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts @@ -0,0 +1,1115 @@ +import { Curation } from '@graphprotocol/contracts' +import { EpochManager } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber, constants, utils } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +const { HashZero } = constants + +// Condition identifiers (matching RewardsCondition.sol) +const INDEXER_INELIGIBLE = utils.id('INDEXER_INELIGIBLE') +const SUBGRAPH_DENIED = utils.id('SUBGRAPH_DENIED') +const CLOSE_ALLOCATION = utils.id('CLOSE_ALLOCATION') +const NO_SIGNAL = utils.id('NO_SIGNAL') + +// Tolerance for fixed-point arithmetic rounding errors (matching Foundry tests) +const REWARDS_TOLERANCE = 20000 + +// Helper to check approximate equality for rewards (allows for rounding errors in fixed-point math) +function expectApproxEq(actual: BigNumber, expected: BigNumber, message: string) { + const diff = actual.sub(expected).abs() + expect( + diff.lte(REWARDS_TOLERANCE), + `${message}: difference ${diff.toString()} exceeds tolerance ${REWARDS_TOLERANCE}`, + ).to.be.true +} +const NO_ALLOCATED_TOKENS = utils.id('NO_ALLOCATED_TOKENS') +const BELOW_MINIMUM_SIGNAL = utils.id('BELOW_MINIMUM_SIGNAL') + +describe('Rewards - Reclaim Addresses', () => { + const graph = hre.graph() + let curator1: SignerWithAddress + let governor: SignerWithAddress + let indexer1: SignerWithAddress + let reclaimWallet: SignerWithAddress + let otherWallet: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let epochManager: EpochManager + let staking: IStaking + let rewardsManager: RewardsManager + + // Derive channel key for indexer used to sign attestations + const channelKey1 = deriveChannelKey() + + const subgraphDeploymentID1 = randomHexBytes() + + const allocationID1 = channelKey1.address + + const metadata = HashZero + + const ISSUANCE_PER_BLOCK = toGRT('200') // 200 GRT every block + + async function setupIndexerAllocation() { + // Setup + await epochManager.connect(governor).setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + before(async function () { + const testAccounts = await graph.getTestAccounts() + curator1 = testAccounts[0] + indexer1 = testAccounts[1] + reclaimWallet = testAccounts[2] + otherWallet = testAccounts[3] + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + epochManager = contracts.EpochManager + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager + + // 200 GRT per block + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + + // Distribute test funds + for (const wallet of [indexer1, curator1]) { + await grt.connect(governor).mint(wallet.address, toGRT('1000000')) + await grt.connect(wallet).approve(staking.address, toGRT('1000000')) + await grt.connect(wallet).approve(curation.address, toGRT('1000000')) + } + + // Set the staking contract as the subgraph service so it can call takeRewards + await rewardsManager.connect(governor).setSubgraphService(staking.address) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('setReclaimAddress', function () { + it('should reject if not governor', async function () { + const tx = rewardsManager.connect(indexer1).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should reject setting reclaim address for NONE', async function () { + const tx = rewardsManager.connect(governor).setReclaimAddress(HashZero, reclaimWallet.address) + await expect(tx).revertedWith('Cannot set reclaim address for NONE') + }) + + it('should set eligibility reclaim address if governor', async function () { + const tx = rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + await expect(tx) + .emit(rewardsManager, 'ReclaimAddressSet') + .withArgs(INDEXER_INELIGIBLE, constants.AddressZero, reclaimWallet.address) + + expect(await rewardsManager.getReclaimAddress(INDEXER_INELIGIBLE)).eq(reclaimWallet.address) + }) + + it('should set subgraph denied reclaim address if governor', async function () { + const tx = rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + await expect(tx) + .emit(rewardsManager, 'ReclaimAddressSet') + .withArgs(SUBGRAPH_DENIED, constants.AddressZero, reclaimWallet.address) + + expect(await rewardsManager.getReclaimAddress(SUBGRAPH_DENIED)).eq(reclaimWallet.address) + }) + + it('should allow setting to zero address', async function () { + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, constants.AddressZero) + await expect(tx) + .emit(rewardsManager, 'ReclaimAddressSet') + .withArgs(INDEXER_INELIGIBLE, reclaimWallet.address, constants.AddressZero) + + expect(await rewardsManager.getReclaimAddress(INDEXER_INELIGIBLE)).eq(constants.AddressZero) + }) + + it('should not emit event when setting same address', async function () { + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + await expect(tx).to.not.emit(rewardsManager, 'ReclaimAddressSet') + }) + }) + + describe('reclaim denied rewards - subgraph denylist', function () { + // Note: With the new denied-period rewards implementation, rewards for denied subgraphs + // are reclaimed at the subgraph level via onSubgraphAllocationUpdate(), not at the + // allocation level via _deniedRewards(). This means: + // - RewardsDenied is NOT emitted (legacy allocation-level event) + // - RewardsReclaimed IS emitted but with address(0) for indexer/allocationID + // - Allocations created while denied have frozen accumulator, so rewards = 0 at close + + it('should mint to reclaim address when subgraph denied and reclaim address set', async function () { + // Setup reclaim address + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Calculate expected rewards (approximate - timing can cause slight variations) + const expectedRewards = toGRT('1400') + + // Check reclaim wallet balance before + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Close allocation - rewards are reclaimed at subgraph level (address(0) for indexer/allocationID) + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + // RewardsDenied is not emitted - denial is handled at subgraph level now + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenied') + // RewardsReclaimed emitted with address(0) for indexer/allocationID (subgraph-level reclaim) + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + // Check reclaim wallet received the rewards (allow for rounding errors) + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expectApproxEq(balanceAfter.sub(balanceBefore), expectedRewards, 'reclaimed rewards') + }) + + it('should reclaim pre-denial rewards via _deniedRewards when denied after allocation', async function () { + // Setup reclaim address BEFORE allocation + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Create allocation FIRST (before deny) + await setupIndexerAllocation() + + // Mine blocks to accrue rewards + await helpers.mineEpoch(epochManager) + + // Deny AFTER allocation — pre-denial rewards exist at the allocation level + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Check reclaim wallet balance before + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Close allocation — pre-denial rewards flow through _deniedRewards → _reclaimRewards + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const parsedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((e) => e !== null) + + // RewardsDenied IS emitted (allocation-level denial for pre-denial rewards) + const deniedEvents = parsedEvents.filter((e) => e!.name === 'RewardsDenied') + expect(deniedEvents.length).to.equal(1, 'RewardsDenied event not found') + expect(deniedEvents[0]!.args[0]).to.equal(indexer1.address) + expect(deniedEvents[0]!.args[1]).to.equal(allocationID1) + + // RewardsReclaimed emitted with actual indexer/allocationID (allocation-level reclaim) + const reclaimEvents = parsedEvents.filter((e) => e!.name === 'RewardsReclaimed') + expect(reclaimEvents.length).to.be.gte(1, 'RewardsReclaimed event not found') + // Find the allocation-level reclaim (has non-zero indexer and allocationID) + const allocationReclaim = reclaimEvents.find((e) => e!.args[2] !== constants.AddressZero) + expect(allocationReclaim).to.not.be.undefined + expect(allocationReclaim!.args[0]).to.equal(SUBGRAPH_DENIED) + expectApproxEq(allocationReclaim!.args[1], toGRT('1400'), 'reclaimed amount') + expect(allocationReclaim!.args[2]).to.equal(indexer1.address) + expect(allocationReclaim!.args[3]).to.equal(allocationID1) + expect(allocationReclaim!.args[4]).to.equal(subgraphDeploymentID1) + + // Reclaim wallet received the pre-denial rewards (may receive additional rewards from subgraph-level reclaim) + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter.sub(balanceBefore)).gte(toGRT('1400')) + }) + + it('should not mint to reclaim address when reclaim address not set', async function () { + // Do NOT set reclaim address (defaults to zero address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Close allocation - no events emitted when no reclaim address configured + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + // RewardsDenied is not emitted - denial is handled at subgraph level now + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenied') + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + }) + + describe('reclaim denied rewards - eligibility', function () { + it('should mint to reclaim address when eligibility denied and reclaim address set', async function () { + // Setup reclaim address + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Calculate expected rewards + const expectedRewards = toGRT('1400') + + // Check reclaim wallet balance before + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Close allocation - should emit both denial and reclaim events + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const parsedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((e) => e !== null) + + // Check RewardsDeniedDueToEligibility event + const denialEvents = parsedEvents.filter((e) => e!.name === 'RewardsDeniedDueToEligibility') + expect(denialEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + expect(denialEvents[0]!.args[0]).to.equal(indexer1.address) + expect(denialEvents[0]!.args[1]).to.equal(allocationID1) + expectApproxEq(denialEvents[0]!.args[2], expectedRewards, 'denied rewards amount') + + // Check RewardsReclaimed event exists and verify args + const reclaimEvents = parsedEvents.filter((e) => e!.name === 'RewardsReclaimed') + expect(reclaimEvents.length).to.be.gte(1, 'RewardsReclaimed event not found') + const reclaimEvent = reclaimEvents.find((e) => e!.args[0] === INDEXER_INELIGIBLE) + expect(reclaimEvent).to.not.be.undefined + expect(reclaimEvent!.args[0]).to.equal(INDEXER_INELIGIBLE) + expectApproxEq(reclaimEvent!.args[1], expectedRewards, 'reclaimed amount') + expect(reclaimEvent!.args[2]).to.equal(indexer1.address) + expect(reclaimEvent!.args[3]).to.equal(allocationID1) + expect(reclaimEvent!.args[4]).to.equal(subgraphDeploymentID1) + + // Check reclaim wallet received the rewards + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expectApproxEq(balanceAfter.sub(balanceBefore), expectedRewards, 'wallet balance increase') + }) + + it('should not mint to reclaim address when reclaim address not set', async function () { + // Do NOT set reclaim address (defaults to zero address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + const expectedRewards = toGRT('1400') + + // Close allocation - should only emit denial event, not reclaim + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const parsedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((e) => e !== null) + + // Check RewardsDeniedDueToEligibility event + const denialEvents = parsedEvents.filter((e) => e!.name === 'RewardsDeniedDueToEligibility') + expect(denialEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + expect(denialEvents[0]!.args[0]).to.equal(indexer1.address) + expect(denialEvents[0]!.args[1]).to.equal(allocationID1) + expectApproxEq(denialEvents[0]!.args[2], expectedRewards, 'denied rewards amount') + + // Check no RewardsReclaimed event + const reclaimEvents = parsedEvents.filter((e) => e!.name === 'RewardsReclaimed') + expect(reclaimEvents.length).to.equal(0, 'RewardsReclaimed event should not be emitted') + }) + }) + + describe('reclaim precedence - first successful reclaim wins', function () { + // Note: With subgraph-level denial, rewards are reclaimed via onSubgraphAllocationUpdate() + // and the allocation-level _deniedRewards() path (which checks eligibility) is not reached + // because rewards = 0 for allocations created while denied. + + it('should reclaim to SUBGRAPH_DENIED when both fail and both addresses configured', async function () { + // Setup BOTH reclaim addresses + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, otherWallet.address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + const expectedRewards = toGRT('1400') + + // Check balances before + const subgraphDeniedBalanceBefore = await grt.balanceOf(reclaimWallet.address) + const indexerIneligibleBalanceBefore = await grt.balanceOf(otherWallet.address) + + // Close allocation - subgraph denial takes precedence (handled at subgraph level) + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + // No allocation-level denial events - handled at subgraph level + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenied') + await expect(tx).to.not.emit(rewardsManager, 'RewardsDeniedDueToEligibility') + // RewardsReclaimed emitted (subgraph-level reclaim) + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + // Only SUBGRAPH_DENIED wallet should receive rewards (allow for rounding errors) + const subgraphDeniedBalanceAfter = await grt.balanceOf(reclaimWallet.address) + const indexerIneligibleBalanceAfter = await grt.balanceOf(otherWallet.address) + + expectApproxEq( + subgraphDeniedBalanceAfter.sub(subgraphDeniedBalanceBefore), + expectedRewards, + 'SUBGRAPH_DENIED wallet balance', + ) + expect(indexerIneligibleBalanceAfter.sub(indexerIneligibleBalanceBefore)).eq(0) + }) + + it('should reclaim to SUBGRAPH_DENIED even when only INDEXER_INELIGIBLE address configured', async function () { + // Setup ONLY INDEXER_INELIGIBLE reclaim address (not SUBGRAPH_DENIED) + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, otherWallet.address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Check balance before + const balanceBefore = await grt.balanceOf(otherWallet.address) + + // Close allocation - subgraph denial is handled at subgraph level, but no SUBGRAPH_DENIED + // reclaim address is configured, so rewards are dropped (not reclaimed) + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + // No allocation-level denial events - handled at subgraph level + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenied') + await expect(tx).to.not.emit(rewardsManager, 'RewardsDeniedDueToEligibility') + // No reclaim because SUBGRAPH_DENIED address not configured (eligibility path not reached) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + + // INDEXER_INELIGIBLE wallet should NOT receive rewards (subgraph denial takes precedence) + const balanceAfter = await grt.balanceOf(otherWallet.address) + expect(balanceAfter.sub(balanceBefore)).eq(0) + }) + + it('should reclaim to INDEXER_INELIGIBLE when both fail but only INDEXER_INELIGIBLE address configured (pre-denial allocation)', async function () { + // This tests the ternary in _deniedRewards that falls back to INDEXER_INELIGIBLE + // when SUBGRAPH_DENIED address is not configured + + // Setup ONLY INDEXER_INELIGIBLE reclaim address (not SUBGRAPH_DENIED) + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Create allocation FIRST (before denial) - this is the key difference + await setupIndexerAllocation() + + // Mine blocks to accrue rewards + await helpers.mineEpoch(epochManager) + + // NOW deny the subgraph (after allocation exists) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + const expectedRewards = toGRT('1400') + + // Check balance before + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Close allocation - pre-denial rewards flow through _deniedRewards + // Both conditions are true, but SUBGRAPH_DENIED address is not set + // So it should fall back to INDEXER_INELIGIBLE + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // RewardsDenied IS emitted (allocation-level denial for pre-denial rewards) + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + // RewardsDeniedDueToEligibility IS emitted + await expect(tx).emit(rewardsManager, 'RewardsDeniedDueToEligibility') + // RewardsReclaimed should emit with INDEXER_INELIGIBLE reason (fallback) + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + // INDEXER_INELIGIBLE wallet should receive rewards (fallback from SUBGRAPH_DENIED) + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expectApproxEq(balanceAfter.sub(balanceBefore), expectedRewards, 'INDEXER_INELIGIBLE wallet balance') + }) + + it('should drop rewards when both fail and neither address configured', async function () { + // Do NOT set any reclaim addresses + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Close allocation - no events, rewards dropped + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenied') + await expect(tx).to.not.emit(rewardsManager, 'RewardsDeniedDueToEligibility') + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should drop rewards when subgraph denied without address even if indexer eligible', async function () { + // Do NOT set SUBGRAPH_DENIED reclaim address + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Setup eligibility oracle that ALLOWS (indexer is eligible) + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(true) // Allow + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation + await setupIndexerAllocation() + + // Jump to next epoch + await helpers.mineEpoch(epochManager) + + // Close allocation - no events because subgraph denial handled at subgraph level + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenied') + await expect(tx).to.not.emit(rewardsManager, 'RewardsDeniedDueToEligibility') + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + }) + }) + + describe('reclaimRewards - force close allocation', function () { + let mockSubgraphService: any + + beforeEach(async function () { + // Deploy mock subgraph service + const MockSubgraphServiceFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockSubgraphService.sol:MockSubgraphService', + ) + mockSubgraphService = await MockSubgraphServiceFactory.deploy() + await mockSubgraphService.deployed() + + // Set it as the subgraph service in rewards manager + await rewardsManager.connect(governor).setSubgraphService(mockSubgraphService.address) + }) + + it('should reclaim rewards when reclaim address is set', async function () { + // Set reclaim address for ForceCloseAllocation + await rewardsManager.connect(governor).setReclaimAddress(CLOSE_ALLOCATION, reclaimWallet.address) + + // Setup allocation in real staking contract + await setupIndexerAllocation() + + // Also set allocation data in mock so RewardsManager can query it + const tokensAllocated = toGRT('12500') + await mockSubgraphService.setAllocation( + allocationID1, + true, // isActive + indexer1.address, + subgraphDeploymentID1, + tokensAllocated, + 0, // accRewardsPerAllocatedToken starts at 0 + 0, // accRewardsPending + ) + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) + + // Jump to next epoch to accrue rewards + await helpers.mineEpoch(epochManager) + + // Check balance before + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Call reclaimRewards via mock subgraph service + const tx = await mockSubgraphService.callReclaimRewards(rewardsManager.address, CLOSE_ALLOCATION, allocationID1) + + // Verify event was emitted (don't check exact amount, it depends on rewards calculation) + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + // Check balance after - should have increased + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + const rewardsClaimed = balanceAfter.sub(balanceBefore) + expect(rewardsClaimed).to.be.gt(0) + }) + + it('should not reclaim when reclaim address is not set', async function () { + // Do NOT set reclaim address (defaults to zero) + + // Setup allocation in real staking contract + await setupIndexerAllocation() + + // Also set allocation data in mock + const tokensAllocated = toGRT('12500') + await mockSubgraphService.setAllocation( + allocationID1, + true, + indexer1.address, + subgraphDeploymentID1, + tokensAllocated, + 0, + 0, + ) + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) + + // Jump to next epoch to accrue rewards + await helpers.mineEpoch(epochManager) + + // Call reclaimRewards via mock subgraph service - should not emit RewardsReclaimed + const tx = await mockSubgraphService.callReclaimRewards(rewardsManager.address, CLOSE_ALLOCATION, allocationID1) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should return 0 and not emit when reclaim address is not set and no rewards', async function () { + // Do NOT set reclaim address (zero address) + + // Setup allocation but mark it as inactive (no rewards) + const tokensAllocated = toGRT('12500') + await mockSubgraphService.setAllocation( + allocationID1, + false, // NOT active - this will return 0 rewards + indexer1.address, + subgraphDeploymentID1, + tokensAllocated, + 0, + 0, + ) + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) + + // Call reclaimRewards - should return 0 and not emit + const result = await mockSubgraphService.callStatic.callReclaimRewards( + rewardsManager.address, + CLOSE_ALLOCATION, + allocationID1, + ) + expect(result).eq(0) + + const tx = await mockSubgraphService.callReclaimRewards(rewardsManager.address, CLOSE_ALLOCATION, allocationID1) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should return 0 when reason is NONE', async function () { + // Setup allocation in real staking contract + await setupIndexerAllocation() + + // Also set allocation data in mock so RewardsManager can query it + const tokensAllocated = toGRT('12500') + await mockSubgraphService.setAllocation( + allocationID1, + true, + indexer1.address, + subgraphDeploymentID1, + tokensAllocated, + 0, + 0, + ) + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) + + // Jump to next epoch to accrue rewards + await helpers.mineEpoch(epochManager) + + // Call reclaimRewards with NONE (HashZero) - should return 0 + const result = await mockSubgraphService.callStatic.callReclaimRewards( + rewardsManager.address, + HashZero, + allocationID1, + ) + expect(result).eq(0) + + // Verify no RewardsReclaimed event emitted + const tx = await mockSubgraphService.callReclaimRewards(rewardsManager.address, HashZero, allocationID1) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should reject when called by unauthorized address', async function () { + // Try to call reclaimRewards directly from indexer1 (not the subgraph service) + const abiCoder = hre.ethers.utils.defaultAbiCoder + const selector = hre.ethers.utils.id('reclaimRewards(bytes32,address)').slice(0, 10) + const params = abiCoder.encode(['bytes32', 'address'], [CLOSE_ALLOCATION, allocationID1]) + const data = selector + params.slice(2) + + const tx = indexer1.sendTransaction({ + to: rewardsManager.address, + data: data, + }) + await expect(tx).revertedWith('Not a rewards issuer') + }) + }) + + describe('setDefaultReclaimAddress', function () { + it('should reject if not governor', async function () { + const tx = rewardsManager.connect(indexer1).setDefaultReclaimAddress(reclaimWallet.address) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set default reclaim address if governor', async function () { + const tx = rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + await expect(tx) + .emit(rewardsManager, 'DefaultReclaimAddressSet') + .withArgs(constants.AddressZero, reclaimWallet.address) + + // Verify the getter returns the correct value + expect(await rewardsManager.getDefaultReclaimAddress()).eq(reclaimWallet.address) + }) + + it('should allow setting to zero address', async function () { + await rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setDefaultReclaimAddress(constants.AddressZero) + await expect(tx) + .emit(rewardsManager, 'DefaultReclaimAddressSet') + .withArgs(reclaimWallet.address, constants.AddressZero) + }) + + it('should not emit event when setting same address', async function () { + await rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + await expect(tx).to.not.emit(rewardsManager, 'DefaultReclaimAddressSet') + }) + }) + + describe('default reclaim address fallback', function () { + beforeEach(async function () { + await setupIndexerAllocation() + // Set governor as the subgraph availability oracle for setDenied calls + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + }) + + it('should use default reclaim address when reason-specific not set', async function () { + // Set default but NOT SUBGRAPH_DENIED specific + await rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + + // Deny the subgraph + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Mine blocks to accrue rewards + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger reclaim via onSubgraphAllocationUpdate + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should reclaim to default address with SUBGRAPH_DENIED reason + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + + it('should prefer reason-specific address over default', async function () { + // Set both default AND SUBGRAPH_DENIED specific + await rewardsManager.connect(governor).setDefaultReclaimAddress(otherWallet.address) + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + + // Deny the subgraph + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Mine blocks to accrue rewards + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + const otherBalanceBefore = await grt.balanceOf(otherWallet.address) + + // Trigger reclaim + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + const otherBalanceAfter = await grt.balanceOf(otherWallet.address) + + // Should go to reason-specific, not default + expect(balanceAfter).gt(balanceBefore) + expect(otherBalanceAfter).eq(otherBalanceBefore) + }) + }) + + describe('reclaim NO_SIGNAL - zero total signal', function () { + it('should reclaim when no signal and NO_SIGNAL address set', async function () { + // Set reclaim address for NO_SIGNAL + await rewardsManager.connect(governor).setReclaimAddress(NO_SIGNAL, reclaimWallet.address) + + // Don't create any signal - just mine blocks + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger updateAccRewardsPerSignal (called internally when signal changes, or directly) + const tx = rewardsManager.connect(governor).updateAccRewardsPerSignal() + + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + + it('should drop rewards when no signal and no reclaim address', async function () { + // Don't set any reclaim address - just mine blocks + await helpers.mine(5) + + // Trigger updateAccRewardsPerSignal + const tx = rewardsManager.connect(governor).updateAccRewardsPerSignal() + + // Should not emit RewardsReclaimed (dropped) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should use default reclaim address for NO_SIGNAL when specific not set', async function () { + // Set default but NOT NO_SIGNAL specific + await rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).updateAccRewardsPerSignal() + + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + }) + + describe('reclaim NO_ALLOCATED_TOKENS - signal but no allocations', function () { + it('should reclaim when signal exists but no allocations and NO_ALLOCATED_TOKENS address set', async function () { + // Set reclaim address for NO_ALLOCATED_TOKENS + await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATED_TOKENS, reclaimWallet.address) + + // Create signal but NO allocation + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Mine blocks to accrue rewards + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger onSubgraphAllocationUpdate - will see signal but no allocations + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + + it('should drop rewards when no allocations and no reclaim address', async function () { + // Create signal but NO allocation, and don't set reclaim address + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + await helpers.mine(5) + + // Trigger onSubgraphAllocationUpdate + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should not emit RewardsReclaimed (dropped) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + }) + + describe('reclaim BELOW_MINIMUM_SIGNAL', function () { + const MINIMUM_SIGNAL = toGRT('1000') + + beforeEach(async function () { + // Set minimum signal threshold + await rewardsManager.connect(governor).setMinimumSubgraphSignal(MINIMUM_SIGNAL) + }) + + it('should reclaim when signal below minimum and BELOW_MINIMUM_SIGNAL address set', async function () { + // Set reclaim address for BELOW_MINIMUM_SIGNAL + await rewardsManager.connect(governor).setReclaimAddress(BELOW_MINIMUM_SIGNAL, reclaimWallet.address) + + // Create signal BELOW minimum (minimum is 1000, we signal 500) + const signalled1 = toGRT('500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Mine blocks + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger onSubgraphAllocationUpdate + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + + it('should not reclaim when signal at or above minimum', async function () { + // Set reclaim address + await rewardsManager.connect(governor).setReclaimAddress(BELOW_MINIMUM_SIGNAL, reclaimWallet.address) + + // Create signal AT minimum + const signalled1 = MINIMUM_SIGNAL + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Also need an allocation for rewards to accumulate normally + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + await helpers.mine(5) + + // Trigger onSubgraphAllocationUpdate + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should NOT emit RewardsReclaimed for BELOW_MINIMUM_SIGNAL + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should drop rewards when below minimum and no reclaim address', async function () { + // Don't set reclaim address + // Create signal BELOW minimum + const signalled1 = toGRT('500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + await helpers.mine(5) + + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should not emit RewardsReclaimed (dropped) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should use BELOW_MINIMUM_SIGNAL when denied but SUBGRAPH_DENIED address not configured', async function () { + // This tests line 574: the branch where subgraph is denied but reclaim address is zero, + // so it falls back to BELOW_MINIMUM_SIGNAL + + // Set BELOW_MINIMUM_SIGNAL address but NOT SUBGRAPH_DENIED + await rewardsManager.connect(governor).setReclaimAddress(BELOW_MINIMUM_SIGNAL, reclaimWallet.address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Create signal BELOW minimum (minimum is 1000, we signal 500) + const signalled1 = toGRT('500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Mine blocks + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger onSubgraphAllocationUpdate + // Subgraph is denied but no SUBGRAPH_DENIED address, so should fall back to BELOW_MINIMUM_SIGNAL + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should reclaim to BELOW_MINIMUM_SIGNAL address + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + }) + + describe('dual denial - SUBGRAPH_DENIED takes precedence when configured', function () { + it('should reclaim to SUBGRAPH_DENIED when both conditions true and SUBGRAPH_DENIED address configured (pre-denial allocation)', async function () { + // This tests line 747-748: when both denied and ineligible, and SUBGRAPH_DENIED IS configured + + // Setup BOTH reclaim addresses + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, otherWallet.address) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Create allocation FIRST (before denial) + await setupIndexerAllocation() + + // Mine blocks to accrue rewards + await helpers.mineEpoch(epochManager) + + // NOW deny the subgraph (after allocation exists) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Check balances before + const subgraphDeniedBalanceBefore = await grt.balanceOf(reclaimWallet.address) + const indexerIneligibleBalanceBefore = await grt.balanceOf(otherWallet.address) + + // Close allocation - pre-denial rewards flow through _deniedRewards + // Both conditions are true, SUBGRAPH_DENIED IS configured, so it should use SUBGRAPH_DENIED + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // RewardsDenied IS emitted + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + // RewardsDeniedDueToEligibility IS emitted + await expect(tx).emit(rewardsManager, 'RewardsDeniedDueToEligibility') + // RewardsReclaimed should emit with SUBGRAPH_DENIED reason + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + // SUBGRAPH_DENIED wallet should receive rewards (not INDEXER_INELIGIBLE) + const subgraphDeniedBalanceAfter = await grt.balanceOf(reclaimWallet.address) + const indexerIneligibleBalanceAfter = await grt.balanceOf(otherWallet.address) + + expect(subgraphDeniedBalanceAfter.sub(subgraphDeniedBalanceBefore)).gt(0) + expect(indexerIneligibleBalanceAfter.sub(indexerIneligibleBalanceBefore)).eq(0) + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts new file mode 100644 index 000000000..930c2b21c --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts @@ -0,0 +1,564 @@ +import { Curation } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toBN, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { constants } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +const { HashZero } = constants + +/** + * Invariant: signal/allocation update accounting. + * + * When `onSubgraphSignalUpdate()` runs before `onSubgraphAllocationUpdate()` in the SAME BLOCK, + * the per-signal delta is zero. Rewards already tracked in `accRewardsForSubgraph` must still be + * distributed to allocations via the snapshot delta + * (`accRewardsForSubgraph - accRewardsForSubgraphSnapshot`), rather than relying on the per-signal + * delta alone. Distribution must never depend on the ordering of these two calls within a block. + * + * IMPORTANT: These tests use evm_setAutomine to batch transactions into one block so the + * per-signal delta is zero, exercising the snapshot-delta path. + */ +describe('Rewards: Signal and Allocation Update Accounting', () => { + const graph = hre.graph() + let governor: SignerWithAddress + let curator: SignerWithAddress + let indexer: SignerWithAddress + + let fixture: NetworkFixture + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let staking: IStaking + let rewardsManager: RewardsManager + + const channelKey = deriveChannelKey() + const subgraphDeploymentID = randomHexBytes() + const allocationID = channelKey.address + const metadata = HashZero + + const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT every block + const tokensToSignal = toGRT('1000') + const tokensToStake = toGRT('100000') + const tokensToAllocate = toGRT('10000') + + before(async function () { + ;[curator, indexer] = await graph.getTestAccounts() + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager as RewardsManager + + // Set the staking contract as the subgraph service so it can call takeRewards + await rewardsManager.connect(governor).setSubgraphService(staking.address) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + async function setupSubgraphWithAllocation() { + // Setup: curator signals on subgraph + await grt.connect(governor).mint(curator.address, tokensToSignal) + await grt.connect(curator).approve(curation.address, tokensToSignal) + await curation.connect(curator).mint(subgraphDeploymentID, tokensToSignal, 0) + + // Setup: indexer stakes and allocates + await grt.connect(governor).mint(indexer.address, tokensToStake) + await grt.connect(indexer).approve(staking.address, tokensToStake) + await staking.connect(indexer).stake(tokensToStake) + await staking + .connect(indexer) + .allocateFrom( + indexer.address, + subgraphDeploymentID, + tokensToAllocate, + allocationID, + metadata, + await channelKey.generateProof(indexer.address), + ) + } + + describe('onSubgraphSignalUpdate followed by onSubgraphAllocationUpdate', function () { + it('should properly distribute rewards when signal update precedes allocation update (same block)', async function () { + await setupSubgraphWithAllocation() + + // Advance blocks to accumulate rewards + await helpers.mine(100) + + // Get expected rewards before any updates + const expectedRewardsForSubgraph = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(expectedRewardsForSubgraph).to.be.gt(0, 'Should have accumulated rewards') + + // Get initial state + const subgraphBefore = await rewardsManager.subgraphs(subgraphDeploymentID) + const accRewardsPerAllocatedTokenBefore = subgraphBefore.accRewardsPerAllocatedToken + + // Disable automine to batch transactions into the same block + await hre.network.provider.send('evm_setAutomine', [false]) + + try { + // First: call onSubgraphSignalUpdate (this zeros the per-signal delta) + // This simulates what happens when a curator mints/burns signal + const signalTx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // Second: call onSubgraphAllocationUpdate (in same block, per-signal delta is 0) + // This simulates what happens when an allocation is opened/closed + const allocTx = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Mine both transactions in the same block + await hre.network.provider.send('evm_mine') + + // Wait for both transactions to be mined + await signalTx.wait() + await allocTx.wait() + } finally { + // Re-enable automine + await hre.network.provider.send('evm_setAutomine', [true]) + } + + // Verify rewards were tracked at subgraph level + const subgraphAfterSignal = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(subgraphAfterSignal.accRewardsForSubgraph).to.be.gt( + 0, + 'accRewardsForSubgraph should be updated after signal update', + ) + + // Get final state + const subgraphAfterAllocation = await rewardsManager.subgraphs(subgraphDeploymentID) + + // accRewardsPerAllocatedToken must advance via the snapshot delta even when the per-signal + // delta is zero, so accumulated rewards reach allocations regardless of update ordering. + expect(subgraphAfterAllocation.accRewardsPerAllocatedToken).to.be.gt( + accRewardsPerAllocatedTokenBefore, + 'accRewardsPerAllocatedToken should increase when a signal update precedes an allocation update in the same block', + ) + + // Verify snapshot consistency + expect(subgraphAfterAllocation.accRewardsForSubgraphSnapshot).to.equal( + subgraphAfterAllocation.accRewardsForSubgraph, + 'Snapshots should be in sync after updates', + ) + }) + + it('should not brick rewards when signal update zeros the per-signal delta (same block)', async function () { + await setupSubgraphWithAllocation() + + // Advance blocks + await helpers.mine(100) + + // Get the view function result (what rewards SHOULD be) before any updates + // Note: We call this to ensure the function works, but we verify via stored state below + await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID) + + // Disable automine to batch transactions into the same block + await hre.network.provider.send('evm_setAutomine', [false]) + + try { + // Call signal update first (zeros per-signal delta and accumulates rewards in accRewardsForSubgraph) + const signalTx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // Call allocation update (per-signal delta is now 0, but rewards are in accRewardsForSubgraph) + const allocTx = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Mine both transactions in the same block + await hre.network.provider.send('evm_mine') + + // Wait for both transactions to be mined + await signalTx.wait() + await allocTx.wait() + } finally { + // Re-enable automine + await hre.network.provider.send('evm_setAutomine', [true]) + } + + // Get the rewards accumulated in accRewardsForSubgraph + const afterSignal = await rewardsManager.subgraphs(subgraphDeploymentID) + const rewardsAccumulated = afterSignal.accRewardsForSubgraph + + // These rewards should eventually be distributed to allocations + expect(rewardsAccumulated).to.be.gt(0, 'Rewards should be accumulated at subgraph level') + + // Get stored state + const subgraph = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Even when the per-signal delta is zero, accRewardsPerAllocatedToken must reflect the + // rewards accumulated in accRewardsForSubgraph via the snapshot delta, so they are + // distributed to allocations rather than stranded. + expect(subgraph.accRewardsPerAllocatedToken).to.be.gt( + 0, + 'accRewardsPerAllocatedToken should be non-zero so accumulated rewards are distributed, not stranded', + ) + + // Verify view function and stored state are consistent + const [viewAccRewardsPerAllocatedToken] = + await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID) + + // The view should equal the stored value (since snapshots are synced) + expect(viewAccRewardsPerAllocatedToken).to.equal( + subgraph.accRewardsPerAllocatedToken, + 'View function should match stored state after updates', + ) + }) + + it('should handle multiple signal updates without losing rewards (same block allocation)', async function () { + await setupSubgraphWithAllocation() + + // Advance blocks + await helpers.mine(50) + + // First signal update + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const afterFirstSignal = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Advance more blocks + await helpers.mine(50) + + // Disable automine to batch signal + allocation into same block + await hre.network.provider.send('evm_setAutomine', [false]) + + try { + // Second signal update (without allocation update in between) + const signalTx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // Allocation update in the same block (per-signal delta is 0) + const allocTx = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Mine both in the same block + await hre.network.provider.send('evm_mine') + + await signalTx.wait() + await allocTx.wait() + } finally { + await hre.network.provider.send('evm_setAutomine', [true]) + } + + const afterSecondSignal = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Rewards should have accumulated + expect(afterSecondSignal.accRewardsForSubgraph).to.be.gt( + afterFirstSignal.accRewardsForSubgraph, + 'Rewards should accumulate across signal updates', + ) + + const afterAllocation = await rewardsManager.subgraphs(subgraphDeploymentID) + + // All accumulated rewards should be distributed + expect(afterAllocation.accRewardsPerAllocatedToken).to.be.gt( + 0, + 'Rewards from multiple signal updates should be distributed', + ) + + // Snapshots should be in sync + expect(afterAllocation.accRewardsForSubgraphSnapshot).to.equal( + afterAllocation.accRewardsForSubgraph, + 'Snapshots should be in sync', + ) + }) + }) + + describe('snapshot consistency in reclaim paths', function () { + it('should update accRewardsForSubgraphSnapshot when rewards are reclaimed due to denial', async function () { + await setupSubgraphWithAllocation() + + // Deny the subgraph + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID, true) + + // Advance blocks to accumulate rewards + await helpers.mine(100) + + // Get state before + const subgraphBefore = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Call allocation update - should reclaim (not distribute) rewards + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Get state after + const subgraphAfter = await rewardsManager.subgraphs(subgraphDeploymentID) + + // accRewardsPerAllocatedToken should NOT increase (rewards reclaimed, not distributed) + expect(subgraphAfter.accRewardsPerAllocatedToken).to.equal( + subgraphBefore.accRewardsPerAllocatedToken, + 'accRewardsPerAllocatedToken should not increase when denied', + ) + + // accRewardsForSubgraphSnapshot must advance in the reclaim path so the same rewards + // cannot be reclaimed again on a later update. + expect(subgraphAfter.accRewardsForSubgraphSnapshot).to.be.gte( + subgraphBefore.accRewardsForSubgraphSnapshot, + 'accRewardsForSubgraphSnapshot should be updated in reclaim path', + ) + }) + + it('should not double-reclaim rewards after snapshot update', async function () { + await setupSubgraphWithAllocation() + + // Deny the subgraph + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID, true) + + // Advance blocks + await helpers.mine(100) + + // First allocation update - reclaims rewards + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const afterFirstReclaim = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Second allocation update - each tx advances a block, so there's 1 more block of rewards + // The key invariant is that rewards are properly accounted for, not double-reclaimed + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const afterSecondReclaim = await rewardsManager.subgraphs(subgraphDeploymentID) + + // The snapshot should have advanced by at most 1 block's worth of rewards + // (Each transaction creates a new block in Hardhat) + const maxOneBlockReward = ISSUANCE_PER_BLOCK.mul(tokensToSignal).div(await grt.balanceOf(curation.address)) + + const snapshotDiff = afterSecondReclaim.accRewardsForSubgraphSnapshot.sub( + afterFirstReclaim.accRewardsForSubgraphSnapshot, + ) + + // The difference should be at most one block's worth of rewards + expect(snapshotDiff).to.be.lte( + maxOneBlockReward.mul(2), // Allow for rounding and timing + 'Should only process one block worth of new rewards', + ) + + // Verify accRewardsPerAllocatedToken didn't increase (rewards still reclaimed, not distributed) + expect(afterSecondReclaim.accRewardsPerAllocatedToken).to.equal( + afterFirstReclaim.accRewardsPerAllocatedToken, + 'accRewardsPerAllocatedToken should not change during reclaim', + ) + }) + }) + + describe('onSubgraphSignalUpdate on denied subgraph', function () { + it('should reclaim rewards when onSubgraphSignalUpdate is called on denied subgraph', async function () { + await setupSubgraphWithAllocation() + + // Configure reclaim address for SUBGRAPH_DENIED + const SUBGRAPH_DENIED = hre.ethers.utils.id('SUBGRAPH_DENIED') + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, governor.address) + + // Verify reclaim address was set + const reclaimAddr = await rewardsManager.getReclaimAddress(SUBGRAPH_DENIED) + expect(reclaimAddr).to.equal(governor.address, 'Reclaim address should be set') + + // Deny the subgraph + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID, true) + + // Record state after denial (setDenied calls onSubgraphAllocationUpdate internally) + const afterDenial = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Advance blocks - rewards should accumulate + await helpers.mine(100) + + // Call onSubgraphSignalUpdate (simulates curator action) + // For a denied subgraph, rewards are reclaimed immediately rather than accumulated. + const tx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const receipt = await tx.wait() + const afterSignalUpdate = await rewardsManager.subgraphs(subgraphDeploymentID) + + // For a denied subgraph, accRewardsForSubgraph must NOT change + // (rewards are reclaimed directly, not stored) + expect(afterSignalUpdate.accRewardsForSubgraph).to.equal( + afterDenial.accRewardsForSubgraph, + 'accRewardsForSubgraph should not change for denied subgraphs (rewards reclaimed)', + ) + + // Verify reclaim event was emitted + const reclaimEvent = receipt.events?.find((e) => e.event === 'RewardsReclaimed') + expect(reclaimEvent).to.not.be.undefined + // Event args: (reason, rewards, indexer, allocationId, subgraphDeploymentId) + const rewards = reclaimEvent!.args![1] // rewards is second arg + expect(rewards).to.be.gt(0, 'Should have reclaimed rewards') + }) + + it('should accumulate rewards for claimable subgraphs in onSubgraphSignalUpdate', async function () { + await setupSubgraphWithAllocation() + + // Record initial state (subgraph is claimable by default) + const initialState = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Advance blocks - rewards should accumulate + await helpers.mine(100) + + // Call onSubgraphSignalUpdate + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const afterSignalUpdate = await rewardsManager.subgraphs(subgraphDeploymentID) + + // For claimable subgraphs: accRewardsForSubgraph SHOULD increase + expect(afterSignalUpdate.accRewardsForSubgraph).to.be.gt( + initialState.accRewardsForSubgraph, + 'accRewardsForSubgraph should increase for claimable subgraphs', + ) + }) + + it('view function getAccRewardsForSubgraph should not jump during denial', async function () { + await setupSubgraphWithAllocation() + + // Accumulate some rewards while claimable + await helpers.mine(50) + + // Deny the subgraph (setDenied distributes pre-denial rewards via onSubgraphAllocationUpdate) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID, true) + + // Record view value immediately after denial + const rewardsAtDenial = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(rewardsAtDenial).to.be.gt(0, 'Should have accumulated pre-denial rewards') + + // Advance blocks during denial + await helpers.mine(100) + + // View function should return SAME value (no jump up during denial) + const rewardsDuringDenial = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(rewardsDuringDenial).to.equal(rewardsAtDenial, 'View should not increase during denial') + + // A signal update on a denied subgraph reclaims the accumulated rewards, so the view stays + // stable and does not jump on the next allocation update. + // Configure reclaim address so rewards are reclaimed + const SUBGRAPH_DENIED = hre.ethers.utils.id('SUBGRAPH_DENIED') + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, governor.address) + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // View function should STILL return same value (rewards reclaimed, not accumulated) + const rewardsAfterSignalUpdate = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(rewardsAfterSignalUpdate).to.equal(rewardsAtDenial, 'View should not jump after signal update') + + // Mine more blocks + await helpers.mine(50) + + // Call allocation update + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // View should STILL be stable (rewards reclaimed, not accumulated) + const rewardsAfterAllocationUpdate = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(rewardsAfterAllocationUpdate).to.equal(rewardsAtDenial, 'View should not jump after allocation update') + }) + }) + + describe('onSubgraphSignalUpdate with no allocations', function () { + it('should reclaim as NO_ALLOCATED_TOKENS when signal exists but no allocations', async function () { + // Setup: only signal, no allocation + await grt.connect(governor).mint(curator.address, tokensToSignal) + await grt.connect(curator).approve(curation.address, tokensToSignal) + await curation.connect(curator).mint(subgraphDeploymentID, tokensToSignal, 0) + + // Configure reclaim address for NO_ALLOCATED_TOKENS + const NO_ALLOCATED_TOKENS = hre.ethers.utils.id('NO_ALLOCATED_TOKENS') + await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATED_TOKENS, governor.address) + + // Record initial state + const initialState = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Advance blocks - rewards should accumulate + await helpers.mine(100) + + // Call onSubgraphSignalUpdate - should reclaim as NO_ALLOCATED_TOKENS + const tx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const receipt = await tx.wait() + const afterSignalUpdate = await rewardsManager.subgraphs(subgraphDeploymentID) + + // accRewardsForSubgraph should NOT change (rewards reclaimed, not accumulated) + expect(afterSignalUpdate.accRewardsForSubgraph).to.equal( + initialState.accRewardsForSubgraph, + 'accRewardsForSubgraph should not change when no allocations (rewards reclaimed)', + ) + + // Verify reclaim event was emitted with NO_ALLOCATED_TOKENS reason + const reclaimEvent = receipt.events?.find((e) => e.event === 'RewardsReclaimed') + expect(reclaimEvent).to.not.be.undefined + expect(reclaimEvent!.args![0]).to.equal(NO_ALLOCATED_TOKENS, 'Should reclaim with NO_ALLOCATED_TOKENS reason') + expect(reclaimEvent!.args![1]).to.be.gt(0, 'Should have reclaimed rewards') + }) + + it('view function should not show phantom rewards when no allocations', async function () { + // Setup: only signal, no allocation + await grt.connect(governor).mint(curator.address, tokensToSignal) + await grt.connect(curator).approve(curation.address, tokensToSignal) + await curation.connect(curator).mint(subgraphDeploymentID, tokensToSignal, 0) + + // Record view immediately after signal + const viewAfterSignal = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + + // Advance blocks + await helpers.mine(100) + + // Configure reclaim and call signal update + const NO_ALLOCATED_TOKENS = hre.ethers.utils.id('NO_ALLOCATED_TOKENS') + await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATED_TOKENS, governor.address) + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // View should remain stable (rewards reclaimed) + const viewAfterReclaim = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(viewAfterReclaim).to.equal(viewAfterSignal, 'View should not grow when no allocations') + }) + }) + + describe('invariant: no rewards lost or double-counted', function () { + it('should maintain accounting invariant across mixed updates (with same-block scenarios)', async function () { + await setupSubgraphWithAllocation() + + // Sequence exercising the same-block signal/allocation ordering + await helpers.mine(25) + + // First: signal update followed by allocation update in SAME BLOCK + await hre.network.provider.send('evm_setAutomine', [false]) + try { + const signalTx1 = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const allocTx1 = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + await hre.network.provider.send('evm_mine') + await signalTx1.wait() + await allocTx1.wait() + } finally { + await hre.network.provider.send('evm_setAutomine', [true]) + } + + await helpers.mine(25) + + // Second: double signal update followed by allocation update in SAME BLOCK + await hre.network.provider.send('evm_setAutomine', [false]) + try { + const signalTx2 = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const signalTx3 = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const allocTx2 = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + await hre.network.provider.send('evm_mine') + await signalTx2.wait() + await signalTx3.wait() + await allocTx2.wait() + } finally { + await hre.network.provider.send('evm_setAutomine', [true]) + } + + // Final state check + const finalSubgraph = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Key invariant: snapshots should be in sync + expect(finalSubgraph.accRewardsForSubgraphSnapshot).to.equal( + finalSubgraph.accRewardsForSubgraph, + 'INVARIANT VIOLATED: accRewardsForSubgraphSnapshot != accRewardsForSubgraph', + ) + + // Rewards should have been distributed + expect(finalSubgraph.accRewardsPerAllocatedToken).to.be.gt( + 0, + 'Rewards should have been distributed to allocations', + ) + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-snapshot-inversion.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-snapshot-inversion.test.ts new file mode 100644 index 000000000..da09d182e --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-snapshot-inversion.test.ts @@ -0,0 +1,444 @@ +import { Curation } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber, constants, utils } from 'ethers' +import hre from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +const { HashZero } = constants + +/** + * Tests for snapshot inversion on upgrade. + * + * Terminology: + * A = accRewardsForSubgraph (stored accumulator, set at signal updates) + * S = accRewardsForSubgraphSnapshot (stored snapshot, set at allocation updates) + * P = rewardsSinceSignalSnapshot (pending rewards since last signal snapshot) + * + * For affected subgraphs, on-chain storage can hold an inverted snapshot state + * where A < S: the snapshot S leads (it was written from a view value of + * storage + pending) while the accumulator A lags at its stored value. + * + * Invariant the reward math must uphold: with an inverted state (A < S), the + * computation of A + P - S must never underflow. It does so by adding pending + * first and subtracting the snapshot last — `A.add(P).sub(S)` — so the + * intermediate `A + P` stays non-negative. Since P covers T1→now and the gap + * S - A covers T1→T2, and now >= T2, we have S - A <= P, so S <= A + P always + * holds; no clamping is needed. Subtracting the gap S - A discards rewards + * already distributed, preventing double-counting. + * + * These tests use `hardhat_setStorageAt` to construct the inverted storage state + * directly so the invariant can be exercised for affected subgraphs. + */ +describe('Rewards: Snapshot Inversion', () => { + const graph = hre.graph() + let governor: SignerWithAddress + let curator: SignerWithAddress + let indexer: SignerWithAddress + + let fixture: NetworkFixture + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let staking: IStaking + let rewardsManager: RewardsManager + + const channelKey = deriveChannelKey() + const subgraphDeploymentID = randomHexBytes() + const allocationID = channelKey.address + const metadata = HashZero + + const tokensToSignal = toGRT('1000') + const tokensToStake = toGRT('100000') + const tokensToAllocate = toGRT('10000') + + // Storage slot for the `subgraphs` mapping in RewardsManagerV1Storage. + // Computed by counting all inherited storage variables: + // Managed: controller(0), _addressCache(1), __gap[10](2-11) = 12 slots + // V1Storage: __DEPRECATED_issuanceRate(12), accRewardsPerSignal(13), + // accRewardsPerSignalLastBlockUpdated(14), subgraphAvailabilityOracle(15), + // subgraphs(16) + const SUBGRAPHS_MAPPING_SLOT = 16 + + /** + * Compute the storage slot for a field within a Subgraph struct in the subgraphs mapping. + * + * For `mapping(bytes32 => Subgraph)` at slot S, key K: + * base = keccak256(abi.encode(K, S)) + * field 0 (accRewardsForSubgraph) = base + 0 + * field 1 (accRewardsForSubgraphSnapshot) = base + 1 + * field 2 (accRewardsPerSignalSnapshot) = base + 2 + * field 3 (accRewardsPerAllocatedToken) = base + 3 + */ + function subgraphStorageSlot(subgraphId: string, fieldOffset: number): string { + const baseSlot = utils.keccak256( + utils.defaultAbiCoder.encode(['bytes32', 'uint256'], [subgraphId, SUBGRAPHS_MAPPING_SLOT]), + ) + return utils.hexZeroPad(BigNumber.from(baseSlot).add(fieldOffset).toHexString(), 32) + } + + /** + * Set a uint256 value at a specific storage slot of the RewardsManager proxy. + */ + async function setStorage(slot: string, value: BigNumber): Promise { + await hre.network.provider.send('hardhat_setStorageAt', [ + rewardsManager.address, + slot, + utils.hexZeroPad(value.toHexString(), 32), + ]) + } + + /** + * Create the inverted snapshot state that exists on-chain for affected subgraphs. + * + * Sets: accRewardsForSubgraphSnapshot = accRewardsForSubgraph + gap + * This is the state left by the old `onSubgraphAllocationUpdate` which wrote + * the snapshot from a view function (storage + pending), while leaving + * accRewardsForSubgraph at its stored value. + */ + async function createInvertedState(subgraphId: string, gap: BigNumber): Promise { + const subgraph = await rewardsManager.subgraphs(subgraphId) + const currentAccRewards = subgraph.accRewardsForSubgraph + const invertedSnapshot = currentAccRewards.add(gap) + + // Write accRewardsForSubgraphSnapshot = currentAccRewards + gap (field offset 1) + const snapshotSlot = subgraphStorageSlot(subgraphId, 1) + await setStorage(snapshotSlot, invertedSnapshot) + + // Verify the inversion was written correctly + const after = await rewardsManager.subgraphs(subgraphId) + expect(after.accRewardsForSubgraphSnapshot).to.equal(invertedSnapshot) + expect(after.accRewardsForSubgraph).to.be.lt(after.accRewardsForSubgraphSnapshot) + } + + before(async function () { + ;[curator, indexer] = await graph.getTestAccounts() + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager as RewardsManager + + // Set the staking contract as the subgraph service so RewardsManager + // can see allocations via _getSubgraphAllocatedTokens() + await rewardsManager.connect(governor).setSubgraphService(staking.address) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + async function setupSubgraphWithAllocation() { + // Set issuance rate (200 GRT/block) — the fixture defaults to 0 + await rewardsManager.connect(governor).setIssuancePerBlock(toGRT('200')) + + // Curator signals on subgraph + await grt.connect(governor).mint(curator.address, tokensToSignal) + await grt.connect(curator).approve(curation.address, tokensToSignal) + await curation.connect(curator).mint(subgraphDeploymentID, tokensToSignal, 0) + + // Indexer stakes and allocates + await grt.connect(governor).mint(indexer.address, tokensToStake) + await grt.connect(indexer).approve(staking.address, tokensToStake) + await staking.connect(indexer).stake(tokensToStake) + await staking + .connect(indexer) + .allocateFrom( + indexer.address, + subgraphDeploymentID, + tokensToAllocate, + allocationID, + metadata, + await channelKey.generateProof(indexer.address), + ) + + // Accumulate some rewards + await helpers.mine(50) + + // Sync subgraph state so we have non-zero accRewardsForSubgraph + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + } + + describe('storage slot verification', function () { + it('should correctly compute and write to subgraph storage slots', async function () { + await setupSubgraphWithAllocation() + + // Read current state + const before = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(before.accRewardsForSubgraph).to.not.equal(0, 'precondition: should have accumulated rewards') + + // Write a known value to accRewardsForSubgraphSnapshot (field 1) + const testValue = BigNumber.from('12345678901234567890') + const snapshotSlot = subgraphStorageSlot(subgraphDeploymentID, 1) + await setStorage(snapshotSlot, testValue) + + // Read back and verify + const after = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(after.accRewardsForSubgraphSnapshot).to.equal(testValue) + // Other fields should be unchanged + expect(after.accRewardsForSubgraph).to.equal(before.accRewardsForSubgraph) + expect(after.accRewardsPerSignalSnapshot).to.equal(before.accRewardsPerSignalSnapshot) + expect(after.accRewardsPerAllocatedToken).to.equal(before.accRewardsPerAllocatedToken) + }) + }) + + describe('inverted state: accumulated < snapshot', function () { + it('should not revert on onSubgraphSignalUpdate with inverted state', async function () { + await setupSubgraphWithAllocation() + + // Create the pre-upgrade inverted state (snapshot > accumulated by ~7000 GRT) + const gap = toGRT('7000') + await createInvertedState(subgraphDeploymentID, gap) + + // Advance enough blocks so P > gap. At ~200 GRT/block, 50 blocks ≈ 10,000 GRT > 7,000. + await helpers.mine(50) + + // With an inverted state (A < S), the update must not revert: pending P is + // added before the snapshot S is subtracted (A.add(P).sub(S)), so the + // intermediate A + P stays non-negative and A + P >= S always holds. + await expect(rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID)).to.not.be.reverted + }) + + it('should not revert on onSubgraphAllocationUpdate with inverted state', async function () { + await setupSubgraphWithAllocation() + + const gap = toGRT('7000') + await createInvertedState(subgraphDeploymentID, gap) + + await helpers.mine(50) + + await expect(rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID)).to.not.be.reverted + }) + + it('should sync snapshots after first successful call', async function () { + await setupSubgraphWithAllocation() + + const gap = toGRT('7000') + await createInvertedState(subgraphDeploymentID, gap) + + await helpers.mine(50) + + // First call with inverted state + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // After processing the inverted state, snapshots should be synced + const after = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(after.accRewardsForSubgraphSnapshot).to.equal( + after.accRewardsForSubgraph, + 'snapshot should equal accumulated after processing inverted state', + ) + + // Subsequent calls should work normally + await helpers.mine(10) + await expect(rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID)).to.not.be.reverted + + const afterSecond = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(afterSecond.accRewardsForSubgraphSnapshot).to.equal(afterSecond.accRewardsForSubgraph) + }) + }) + + describe('accounting correctness with inverted state', function () { + it('should correctly compute undistributed rewards: (A+P).sub(S)', async function () { + await setupSubgraphWithAllocation() + + // Record state before inversion + const before = await rewardsManager.subgraphs(subgraphDeploymentID) + const perAllocBefore = before.accRewardsPerAllocatedToken + + // Create inversion with a small gap (smaller than rewards that will accrue) + const gap = toGRT('500') + await createInvertedState(subgraphDeploymentID, gap) + + // Advance enough blocks that S < A + P (i.e., new rewards exceed the gap) + // With 200 GRT/block and only one subgraph signalled, each block adds ~200 GRT of P + // 10 blocks ≈ 2000 GRT of P, gap = 500 GRT + // So (A + P) - S = A + 2000 - (A + 500) = 1500 GRT undistributed + await helpers.mine(10) + + // Call allocation update to distribute rewards + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + const after = await rewardsManager.subgraphs(subgraphDeploymentID) + + // accRewardsPerAllocatedToken should increase (rewards were distributed) + expect(perAllocBefore).to.be.lt(after.accRewardsPerAllocatedToken, 'should distribute rewards: 0 < (A + P) - S') + + // The distributed amount should be less than total new rewards (P) + // because the gap represents already-distributed rewards + // Undistributed = (A + P) - S = P - gap (since S = A + gap) + // If P ≈ 2000 GRT and gap = 500 GRT, undistributed ≈ 1500 GRT + // Subtracting the gap is what keeps this from being P ≈ 2000 GRT (double-counting) + + // Verify snapshots are synced + expect(after.accRewardsForSubgraphSnapshot).to.equal(after.accRewardsForSubgraph) + }) + + it('should not double-count: distributed rewards account for the gap', async function () { + await setupSubgraphWithAllocation() + + // Get a reference: how many rewards are distributed in normal operation + const stateBefore = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Create a scenario where gap = 500 GRT + const gap = toGRT('500') + await createInvertedState(subgraphDeploymentID, gap) + + await helpers.mine(20) + + // Process the inverted state + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const afterInverted = await rewardsManager.subgraphs(subgraphDeploymentID) + const perAllocAfterInverted = afterInverted.accRewardsPerAllocatedToken + + // Now do a SECOND allocation update with normal state (snapshots are synced) + await helpers.mine(20) + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const afterNormal = await rewardsManager.subgraphs(subgraphDeploymentID) + + // The second update should distribute ~20 blocks worth of rewards + // The first update distributed less (because gap was subtracted) + // This proves no double-counting: the gap was properly deducted + const firstDelta = perAllocAfterInverted.sub(stateBefore.accRewardsPerAllocatedToken) + const secondDelta = afterNormal.accRewardsPerAllocatedToken.sub(perAllocAfterInverted) + + // First delta < second delta because the gap was subtracted + // (both periods have ~20 blocks, but first period deducts the 500 GRT gap) + expect(firstDelta).to.be.lt(secondDelta, 'first update should distribute less due to gap deduction') + }) + + it('should distribute exactly P - gap rewards (gap deducted from pending)', async function () { + await setupSubgraphWithAllocation() + + // Sync state so we have a clean baseline + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const baseline = await rewardsManager.subgraphs(subgraphDeploymentID) + const perAllocBaseline = baseline.accRewardsPerAllocatedToken + + // Create inversion with a known gap + const gap = toGRT('500') + await createInvertedState(subgraphDeploymentID, gap) + + // Mine blocks, then do a normal (non-inverted) reference run in a parallel universe + // We can't do that, but we CAN check that the gap is properly deducted by + // comparing inverted vs non-inverted runs over the same block count. + + // First: process the inverted state + await helpers.mine(20) + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const afterInverted = await rewardsManager.subgraphs(subgraphDeploymentID) + const invertedDelta = afterInverted.accRewardsPerAllocatedToken.sub(perAllocBaseline) + + // Second: run the same block count with synced state (no gap) + await helpers.mine(20) + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const afterNormal = await rewardsManager.subgraphs(subgraphDeploymentID) + const normalDelta = afterNormal.accRewardsPerAllocatedToken.sub(afterInverted.accRewardsPerAllocatedToken) + + // The inverted run should distribute LESS because the gap was subtracted. + // Both periods have ~20 blocks of rewards, but the inverted period deducts 500 GRT. + expect(invertedDelta).to.be.lt(normalDelta, 'inverted period should distribute less due to gap deduction') + expect(invertedDelta).to.not.equal(0, 'should still distribute some rewards (gap < P)') + }) + }) + + describe('normal operation (no inversion)', function () { + it('should produce identical results when A == S (synced snapshot steady state)', async function () { + await setupSubgraphWithAllocation() + + // Ensure snapshots are synced (normal state) + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const synced = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(synced.accRewardsForSubgraphSnapshot).to.equal(synced.accRewardsForSubgraph) + + const perAllocBefore = synced.accRewardsPerAllocatedToken + + // Advance and update - this is the normal steady-state path + await helpers.mine(20) + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + const after = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Rewards should be distributed normally + expect(perAllocBefore).to.be.lt(after.accRewardsPerAllocatedToken) + expect(after.accRewardsForSubgraphSnapshot).to.equal(after.accRewardsForSubgraph) + }) + + it('should handle zero rewards gracefully (same block, no new rewards)', async function () { + await setupSubgraphWithAllocation() + + // Sync state + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Call again immediately (same block via automine off) + await hre.network.provider.send('evm_setAutomine', [false]) + try { + const tx = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + await hre.network.provider.send('evm_mine') + await tx.wait() + } finally { + await hre.network.provider.send('evm_setAutomine', [true]) + } + + const after = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Per-alloc-token should be unchanged (zero rewards in same block) + // Note: the transaction itself mines a block, so there may be minimal reward + expect(after.accRewardsForSubgraphSnapshot).to.equal(after.accRewardsForSubgraph) + }) + }) + + describe('realistic pre-upgrade scenario', function () { + it('should handle the exact Arbitrum Sepolia state pattern', async function () { + await setupSubgraphWithAllocation() + + // Simulate: + // 1. Old onSubgraphSignalUpdate wrote accRewardsForSubgraph = X (signal-level view value) + // 2. Old onSubgraphAllocationUpdate wrote accRewardsForSubgraphSnapshot = X + delta + // (via getAccRewardsForSubgraph view which returns storage + pending) + // 3. Proxy upgrade preserves this state + // 4. New code calls _updateSubgraphRewards: A.sub(S) underflows + + // Read current A value + const state = await rewardsManager.subgraphs(subgraphDeploymentID) + const A = state.accRewardsForSubgraph + + // Set S = A + 7235 GRT (matching the ~7235 GRT gap observed on Arbitrum Sepolia) + const observedGap = toGRT('7235') + const accSlot = subgraphStorageSlot(subgraphDeploymentID, 1) + await setStorage(accSlot, A.add(observedGap)) + + // Verify the inversion + const inverted = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(inverted.accRewardsForSubgraph).to.be.lt(inverted.accRewardsForSubgraphSnapshot) + + // Advance blocks (some time passes after upgrade before first interaction) + await helpers.mine(50) + + // First interaction after "upgrade": should NOT revert + await expect(rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID)).to.not.be.reverted + + // State should be healed + const healed = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(healed.accRewardsForSubgraphSnapshot).to.equal(healed.accRewardsForSubgraph) + + // All subsequent operations should work + await helpers.mine(10) + await expect(rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID)).to.not.be.reverted + + await helpers.mine(10) + await expect(rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID)).to.not.be.reverted + }) + }) +}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts new file mode 100644 index 000000000..58338cac8 --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts @@ -0,0 +1,483 @@ +import { Curation } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { GraphNetworkContracts, helpers, randomAddress, randomHexBytes, toGRT } from '@graphprotocol/sdk' +import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { constants } from 'ethers' +import hre from 'hardhat' +import { network } from 'hardhat' + +import { NetworkFixture } from '../lib/fixtures' + +// TODO: Behavior change - HorizonRewardsAssigned is no longer emitted when rewards == 0 +// Set to true if the old behavior is restored (emitting event for zero rewards) +const EMIT_EVENT_FOR_ZERO_REWARDS = false + +describe('Rewards - SubgraphService', () => { + const graph = hre.graph() + let curator1: SignerWithAddress + let governor: SignerWithAddress + let indexer1: SignerWithAddress + + let fixture: NetworkFixture + + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let rewardsManager: RewardsManager + + const subgraphDeploymentID1 = randomHexBytes() + const allocationID1 = randomAddress() + + const ISSUANCE_PER_BLOCK = toGRT('200') // 200 GRT every block + + before(async function () { + const testAccounts = await graph.getTestAccounts() + curator1 = testAccounts[0] + indexer1 = testAccounts[1] + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + rewardsManager = contracts.RewardsManager + + // 200 GRT per block + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + + // Distribute test funds + for (const wallet of [indexer1, curator1]) { + await grt.connect(governor).mint(wallet.address, toGRT('1000000')) + await grt.connect(wallet).approve(curation.address, toGRT('1000000')) + } + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('subgraph service configuration', function () { + it('should reject setSubgraphService if unauthorized', async function () { + const newService = randomAddress() + const tx = rewardsManager.connect(indexer1).setSubgraphService(newService) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set subgraph service if governor', async function () { + const newService = randomAddress() + const tx = rewardsManager.connect(governor).setSubgraphService(newService) + + await expect(tx).emit(rewardsManager, 'SubgraphServiceSet').withArgs(constants.AddressZero, newService) + + expect(await rewardsManager.subgraphService()).eq(newService) + }) + + it('should allow setting to zero address', async function () { + const service = randomAddress() + await rewardsManager.connect(governor).setSubgraphService(service) + + const tx = rewardsManager.connect(governor).setSubgraphService(constants.AddressZero) + await expect(tx).emit(rewardsManager, 'SubgraphServiceSet').withArgs(service, constants.AddressZero) + + expect(await rewardsManager.subgraphService()).eq(constants.AddressZero) + }) + + it('should emit event when setting different address', async function () { + const service1 = randomAddress() + const service2 = randomAddress() + + await rewardsManager.connect(governor).setSubgraphService(service1) + + // Setting a different address should emit event + const tx = await rewardsManager.connect(governor).setSubgraphService(service2) + await expect(tx).emit(rewardsManager, 'SubgraphServiceSet').withArgs(service1, service2) + }) + }) + + describe('subgraph service as rewards issuer', function () { + let mockSubgraphService: any + + beforeEach(async function () { + // Deploy mock SubgraphService + const MockSubgraphServiceFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockSubgraphService.sol:MockSubgraphService', + ) + mockSubgraphService = await MockSubgraphServiceFactory.deploy() + await mockSubgraphService.deployed() + + // Set it on RewardsManager + await rewardsManager.connect(governor).setSubgraphService(mockSubgraphService.address) + }) + + describe('getRewards from subgraph service', function () { + it('should calculate rewards for subgraph service allocations', async function () { + // Setup: Create signal for rewards calculation + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Setup allocation data in mock + const tokensAllocated = toGRT('12500') + await mockSubgraphService.setAllocation( + allocationID1, + true, // isActive + indexer1.address, + subgraphDeploymentID1, + tokensAllocated, + 0, // accRewardsPerAllocatedToken + 0, // accRewardsPending + ) + + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) + + // Mine some blocks to accrue rewards + await helpers.mine(10) + + // Get rewards - should return calculated amount + const rewards = await rewardsManager.getRewards(mockSubgraphService.address, allocationID1) + expect(rewards).to.be.gt(0) + }) + + it('should return zero for inactive allocation', async function () { + // Setup allocation as inactive + await mockSubgraphService.setAllocation( + allocationID1, + false, // isActive = false + indexer1.address, + subgraphDeploymentID1, + toGRT('12500'), + 0, + 0, + ) + + const rewards = await rewardsManager.getRewards(mockSubgraphService.address, allocationID1) + expect(rewards).to.equal(0) + }) + + it('should reject getRewards from non-rewards-issuer contract', async function () { + const randomContract = randomAddress() + const tx = rewardsManager.getRewards(randomContract, allocationID1) + await expect(tx).revertedWith('Not a rewards issuer') + }) + }) + + describe('takeRewards from subgraph service', function () { + it('should take rewards through subgraph service', async function () { + // Setup: Create signal for rewards calculation + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Setup allocation data in mock + const tokensAllocated = toGRT('12500') + await mockSubgraphService.setAllocation( + allocationID1, + true, // isActive + indexer1.address, + subgraphDeploymentID1, + tokensAllocated, + 0, // accRewardsPerAllocatedToken + 0, // accRewardsPending + ) + + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) + + // Mine some blocks to accrue rewards + await helpers.mine(10) + + // Before state + const beforeSubgraphServiceBalance = await grt.balanceOf(mockSubgraphService.address) + const beforeTotalSupply = await grt.totalSupply() + + // Impersonate the mock subgraph service contract + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [mockSubgraphService.address], + }) + await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) + + const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) + + // Take rewards (called by subgraph service) + const tx = await rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) + const receipt = await tx.wait() + + // Stop impersonating + await network.provider.request({ + method: 'hardhat_stopImpersonatingAccount', + params: [mockSubgraphService.address], + }) + + // Parse the event + const event = receipt.logs + .map((log: any) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .find((e: any) => e?.name === 'HorizonRewardsAssigned') + + expect(event).to.not.be.undefined + expect(event?.args.indexer).to.equal(indexer1.address) + expect(event?.args.allocationID).to.equal(allocationID1) + expect(event?.args.amount).to.be.gt(0) + + // After state - verify tokens minted to subgraph service + const afterSubgraphServiceBalance = await grt.balanceOf(mockSubgraphService.address) + const afterTotalSupply = await grt.totalSupply() + + expect(afterSubgraphServiceBalance).to.be.gt(beforeSubgraphServiceBalance) + expect(afterTotalSupply).to.be.gt(beforeTotalSupply) + }) + + it('should return zero rewards for inactive allocation', async function () { + // Setup allocation as inactive + await mockSubgraphService.setAllocation( + allocationID1, + false, // isActive = false + indexer1.address, + subgraphDeploymentID1, + toGRT('12500'), + 0, + 0, + ) + + // Impersonate the mock subgraph service contract + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [mockSubgraphService.address], + }) + await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) + + const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) + + // Take rewards should return 0 + const tx = rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } + + // Stop impersonating + await network.provider.request({ + method: 'hardhat_stopImpersonatingAccount', + params: [mockSubgraphService.address], + }) + }) + + it('should reject takeRewards from non-rewards-issuer contract', async function () { + const tx = rewardsManager.connect(indexer1).takeRewards(allocationID1) + await expect(tx).revertedWith('Caller must be a rewards issuer') + }) + + it('should handle zero rewards scenario', async function () { + // Setup with zero issuance + await rewardsManager.connect(governor).setIssuancePerBlock(0) + + // Setup allocation + await mockSubgraphService.setAllocation( + allocationID1, + true, + indexer1.address, + subgraphDeploymentID1, + toGRT('12500'), + 0, + 0, + ) + + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, toGRT('12500')) + + // Mine blocks + await helpers.mine(10) + + // Impersonate the mock subgraph service contract + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [mockSubgraphService.address], + }) + await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) + + const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) + + // Take rewards should succeed with 0 amount + const tx = rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } + + // Stop impersonating + await network.provider.request({ + method: 'hardhat_stopImpersonatingAccount', + params: [mockSubgraphService.address], + }) + }) + }) + + describe('mixed allocations from staking and subgraph service', function () { + it('should account for both staking and subgraph service allocations in getAccRewardsPerAllocatedToken', async function () { + // This test verifies that getSubgraphAllocatedTokens is called for both issuers + // and rewards are distributed proportionally + + // Setup: Create signal + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Setup subgraph service allocation + const tokensFromSubgraphService = toGRT('5000') + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensFromSubgraphService) + + // Note: We can't easily create a real staking allocation in this test + // but the contract code at lines 381-388 loops through both issuers + // and sums their allocated tokens. This test verifies the subgraph service path. + + // Mine some blocks + await helpers.mine(5) + + // Get accumulated rewards per allocated token + const [accRewardsPerAllocatedToken, accRewardsForSubgraph] = + await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1) + + // Should have calculated rewards based on subgraph service allocations + expect(accRewardsPerAllocatedToken).to.be.gt(0) + expect(accRewardsForSubgraph).to.be.gt(0) + }) + + it('should handle case where only subgraph service has allocations', async function () { + // Setup: Create signal + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Only subgraph service has allocations + const tokensFromSubgraphService = toGRT('10000') + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensFromSubgraphService) + + // Mine blocks + await helpers.mine(5) + + // Get rewards + const [accRewardsPerAllocatedToken] = await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1) + + expect(accRewardsPerAllocatedToken).to.be.gt(0) + }) + + it('should return zero when neither issuer has allocations', async function () { + // Setup: Create signal but no allocations + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // No allocations from either issuer + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, 0) + + // Mine blocks + await helpers.mine(5) + + // Get rewards - Option B: with no allocations, rewards are reclaimed (NO_ALLOCATED_TOKENS) + // so both accRewardsPerAllocatedToken and accRewardsForSubgraph remain 0 + const [accRewardsPerAllocatedToken, accRewardsForSubgraph] = + await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1) + + expect(accRewardsPerAllocatedToken).to.equal(0) + expect(accRewardsForSubgraph).to.equal(0) // Option B: rewards reclaimed when no allocations + }) + }) + + describe('subgraph service with denylist and eligibility', function () { + it('should deny rewards from subgraph service when subgraph is on denylist', async function () { + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Setup allocation with some pending rewards so rewards > 0 + await mockSubgraphService.setAllocation( + allocationID1, + true, + indexer1.address, + subgraphDeploymentID1, + toGRT('12500'), + 0, + toGRT('100'), // accRewardsPending > 0 so rewards will be calculated + ) + + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, toGRT('12500')) + + // Impersonate the mock subgraph service contract + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [mockSubgraphService.address], + }) + await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) + + const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) + + // Take rewards should be denied + const tx = rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + + // Stop impersonating + await network.provider.request({ + method: 'hardhat_stopImpersonatingAccount', + params: [mockSubgraphService.address], + }) + }) + + it('should deny rewards from subgraph service when indexer is ineligible', async function () { + // Setup REO that denies indexer1 + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockREO = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny by default + await mockREO.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockREO.address) + + // Setup: Create signal + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Setup allocation + const tokensAllocated = toGRT('12500') + await mockSubgraphService.setAllocation( + allocationID1, + true, + indexer1.address, + subgraphDeploymentID1, + tokensAllocated, + 0, + 0, + ) + + await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) + + // Mine blocks to accrue rewards + await helpers.mine(5) + + // Impersonate the mock subgraph service contract + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [mockSubgraphService.address], + }) + await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) + + const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) + + // Take rewards should be denied due to eligibility + const tx = rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) + await expect(tx).emit(rewardsManager, 'RewardsDeniedDueToEligibility') + + // Stop impersonating + await network.provider.request({ + method: 'hardhat_stopImpersonatingAccount', + params: [mockSubgraphService.address], + }) + }) + }) + }) +}) diff --git a/packages/contracts/test/tests/unit/rewards/rewards.test.ts b/packages/contracts-test/tests/unit/rewards/rewards.test.ts similarity index 72% rename from packages/contracts/test/tests/unit/rewards/rewards.test.ts rename to packages/contracts-test/tests/unit/rewards/rewards.test.ts index e6171cc13..240d78178 100644 --- a/packages/contracts/test/tests/unit/rewards/rewards.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards.test.ts @@ -15,15 +15,23 @@ import { import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { BigNumber as BN } from 'bignumber.js' import { expect } from 'chai' -import { BigNumber, constants } from 'ethers' +import { BigNumber, constants, utils } from 'ethers' import hre from 'hardhat' import { NetworkFixture } from '../lib/fixtures' const MAX_PPM = 1000000 +// TODO: Behavior change - HorizonRewardsAssigned is no longer emitted when rewards == 0 +// Set to true if the old behavior is restored (emitting event for zero rewards) +const EMIT_EVENT_FOR_ZERO_REWARDS = false + const { HashZero, WeiPerEther } = constants +// Condition identifiers (matching RewardsCondition.sol) +const INDEXER_INELIGIBLE = utils.id('INDEXER_INELIGIBLE') +const SUBGRAPH_DENIED = utils.id('SUBGRAPH_DENIED') + const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] describe('Rewards', () => { @@ -151,6 +159,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 () { @@ -330,39 +342,68 @@ describe('Rewards', () => { describe('getAccRewardsForSubgraph', function () { it('accrued for each subgraph', async function () { - // Curator1 - Update total signalled + // Setup: signal and allocations for two subgraphs const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - const tracker1 = await RewardsTracker.create() - - // Curator2 - Update total signalled const signalled2 = toGRT('500') + const tokensToStake = toGRT('100000') + const tokensToAllocate = toGRT('10000') + + // Mint signal for both subgraphs + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) await curation.connect(curator2).mint(subgraphDeploymentID2, signalled2, 0) - // Snapshot - const tracker2 = await RewardsTracker.create() - await tracker1.snapshot() + // Create allocations for both subgraphs so rewards are accumulated (not reclaimed as NO_ALLOCATED_TOKENS) + await grt.connect(governor).mint(indexer1.address, tokensToStake) + await grt.connect(indexer1).approve(staking.address, tokensToStake) + await staking.connect(indexer1).stake(tokensToStake) - // Jump + const channelKey1 = deriveChannelKey() + await staking + .connect(indexer1) + .allocate( + subgraphDeploymentID1, + tokensToAllocate, + channelKey1.address, + HashZero, + await channelKey1.generateProof(indexer1.address), + ) + + const channelKey2 = deriveChannelKey() + await staking + .connect(indexer1) + .allocate( + subgraphDeploymentID2, + tokensToAllocate, + channelKey2.address, + HashZero, + await channelKey2.generateProof(indexer1.address), + ) + + // Record starting point for both subgraphs + const startRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) + const startRewardsSG2 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID2) + + // Jump blocks to accrue rewards await helpers.mine(ISSUANCE_RATE_PERIODS) - // Snapshot - await tracker1.snapshot() - await tracker2.snapshot() + // Get final rewards + const endRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) + const endRewardsSG2 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID2) - // Calculate rewards - const rewardsPerSignal1 = tracker1.accumulated - const rewardsPerSignal2 = tracker2.accumulated - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) - const expectedRewardsSG2 = rewardsPerSignal2.mul(signalled2).div(WeiPerEther) + // Calculate accrued rewards during the period + const accruedSG1 = endRewardsSG1.sub(startRewardsSG1) + const accruedSG2 = endRewardsSG2.sub(startRewardsSG2) - // Get rewards from contract - const contractRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) - const contractRewardsSG2 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID2) + // Verify proportional distribution: SG1 has 75% of signal (1500/2000), SG2 has 25% (500/2000) + // So SG1 should accrue 3x the rewards of SG2 + const totalAccrued = accruedSG1.add(accruedSG2) + expect(totalAccrued).to.be.gt(0, 'Should have accrued rewards') - // Check - expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1)) - expect(toRound(expectedRewardsSG2)).eq(toRound(contractRewardsSG2)) + // Check proportional distribution (allow small rounding error) + const sg1Share = accruedSG1.mul(100).div(totalAccrued) + const sg2Share = accruedSG2.mul(100).div(totalAccrued) + expect(sg1Share.toNumber()).to.be.closeTo(75, 1, 'SG1 should have ~75% of rewards') + expect(sg2Share.toNumber()).to.be.closeTo(25, 1, 'SG2 should have ~25% of rewards') }) it('should return zero rewards when subgraph signal is below minimum threshold', async function () { @@ -388,6 +429,24 @@ describe('Rewards', () => { // Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Create an allocation so rewards are accumulated (not reclaimed as NO_ALLOCATED_TOKENS) + const tokensToStake = toGRT('100000') + const tokensToAllocate = toGRT('10000') + await grt.connect(governor).mint(indexer1.address, tokensToStake) + await grt.connect(indexer1).approve(staking.address, tokensToStake) + await staking.connect(indexer1).stake(tokensToStake) + const channelKey = deriveChannelKey() + await staking + .connect(indexer1) + .allocate( + subgraphDeploymentID1, + tokensToAllocate, + channelKey.address, + HashZero, + await channelKey.generateProof(indexer1.address), + ) + // Snapshot const tracker1 = await RewardsTracker.create() @@ -471,7 +530,10 @@ describe('Rewards', () => { await helpers.mine(ISSUANCE_RATE_PERIODS) // Prepare expected results - const expectedSubgraphRewards = toGRT('1400') // 7 blocks since signaling to when we do getAccRewardsForSubgraph + // With Option B model: accRewardsForSubgraph only tracks DISTRIBUTABLE rewards (not reclaimed) + // 7 blocks total: 2 blocks before allocation (reclaimed, NOT in accRewardsForSubgraph) + 5 blocks after allocation + const expectedSubgraphRewards = toGRT('1000') // only distributable rewards (5 blocks) + // accRewardsPerAllocatedToken reflects distributable rewards (5 blocks) const expectedRewardsAT = toGRT('0.08') // allocated during 5 blocks: 1000 GRT, divided by 12500 allocated tokens // Update @@ -711,9 +773,13 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'HorizonRewardsAssigned') - .withArgs(indexer1.address, allocationID1, toBN(0)) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } }) it('does not revert with an underflow if the minimum signal changes, and signal came after allocation', async function () { @@ -729,9 +795,13 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'HorizonRewardsAssigned') - .withArgs(indexer1.address, allocationID1, toBN(0)) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } }) it('does not revert if signal was already under minimum', async function () { @@ -746,9 +816,13 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'HorizonRewardsAssigned') - .withArgs(indexer1.address, allocationID1, toBN(0)) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx) + .emit(rewardsManager, 'HorizonRewardsAssigned') + .withArgs(indexer1.address, allocationID1, toBN(0)) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } }) it('should distribute rewards on closed allocation and send to destination', async function () { @@ -857,15 +931,22 @@ describe('Rewards', () => { }) it('should deny rewards if subgraph on denylist', async function () { - // Setup + // Setup: create allocation BEFORE denying the subgraph await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) - await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Create allocation await setupIndexerAllocation() - // Jump + // Jump to earn some rewards await helpers.mineEpoch(epochManager) - // Close allocation. At this point rewards should be collected for that indexer + // Now deny the subgraph - this freezes accRewardsPerAllocatedToken + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Close allocation - pre-denial rewards should be denied const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) }) @@ -889,7 +970,11 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be zero const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } // After state - should be unchanged since no rewards were minted const afterTokenSupply = await grt.totalSupply() @@ -899,6 +984,235 @@ describe('Rewards', () => { expect(afterTokenSupply).eq(beforeTokenSupply) expect(afterStakingBalance).eq(beforeStakingBalance) }) + + it('should handle zero rewards with denylist and reclaim address', async function () { + // Setup reclaim address for SubgraphDenied + const reclaimWallet = assetHolder + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation with zero rewards (no signal) + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Close allocation immediately (same epoch) - should have zero rewards + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // Should not emit events for zero rewards + await expect(tx).to.not.emit(rewardsManager, 'RewardsDenied') + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } + }) + + it('should handle zero rewards with eligibility oracle and reclaim address', async function () { + // Setup reclaim address for IndexerIneligible + const reclaimWallet = assetHolder + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setProviderEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup allocation with zero rewards (no signal) + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Close allocation immediately (same epoch) - should have zero rewards + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // Should not emit events for zero rewards + await expect(tx).to.not.emit(rewardsManager, 'RewardsDeniedDueToEligibility') + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + if (EMIT_EVENT_FOR_ZERO_REWARDS) { + await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) + } else { + await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') + } + }) + + it('should allow collecting pre-denial rewards after undeny', async function () { + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Align with epoch boundary + await helpers.mineEpoch(epochManager) + + // Create allocation + await setupIndexerAllocation() + + // Jump to earn rewards + await helpers.mineEpoch(epochManager) + + // Check rewards exist before deny + const rewardsBefore = await rewardsManager.getRewards(staking.address, allocationID1) + expect(rewardsBefore).gt(0) + + // Deny the subgraph + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Jump while denied (these rewards should be reclaimed, not available) + await helpers.mineEpoch(epochManager) + + // Undeny the subgraph + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, false) + + // Before state + const beforeTokenSupply = await grt.totalSupply() + const beforeIndexerStake = await staking.getIndexerStakedTokens(indexer1.address) + + // Close allocation - should receive pre-denial rewards (frozen value) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Should emit HorizonRewardsAssigned with non-zero rewards + const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + expect(event.indexer).eq(indexer1.address) + expect(event.allocationID).eq(allocationID1) + expect(event.amount).gt(0) + + // After state - tokens should have been minted + const afterTokenSupply = await grt.totalSupply() + const afterIndexerStake = await staking.getIndexerStakedTokens(indexer1.address) + expect(afterTokenSupply).gt(beforeTokenSupply) + expect(afterIndexerStake).gt(beforeIndexerStake) + }) + + it('allocation created while denied should only earn post-undeny rewards', async function () { + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Align with epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup signal first + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Deny the subgraph BEFORE creating allocation + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Jump while denied + await helpers.mineEpoch(epochManager) + + // Create allocation while denied - snapshot = frozen accRewardsPerAllocatedToken + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Jump while still denied + await helpers.mineEpoch(epochManager) + + // Undeny the subgraph + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, false) + + // Jump to earn post-undeny rewards + await helpers.mineEpoch(epochManager) + + // Before state + const beforeTokenSupply = await grt.totalSupply() + + // Close allocation + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // After state + const afterTokenSupply = await grt.totalSupply() + + // Should have earned ONLY post-undeny rewards (not denied-period rewards) + // The rewards should be small since only 1 epoch passed after undeny + const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + expect(event.amount).gt(0) + expect(afterTokenSupply).gt(beforeTokenSupply) + }) + + it('should reclaim denied-period rewards via onSubgraphAllocationUpdate', async function () { + // Setup reclaim address + const reclaimWallet = assetHolder + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Align with epoch boundary + await helpers.mineEpoch(epochManager) + + // Setup signal and allocation + await setupIndexerAllocation() + + // Jump to earn rewards + await helpers.mineEpoch(epochManager) + + // Deny the subgraph - this freezes accRewardsPerAllocatedToken + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Record reclaim wallet balance + const beforeReclaimBalance = await grt.balanceOf(reclaimWallet.address) + + // Jump while denied - new rewards should be reclaimed + await helpers.mineEpoch(epochManager) + + // Trigger onSubgraphAllocationUpdate by creating another allocation + // This will reclaim the denied-period rewards + // Use allocationID2 which already has a matching channelKey2 + const tokensToAllocate2 = toGRT('5000') + await staking.connect(indexer1).stake(tokensToAllocate2) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate2, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) + + // Reclaim wallet should have received rewards + const afterReclaimBalance = await grt.balanceOf(reclaimWallet.address) + expect(afterReclaimBalance).gt(beforeReclaimBalance) + }) }) }) @@ -1053,12 +1367,12 @@ describe('Rewards', () => { // signal in two subgraphs in the same block const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2] + await hre.network.provider.send('evm_setAutomine', [false]) for (const sub of subgraphs) { await curation.connect(curator1).mint(sub, toGRT('1500'), 0) } - - // snapshot block before any accrual (we substract 1 because accrual starts after the first mint happens) - const b1 = await epochManager.blockNum().then((x) => x.toNumber() - 1) + await hre.network.provider.send('evm_mine') + await hre.network.provider.send('evm_setAutomine', [true]) // allocate const tokensToAllocate = toGRT('12500') @@ -1078,6 +1392,9 @@ describe('Rewards', () => { .then((tx) => tx.data), ]) + // snapshot block after allocation (rewards before allocation were reclaimed for subgraph1) + const b1 = await epochManager.blockNum().then((x) => x.toNumber()) + // move time fwd await helpers.mineEpoch(epochManager) @@ -1091,8 +1408,12 @@ describe('Rewards', () => { const accrual = await getRewardsAccrual(subgraphs) const b2 = await epochManager.blockNum().then((x) => x.toNumber()) - // round comparison because there is a small precision error due to dividing and accrual per signal - expect(toRound(accrual.all)).eq(toRound(ISSUANCE_PER_BLOCK.mul(b2 - b1))) + // Only check subgraph1 (with allocation) - subgraph2 has no allocation so its rewards + // are calculated from signal time, not from allocation time + // Each subgraph gets half the issuance (equal signal) + // Small tolerance for fixed-point arithmetic rounding + const expectedSg1Rewards = ISSUANCE_PER_BLOCK.div(2).mul(b2 - b1) + expect(toRound(accrual.sg1.mul(100).div(expectedSg1Rewards))).eq(toRound(BigNumber.from(100))) }) }) }) diff --git a/packages/contracts/test/tests/unit/rewards/subgraphAvailability.test.ts b/packages/contracts-test/tests/unit/rewards/subgraphAvailability.test.ts similarity index 99% rename from packages/contracts/test/tests/unit/rewards/subgraphAvailability.test.ts rename to packages/contracts-test/tests/unit/rewards/subgraphAvailability.test.ts index 988a84aba..a4afbffa5 100644 --- a/packages/contracts/test/tests/unit/rewards/subgraphAvailability.test.ts +++ b/packages/contracts-test/tests/unit/rewards/subgraphAvailability.test.ts @@ -344,7 +344,7 @@ describe('SubgraphAvailabilityManager', () => { const tx = await subgraphAvailabilityManager.connect(oracleThree).voteMany(subgraphs, denied, 2) await expect(tx).to.emit(rewardsManager, 'RewardsDenylistUpdated').withArgs(subgraphDeploymentID1, tx.blockNumber) - await expect(tx).to.emit(rewardsManager, 'RewardsDenylistUpdated').withArgs(subgraphDeploymentID2, 0) + // subgraphDeploymentID2 voted false but was never denied, so setDenied is idempotent (no event) await expect(tx).to.emit(rewardsManager, 'RewardsDenylistUpdated').withArgs(subgraphDeploymentID3, tx.blockNumber) // check that subgraphs are denied diff --git a/packages/contracts/test/tests/unit/serviceRegisty.test.ts b/packages/contracts-test/tests/unit/serviceRegisty.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/serviceRegisty.test.ts rename to packages/contracts-test/tests/unit/serviceRegisty.test.ts diff --git a/packages/contracts/test/tests/unit/staking/allocation.test.ts b/packages/contracts-test/tests/unit/staking/allocation.test.ts similarity index 99% rename from packages/contracts/test/tests/unit/staking/allocation.test.ts rename to 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/configuration.test.ts b/packages/contracts-test/tests/unit/staking/configuration.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/staking/configuration.test.ts rename to packages/contracts-test/tests/unit/staking/configuration.test.ts diff --git a/packages/contracts/test/tests/unit/staking/delegation.test.ts b/packages/contracts-test/tests/unit/staking/delegation.test.ts similarity index 98% rename from packages/contracts/test/tests/unit/staking/delegation.test.ts rename to 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/contracts/test/tests/unit/staking/l2Transfer.test.ts b/packages/contracts-test/tests/unit/staking/l2Transfer.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/staking/l2Transfer.test.ts rename to packages/contracts-test/tests/unit/staking/l2Transfer.test.ts diff --git a/packages/contracts/test/tests/unit/staking/rebate.test.ts b/packages/contracts-test/tests/unit/staking/rebate.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/staking/rebate.test.ts rename to packages/contracts-test/tests/unit/staking/rebate.test.ts diff --git a/packages/contracts/test/tests/unit/staking/staking.test.ts b/packages/contracts-test/tests/unit/staking/staking.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/staking/staking.test.ts rename to packages/contracts-test/tests/unit/staking/staking.test.ts diff --git a/packages/contracts/test/tests/unit/upgrade/admin.test.ts b/packages/contracts-test/tests/unit/upgrade/admin.test.ts similarity index 100% rename from packages/contracts/test/tests/unit/upgrade/admin.test.ts rename to packages/contracts-test/tests/unit/upgrade/admin.test.ts diff --git a/packages/contracts/test/tsconfig.json b/packages/contracts-test/tsconfig.json similarity index 100% rename from packages/contracts/test/tsconfig.json rename to packages/contracts-test/tsconfig.json diff --git a/packages/contracts/test/utils/coverage.ts b/packages/contracts-test/utils/coverage.ts similarity index 100% rename from packages/contracts/test/utils/coverage.ts rename to packages/contracts-test/utils/coverage.ts diff --git a/packages/contracts/.solhint.json b/packages/contracts/.solhint.json index d30847305..780d82f39 100644 --- a/packages/contracts/.solhint.json +++ b/packages/contracts/.solhint.json @@ -1,3 +1,3 @@ { - "extends": ["solhint:recommended", "./../../.solhint.json"] + "extends": "./../../.solhint.json" } diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index dc327f815..ba90039ca 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -7,6 +7,7 @@ import 'solidity-coverage' // for coverage script import 'dotenv/config' import '@nomicfoundation/hardhat-verify' +import { vars } from 'hardhat/config' import { HardhatUserConfig } from 'hardhat/config' // Default mnemonic for basic hardhat network @@ -57,7 +58,16 @@ const config: HardhatUserConfig = { }, }, etherscan: { - apiKey: process.env.ARBISCAN_API_KEY, + // Use ARBISCAN_API_KEY for Arbitrum networks + // For mainnet Ethereum, use ETHERSCAN_API_KEY + // Check both keystore (vars) and environment variable + apiKey: vars.has('ARBISCAN_API_KEY') ? vars.get('ARBISCAN_API_KEY') : (process.env.ARBISCAN_API_KEY ?? ''), + }, + sourcify: { + enabled: false, + }, + blockscout: { + enabled: false, }, typechain: { outDir: 'types', diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 81f9b1d16..a7a4933c8 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -8,7 +8,8 @@ "main": "index.js", "repository": { "type": "git", - "url": "git+https://github.com/graphprotocol/contracts.git" + "url": "git+https://github.com/graphprotocol/contracts", + "directory": "packages/contracts" }, "author": "Edge & Node", "license": "GPL-2.0-or-later", @@ -19,7 +20,7 @@ "types": "index.d.ts", "scripts": { "prepack": "pnpm build", - "clean": "rm -rf artifacts/ cache/ types/ abis/ build/ dist/ coverage/", + "clean": "rm -rf artifacts/ cache/ types/ abis/ build/ dist/ coverage/ test/node_modules/", "build": "pnpm build:self", "build:self": "pnpm compile", "compile": "hardhat compile --quiet", @@ -99,5 +100,14 @@ "winston": "^3.3.3", "yaml": "^1.10.2", "yargs": "^17.0.0" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + }, + "./artifacts/*": "./artifacts/*", + "./types": "./types/index.ts", + "./types/*": "./types/*" } } diff --git a/packages/contracts/task/hardhat.config.ts b/packages/contracts/task/hardhat.config.ts index 8d135decc..aa4223a88 100644 --- a/packages/contracts/task/hardhat.config.ts +++ b/packages/contracts/task/hardhat.config.ts @@ -155,16 +155,6 @@ const config: HardhatUserConfig = { arbitrumGoerli: process.env.ARBISCAN_API_KEY || '', arbitrumSepolia: process.env.ARBISCAN_API_KEY || '', }, - customChains: [ - { - network: 'arbitrumSepolia', - chainId: 421614, - urls: { - apiURL: 'https://api-sepolia.arbiscan.io/api', - browserURL: 'https://sepolia.arbiscan.io', - }, - }, - ], }, typechain: { outDir: '../types', diff --git a/packages/contracts/task/package.json b/packages/contracts/task/package.json index 5d84ec8ba..fd9bb85d1 100644 --- a/packages/contracts/task/package.json +++ b/packages/contracts/task/package.json @@ -26,7 +26,7 @@ "dependencies": { "@graphprotocol/contracts": "workspace:^", "@graphprotocol/sdk": "0.6.0", - "axios": "^1.9.0", + "axios": "^1.16.1", "console-table-printer": "^2.14.1" }, "devDependencies": { diff --git a/packages/contracts/task/tasks/bridge/deposits.ts b/packages/contracts/task/tasks/bridge/deposits.ts index e6e64d70b..366d55a0d 100644 --- a/packages/contracts/task/tasks/bridge/deposits.ts +++ b/packages/contracts/task/tasks/bridge/deposits.ts @@ -1,6 +1,3 @@ -// Import type extensions to make hre.graph available -import '@graphprotocol/sdk/gre/type-extensions' - import { L1ToL2MessageStatus } from '@arbitrum/sdk' import { getL1ToL2MessageStatus } from '@graphprotocol/sdk' import { GraphRuntimeEnvironmentOptions, greTask } from '@graphprotocol/sdk/gre' diff --git a/packages/contracts/task/tasks/bridge/withdrawals.ts b/packages/contracts/task/tasks/bridge/withdrawals.ts index 2a51a3ed5..e0f729385 100644 --- a/packages/contracts/task/tasks/bridge/withdrawals.ts +++ b/packages/contracts/task/tasks/bridge/withdrawals.ts @@ -1,6 +1,3 @@ -// Import type extensions to make hre.graph available -import '@graphprotocol/sdk/gre/type-extensions' - import { L2ToL1MessageStatus } from '@arbitrum/sdk' import { getL2ToL1MessageStatus } from '@graphprotocol/sdk' import { GraphRuntimeEnvironmentOptions, greTask } from '@graphprotocol/sdk/gre' diff --git a/packages/contracts/task/tasks/verify/verify.ts b/packages/contracts/task/tasks/verify/verify.ts index f4462ad77..def5b8c06 100644 --- a/packages/contracts/task/tasks/verify/verify.ts +++ b/packages/contracts/task/tasks/verify/verify.ts @@ -1,6 +1,3 @@ -// Import type extensions to make hre.graph available -import '@graphprotocol/sdk/gre/type-extensions' - import { ContractConfigParam, getContractConfig, readConfig } from '@graphprotocol/contracts-task' import { GraphRuntimeEnvironmentOptions, greTask } from '@graphprotocol/sdk/gre' import fs from 'fs' diff --git a/packages/contracts/test/contracts b/packages/contracts/test/contracts deleted file mode 120000 index 0989a2ba8..000000000 --- a/packages/contracts/test/contracts +++ /dev/null @@ -1 +0,0 @@ -../contracts \ No newline at end of file diff --git a/packages/contracts/test/prettier.config.cjs b/packages/contracts/test/prettier.config.cjs deleted file mode 100644 index 8eb0a0bee..000000000 --- a/packages/contracts/test/prettier.config.cjs +++ /dev/null @@ -1,5 +0,0 @@ -const baseConfig = require('../prettier.config.cjs') - -module.exports = { - ...baseConfig, -} diff --git a/packages/data-edge/.solhint.json b/packages/data-edge/.solhint.json index d30847305..780d82f39 100644 --- a/packages/data-edge/.solhint.json +++ b/packages/data-edge/.solhint.json @@ -1,3 +1,3 @@ { - "extends": ["solhint:recommended", "./../../.solhint.json"] + "extends": "./../../.solhint.json" } diff --git a/packages/data-edge/hardhat.config.ts b/packages/data-edge/hardhat.config.ts index d427a93eb..6dee140d0 100644 --- a/packages/data-edge/hardhat.config.ts +++ b/packages/data-edge/hardhat.config.ts @@ -1,15 +1,12 @@ import '@typechain/hardhat' -// Plugins -import '@nomiclabs/hardhat-ethers' -import '@nomiclabs/hardhat-etherscan' -import '@nomiclabs/hardhat-waffle' +import '@nomicfoundation/hardhat-ethers' +import '@nomicfoundation/hardhat-chai-matchers' +import '@nomicfoundation/hardhat-verify' import 'hardhat-abi-exporter' import 'hardhat-gas-reporter' import 'hardhat-contract-sizer' -import '@openzeppelin/hardhat-upgrades' import 'solidity-coverage' -import '@tenderly/hardhat-tenderly' -import 'hardhat-secure-accounts' // for graph config +import 'hardhat-secure-accounts' // Tasks import './tasks/craft-calldata' import './tasks/post-calldata' @@ -29,20 +26,12 @@ interface NetworkConfig { const networkConfigs: NetworkConfig[] = [ { network: 'mainnet', chainId: 1 }, - { network: 'ropsten', chainId: 3 }, - { network: 'rinkeby', chainId: 4 }, - { network: 'kovan', chainId: 42 }, { network: 'sepolia', chainId: 11155111 }, { network: 'arbitrum-one', chainId: 42161, url: 'https://arb1.arbitrum.io/rpc', }, - { - network: 'arbitrum-goerli', - chainId: 421613, - url: 'https://goerli-rollup.arbitrum.io/rpc', - }, { network: 'arbitrum-sepolia', chainId: 421614, @@ -89,10 +78,6 @@ task('accounts', 'Prints the list of accounts', async (_, bre) => { // Config const config: HardhatUserConfig = { - graph: { - addressBook: process.env.ADDRESS_BOOK || 'addresses.json', - disableSecureAccounts: true, - }, paths: { sources: './contracts', tests: './test', @@ -101,7 +86,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: '0.8.12', + version: '0.8.35', settings: { optimizer: { enabled: true, @@ -140,22 +125,10 @@ const config: HardhatUserConfig = { etherscan: { apiKey: { mainnet: process.env.ETHERSCAN_API_KEY, - goerli: process.env.ETHERSCAN_API_KEY, sepolia: process.env.ETHERSCAN_API_KEY, arbitrumOne: process.env.ARBISCAN_API_KEY, - arbitrumGoerli: process.env.ARBISCAN_API_KEY, arbitrumSepolia: process.env.ARBISCAN_API_KEY, }, - customChains: [ - { - network: 'arbitrumSepolia', - chainId: 421614, - urls: { - apiURL: 'https://api-sepolia.arbiscan.io/api', - browserURL: 'https://sepolia.arbiscan.io', - }, - }, - ], }, gasReporter: { enabled: process.env.REPORT_GAS ? true : false, @@ -165,17 +138,13 @@ const config: HardhatUserConfig = { }, typechain: { outDir: 'build/types', - target: 'ethers-v5', + target: 'ethers-v6', }, abiExporter: { path: './build/abis', clear: false, flat: true, }, - tenderly: { - project: process.env.TENDERLY_PROJECT, - username: process.env.TENDERLY_USERNAME, - }, contractSizer: { alphaSort: true, runOnCompile: false, diff --git a/packages/data-edge/package.json b/packages/data-edge/package.json index c97514031..15b97d050 100644 --- a/packages/data-edge/package.json +++ b/packages/data-edge/package.json @@ -7,8 +7,6 @@ "license": "GPL-2.0-or-later", "main": "index.js", "scripts": { - "prepare": "cd ../.. && husky install packages/contracts/.husky", - "prepublishOnly": "scripts/prepublish", "build": "pnpm build:self", "build:self": "scripts/build", "clean": "rm -rf build/ cache/ dist/ reports/ artifacts/", @@ -35,43 +33,30 @@ "LICENSE" ], "devDependencies": { - "@ethersproject/abi": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/providers": "^5.7.0", - "@nomiclabs/hardhat-ethers": "^2.0.2", - "@nomiclabs/hardhat-etherscan": "^3.1.2", - "@nomiclabs/hardhat-waffle": "^2.0.1", - "@openzeppelin/contracts": "^4.5.0", - "@openzeppelin/hardhat-upgrades": "^1.8.2", - "@tenderly/api-client": "^1.0.13", - "@tenderly/hardhat-tenderly": "^1.0.13", - "@typechain/ethers-v5": "^10.2.1", - "@typechain/hardhat": "^6.1.6", + "@nomicfoundation/hardhat-chai-matchers": "catalog:", + "@nomicfoundation/hardhat-ethers": "catalog:", + "@nomicfoundation/hardhat-verify": "catalog:", + "@typechain/ethers-v6": "^0.5.0", + "@typechain/hardhat": "catalog:", "@types/mocha": "^9.0.0", - "@types/node": "^20.17.50", + "@types/node": "catalog:", "@types/sinon-chai": "^3.2.12", - "chai": "^4.2.0", - "dotenv": "^16.0.0", + "chai": "catalog:", + "dotenv": "catalog:", "eslint": "catalog:", - "ethereum-waffle": "^3.0.2", - "ethers": "^5.7.2", - "ethlint": "^1.2.5", + "ethers": "catalog:", "hardhat": "catalog:", "hardhat-abi-exporter": "^2.2.0", - "hardhat-contract-sizer": "^2.0.3", - "hardhat-gas-reporter": "^1.0.4", - "hardhat-secure-accounts": "0.0.6", - "husky": "^7.0.4", - "lint-staged": "^12.3.5", - "lodash": "^4.17.21", - "markdownlint-cli": "0.45.0", + "hardhat-contract-sizer": "catalog:", + "hardhat-gas-reporter": "catalog:", + "hardhat-secure-accounts": "catalog:", + "markdownlint-cli": "catalog:", "prettier": "catalog:", "prettier-plugin-solidity": "catalog:", "solhint": "catalog:", "solidity-coverage": "^0.8.16", - "truffle-flattener": "^1.4.4", - "ts-node": ">=8.0.0", - "typechain": "^8.3.0", + "ts-node": "catalog:", + "typechain": "catalog:", "typescript": "catalog:" } } diff --git a/packages/data-edge/tasks/craft-calldata.ts b/packages/data-edge/tasks/craft-calldata.ts index 8e285886c..855478f68 100644 --- a/packages/data-edge/tasks/craft-calldata.ts +++ b/packages/data-edge/tasks/craft-calldata.ts @@ -1,5 +1,3 @@ -import '@nomiclabs/hardhat-ethers' - import { Contract } from 'ethers' import { task } from 'hardhat/config' @@ -35,15 +33,13 @@ task('data:craft', 'Build calldata') .addParam('selector', 'Selector name') .addParam('data', 'Call data to post') .setAction(async (taskArgs, hre) => { - // parse input const edgeAddress = taskArgs.edge const calldata = taskArgs.data const selector = taskArgs.selector - // build data const abi = getAbiForSelector(selector) const contract = getContract(edgeAddress, abi, hre.ethers.provider) - const tx = await contract.populateTransaction[selector](calldata) + const tx = await contract[selector].populateTransaction(calldata) const txData = tx.data console.log(txData) }) diff --git a/packages/data-edge/tasks/deploy.ts b/packages/data-edge/tasks/deploy.ts index 57a216a9c..ca142b1e2 100644 --- a/packages/data-edge/tasks/deploy.ts +++ b/packages/data-edge/tasks/deploy.ts @@ -1,5 +1,3 @@ -import '@nomiclabs/hardhat-ethers' - import { promises as fs } from 'fs' import { task } from 'hardhat/config' @@ -31,25 +29,25 @@ task('data-edge:deploy', 'Deploy a DataEdge contract') console.log(`Deploying contract...`) const contract = await factory.deploy() - const tx = contract.deployTransaction + const tx = contract.deploymentTransaction()! - // The address the Contract WILL have once mined - console.log(`> deployer: ${await contract.signer.getAddress()}`) - console.log(`> contract: ${contract.address}`) + const contractAddress = await contract.getAddress() + const [signer] = await hre.ethers.getSigners() + console.log(`> deployer: ${await signer.getAddress()}`) + console.log(`> contract: ${contractAddress}`) console.log( - `> tx: ${tx.hash} nonce:${tx.nonce} limit: ${tx.gasLimit.toString()} gas: ${tx.gasPrice.toNumber() / 1e9} (gwei)`, + `> tx: ${tx.hash} nonce:${tx.nonce} limit: ${tx.gasLimit.toString()} gas: ${Number(tx.gasPrice) / 1e9} (gwei)`, ) - // The contract is NOT deployed yet; we must wait until it is mined - await contract.deployed() + await contract.waitForDeployment() console.log(`Done!`) // Update addresses.json - const chainId = hre.network.config.chainId.toString() + const chainId = hre.network.config.chainId!.toString() if (!addresses[chainId]) { addresses[chainId] = {} } const deployName = `${taskArgs.deployName}${taskArgs.contract}` - addresses[chainId][deployName] = contract.address - return fs.writeFile('addresses.json', JSON.stringify(addresses, null, 2)) + addresses[chainId][deployName] = contractAddress + return fs.writeFile('addresses.json', JSON.stringify(addresses, null, 2) + '\n') }) diff --git a/packages/data-edge/tasks/post-calldata.ts b/packages/data-edge/tasks/post-calldata.ts index fbededfbc..edd455511 100644 --- a/packages/data-edge/tasks/post-calldata.ts +++ b/packages/data-edge/tasks/post-calldata.ts @@ -1,30 +1,28 @@ -import '@nomiclabs/hardhat-ethers' - import { task } from 'hardhat/config' task('data:post', 'Post calldata') .addParam('edge', 'Address of the data edge contract') .addParam('data', 'Call data to post') .setAction(async (taskArgs, hre) => { - // prepare data const edgeAddress = taskArgs.edge const txData = taskArgs.data + const [signer] = await hre.ethers.getSigners() const contract = await hre.ethers.getContractAt('DataEdge', edgeAddress) + const contractAddress = await contract.getAddress() const txRequest = { data: txData, - to: contract.address, + to: contractAddress, } - // send transaction console.log(`Sending data...`) - console.log(`> edge: ${contract.address}`) - console.log(`> sender: ${await contract.signer.getAddress()}`) + console.log(`> edge: ${contractAddress}`) + console.log(`> sender: ${await signer.getAddress()}`) console.log(`> payload: ${txData}`) - const tx = await contract.signer.sendTransaction(txRequest) + const tx = await signer.sendTransaction(txRequest) console.log( - `> tx: ${tx.hash} nonce:${tx.nonce} limit: ${tx.gasLimit.toString()} gas: ${tx.gasPrice.toNumber() / 1e9} (gwei)`, + `> tx: ${tx.hash} nonce:${tx.nonce} limit: ${tx.gasLimit.toString()} gas: ${Number(tx.gasPrice) / 1e9} (gwei)`, ) const rx = await tx.wait() - console.log('> rx: ', rx.status == 1 ? 'success' : 'failed') + console.log('> rx: ', rx!.status == 1 ? 'success' : 'failed') console.log(`Done!`) }) diff --git a/packages/data-edge/test/dataedge.test.ts b/packages/data-edge/test/dataedge.test.ts index 479758881..b96257786 100644 --- a/packages/data-edge/test/dataedge.test.ts +++ b/packages/data-edge/test/dataedge.test.ts @@ -1,57 +1,43 @@ -import '@nomiclabs/hardhat-ethers' - -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import { ethers } from 'hardhat' -import { DataEdge, DataEdge__factory } from '../build/types' - -const { getContractFactory, getSigners } = ethers -const { id, hexConcat, randomBytes, hexlify, defaultAbiCoder } = ethers.utils +import { DataEdge } from '../build/types' describe('DataEdge', () => { let edge: DataEdge - let me: SignerWithAddress + let me: Awaited>[0] beforeEach(async () => { - ;[me] = await getSigners() + ;[me] = await ethers.getSigners() - const factory = (await getContractFactory('DataEdge', me)) as DataEdge__factory + const factory = await ethers.getContractFactory('DataEdge', me) edge = await factory.deploy() - await edge.deployed() + await edge.waitForDeployment() }) describe('submit data', () => { it('post any arbitrary data as selector', async () => { - // virtual function call const txRequest = { data: '0x123123', - to: edge.address, + to: await edge.getAddress(), } - // send transaction const tx = await me.sendTransaction(txRequest) const rx = await tx.wait() - // transaction must work - it just stores data - expect(rx.status).eq(1) + expect(rx!.status).eq(1) }) it('post long calldata', async () => { - // virtual function call - const selector = id('setEpochBlocksPayload(bytes)').slice(0, 10) - // calldata payload - const messageBlocks = hexlify(randomBytes(1000)) - const txCalldata = defaultAbiCoder.encode(['bytes'], [messageBlocks]) // we abi encode to allow the subgraph to decode it properly - const txData = hexConcat([selector, txCalldata]) - // craft full transaction + const selector = ethers.id('setEpochBlocksPayload(bytes)').slice(0, 10) + const messageBlocks = ethers.hexlify(ethers.randomBytes(1000)) + const txCalldata = ethers.AbiCoder.defaultAbiCoder().encode(['bytes'], [messageBlocks]) + const txData = ethers.concat([selector, txCalldata]) const txRequest = { data: txData, - to: edge.address, + to: await edge.getAddress(), } - // send transaction const tx = await me.sendTransaction(txRequest) const rx = await tx.wait() - // transaction must work - it just stores data - expect(rx.status).eq(1) + expect(rx!.status).eq(1) }) }) }) diff --git a/packages/data-edge/test/eventful-dataedge.test.ts b/packages/data-edge/test/eventful-dataedge.test.ts index 8bdf86a2e..974dde5dc 100644 --- a/packages/data-edge/test/eventful-dataedge.test.ts +++ b/packages/data-edge/test/eventful-dataedge.test.ts @@ -1,63 +1,47 @@ -import '@nomiclabs/hardhat-ethers' - -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' import { ethers } from 'hardhat' -import { EventfulDataEdge, EventfulDataEdge__factory } from '../build/types' - -const { getContractFactory, getSigners } = ethers -const { id, hexConcat, randomBytes, hexlify, defaultAbiCoder } = ethers.utils +import { EventfulDataEdge } from '../build/types' describe('EventfulDataEdge', () => { let edge: EventfulDataEdge - let me: SignerWithAddress + let me: Awaited>[0] beforeEach(async () => { - ;[me] = await getSigners() + ;[me] = await ethers.getSigners() - const factory = (await getContractFactory('EventfulDataEdge', me)) as EventfulDataEdge__factory + const factory = await ethers.getContractFactory('EventfulDataEdge', me) edge = await factory.deploy() - await edge.deployed() + await edge.waitForDeployment() }) describe('submit data', () => { it('post any arbitrary data as selector', async () => { - // virtual function call const txRequest = { data: '0x123123', - to: edge.address, + to: await edge.getAddress(), } - // send transaction const tx = await me.sendTransaction(txRequest) const rx = await tx.wait() - // transaction must work - it just stores data - expect(rx.status).eq(1) - // emit log event - const event = edge.interface.parseLog(rx.logs[0]).args - expect(event.data).eq(txRequest.data) + expect(rx!.status).eq(1) + const event = edge.interface.parseLog({ topics: rx!.logs[0].topics as string[], data: rx!.logs[0].data }) + expect(event!.args.data).eq(txRequest.data) }) it('post long calldata', async () => { - // virtual function call - const selector = id('setEpochBlocksPayload(bytes)').slice(0, 10) - // calldata payload - const messageBlocks = hexlify(randomBytes(1000)) - const txCalldata = defaultAbiCoder.encode(['bytes'], [messageBlocks]) // we abi encode to allow the subgraph to decode it properly - const txData = hexConcat([selector, txCalldata]) - // craft full transaction + const selector = ethers.id('setEpochBlocksPayload(bytes)').slice(0, 10) + const messageBlocks = ethers.hexlify(ethers.randomBytes(1000)) + const txCalldata = ethers.AbiCoder.defaultAbiCoder().encode(['bytes'], [messageBlocks]) + const txData = ethers.concat([selector, txCalldata]) const txRequest = { data: txData, - to: edge.address, + to: await edge.getAddress(), } - // send transaction const tx = await me.sendTransaction(txRequest) const rx = await tx.wait() - // transaction must work - it just stores data - expect(rx.status).eq(1) - // emit log event - const event = edge.interface.parseLog(rx.logs[0]).args - expect(event.data).eq(txRequest.data) + expect(rx!.status).eq(1) + const event = edge.interface.parseLog({ topics: rx!.logs[0].topics as string[], data: rx!.logs[0].data }) + expect(event!.args.data).eq(txRequest.data) }) }) }) diff --git a/packages/deployment/.gitignore b/packages/deployment/.gitignore new file mode 100644 index 000000000..d48c62c73 --- /dev/null +++ b/packages/deployment/.gitignore @@ -0,0 +1,4 @@ +deployments/ +fork/ +txs/ +lib/generated/ diff --git a/packages/deployment/.markdownlint.json b/packages/deployment/.markdownlint.json new file mode 100644 index 000000000..18947b0be --- /dev/null +++ b/packages/deployment/.markdownlint.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.markdownlint.json" +} diff --git a/packages/deployment/CLAUDE.md b/packages/deployment/CLAUDE.md new file mode 100644 index 000000000..598c3baf4 --- /dev/null +++ b/packages/deployment/CLAUDE.md @@ -0,0 +1,25 @@ +# packages/deployment - Claude Code Guidance + +Parent: [../CLAUDE.md](../../CLAUDE.md) + +## Required Reading + +Before modifying any deployment scripts in `deploy/`, read: + +- [ImplementationPrinciples.md](docs/deploy/ImplementationPrinciples.md) - Core patterns and rules for all deploy scripts + +## Key Rules (from principles) + +- **`saveGovernanceTx` returns** - governance TX generation returns (not exit), downstream scripts check their own preconditions +- **Idempotent scripts** - check on-chain state, skip if already done +- **Shared precondition checks** - use `lib/preconditions.ts` for configure/transfer checks, not inline copies +- **Package imports** - use `@graphprotocol/deployment/...` not relative paths +- **Contract registry** - use `Contracts.X` not string literals +- **Standard numbering** - `01_deploy`, `02_upgrade`, ..., `09_end` + +## Additional Documentation + +- [GovernanceWorkflow.md](docs/GovernanceWorkflow.md) - Governance TX generation and execution +- [LocalForkTesting.md](docs/LocalForkTesting.md) - Fork mode testing workflow +- [Architecture.md](docs/Architecture.md) - Package architecture +- [Design.md](docs/Design.md) - Design decisions diff --git a/packages/deployment/README.md b/packages/deployment/README.md new file mode 100644 index 000000000..cce3d1c89 --- /dev/null +++ b/packages/deployment/README.md @@ -0,0 +1,84 @@ +# Graph Protocol Contracts - Unified Deployment + +Unified deployment package for Graph Protocol contracts. + +## Quick Start + +```bash +cd packages/deployment + +# Read-only status (no --tags = no mutations) +npx hardhat deploy:status --network arbitrumSepolia +npx hardhat deploy --tags GIP-0088 --network arbitrumSepolia + +# Component lifecycle (single contract) +npx hardhat deploy --tags IssuanceAllocator,deploy --network arbitrumSepolia +npx hardhat deploy --tags IssuanceAllocator,configure --network arbitrumSepolia +npx hardhat deploy --tags IssuanceAllocator,transfer --network arbitrumSepolia + +# Goal-driven (full GIP-0088 deployment) +npx hardhat deploy --tags GIP-0088:upgrade,deploy --network arbitrumSepolia +npx hardhat deploy --tags GIP-0088:upgrade,configure --network arbitrumSepolia +npx hardhat deploy --tags GIP-0088:upgrade,transfer --network arbitrumSepolia +npx hardhat deploy --tags GIP-0088:upgrade,upgrade --network arbitrumSepolia +``` + +See [docs/Gip0088.md](./docs/Gip0088.md) for the full GIP-0088 workflow. + +## Deployment Flow + +Each script is idempotent and goal-seeking: it checks on-chain state and either does what's needed or returns. Scripts that need governance authority build a TX batch and either execute it directly (deployer has permission) or save it for the Safe (`saveGovernanceTx` returns — does not exit). + +``` +sync → deploy → configure → transfer → upgrade (governance batch) + │ │ │ │ │ + │ │ │ │ └─► Bundle proxy upgrades + deferred config + │ │ │ └─► Revoke deployer role + transfer ProxyAdmin + │ │ └─► Deployer-only role grants and params + │ └─► Deploy impl + proxy if needed; store pendingImplementation + └─► Import on-chain state into address books +``` + +## Structure + +``` +packages/deployment/ +├── deploy/ # rocketh deploy scripts (numbered per component) +│ ├── common/ # 00_sync.ts +│ ├── horizon/ # RM, HS, PE, L2Curation, RC +│ ├── service/ # SubgraphService, DisputeManager +│ ├── allocate/ # IssuanceAllocator, DefaultAllocation, DirectAllocation +│ ├── agreement/ # RecurringAgreementManager +│ ├── rewards/ # RewardsEligibilityOracle, Reclaim +│ └── gip/0088/ # GIP-0088 goal orchestration +├── lib/ # Shared utilities (preconditions, registry, tags, ABIs) +├── tasks/ # Hardhat tasks (deploy:*) +├── docs/ # Documentation +└── test/ # Unit tests +``` + +## Available Tasks + +```bash +npx hardhat deploy:status --network arbitrumOne # Show deployment and integration status +npx hardhat deploy:list-pending --network arbitrumOne # List pending implementations +npx hardhat deploy:reset-fork --network localhost # Reset fork state (for testing) +npx hardhat deploy --tags sync --network arbitrumOne # Sync address books with on-chain state +``` + +## Testing + +```bash +pnpm test + +# Fork-based tests +FORK_NETWORK=arbitrumSepolia ARBITRUM_SEPOLIA_RPC= pnpm test +``` + +## See Also + +- [docs/deploy/ImplementationPrinciples.md](./docs/deploy/ImplementationPrinciples.md) - Core design principles and patterns +- [docs/Architecture.md](./docs/Architecture.md) - Package structure and tags +- [docs/GovernanceWorkflow.md](./docs/GovernanceWorkflow.md) - Detailed governance workflow +- [docs/Design.md](./docs/Design.md) - Technical design documentation +- [docs/LocalForkTesting.md](./docs/LocalForkTesting.md) - Fork-based and local network testing diff --git a/packages/deployment/config/arbitrumOne.json5 b/packages/deployment/config/arbitrumOne.json5 new file mode 100644 index 000000000..85baff79a --- /dev/null +++ b/packages/deployment/config/arbitrumOne.json5 @@ -0,0 +1,12 @@ +{ + // Deployment configuration for Arbitrum One (mainnet) + // Values here are committed for reference and reproducibility. + + IssuanceAllocator: { + // RAM allocation: how much issuance flows to RecurringAgreementManager + // ramAllocatorMintingGrtPerBlock: GRT per block minted by IA and sent to RAM + // ramSelfMintingGrtPerBlock: 0 (RAM does not self-mint) + ramAllocatorMintingGrtPerBlock: '6', + ramSelfMintingGrtPerBlock: '0', + }, +} diff --git a/packages/deployment/config/arbitrumSepolia.json5 b/packages/deployment/config/arbitrumSepolia.json5 new file mode 100644 index 000000000..3944d469d --- /dev/null +++ b/packages/deployment/config/arbitrumSepolia.json5 @@ -0,0 +1,12 @@ +{ + // Deployment configuration for Arbitrum Sepolia (testnet) + // Values here are committed for reference and reproducibility. + + IssuanceAllocator: { + // RAM allocation: how much issuance flows to RecurringAgreementManager + // ramAllocatorMintingGrtPerBlock: GRT per block minted by IA and sent to RAM + // ramSelfMintingGrtPerBlock: GRT per block (0 = RAM does not self-mint) + ramAllocatorMintingGrtPerBlock: '0.5', + ramSelfMintingGrtPerBlock: '0', + }, +} diff --git a/packages/deployment/config/localNetwork.json5 b/packages/deployment/config/localNetwork.json5 new file mode 100644 index 000000000..09d9340ee --- /dev/null +++ b/packages/deployment/config/localNetwork.json5 @@ -0,0 +1,11 @@ +{ + // Deployment configuration for local-network (docker-compose dev stack) + // Local network uses generous rates for fast iteration and testing. + + IssuanceAllocator: { + // RAM allocation: how much issuance flows to RecurringAgreementManager + // Local network uses a high rate so agreements accumulate meaningful rewards quickly + ramAllocatorMintingGrtPerBlock: '6', + ramSelfMintingGrtPerBlock: '0', + }, +} diff --git a/packages/deployment/deploy/agreement/manager/01_deploy.ts b/packages/deployment/deploy/agreement/manager/01_deploy.ts new file mode 100644 index 000000000..dabd71cfb --- /dev/null +++ b/packages/deployment/deploy/agreement/manager/01_deploy.ts @@ -0,0 +1,16 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { requireDeployer, requireGraphToken } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createProxyDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createProxyDeployModule( + Contracts.issuance.RecurringAgreementManager, + (env) => { + const paymentsEscrow = env.getOrNull('PaymentsEscrow') + if (!paymentsEscrow) throw new Error('Missing PaymentsEscrow deployment after sync.') + return { + constructorArgs: [requireGraphToken(env).address, paymentsEscrow.address], + initializeArgs: [requireDeployer(env)], + } + }, + { prerequisites: [Contracts.horizon.L2GraphToken, Contracts.horizon.PaymentsEscrow] }, +) diff --git a/packages/deployment/deploy/agreement/manager/02_upgrade.ts b/packages/deployment/deploy/agreement/manager/02_upgrade.ts new file mode 100644 index 000000000..70b140182 --- /dev/null +++ b/packages/deployment/deploy/agreement/manager/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.issuance.RecurringAgreementManager) diff --git a/packages/deployment/deploy/agreement/manager/04_configure.ts b/packages/deployment/deploy/agreement/manager/04_configure.ts new file mode 100644 index 000000000..0d0d7b1a2 --- /dev/null +++ b/packages/deployment/deploy/agreement/manager/04_configure.ts @@ -0,0 +1,225 @@ +import { ACCESS_CONTROL_ENUMERABLE_ABI, ISSUANCE_TARGET_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { supportsInterface } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getGovernor, getPauseGuardian } from '@graphprotocol/deployment/lib/controller-utils.js' +import { ComponentTags, DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContract, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { checkRAMConfigured } from '@graphprotocol/deployment/lib/preconditions.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph, tx } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' +import { encodeFunctionData, keccak256, toHex } from 'viem' + +/** + * Configure RecurringAgreementManager + * + * Grants: + * - COLLECTOR_ROLE to RecurringCollector + * - DATA_SERVICE_ROLE to SubgraphService + * - GOVERNOR_ROLE to protocol governor + * - PAUSE_ROLE to pause guardian + * + * Sets: + * - IssuanceAllocator as RAM's issuance source + * + * Idempotent: checks on-chain state, skips if already configured. + * + * Usage: + * pnpm hardhat deploy --tags RecurringAgreementManager:configure --network + */ +export default createActionModule( + Contracts.issuance.RecurringAgreementManager, + DeploymentActions.CONFIGURE, + async (env) => { + const client = graph.getPublicClient(env) as PublicClient + const governor = await getGovernor(env) + const pauseGuardian = await getPauseGuardian(env) + + const ram = requireContract(env, Contracts.issuance.RecurringAgreementManager) + const rc = requireContract(env, Contracts.horizon.RecurringCollector) + const ss = requireContract(env, Contracts['subgraph-service'].SubgraphService) + const ia = requireContract(env, Contracts.issuance.IssuanceAllocator) + + env.showMessage(`\n========== Configure ${Contracts.issuance.RecurringAgreementManager.name} ==========`) + env.showMessage(`RAM: ${ram.address}`) + env.showMessage(`RC: ${rc.address}`) + env.showMessage(`SS: ${ss.address}`) + env.showMessage(`IA: ${ia.address}`) + + // Check if already configured (shared precondition check) + const precondition = await checkRAMConfigured( + client, + ram.address, + rc.address, + ss.address, + ia.address, + governor, + pauseGuardian, + ) + if (precondition.done) { + env.showMessage(`\n✅ ${Contracts.issuance.RecurringAgreementManager.name} already configured\n`) + return + } + + // Role constants + const COLLECTOR_ROLE = keccak256(toHex('COLLECTOR_ROLE')) + const DATA_SERVICE_ROLE = keccak256(toHex('DATA_SERVICE_ROLE')) + const GOVERNOR_ROLE = keccak256(toHex('GOVERNOR_ROLE')) + const PAUSE_ROLE = keccak256(toHex('PAUSE_ROLE')) + + // Check what still needs configuring + env.showMessage('\n📋 Checking current configuration...\n') + + const rcHasCollectorRole = (await client.readContract({ + address: ram.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [COLLECTOR_ROLE, rc.address as `0x${string}`], + })) as boolean + env.showMessage(` RC COLLECTOR_ROLE: ${rcHasCollectorRole ? '✓' : '✗'}`) + + const ssHasDataServiceRole = (await client.readContract({ + address: ram.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [DATA_SERVICE_ROLE, ss.address as `0x${string}`], + })) as boolean + env.showMessage(` SS DATA_SERVICE_ROLE: ${ssHasDataServiceRole ? '✓' : '✗'}`) + + // Check role grants + const governorHasRole = (await client.readContract({ + address: ram.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [GOVERNOR_ROLE, governor as `0x${string}`], + })) as boolean + env.showMessage(` Governor GOVERNOR_ROLE: ${governorHasRole ? '✓' : '✗'}`) + + const pauseGuardianHasRole = (await client.readContract({ + address: ram.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [PAUSE_ROLE, pauseGuardian as `0x${string}`], + })) as boolean + env.showMessage(` PauseGuardian PAUSE_ROLE: ${pauseGuardianHasRole ? '✓' : '✗'}`) + + // Determine executor: deployer (fresh) or governor (prod) + const deployer = requireDeployer(env) + const deployerIsGovernor = (await client.readContract({ + address: ram.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [GOVERNOR_ROLE, deployer as `0x${string}`], + })) as boolean + + if (!deployerIsGovernor) { + env.showMessage(`\n ○ Deployer does not have GOVERNOR_ROLE — skipping (governance TX in upgrade step)\n`) + return + } + + // Build TX list for missing configuration + const txs: Array<{ to: string; data: `0x${string}`; label: string }> = [] + + if (!rcHasCollectorRole) { + txs.push({ + to: ram.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [COLLECTOR_ROLE, rc.address as `0x${string}`], + }), + label: `grantRole(COLLECTOR_ROLE, ${rc.address})`, + }) + } + + if (!ssHasDataServiceRole) { + txs.push({ + to: ram.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [DATA_SERVICE_ROLE, ss.address as `0x${string}`], + }), + label: `grantRole(DATA_SERVICE_ROLE, ${ss.address})`, + }) + } + + if (!governorHasRole) { + txs.push({ + to: ram.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [GOVERNOR_ROLE, governor as `0x${string}`], + }), + label: `grantRole(GOVERNOR_ROLE, ${governor})`, + }) + } + + if (!pauseGuardianHasRole) { + txs.push({ + to: ram.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [PAUSE_ROLE, pauseGuardian as `0x${string}`], + }), + label: `grantRole(PAUSE_ROLE, ${pauseGuardian})`, + }) + } + + // Check issuance allocator — skip if IA doesn't support the interface yet (pending upgrade) + let iaConfigured = false + try { + const currentIA = (await client.readContract({ + address: ram.address as `0x${string}`, + abi: ISSUANCE_TARGET_ABI, + functionName: 'getIssuanceAllocator', + })) as string + iaConfigured = currentIA.toLowerCase() === ia.address.toLowerCase() + env.showMessage(` IssuanceAllocator: ${iaConfigured ? '✓' : '✗'} (current: ${currentIA})`) + } catch { + env.showMessage(` IssuanceAllocator: ✗ (getter not available)`) + } + + if (!iaConfigured) { + const IISSUANCE_ALLOCATION_DISTRIBUTION_ID = '0x79da37fc' // type(IIssuanceAllocationDistribution).interfaceId + const iaSupported = await supportsInterface(client, ia.address, IISSUANCE_ALLOCATION_DISTRIBUTION_ID) + if (iaSupported) { + txs.push({ + to: ram.address, + data: encodeFunctionData({ + abi: ISSUANCE_TARGET_ABI, + functionName: 'setIssuanceAllocator', + args: [ia.address as `0x${string}`], + }), + label: `setIssuanceAllocator(${ia.address})`, + }) + } else { + env.showMessage(` ○ IA does not yet support IIssuanceAllocationDistribution — skipping setIssuanceAllocator`) + } + } + + if (txs.length === 0) return + + env.showMessage('\n🔨 Executing configuration as deployer...\n') + const txFn = tx(env) + for (const t of txs) { + await txFn({ account: deployer, to: t.to as `0x${string}`, data: t.data }) + env.showMessage(` ✓ ${t.label}`) + } + env.showMessage(`\n✅ ${Contracts.issuance.RecurringAgreementManager.name} configuration complete!\n`) + }, + { + extraDependencies: [ + ComponentTags.RECURRING_COLLECTOR, + ComponentTags.SUBGRAPH_SERVICE, + ComponentTags.ISSUANCE_ALLOCATOR, + ], + prerequisites: [ + Contracts.horizon.RecurringCollector, + Contracts['subgraph-service'].SubgraphService, + Contracts.issuance.IssuanceAllocator, + ], + }, +) diff --git a/packages/deployment/deploy/agreement/manager/05_transfer_governance.ts b/packages/deployment/deploy/agreement/manager/05_transfer_governance.ts new file mode 100644 index 000000000..50d3f7582 --- /dev/null +++ b/packages/deployment/deploy/agreement/manager/05_transfer_governance.ts @@ -0,0 +1,60 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + requireContract, + requireDeployer, + transferProxyAdminOwnership, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { checkDeployerRevoked } from '@graphprotocol/deployment/lib/preconditions.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { execute, graph, read } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Transfer RecurringAgreementManager governance from deployer + * + * - Revoke GOVERNOR_ROLE from deployment account + * - Transfer ProxyAdmin ownership to governor + * + * Role grants (GOVERNOR_ROLE, PAUSE_ROLE, COLLECTOR_ROLE, DATA_SERVICE_ROLE) + * happen in 04_configure.ts. This script only revokes deployer access. + * + * Idempotent: checks on-chain state, skips if already transferred. + * + * Usage: + * pnpm hardhat deploy --tags RecurringAgreementManager,transfer --network + */ +export default createActionModule( + Contracts.issuance.RecurringAgreementManager, + DeploymentActions.TRANSFER, + async (env) => { + const readFn = read(env) + const executeFn = execute(env) + const client = graph.getPublicClient(env) as PublicClient + const deployer = requireDeployer(env) + const ram = requireContract(env, Contracts.issuance.RecurringAgreementManager) + + env.showMessage(`\n========== Transfer ${Contracts.issuance.RecurringAgreementManager.name} ==========`) + + // Check if deployer GOVERNOR_ROLE already revoked (shared precondition check) + const precondition = await checkDeployerRevoked(client, ram.address, deployer) + if (precondition.done) { + env.showMessage(`✓ Deployer GOVERNOR_ROLE already revoked`) + } else { + const GOVERNOR_ROLE = (await readFn(ram, { functionName: 'GOVERNOR_ROLE' })) as `0x${string}` + + env.showMessage(`🔨 Revoking deployer GOVERNOR_ROLE...`) + await executeFn(ram, { + account: deployer, + functionName: 'revokeRole', + args: [GOVERNOR_ROLE, deployer], + }) + env.showMessage(` ✓ revokeRole(GOVERNOR_ROLE) executed`) + } + + // Transfer ProxyAdmin ownership to governor + await transferProxyAdminOwnership(env, Contracts.issuance.RecurringAgreementManager) + + env.showMessage(`\n✅ ${Contracts.issuance.RecurringAgreementManager.name} governance transferred!\n`) + }, +) diff --git a/packages/deployment/deploy/agreement/manager/09_end.ts b/packages/deployment/deploy/agreement/manager/09_end.ts new file mode 100644 index 000000000..c68c1db6a --- /dev/null +++ b/packages/deployment/deploy/agreement/manager/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.issuance.RecurringAgreementManager) diff --git a/packages/deployment/deploy/agreement/manager/10_status.ts b/packages/deployment/deploy/agreement/manager/10_status.ts new file mode 100644 index 000000000..d7e3f98bc --- /dev/null +++ b/packages/deployment/deploy/agreement/manager/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.issuance.RecurringAgreementManager) diff --git a/packages/deployment/deploy/allocate/allocator/01_deploy.ts b/packages/deployment/deploy/allocate/allocator/01_deploy.ts new file mode 100644 index 000000000..58bd3ca30 --- /dev/null +++ b/packages/deployment/deploy/allocate/allocator/01_deploy.ts @@ -0,0 +1,12 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { requireContract, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createProxyDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createProxyDeployModule( + Contracts.issuance.IssuanceAllocator, + (env) => ({ + constructorArgs: [requireContract(env, Contracts.horizon.L2GraphToken).address], + initializeArgs: [requireDeployer(env)], + }), + { prerequisites: [Contracts.horizon.L2GraphToken] }, +) diff --git a/packages/deployment/deploy/allocate/allocator/02_upgrade.ts b/packages/deployment/deploy/allocate/allocator/02_upgrade.ts new file mode 100644 index 000000000..8f012a025 --- /dev/null +++ b/packages/deployment/deploy/allocate/allocator/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.issuance.IssuanceAllocator) diff --git a/packages/deployment/deploy/allocate/allocator/04_configure.ts b/packages/deployment/deploy/allocate/allocator/04_configure.ts new file mode 100644 index 000000000..d46243e74 --- /dev/null +++ b/packages/deployment/deploy/allocate/allocator/04_configure.ts @@ -0,0 +1,168 @@ +import { ACCESS_CONTROL_ENUMERABLE_ABI, REWARDS_MANAGER_DEPRECATED_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getGovernor, getPauseGuardian } from '@graphprotocol/deployment/lib/controller-utils.js' +import { ComponentTags, DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContracts, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { checkIAConfigured } from '@graphprotocol/deployment/lib/preconditions.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph, read, tx } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +/** + * Configure IssuanceAllocator + * + * - Sets issuance rate to match RewardsManager + * - Configures RM as 100% self-minting target + * - Grants GOVERNOR_ROLE to protocol governor + * - Grants PAUSE_ROLE to pause guardian + * + * If deployer has GOVERNOR_ROLE (fresh deploy), executes directly. + * If governance transferred, generates governance TX or executes via governor. + * + * Idempotent: checks on-chain state, skips if already configured. + * + * Usage: + * pnpm hardhat deploy --tags IssuanceAllocator,configure --network + */ +export default createActionModule( + Contracts.issuance.IssuanceAllocator, + DeploymentActions.CONFIGURE, + async (env) => { + const readFn = read(env) + const deployer = requireDeployer(env) + const governor = await getGovernor(env) + const pauseGuardian = await getPauseGuardian(env) + + const [issuanceAllocator, rewardsManager] = requireContracts(env, [ + Contracts.issuance.IssuanceAllocator, + Contracts.horizon.RewardsManager, + ]) + + const client = graph.getPublicClient(env) as PublicClient + + env.showMessage(`\n========== Configure ${Contracts.issuance.IssuanceAllocator.name} ==========`) + env.showMessage(`${Contracts.issuance.IssuanceAllocator.name}: ${issuanceAllocator.address}`) + env.showMessage(`${Contracts.horizon.RewardsManager.name}: ${rewardsManager.address}`) + + // Check if already configured (shared precondition check) + const precondition = await checkIAConfigured( + client, + issuanceAllocator.address, + rewardsManager.address, + governor, + pauseGuardian, + ) + if (precondition.done) { + env.showMessage(`\n✅ ${Contracts.issuance.IssuanceAllocator.name} already configured\n`) + return + } + + // Get RM issuance rate (target for IA) + const rmIssuanceRate = (await client.readContract({ + address: rewardsManager.address as `0x${string}`, + abi: REWARDS_MANAGER_DEPRECATED_ABI, + functionName: 'issuancePerBlock', + })) as bigint + + if (rmIssuanceRate === 0n) { + env.showMessage(`\n ○ RM.issuancePerBlock is 0 — skipping IA configure\n`) + return + } + + // Determine what still needs configuring + env.showMessage('\n📋 Checking current configuration...\n') + + const iaIssuanceRate = (await readFn(issuanceAllocator, { functionName: 'getIssuancePerBlock' })) as bigint + const rateOk = iaIssuanceRate === rmIssuanceRate && iaIssuanceRate > 0n + env.showMessage(` Issuance rate: ${rateOk ? '✓' : '✗'} (IA: ${iaIssuanceRate}, RM: ${rmIssuanceRate})`) + + // Check role grants + const GOVERNOR_ROLE = (await readFn(issuanceAllocator, { functionName: 'GOVERNOR_ROLE' })) as `0x${string}` + const PAUSE_ROLE = (await readFn(issuanceAllocator, { functionName: 'PAUSE_ROLE' })) as `0x${string}` + + const governorHasRole = (await readFn(issuanceAllocator, { + functionName: 'hasRole', + args: [GOVERNOR_ROLE, governor], + })) as boolean + env.showMessage(` Governor GOVERNOR_ROLE: ${governorHasRole ? '✓' : '✗'}`) + + const pauseGuardianHasRole = (await readFn(issuanceAllocator, { + functionName: 'hasRole', + args: [PAUSE_ROLE, pauseGuardian], + })) as boolean + env.showMessage(` PauseGuardian PAUSE_ROLE: ${pauseGuardianHasRole ? '✓' : '✗'}`) + + // Determine executor: deployer if has GOVERNOR_ROLE, else protocol governor + const deployerHasRole = (await readFn(issuanceAllocator, { + functionName: 'hasRole', + args: [GOVERNOR_ROLE, deployer], + })) as boolean + + // Build TX data for missing configuration + const txs: Array<{ to: string; data: `0x${string}`; label: string }> = [] + + if (!rateOk) { + txs.push({ + to: issuanceAllocator.address, + data: encodeFunctionData({ + abi: [ + { + inputs: [{ type: 'uint256' }], + name: 'setIssuancePerBlock', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ], + functionName: 'setIssuancePerBlock', + args: [rmIssuanceRate], + }), + label: `setIssuancePerBlock(${rmIssuanceRate})`, + }) + } + + if (!governorHasRole) { + txs.push({ + to: issuanceAllocator.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [GOVERNOR_ROLE, governor as `0x${string}`], + }), + label: `grantRole(GOVERNOR_ROLE, ${governor})`, + }) + } + + if (!pauseGuardianHasRole) { + txs.push({ + to: issuanceAllocator.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [PAUSE_ROLE, pauseGuardian as `0x${string}`], + }), + label: `grantRole(PAUSE_ROLE, ${pauseGuardian})`, + }) + } + + if (!deployerHasRole) { + env.showMessage(`\n ○ Deployer does not have GOVERNOR_ROLE — skipping (governance TX in upgrade step)\n`) + return + } + + if (txs.length === 0) return + + env.showMessage('\n🔨 Executing configuration as deployer...\n') + const txFn = tx(env) + for (const t of txs) { + await txFn({ account: deployer, to: t.to as `0x${string}`, data: t.data }) + env.showMessage(` ✓ ${t.label}`) + } + env.showMessage(`\n✅ ${Contracts.issuance.IssuanceAllocator.name} configuration complete!\n`) + }, + { + extraDependencies: [ComponentTags.REWARDS_MANAGER], + prerequisites: [Contracts.horizon.RewardsManager], + }, +) diff --git a/packages/deployment/deploy/allocate/allocator/06_transfer_governance.ts b/packages/deployment/deploy/allocate/allocator/06_transfer_governance.ts new file mode 100644 index 000000000..b960839b7 --- /dev/null +++ b/packages/deployment/deploy/allocate/allocator/06_transfer_governance.ts @@ -0,0 +1,61 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getGovernor } from '@graphprotocol/deployment/lib/controller-utils.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + requireContracts, + requireDeployer, + transferProxyAdminOwnership, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { checkDeployerRevoked } from '@graphprotocol/deployment/lib/preconditions.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { execute, graph, read } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Transfer IssuanceAllocator governance from deployer to protocol governor + * + * - Revoke GOVERNOR_ROLE from deployment account + * - Transfer ProxyAdmin ownership to governor + * + * Role grants (GOVERNOR_ROLE to governor, PAUSE_ROLE to pauseGuardian) happen + * in 04_configure.ts. This script only revokes deployer access. + * + * Idempotent: checks on-chain state, skips if already transferred. + * + * Usage: + * pnpm hardhat deploy --tags IssuanceAllocator,transfer --network + */ +export default createActionModule(Contracts.issuance.IssuanceAllocator, DeploymentActions.TRANSFER, async (env) => { + const readFn = read(env) + const executeFn = execute(env) + const client = graph.getPublicClient(env) as PublicClient + + const deployer = requireDeployer(env) + const governor = await getGovernor(env) + const [issuanceAllocator] = requireContracts(env, [Contracts.issuance.IssuanceAllocator]) + + env.showMessage(`\n========== Transfer ${Contracts.issuance.IssuanceAllocator.name} ==========`) + env.showMessage(`Deployer: ${deployer}`) + env.showMessage(`Governor: ${governor}\n`) + + // Check if deployer GOVERNOR_ROLE already revoked (shared precondition check) + const precondition = await checkDeployerRevoked(client, issuanceAllocator.address, deployer) + if (precondition.done) { + env.showMessage(`✓ Deployer GOVERNOR_ROLE already revoked`) + } else { + const GOVERNOR_ROLE = (await readFn(issuanceAllocator, { functionName: 'GOVERNOR_ROLE' })) as `0x${string}` + + env.showMessage(`🔨 Revoking deployer GOVERNOR_ROLE...`) + await executeFn(issuanceAllocator, { + account: deployer, + functionName: 'revokeRole', + args: [GOVERNOR_ROLE, deployer], + }) + env.showMessage(` ✓ revokeRole(GOVERNOR_ROLE) executed`) + } + + // Transfer ProxyAdmin ownership to governor + await transferProxyAdminOwnership(env, Contracts.issuance.IssuanceAllocator) + + env.showMessage(`\n✅ ${Contracts.issuance.IssuanceAllocator.name} governance transferred!\n`) +}) diff --git a/packages/deployment/deploy/allocate/allocator/09_end.ts b/packages/deployment/deploy/allocate/allocator/09_end.ts new file mode 100644 index 000000000..272c2915e --- /dev/null +++ b/packages/deployment/deploy/allocate/allocator/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.issuance.IssuanceAllocator) diff --git a/packages/deployment/deploy/allocate/allocator/10_status.ts b/packages/deployment/deploy/allocate/allocator/10_status.ts new file mode 100644 index 000000000..23df5d817 --- /dev/null +++ b/packages/deployment/deploy/allocate/allocator/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.issuance.IssuanceAllocator) diff --git a/packages/deployment/deploy/allocate/default/01_deploy.ts b/packages/deployment/deploy/allocate/default/01_deploy.ts new file mode 100644 index 000000000..311c11b1b --- /dev/null +++ b/packages/deployment/deploy/allocate/default/01_deploy.ts @@ -0,0 +1,39 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { ComponentTags, DeploymentActions, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { deployProxyContract, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +/** + * Deploy DefaultAllocation proxy — IA's default target for unallocated issuance + * + * Uses the shared DirectAllocation_Implementation. + * Initialized with deployer as governor (transferred in transfer step). + * + * Usage: + * pnpm hardhat deploy --tags DefaultAllocation,deploy --network + */ +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.DEPLOY)) return + await syncComponentsFromRegistry(env, [ + Contracts.issuance.DirectAllocation_Implementation, + Contracts.issuance.DefaultAllocation, + ]) + + env.showMessage(`\n📦 Deploying DefaultAllocation proxy...`) + env.showMessage(` Shared implementation: ${Contracts.issuance.DirectAllocation_Implementation.name}`) + + await deployProxyContract(env, { + contract: Contracts.issuance.DefaultAllocation, + sharedImplementation: Contracts.issuance.DirectAllocation_Implementation, + initializeArgs: [requireDeployer(env)], + }) + + env.showMessage('\n✓ DefaultAllocation deployment complete') +} + +func.tags = [ComponentTags.DEFAULT_ALLOCATION] +func.dependencies = [ComponentTags.DIRECT_ALLOCATION_IMPL] +func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) + +export default func diff --git a/packages/deployment/deploy/allocate/default/02_upgrade.ts b/packages/deployment/deploy/allocate/default/02_upgrade.ts new file mode 100644 index 000000000..2bb15a1da --- /dev/null +++ b/packages/deployment/deploy/allocate/default/02_upgrade.ts @@ -0,0 +1,27 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { ComponentTags, DeploymentActions, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { upgradeImplementation } from '@graphprotocol/deployment/lib/upgrade-implementation.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +// DefaultAllocation Upgrade +// +// Upgrades DefaultAllocation proxy to DirectAllocation implementation via per-proxy ProxyAdmin. + +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.UPGRADE)) return + await syncComponentsFromRegistry(env, [ + Contracts.issuance.DirectAllocation_Implementation, + Contracts.issuance.DefaultAllocation, + ]) + await upgradeImplementation(env, Contracts.issuance.DefaultAllocation, { + implementationName: 'DirectAllocation', + }) + await syncComponentsFromRegistry(env, [Contracts.issuance.DefaultAllocation]) +} + +func.tags = [ComponentTags.DEFAULT_ALLOCATION] +func.dependencies = [ComponentTags.DIRECT_ALLOCATION_IMPL] +func.skip = async () => shouldSkipAction(DeploymentActions.UPGRADE) + +export default func diff --git a/packages/deployment/deploy/allocate/default/04_configure.ts b/packages/deployment/deploy/allocate/default/04_configure.ts new file mode 100644 index 000000000..528531ff6 --- /dev/null +++ b/packages/deployment/deploy/allocate/default/04_configure.ts @@ -0,0 +1,119 @@ +import { ACCESS_CONTROL_ENUMERABLE_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getGovernor, getPauseGuardian } from '@graphprotocol/deployment/lib/controller-utils.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContract, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { checkDefaultAllocationConfigured } from '@graphprotocol/deployment/lib/preconditions.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph, read, tx } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +/** + * Configure DefaultAllocation + * + * - Grants GOVERNOR_ROLE to protocol governor + * - Grants PAUSE_ROLE to pause guardian + * + * Note: IA.setDefaultTarget(DA) is an activation step in issuance-connect, + * not a configure step (requires IA to have minter role). + * + * Idempotent: checks on-chain state, skips if already configured. + * + * Usage: + * pnpm hardhat deploy --tags DefaultAllocation,configure --network + */ +export default createActionModule(Contracts.issuance.DefaultAllocation, DeploymentActions.CONFIGURE, async (env) => { + const client = graph.getPublicClient(env) as PublicClient + const readFn = read(env) + const deployer = requireDeployer(env) + const governor = await getGovernor(env) + const pauseGuardian = await getPauseGuardian(env) + + const defaultAllocation = requireContract(env, Contracts.issuance.DefaultAllocation) + + env.showMessage(`\n========== Configure ${Contracts.issuance.DefaultAllocation.name} ==========`) + env.showMessage(`DefaultAllocation: ${defaultAllocation.address}`) + + // Check if already configured (shared precondition check) + const precondition = await checkDefaultAllocationConfigured( + client, + defaultAllocation.address, + governor, + pauseGuardian, + ) + if (precondition.done) { + env.showMessage(`\n✅ ${Contracts.issuance.DefaultAllocation.name} already configured\n`) + return + } + + env.showMessage('\n📋 Checking current configuration...\n') + + const GOVERNOR_ROLE = (await readFn(defaultAllocation, { functionName: 'GOVERNOR_ROLE' })) as `0x${string}` + const PAUSE_ROLE = (await readFn(defaultAllocation, { functionName: 'PAUSE_ROLE' })) as `0x${string}` + + const governorHasRole = (await client.readContract({ + address: defaultAllocation.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [GOVERNOR_ROLE, governor as `0x${string}`], + })) as boolean + env.showMessage(` Governor GOVERNOR_ROLE: ${governorHasRole ? '✓' : '✗'}`) + + const pauseGuardianHasRole = (await client.readContract({ + address: defaultAllocation.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [PAUSE_ROLE, pauseGuardian as `0x${string}`], + })) as boolean + env.showMessage(` PauseGuardian PAUSE_ROLE: ${pauseGuardianHasRole ? '✓' : '✗'}`) + + const deployerHasRole = (await client.readContract({ + address: defaultAllocation.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [GOVERNOR_ROLE, deployer as `0x${string}`], + })) as boolean + + const txs: Array<{ to: string; data: `0x${string}`; label: string }> = [] + + if (!governorHasRole) { + txs.push({ + to: defaultAllocation.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [GOVERNOR_ROLE, governor as `0x${string}`], + }), + label: `grantRole(GOVERNOR_ROLE, ${governor})`, + }) + } + + if (!pauseGuardianHasRole) { + txs.push({ + to: defaultAllocation.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [PAUSE_ROLE, pauseGuardian as `0x${string}`], + }), + label: `grantRole(PAUSE_ROLE, ${pauseGuardian})`, + }) + } + + if (!deployerHasRole) { + env.showMessage(`\n ○ Deployer does not have GOVERNOR_ROLE — skipping (governance TX in upgrade step)\n`) + return + } + + if (txs.length === 0) return + + env.showMessage('\n🔨 Executing role grants as deployer...\n') + const txFn = tx(env) + for (const t of txs) { + await txFn({ account: deployer, to: t.to as `0x${string}`, data: t.data }) + env.showMessage(` ✓ ${t.label}`) + } + + env.showMessage(`\n✅ ${Contracts.issuance.DefaultAllocation.name} configuration complete!\n`) +}) diff --git a/packages/deployment/deploy/allocate/default/05_transfer_governance.ts b/packages/deployment/deploy/allocate/default/05_transfer_governance.ts new file mode 100644 index 000000000..af5bcd8e6 --- /dev/null +++ b/packages/deployment/deploy/allocate/default/05_transfer_governance.ts @@ -0,0 +1,51 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + requireContract, + requireDeployer, + transferProxyAdminOwnership, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { checkDeployerRevoked } from '@graphprotocol/deployment/lib/preconditions.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { execute, graph, read } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Transfer DefaultAllocation governance from deployer + * + * - Revoke GOVERNOR_ROLE from deployment account + * - Transfer ProxyAdmin ownership to governor + * + * Role grants happen in 04_configure.ts. + * + * Usage: + * pnpm hardhat deploy --tags DefaultAllocation,transfer --network + */ +export default createActionModule(Contracts.issuance.DefaultAllocation, DeploymentActions.TRANSFER, async (env) => { + const readFn = read(env) + const executeFn = execute(env) + const client = graph.getPublicClient(env) as PublicClient + const deployer = requireDeployer(env) + const da = requireContract(env, Contracts.issuance.DefaultAllocation) + + env.showMessage(`\n========== Transfer ${Contracts.issuance.DefaultAllocation.name} ==========`) + + const precondition = await checkDeployerRevoked(client, da.address, deployer) + if (precondition.done) { + env.showMessage(`✓ Deployer GOVERNOR_ROLE already revoked`) + } else { + const GOVERNOR_ROLE = (await readFn(da, { functionName: 'GOVERNOR_ROLE' })) as `0x${string}` + + env.showMessage(`🔨 Revoking deployer GOVERNOR_ROLE...`) + await executeFn(da, { + account: deployer, + functionName: 'revokeRole', + args: [GOVERNOR_ROLE, deployer], + }) + env.showMessage(` ✓ revokeRole(GOVERNOR_ROLE) executed`) + } + + await transferProxyAdminOwnership(env, Contracts.issuance.DefaultAllocation) + + env.showMessage(`\n✅ ${Contracts.issuance.DefaultAllocation.name} governance transferred!\n`) +}) diff --git a/packages/deployment/deploy/allocate/default/09_end.ts b/packages/deployment/deploy/allocate/default/09_end.ts new file mode 100644 index 000000000..cacd93b61 --- /dev/null +++ b/packages/deployment/deploy/allocate/default/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.issuance.DefaultAllocation) diff --git a/packages/deployment/deploy/allocate/default/10_status.ts b/packages/deployment/deploy/allocate/default/10_status.ts new file mode 100644 index 000000000..012cc8be3 --- /dev/null +++ b/packages/deployment/deploy/allocate/default/10_status.ts @@ -0,0 +1,10 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +/** + * DefaultAllocation status — show detailed state of the default allocation proxy + * + * Usage: + * pnpm hardhat deploy --tags DefaultAllocation --network + */ +export default createStatusModule(Contracts.issuance.DefaultAllocation) diff --git a/packages/deployment/deploy/allocate/direct/01_impl.ts b/packages/deployment/deploy/allocate/direct/01_impl.ts new file mode 100644 index 000000000..6ff6d6a56 --- /dev/null +++ b/packages/deployment/deploy/allocate/direct/01_impl.ts @@ -0,0 +1,74 @@ +import { loadDirectAllocationArtifact } from '@graphprotocol/deployment/lib/artifact-loaders.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { computeArtifactBytecodeHash } from '@graphprotocol/deployment/lib/deploy-implementation.js' +import { buildDeploymentMetadata } from '@graphprotocol/deployment/lib/deployment-metadata.js' +import { ComponentTags, DeploymentActions, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + requireDeployer, + requireGraphToken, + showDeploymentStatus, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { deploy, graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +/** + * Deploy shared DirectAllocation implementation + * + * This implementation is shared by all DirectAllocation proxies + * (DefaultAllocation, ReclaimedRewards). Runs during both deploy AND upgrade + * actions — deploying the implementation is a prerequisite for proxy upgrades. + * + * Rocketh handles idempotency: if bytecode is unchanged, no redeployment occurs. + * + * Usage: + * pnpm hardhat deploy --tags DirectAllocation_Implementation,deploy --network + */ +const func: DeployScriptModule = async (env) => { + // Run for both deploy and upgrade actions + if (shouldSkipAction(DeploymentActions.DEPLOY) && shouldSkipAction(DeploymentActions.UPGRADE)) return + + await syncComponentsFromRegistry(env, [ + Contracts.issuance.DirectAllocation_Implementation, + Contracts.horizon.L2GraphToken, + ]) + + const deployFn = deploy(env) + const deployer = requireDeployer(env) + const graphTokenDep = requireGraphToken(env) + + env.showMessage(`\n📦 Deploying shared ${Contracts.issuance.DirectAllocation_Implementation.name}...`) + + const artifact = loadDirectAllocationArtifact() + const result = await deployFn(Contracts.issuance.DirectAllocation_Implementation.name, { + account: deployer, + artifact, + args: [graphTokenDep.address], + }) + + // Persist to address book — only write metadata on new deployments + // to avoid overwriting stored hash with current artifact when deploy was a no-op + if (result.newlyDeployed) { + const metadata = buildDeploymentMetadata( + result, + computeArtifactBytecodeHash(Contracts.issuance.DirectAllocation_Implementation.artifact!), + ) + if (metadata) { + await graph.updateIssuanceAddressBook(env, { + name: Contracts.issuance.DirectAllocation_Implementation.name, + address: result.address, + deployment: metadata, + }) + } + } + + showDeploymentStatus(env, Contracts.issuance.DirectAllocation_Implementation, result) + + await syncComponentsFromRegistry(env, [Contracts.issuance.DirectAllocation_Implementation]) +} + +func.tags = [ComponentTags.DIRECT_ALLOCATION_IMPL] +func.dependencies = [] +func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) && shouldSkipAction(DeploymentActions.UPGRADE) + +export default func diff --git a/packages/deployment/deploy/common/00_sync.ts b/packages/deployment/deploy/common/00_sync.ts new file mode 100644 index 000000000..de4ff446f --- /dev/null +++ b/packages/deployment/deploy/common/00_sync.ts @@ -0,0 +1,26 @@ +import { SpecialTags } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { runFullSync } from '@graphprotocol/deployment/lib/sync-utils.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +// Sync — full reconciliation between on-chain state and address books. +// +// For every deployable contract in every address book (Horizon, SubgraphService, +// Issuance): +// - Reconcile proxy implementations with on-chain state +// - Import contract addresses into rocketh deployment records +// - Validate prerequisites exist on-chain +// +// This script is the only one tagged with `SpecialTags.SYNC`. It runs when: +// - The user invokes `npx hardhat deploy --tags sync` directly +// - The `deploy:sync` Hardhat task is run (which delegates to the above) +// +// Per-component actions sync the contracts they touch immediately before and +// after their work, so this full sync is no longer required as an automatic +// dependency on every deployment script. + +const func: DeployScriptModule = async (env) => { + await runFullSync(env) +} + +func.tags = [SpecialTags.SYNC] +export default func diff --git a/packages/deployment/deploy/gip/0088/09_end.ts b/packages/deployment/deploy/gip/0088/09_end.ts new file mode 100644 index 000000000..2cb8b7fda --- /dev/null +++ b/packages/deployment/deploy/gip/0088/09_end.ts @@ -0,0 +1,114 @@ +import { PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, REWARDS_MANAGER_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { + addressEquals, + checkIssuanceAllocatorActivation, + isRewardsManagerUpgraded, +} from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getResolvedSettingsForEnv } from '@graphprotocol/deployment/lib/deployment-config.js' +import { DeploymentActions, GoalTags, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContracts } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { DeployScriptModule } from '@rocketh/core/types' +import type { PublicClient } from 'viem' + +/** + * GIP-0088,all — Full GIP-0088 deployment verification + * + * Verifies all non-optional phases are complete: + * - Upgrade: RM upgraded (supports IIssuanceTarget) + * - Eligibility: REO integrated with RM, revertOnIneligible matches config + * - Issuance: IA connected to RM, minter role granted + * + * Does NOT verify optional goals (issuance-close-guard). + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088,all --network + */ +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.ALL)) return + await syncComponentsFromRegistry(env, [ + Contracts.issuance.IssuanceAllocator, + Contracts.horizon.RewardsManager, + Contracts.horizon.L2GraphToken, + Contracts.issuance.RewardsEligibilityOracleA, + ]) + const [issuanceAllocator, rewardsManager, graphToken] = requireContracts(env, [ + Contracts.issuance.IssuanceAllocator, + Contracts.horizon.RewardsManager, + Contracts.horizon.L2GraphToken, + ]) + + const client = graph.getPublicClient(env) as PublicClient + const failures: string[] = [] + + // Verify RM has been upgraded (supports IERC165) + const upgraded = await isRewardsManagerUpgraded(client, rewardsManager.address) + if (!upgraded) { + env.showMessage(`\n❌ ${Contracts.horizon.RewardsManager.name} not upgraded - run GIP-0088:upgrade,upgrade first\n`) + process.exit(1) + } + + // Verify IA activation state (issuance phase) + const activation = await checkIssuanceAllocatorActivation( + client, + issuanceAllocator.address, + rewardsManager.address, + graphToken.address, + ) + + if (!activation.iaIntegrated) failures.push('IA not integrated with RM') + if (!activation.iaMinter) failures.push('IA missing minter role') + + // Verify REO integration (eligibility phase) + const reo = env.getOrNull(Contracts.issuance.RewardsEligibilityOracleA.name) + if (reo) { + const currentOracle = (await client.readContract({ + address: rewardsManager.address as `0x${string}`, + abi: PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + functionName: 'getProviderEligibilityOracle', + })) as string + if (!addressEquals(currentOracle, reo.address)) { + failures.push('REO not integrated with RM') + } + } else { + failures.push('RewardsEligibilityOracleA not deployed') + } + + // Verify revertOnIneligible matches config + const settings = await getResolvedSettingsForEnv(env) + const desiredRevert = settings.rewardsManager.revertOnIneligible + try { + const onChainRevert = (await client.readContract({ + address: rewardsManager.address as `0x${string}`, + abi: REWARDS_MANAGER_ABI, + functionName: 'getRevertOnIneligible', + })) as boolean + if (onChainRevert !== desiredRevert) { + failures.push(`revertOnIneligible mismatch: on-chain=${onChainRevert}, config=${desiredRevert}`) + } + } catch { + failures.push('RM does not support getRevertOnIneligible (not upgraded?)') + } + + if (failures.length > 0) { + env.showMessage(`\n❌ GIP-0088 incomplete:`) + for (const f of failures) env.showMessage(` - ${f}`) + env.showMessage('') + process.exit(1) + } + + env.showMessage(`\n✅ GIP-0088 complete: all contracts deployed, upgraded, and configured\n`) +} + +func.tags = [GoalTags.GIP_0088] +func.dependencies = [ + GoalTags.GIP_0088_UPGRADE, + GoalTags.GIP_0088_ELIGIBILITY_INTEGRATE, + GoalTags.GIP_0088_ISSUANCE_CONNECT, + GoalTags.GIP_0088_ISSUANCE_ALLOCATE, +] +func.skip = async () => shouldSkipAction(DeploymentActions.ALL) + +export default func diff --git a/packages/deployment/deploy/gip/0088/10_status.ts b/packages/deployment/deploy/gip/0088/10_status.ts new file mode 100644 index 000000000..8da7509a8 --- /dev/null +++ b/packages/deployment/deploy/gip/0088/10_status.ts @@ -0,0 +1,179 @@ +import { + IISSUANCE_TARGET_INTERFACE_ID, + ISSUANCE_TARGET_ABI, + PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + SUBGRAPH_SERVICE_CLOSE_GUARD_ABI, +} from '@graphprotocol/deployment/lib/abis.js' +import { getAddressBookForType, getTargetChainIdFromEnv } from '@graphprotocol/deployment/lib/address-book-utils.js' +import { addressEquals, isRewardsManagerUpgraded } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts, type RegistryEntry } from '@graphprotocol/deployment/lib/contract-registry.js' +import { GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { showDetailedComponentStatus, showPendingGovernanceTxs } from '@graphprotocol/deployment/lib/status-detail.js' +import { getContractStatusLine, syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * GIP-0088 Status — Phase-structured deployment state display + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088 --network + */ +export default createStatusModule(GoalTags.GIP_0088, async (env) => { + // Sync the contracts this status touches via env.getOrNull so the read paths + // work without depending on a separate global sync run. + await syncComponentsFromRegistry(env, [ + Contracts.horizon.RewardsManager, + Contracts.horizon.L2GraphToken, + Contracts['subgraph-service'].SubgraphService, + Contracts.issuance.IssuanceAllocator, + Contracts.issuance.RewardsEligibilityOracleA, + Contracts.issuance.RecurringAgreementManager, + ]) + + const client = graph.getPublicClient(env) as PublicClient + const targetChainId = await getTargetChainIdFromEnv(env) + + env.showMessage('\n========== GIP-0088: Full Deployment Status ==========') + + // --- Upgrade phase --- + env.showMessage('\nUpgrade:') + + const upgradeContracts: RegistryEntry[] = [ + Contracts.horizon.RewardsManager, + Contracts.horizon.HorizonStaking, + Contracts['subgraph-service'].SubgraphService, + Contracts['subgraph-service'].DisputeManager, + Contracts.horizon.PaymentsEscrow, + Contracts.horizon.L2Curation, + Contracts.horizon.RecurringCollector, + ] + + const rm = env.getOrNull('RewardsManager') + + for (const contract of upgradeContracts) { + const ab = getAddressBookForType(contract.addressBook, targetChainId) + + const result = await getContractStatusLine(client, contract.addressBook, ab, contract.name) + env.showMessage(` ${result.line}`) + + // RM: semantic check — does the on-chain code support IIssuanceTarget? + if (contract === Contracts.horizon.RewardsManager && result.exists && rm) { + const upgraded = await isRewardsManagerUpgraded(client, rm.address) + env.showMessage(` ${upgraded ? '✓' : '✗'} implements IIssuanceTarget (${IISSUANCE_TARGET_INTERFACE_ID})`) + } + } + + // --- Eligibility phase --- + env.showMessage('\nEligibility:') + await showDetailedComponentStatus(env, Contracts.issuance.RewardsEligibilityOracleA, { showHints: false }) + + // --- Issuance phase --- + env.showMessage('\nIssuance:') + await showDetailedComponentStatus(env, Contracts.issuance.IssuanceAllocator, { showHints: false }) + + const ram = env.getOrNull('RecurringAgreementManager') + if (ram) { + await showDetailedComponentStatus(env, Contracts.issuance.RecurringAgreementManager, { showHints: false }) + } else { + env.showMessage(` ○ RecurringAgreementManager not deployed`) + } + + // --- Activation status --- + env.showMessage('\n--- Activation ---') + + // eligibility-integrate: RM.providerEligibilityOracle == REO_A + if (rm) { + const upgraded = await isRewardsManagerUpgraded(client, rm.address) + if (upgraded) { + const reo = env.getOrNull(Contracts.issuance.RewardsEligibilityOracleA.name) + const currentOracle = (await client.readContract({ + address: rm.address as `0x${string}`, + abi: PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + functionName: 'getProviderEligibilityOracle', + })) as string + + if (reo) { + const integrated = addressEquals(currentOracle, reo.address) + env.showMessage(` ${integrated ? '✓' : '✗'} eligibility-integrate: RM.providerEligibilityOracle == REO_A`) + } else { + env.showMessage(` ○ eligibility-integrate: REO_A not deployed`) + } + + // issuance-connect: RM.issuanceAllocator == IA + minter role + const ia = env.getOrNull('IssuanceAllocator') + if (ia) { + const currentIA = (await client.readContract({ + address: rm.address as `0x${string}`, + abi: ISSUANCE_TARGET_ABI, + functionName: 'getIssuanceAllocator', + })) as string + const iaConnected = addressEquals(currentIA, ia.address) + + const gt = env.getOrNull('L2GraphToken') + let isMinter = false + if (gt) { + const { GRAPH_TOKEN_ABI } = await import('@graphprotocol/deployment/lib/abis.js') + isMinter = (await client.readContract({ + address: gt.address as `0x${string}`, + abi: GRAPH_TOKEN_ABI, + functionName: 'isMinter', + args: [ia.address as `0x${string}`], + })) as boolean + } + + env.showMessage( + ` ${iaConnected && isMinter ? '✓' : '✗'} issuance-connect: RM ↔ IA${!iaConnected ? ' (not connected)' : ''}${!isMinter ? ' (no minter role)' : ''}`, + ) + } else { + env.showMessage(` ○ issuance-connect: IA not deployed`) + } + + // issuance-allocate: IA.getTargetAllocation(RAM) configured + if (ram) { + env.showMessage(` ○ issuance-allocate: check via --tags ${GoalTags.GIP_0088_ISSUANCE_ALLOCATE}`) + } else { + env.showMessage(` ○ issuance-allocate: RAM not deployed`) + } + } else { + env.showMessage(' ○ RM not upgraded (activation blocked)') + } + } else { + env.showMessage(' ○ RM not in address book') + } + + // --- Optional status --- + env.showMessage('\n--- Optional (not planned) ---') + + // issuance-close-guard + const ss = env.getOrNull('SubgraphService') + if (ss) { + try { + const closeGuard = (await client.readContract({ + address: ss.address as `0x${string}`, + abi: SUBGRAPH_SERVICE_CLOSE_GUARD_ABI, + functionName: 'getBlockClosingAllocationWithActiveAgreement', + })) as boolean + env.showMessage(` ${closeGuard ? '✓' : '○'} issuance-close-guard: blockClosingAllocation = ${closeGuard}`) + } catch { + env.showMessage(` ○ issuance-close-guard: SS not upgraded`) + } + } else { + env.showMessage(` ○ issuance-close-guard: SS not deployed`) + } + + // --- Actions --- + env.showMessage('\n--- Actions ---') + env.showMessage(' Deploy & upgrade:') + env.showMessage(' --tags GIP-0088:upgrade,') + env.showMessage(' Activation (after upgrades executed):') + env.showMessage(' --tags GIP-0088:eligibility-integrate') + env.showMessage(' --tags GIP-0088:issuance-connect') + env.showMessage(' --tags GIP-0088:issuance-allocate') + env.showMessage(' Optional:') + env.showMessage(' --tags GIP-0088:issuance-close-guard') + + showPendingGovernanceTxs(env) + env.showMessage('') +}) diff --git a/packages/deployment/deploy/gip/0088/eligibility_integrate.ts b/packages/deployment/deploy/gip/0088/eligibility_integrate.ts new file mode 100644 index 000000000..47bd81f7b --- /dev/null +++ b/packages/deployment/deploy/gip/0088/eligibility_integrate.ts @@ -0,0 +1,74 @@ +import { PROVIDER_ELIGIBILITY_MANAGEMENT_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { applyConfiguration } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { createRMIntegrationCondition } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js' +import { ComponentTags, GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContracts } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +/** + * GIP-0088:eligibility-integrate — Set RewardsEligibilityOracle on RewardsManager + * + * Governance TX: RM.setProviderEligibilityOracle(REO_A) + * + * Skips if oracle already set (any value, not just REO_A) to avoid + * accidentally overriding a live oracle configuration. + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:eligibility-integrate --network + */ +export default createActionModule( + GoalTags.GIP_0088_ELIGIBILITY_INTEGRATE, + async (env) => { + await syncComponentsFromRegistry(env, [ + Contracts.issuance.RewardsEligibilityOracleA, + Contracts.horizon.RewardsManager, + ]) + const [reo, rm] = requireContracts(env, [ + Contracts.issuance.RewardsEligibilityOracleA, + Contracts.horizon.RewardsManager, + ]) + const client = graph.getPublicClient(env) as PublicClient + + // Check if oracle already set — skip if any oracle configured (don't override) + try { + const currentOracle = (await client.readContract({ + address: rm.address as `0x${string}`, + abi: PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + functionName: 'getProviderEligibilityOracle', + })) as string + + if (currentOracle !== ZERO_ADDRESS) { + const isTarget = currentOracle.toLowerCase() === reo.address.toLowerCase() + env.showMessage(`\n ${isTarget ? '✓' : '○'} RM.providerEligibilityOracle already set: ${currentOracle}`) + if (!isTarget) { + env.showMessage(` (not REO_A — skipping to avoid override)`) + } + env.showMessage('') + return + } + } catch { + // Function not available — RM not upgraded, skip + env.showMessage(`\n ○ RM does not support getProviderEligibilityOracle — skipping\n`) + return + } + + const { governor, canSign } = await canSignAsGovernor(env) + + await applyConfiguration(env, client, [createRMIntegrationCondition(reo.address)], { + contractName: `${Contracts.horizon.RewardsManager.name}-REO`, + contractAddress: rm.address, + canExecuteDirectly: canSign, + executor: governor, + }) + }, + { + dependencies: [ComponentTags.REWARDS_MANAGER, ComponentTags.REWARDS_ELIGIBILITY_A], + }, +) diff --git a/packages/deployment/deploy/gip/0088/issuance_allocate.ts b/packages/deployment/deploy/gip/0088/issuance_allocate.ts new file mode 100644 index 000000000..525970477 --- /dev/null +++ b/packages/deployment/deploy/gip/0088/issuance_allocate.ts @@ -0,0 +1,193 @@ +import { ACCESS_CONTROL_ENUMERABLE_ABI, SET_TARGET_ALLOCATION_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js' +import { getResolvedSettingsForEnv } from '@graphprotocol/deployment/lib/deployment-config.js' +import { ComponentTags, GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + createGovernanceTxBuilder, + executeTxBatchDirect, + saveGovernanceTx, +} from '@graphprotocol/deployment/lib/execute-governance.js' +import { formatGRT } from '@graphprotocol/deployment/lib/format.js' +import { requireContracts, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { graph, read, tx } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' +import { encodeFunctionData, keccak256, parseUnits, toHex } from 'viem' + +/** + * GIP-0088:issuance-allocate — Allocate issuance to Recurring Agreement Manager + * + * Calls setTargetAllocation(RAM, allocatorMintingRate, selfMintingRate) so IA + * distributes minted GRT to RAM for agreement-based payments. + * + * Rates are read from config/.json5 (committed per-chain config). + * Skips if rate is 0 (not yet decided). + * + * Idempotent: checks on-chain state, skips if already configured. + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:issuance-allocate --network + */ +export default createActionModule( + GoalTags.GIP_0088_ISSUANCE_ALLOCATE, + async (env) => { + await syncComponentsFromRegistry(env, [ + Contracts.issuance.IssuanceAllocator, + Contracts.issuance.RecurringAgreementManager, + Contracts.horizon.RewardsManager, + ]) + + const client = graph.getPublicClient(env) as PublicClient + const readFn = read(env) + + const iaDep = env.getOrNull(Contracts.issuance.IssuanceAllocator.name) + const ramDep = env.getOrNull(Contracts.issuance.RecurringAgreementManager.name) + if (!iaDep || !ramDep) { + const missing = [!iaDep && 'IssuanceAllocator', !ramDep && 'RecurringAgreementManager'].filter(Boolean) + env.showMessage(`\n ○ Skipping RAM allocation — not deployed: ${missing.join(', ')}\n`) + return + } + const ia = iaDep + const ram = ramDep + + env.showMessage(`\n========== GIP-0088: Issuance Allocate ==========`) + env.showMessage(`IA: ${ia.address}`) + env.showMessage(`RAM: ${ram.address}`) + + // Load resolved settings + const settings = await getResolvedSettingsForEnv(env) + const allocatorMintingRate = parseUnits(settings.issuanceAllocator.ramAllocatorMintingGrtPerBlock, 18) + const selfMintingRate = parseUnits(settings.issuanceAllocator.ramSelfMintingGrtPerBlock, 18) + + if (allocatorMintingRate === 0n && selfMintingRate === 0n) { + env.showMessage('\n⚠️ RAM allocation rates not configured (both 0).') + env.showMessage(' Set ramAllocatorMintingGrtPerBlock in config/.json5') + env.showMessage(' Skipping RAM allocation configuration.\n') + return + } + + // Check current state + env.showMessage('\n📋 Checking current configuration...\n') + env.showMessage( + ` Config: allocatorMintingRate=${formatGRT(allocatorMintingRate)}, selfMintingRate=${formatGRT(selfMintingRate)}`, + ) + + let currentRamAlloc = 0n + let currentRamSelf = 0n + let ramAllocated = false + try { + const allocation = (await readFn(ia, { + functionName: 'getTargetAllocation', + args: [ram.address], + })) as { totalAllocationRate: bigint; allocatorMintingRate: bigint; selfMintingRate: bigint } + currentRamAlloc = allocation.allocatorMintingRate + currentRamSelf = allocation.selfMintingRate + ramAllocated = currentRamAlloc === allocatorMintingRate && currentRamSelf === selfMintingRate + env.showMessage( + ` On-chain: allocator=${formatGRT(currentRamAlloc)}, self=${formatGRT(currentRamSelf)} ${ramAllocated ? '✓' : '✗'}`, + ) + } catch { + env.showMessage(` RAM allocation: ✗ (not configured)`) + } + + if (ramAllocated) { + env.showMessage(`\n✅ RAM allocation already matches config\n`) + return + } + + // The allocator enforces a 100% invariant (sum of all targets == issuancePerBlock). + // RewardsManager was given 100% as self-minting in issuance-connect, so we must + // atomically rebalance: take from RM's self-minting and give to RAM, in the same batch. + const [rewardsManager] = requireContracts(env, [Contracts.horizon.RewardsManager]) + const rmAddress = rewardsManager.address as `0x${string}` + const rmAllocation = (await readFn(ia, { + functionName: 'getTargetAllocation', + args: [rmAddress], + })) as { totalAllocationRate: bigint; allocatorMintingRate: bigint; selfMintingRate: bigint } + env.showMessage( + ` RM on-chain: allocator=${formatGRT(rmAllocation.allocatorMintingRate)}, self=${formatGRT(rmAllocation.selfMintingRate)}`, + ) + + const newRamTotal = allocatorMintingRate + selfMintingRate + const currentRamTotal = currentRamAlloc + currentRamSelf + const delta = newRamTotal - currentRamTotal // signed: >0 RAM grows, <0 RAM shrinks + if (delta > 0n && rmAllocation.selfMintingRate < delta) { + env.showMessage( + `\n❌ Insufficient RM self-minting (${formatGRT(rmAllocation.selfMintingRate)}) to fund RAM increase (${formatGRT(delta)})\n`, + ) + process.exit(1) + } + const newRmSelf = rmAllocation.selfMintingRate - delta + + // Determine executor + const deployer = requireDeployer(env) + const GOVERNOR_ROLE = keccak256(toHex('GOVERNOR_ROLE')) + let deployerIsGovernor = false + try { + deployerIsGovernor = (await client.readContract({ + address: ia.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [GOVERNOR_ROLE, deployer as `0x${string}`], + })) as boolean + } catch { + // Storage not available (stale fork) — fall through to governor path + } + + const setRamData = encodeFunctionData({ + abi: SET_TARGET_ALLOCATION_ABI, + functionName: 'setTargetAllocation', + args: [ram.address as `0x${string}`, allocatorMintingRate, selfMintingRate], + }) + const setRmData = encodeFunctionData({ + abi: SET_TARGET_ALLOCATION_ABI, + functionName: 'setTargetAllocation', + args: [rmAddress, rmAllocation.allocatorMintingRate, newRmSelf], + }) + const ramLabel = `setTargetAllocation(RAM, ${formatGRT(allocatorMintingRate)}, ${formatGRT(selfMintingRate)})` + const rmLabel = `setTargetAllocation(RM, ${formatGRT(rmAllocation.allocatorMintingRate)}, ${formatGRT(newRmSelf)})` + + // Order matters: free budget first, then consume. + // delta > 0 (RAM grows): reduce RM first so default target absorbs the slack. + // delta < 0 (RAM shrinks): reduce RAM first so default target absorbs the slack. + const txs = + delta > 0n + ? [ + { data: setRmData, label: rmLabel }, + { data: setRamData, label: ramLabel }, + ] + : [ + { data: setRamData, label: ramLabel }, + { data: setRmData, label: rmLabel }, + ] + + if (deployerIsGovernor) { + env.showMessage('\n🔨 Executing as deployer...\n') + const txFn = tx(env) + for (const t of txs) { + await txFn({ account: deployer, to: ia.address, data: t.data }) + env.showMessage(` ✓ ${t.label}`) + } + env.showMessage(`\n✅ GIP-0088: Issuance Allocate — RAM allocation configured!\n`) + } else { + const { governor, canSign } = await canSignAsGovernor(env) + + const builder = await createGovernanceTxBuilder(env, `gip-0088-issuance-allocate`) + for (const t of txs) { + builder.addTx({ to: ia.address, value: '0', data: t.data }) + env.showMessage(` + ${t.label}`) + } + + if (canSign) { + env.showMessage('\n🔨 Executing configuration TX batch...\n') + await executeTxBatchDirect(env, builder, governor) + env.showMessage(`\n✅ GIP-0088: Issuance Allocate — RAM allocation configured!\n`) + } else { + saveGovernanceTx(env, builder, `GIP-0088: issuance-allocate`) + } + } + }, + { dependencies: [GoalTags.GIP_0088_ISSUANCE_CONNECT, ComponentTags.RECURRING_AGREEMENT_MANAGER] }, +) diff --git a/packages/deployment/deploy/gip/0088/issuance_close_guard.ts b/packages/deployment/deploy/gip/0088/issuance_close_guard.ts new file mode 100644 index 000000000..55f33040a --- /dev/null +++ b/packages/deployment/deploy/gip/0088/issuance_close_guard.ts @@ -0,0 +1,81 @@ +import { SUBGRAPH_SERVICE_CLOSE_GUARD_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js' +import { ComponentTags, GoalTags, shouldSkipOptionalGoal } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + createGovernanceTxBuilder, + executeTxBatchDirect, + saveGovernanceTx, +} from '@graphprotocol/deployment/lib/execute-governance.js' +import { requireContract } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { DeployScriptModule } from '@rocketh/core/types' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +/** + * GIP-0088:issuance-close-guard — Prevent closing allocations with active agreements + * + * Optional governance TX: SS.setBlockClosingAllocationWithActiveAgreement(true) + * + * Not activated by `all` — requires explicit `--tags GIP-0088:issuance-close-guard`. + * + * Idempotent: reads on-chain state, skips if already enabled. + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:issuance-close-guard --network + */ +const func: DeployScriptModule = async (env) => { + if (shouldSkipOptionalGoal(GoalTags.GIP_0088_ISSUANCE_CLOSE_GUARD)) return + await syncComponentsFromRegistry(env, [Contracts['subgraph-service'].SubgraphService]) + + const client = graph.getPublicClient(env) as PublicClient + const ss = requireContract(env, Contracts['subgraph-service'].SubgraphService) + + env.showMessage(`\n========== GIP-0088: Issuance Close Guard ==========`) + env.showMessage(`${Contracts['subgraph-service'].SubgraphService.name}: ${ss.address}`) + + // Check current state + env.showMessage('\n📋 Checking current configuration...\n') + + const enabled = (await client.readContract({ + address: ss.address as `0x${string}`, + abi: SUBGRAPH_SERVICE_CLOSE_GUARD_ABI, + functionName: 'getBlockClosingAllocationWithActiveAgreement', + })) as boolean + env.showMessage(` blockClosingAllocationWithActiveAgreement: ${enabled ? '✓ true' : '✗ false'}`) + + if (enabled) { + env.showMessage(`\n✅ ${Contracts['subgraph-service'].SubgraphService.name} close guard already enabled\n`) + return + } + + const { governor, canSign } = await canSignAsGovernor(env) + + env.showMessage('\n🔨 Building configuration TX batch...\n') + + const builder = await createGovernanceTxBuilder(env, `gip-0088-issuance-close-guard`) + + const data = encodeFunctionData({ + abi: SUBGRAPH_SERVICE_CLOSE_GUARD_ABI, + functionName: 'setBlockClosingAllocationWithActiveAgreement', + args: [true], + }) + builder.addTx({ to: ss.address, value: '0', data }) + env.showMessage(` + setBlockClosingAllocationWithActiveAgreement(true)`) + + if (canSign) { + env.showMessage('\n🔨 Executing configuration TX batch...\n') + await executeTxBatchDirect(env, builder, governor) + env.showMessage(`\n✅ GIP-0088: allocation close guard enabled\n`) + } else { + saveGovernanceTx(env, builder, `GIP-0088: allocation close guard`) + } +} + +func.tags = [GoalTags.GIP_0088_ISSUANCE_CLOSE_GUARD] +func.dependencies = [ComponentTags.SUBGRAPH_SERVICE] +func.skip = async () => shouldSkipOptionalGoal(GoalTags.GIP_0088_ISSUANCE_CLOSE_GUARD) + +export default func diff --git a/packages/deployment/deploy/gip/0088/issuance_connect.ts b/packages/deployment/deploy/gip/0088/issuance_connect.ts new file mode 100644 index 000000000..30f8c170d --- /dev/null +++ b/packages/deployment/deploy/gip/0088/issuance_connect.ts @@ -0,0 +1,247 @@ +import { + GRAPH_TOKEN_ABI, + ISSUANCE_ALLOCATOR_ABI, + ISSUANCE_TARGET_ABI, + REWARDS_MANAGER_DEPRECATED_ABI, + SET_TARGET_ALLOCATION_ABI, +} from '@graphprotocol/deployment/lib/abis.js' +import { getTargetChainIdFromEnv } from '@graphprotocol/deployment/lib/address-book-utils.js' +import { requireRewardsManagerUpgraded } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js' +import { ComponentTags, GoalTags } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + createGovernanceTxBuilder, + executeTxBatchDirect, + saveGovernanceTx, +} from '@graphprotocol/deployment/lib/execute-governance.js' +import { formatGRT } from '@graphprotocol/deployment/lib/format.js' +import { requireContracts, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +/** + * GIP-0088:issuance-connect — Connect Rewards Manager to Issuance Allocator + * + * - Configure RewardsManager to use IssuanceAllocator + * - Grant minter role to IssuanceAllocator on GraphToken + * + * Idempotent: checks on-chain state, skips if already activated. + * If the provider has access to the governor key, executes directly. + * Otherwise generates governance TX file. + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:issuance-connect --network + */ +export default createActionModule( + GoalTags.GIP_0088_ISSUANCE_CONNECT, + async (env) => { + await syncComponentsFromRegistry(env, [ + Contracts.issuance.IssuanceAllocator, + Contracts.horizon.RewardsManager, + Contracts.horizon.L2GraphToken, + Contracts.issuance.DefaultAllocation, + ]) + + const deployer = requireDeployer(env) + + // Check if the provider can sign as the protocol governor + const { governor, canSign } = await canSignAsGovernor(env) + + const [issuanceAllocator, rewardsManager, graphToken, defaultAllocation] = requireContracts(env, [ + Contracts.issuance.IssuanceAllocator, + Contracts.horizon.RewardsManager, + Contracts.horizon.L2GraphToken, + Contracts.issuance.DefaultAllocation, + ]) + + const iaAddress = issuanceAllocator.address + const rmAddress = rewardsManager.address + const gtAddress = graphToken.address + const daAddress = defaultAllocation.address + + // Create viem client for direct contract calls + const client = graph.getPublicClient(env) as PublicClient + + // Check if RewardsManager supports IIssuanceTarget (has been upgraded) + // Throws error if not upgraded + await requireRewardsManagerUpgraded(client, rmAddress, env) + + const targetChainId = await getTargetChainIdFromEnv(env) + + env.showMessage(`\n========== GIP-0088: Issuance Connect ==========`) + env.showMessage(`Network: ${env.name} (chainId=${targetChainId})`) + env.showMessage(`Deployer: ${deployer}`) + env.showMessage(`Protocol Governor (from Controller): ${governor}`) + env.showMessage(`${Contracts.issuance.IssuanceAllocator.name}: ${iaAddress}`) + env.showMessage(`${Contracts.horizon.RewardsManager.name}: ${rmAddress}`) + env.showMessage(`${Contracts.horizon.L2GraphToken.name}: ${gtAddress}\n`) + + // Check current state + env.showMessage('📋 Checking current activation state...\n') + + const checks = { + iaIntegrated: false, + iaMinter: false, + } + + // Check RM.getIssuanceAllocator() == IA + const currentIA = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: ISSUANCE_TARGET_ABI, + functionName: 'getIssuanceAllocator', + })) as string + checks.iaIntegrated = currentIA.toLowerCase() === iaAddress.toLowerCase() + env.showMessage(` IA integrated: ${checks.iaIntegrated ? '✓' : '✗'} (current: ${currentIA})`) + + // Check GraphToken.isMinter(IA) + checks.iaMinter = (await client.readContract({ + address: gtAddress as `0x${string}`, + abi: GRAPH_TOKEN_ABI, + functionName: 'isMinter', + args: [iaAddress as `0x${string}`], + })) as boolean + env.showMessage(` IA minter: ${checks.iaMinter ? '✓' : '✗'}`) + + // Check RM allocation on IA + let rmAllocationOk = false + try { + const rmAllocation = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getTargetAllocation', + args: [rmAddress as `0x${string}`], + })) as { totalAllocationRate: bigint; allocatorMintingRate: bigint; selfMintingRate: bigint } + const iaRate = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getIssuancePerBlock', + })) as bigint + rmAllocationOk = + rmAllocation.allocatorMintingRate === 0n && rmAllocation.selfMintingRate === iaRate && iaRate > 0n + env.showMessage( + ` RM allocation: ${rmAllocationOk ? '✓' : '✗'} (self: ${formatGRT(rmAllocation.selfMintingRate)}, allocator: ${formatGRT(rmAllocation.allocatorMintingRate)})`, + ) + } catch { + env.showMessage(` RM allocation: ✗ (not set)`) + } + + // All checks passed? + if (checks.iaIntegrated && checks.iaMinter && rmAllocationOk) { + env.showMessage(`\n✅ RM already connected to IssuanceAllocator\n`) + return + } + + // Migration invariant: IA rate must match RM rate before connection + if (!checks.iaIntegrated) { + const rmRate = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: REWARDS_MANAGER_DEPRECATED_ABI, + functionName: 'issuancePerBlock', + })) as bigint + + const iaRate = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getIssuancePerBlock', + })) as bigint + + if (iaRate !== rmRate) { + env.showMessage( + `\n❌ Migration invariant failed: IA.issuancePerBlock (${formatGRT(iaRate)}) != RM.issuancePerBlock (${formatGRT(rmRate)})`, + ) + env.showMessage(` IA must have the same overall rate as RM before connection.\n`) + process.exit(1) + } + + env.showMessage(` Migration invariant: ✓ IA rate == RM rate (${formatGRT(iaRate)})`) + } + + // Build TX batch — order: + // 1. IA.setTargetAllocation(RM, 0, rate) — register RM in IA first + // 2. RM.setIssuanceAllocator(IA) — flip RM to read from a fully-configured IA + // 3. GraphToken.addMinter(IA) — grant IA the minter role + // 4. IA.setDefaultTarget(DA) — install safety-net default + // Conceptually: configure IA's view of RM before RM starts reading from IA. Atomic + // within the batch either way, but this avoids a transient where RM is wired to an + // IA that has no allocation entry for it. + env.showMessage('\n🔨 Building activation TX batch...\n') + + const builder = await createGovernanceTxBuilder(env, `gip-0088-issuance-connect`) + + // 1. IA.setTargetAllocation(RM, 0, rate) — RM as 100% self-minting target + if (!rmAllocationOk) { + const iaRate = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getIssuancePerBlock', + })) as bigint + const data = encodeFunctionData({ + abi: SET_TARGET_ALLOCATION_ABI, + functionName: 'setTargetAllocation', + args: [rmAddress as `0x${string}`, 0n, iaRate], + }) + builder.addTx({ to: iaAddress, value: '0', data }) + env.showMessage(` + IA.setTargetAllocation(RM, 0, ${formatGRT(iaRate)})`) + } + + // 2. RM.setIssuanceAllocator(IA) — RM accepts IA as its allocator + if (!checks.iaIntegrated) { + const data = encodeFunctionData({ + abi: ISSUANCE_TARGET_ABI, + functionName: 'setIssuanceAllocator', + args: [iaAddress as `0x${string}`], + }) + builder.addTx({ to: rmAddress, value: '0', data }) + env.showMessage(` + RewardsManager.setIssuanceAllocator(${iaAddress})`) + } + + // 3. GraphToken.addMinter(IA) — IA needs minter role for allocator-minting + if (!checks.iaMinter) { + const data = encodeFunctionData({ + abi: GRAPH_TOKEN_ABI, + functionName: 'addMinter', + args: [iaAddress as `0x${string}`], + }) + builder.addTx({ to: gtAddress, value: '0', data }) + env.showMessage(` + GraphToken.addMinter(${iaAddress})`) + } + + // 4. IA.setDefaultTarget(DA) — safety net for unallocated issuance + let defaultTargetOk = false + try { + const currentDefault = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getTargetAt', + args: [0n], + })) as string + defaultTargetOk = currentDefault.toLowerCase() === daAddress.toLowerCase() + } catch { + // No targets yet + } + env.showMessage(` DA default target: ${defaultTargetOk ? '✓' : '✗'}`) + + if (!defaultTargetOk) { + const data = encodeFunctionData({ + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'setDefaultTarget', + args: [daAddress as `0x${string}`], + }) + builder.addTx({ to: iaAddress, value: '0', data }) + env.showMessage(` + IA.setDefaultTarget(${daAddress})`) + } + + if (canSign) { + env.showMessage('\n🔨 Executing activation TX batch...\n') + await executeTxBatchDirect(env, builder, governor) + env.showMessage(`\n✅ GIP-0088: Issuance Connect — RM connected to IssuanceAllocator!\n`) + } else { + saveGovernanceTx(env, builder, `GIP-0088: issuance-connect`) + } + }, + { dependencies: [ComponentTags.ISSUANCE_ALLOCATOR, ComponentTags.DEFAULT_ALLOCATION, ComponentTags.REWARDS_MANAGER] }, +) diff --git a/packages/deployment/deploy/gip/0088/upgrade/01_deploy.ts b/packages/deployment/deploy/gip/0088/upgrade/01_deploy.ts new file mode 100644 index 000000000..010564515 --- /dev/null +++ b/packages/deployment/deploy/gip/0088/upgrade/01_deploy.ts @@ -0,0 +1,47 @@ +import { + ComponentTags, + DeploymentActions, + GoalTags, + shouldSkipAction, +} from '@graphprotocol/deployment/lib/deployment-tags.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +/** + * GIP-0088:upgrade — Deploy ALL contracts and implementations + * + * Deploys everything required for GIP-0088 in one step: + * - New implementations for existing proxies (RM, HS, SS, DM, PE, L2Curation) + * - New contracts (RC, IA, DA, Reclaim, RAM, REO A/B) + * + * The eligibility and issuance phases start from configure, not deploy. + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:upgrade,deploy --network + */ +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.DEPLOY)) return + env.showMessage('\n✓ GIP-0088 upgrade: all contracts and implementations deployed\n') +} + +func.tags = [GoalTags.GIP_0088_UPGRADE] +func.dependencies = [ + // New implementations for existing proxies + ComponentTags.REWARDS_MANAGER, + ComponentTags.HORIZON_STAKING, + ComponentTags.SUBGRAPH_SERVICE, + ComponentTags.DISPUTE_MANAGER, + ComponentTags.PAYMENTS_ESCROW, + ComponentTags.L2_CURATION, + // New contracts (proxy + implementation) + ComponentTags.RECURRING_COLLECTOR, + ComponentTags.ISSUANCE_ALLOCATOR, + ComponentTags.DIRECT_ALLOCATION_IMPL, + ComponentTags.DEFAULT_ALLOCATION, + ComponentTags.REWARDS_RECLAIM, + ComponentTags.RECURRING_AGREEMENT_MANAGER, + ComponentTags.REWARDS_ELIGIBILITY_A, + ComponentTags.REWARDS_ELIGIBILITY_B, +] +func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) + +export default func diff --git a/packages/deployment/deploy/gip/0088/upgrade/02_configure.ts b/packages/deployment/deploy/gip/0088/upgrade/02_configure.ts new file mode 100644 index 000000000..94e431e52 --- /dev/null +++ b/packages/deployment/deploy/gip/0088/upgrade/02_configure.ts @@ -0,0 +1,40 @@ +import { + ComponentTags, + DeploymentActions, + GoalTags, + shouldSkipAction, +} from '@graphprotocol/deployment/lib/deployment-tags.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +/** + * GIP-0088:upgrade — Configure all contracts (deployer-only) + * + * Checkpoint: component 04_configure scripts do the work. + * + * Only items the deployer can perform run here. Items that require GOVERNOR_ROLE + * on contracts the deployer doesn't yet control (e.g. RC.setPauseGuardian, RM + * integration with Reclaim, deferred role grants on new contracts) are bundled + * into the upgrade governance batch by `04_upgrade.ts`. RC's `04_configure` + * is read-only — it just reports state. + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:upgrade,configure --network + */ +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.CONFIGURE)) return + env.showMessage('\n✓ GIP-0088 upgrade: contracts configured\n') +} + +func.tags = [GoalTags.GIP_0088_UPGRADE] +func.dependencies = [ + ComponentTags.RECURRING_COLLECTOR, + ComponentTags.ISSUANCE_ALLOCATOR, + ComponentTags.DEFAULT_ALLOCATION, + ComponentTags.REWARDS_RECLAIM, + ComponentTags.RECURRING_AGREEMENT_MANAGER, + ComponentTags.REWARDS_ELIGIBILITY_A, + ComponentTags.REWARDS_ELIGIBILITY_B, +] +func.skip = async () => shouldSkipAction(DeploymentActions.CONFIGURE) + +export default func diff --git a/packages/deployment/deploy/gip/0088/upgrade/03_transfer.ts b/packages/deployment/deploy/gip/0088/upgrade/03_transfer.ts new file mode 100644 index 000000000..272aa8f8c --- /dev/null +++ b/packages/deployment/deploy/gip/0088/upgrade/03_transfer.ts @@ -0,0 +1,39 @@ +import { + ComponentTags, + DeploymentActions, + GoalTags, + shouldSkipAction, +} from '@graphprotocol/deployment/lib/deployment-tags.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +/** + * GIP-0088:upgrade — Transfer governance of all new contracts to protocol governor + * + * Checkpoint: component transfer scripts do the work. + * Covers all new contracts that were deployed with deployer as governor. + * + * Must run AFTER configure (deployer needs GOVERNOR_ROLE to configure) + * and BEFORE upgrade (governance must own proxies before upgrade TXs). + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:upgrade,transfer --network + */ +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.TRANSFER)) return + env.showMessage('\n✓ GIP-0088 upgrade: governance transferred\n') +} + +func.tags = [GoalTags.GIP_0088_UPGRADE] +func.dependencies = [ + ComponentTags.RECURRING_COLLECTOR, + ComponentTags.ISSUANCE_ALLOCATOR, + ComponentTags.DEFAULT_ALLOCATION, + ComponentTags.RECURRING_AGREEMENT_MANAGER, + ComponentTags.REWARDS_RECLAIM, + ComponentTags.REWARDS_ELIGIBILITY_A, + ComponentTags.REWARDS_ELIGIBILITY_B, + ComponentTags.REWARDS_ELIGIBILITY_MOCK, +] +func.skip = async () => shouldSkipAction(DeploymentActions.TRANSFER) + +export default func diff --git a/packages/deployment/deploy/gip/0088/upgrade/04_upgrade.ts b/packages/deployment/deploy/gip/0088/upgrade/04_upgrade.ts new file mode 100644 index 000000000..cdec30636 --- /dev/null +++ b/packages/deployment/deploy/gip/0088/upgrade/04_upgrade.ts @@ -0,0 +1,447 @@ +import { + ACCESS_CONTROL_ENUMERABLE_ABI, + ISSUANCE_ALLOCATOR_ABI, + ISSUANCE_TARGET_ABI, + RECURRING_COLLECTOR_PAUSE_ABI, + REWARDS_MANAGER_ABI, + REWARDS_MANAGER_DEPRECATED_ABI, +} from '@graphprotocol/deployment/lib/abis.js' +import { getAddressBookForType, getTargetChainIdFromEnv } from '@graphprotocol/deployment/lib/address-book-utils.js' +import { checkConfigurationStatus } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { getREOConditions } from '@graphprotocol/deployment/lib/contract-checks.js' +import { + type AddressBookType, + CONTRACT_REGISTRY, + type ContractMetadata, + Contracts, +} from '@graphprotocol/deployment/lib/contract-registry.js' +import { canSignAsGovernor, getPauseGuardian } from '@graphprotocol/deployment/lib/controller-utils.js' +import { getResolvedSettingsForEnv, type ResolvedSettings } from '@graphprotocol/deployment/lib/deployment-config.js' +import { DeploymentActions, GoalTags, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + createGovernanceTxBuilder, + executeTxBatchDirect, + saveGovernanceTx, +} from '@graphprotocol/deployment/lib/execute-governance.js' +import { formatGRT } from '@graphprotocol/deployment/lib/format.js' +import { + checkDefaultAllocationConfigured, + checkIAConfigured, + checkRAMConfigured, + checkReclaimRMIntegration, + checkReclaimRoles, + checkRMRevertOnIneligible, +} from '@graphprotocol/deployment/lib/preconditions.js' +import { runFullSync } from '@graphprotocol/deployment/lib/sync-utils.js' +import type { TxBuilder } from '@graphprotocol/deployment/lib/tx-builder.js' +import { buildUpgradeTxs } from '@graphprotocol/deployment/lib/upgrade-implementation.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { DeployScriptModule, Environment } from '@rocketh/core/types' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +/** + * GIP-0088:upgrade — Build the governance batch + * + * Single goal: assemble one TX batch that advances the deployment past the + * governance boundary. The batch contains three groups, each of which skips + * items already on-chain: + * + * 1. Proxy upgrades — every deployable proxy with a pendingImplementation + * 2. Existing-contract config — RC.setPauseGuardian, RM.setDefaultReclaimAddress + * 3. Deferred new-contract config — IA/DA/RAM/Reclaim/REO role grants and + * params that the deployer couldn't perform (no GOVERNOR_ROLE) or that + * depend on RM being upgraded + * + * Each helper takes the builder, adds zero or more TXs, and returns the count + * it added. The orchestrator just sums them, prints the result, and either + * executes or saves the batch. + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:upgrade,upgrade --network + * pnpm hardhat deploy:execute-governance --network + */ +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.UPGRADE)) return + + // The orchestration batch reads every deployable contract across all three + // address books, so we need a full sync first rather than a per-component one. + await runFullSync(env) + + const targetChainId = await getTargetChainIdFromEnv(env) + const { governor, canSign } = await canSignAsGovernor(env) + const pauseGuardian = await getPauseGuardian(env) + const client = graph.getPublicClient(env) as PublicClient + + env.showMessage('\n========== GIP-0088 Upgrade: Proxy Upgrades ==========\n') + + const builder = await createGovernanceTxBuilder(env, 'gip-0088-upgrades', { + name: 'GIP-0088 Proxy Upgrades', + description: 'Upgrade all proxy contracts with pending implementations', + }) + + const proxyCount = await collectProxyUpgrades(env, builder, targetChainId) + + const settings = await getResolvedSettingsForEnv(env) + + env.showMessage('\nOutstanding configuration:') + const existingCount = await collectExistingContractConfig(env, builder, client, pauseGuardian, settings) + const newCount = await collectDeferredNewContractConfig(env, builder, client, targetChainId, governor, pauseGuardian) + + const total = proxyCount + existingCount + newCount + if (total === 0) { + env.showMessage(' No pending upgrades found\n') + return + } + + if (canSign) { + env.showMessage('\n🔨 Executing upgrade TX batch...\n') + await executeTxBatchDirect(env, builder, governor) + env.showMessage('\n✅ GIP-0088 Upgrade: All proxy upgrades executed\n') + } else { + saveGovernanceTx(env, builder, 'GIP-0088 Proxy Upgrades') + } +} + +func.tags = [GoalTags.GIP_0088_UPGRADE] +func.skip = async () => shouldSkipAction(DeploymentActions.UPGRADE) + +export default func + +// ============================================================================ +// Group 1 — Proxy upgrades +// ============================================================================ + +/** + * Iterate every deployable proxy in the registry. For each one with a + * pendingImplementation in its address book, add the proxy upgrade TX. + */ +async function collectProxyUpgrades(env: Environment, builder: TxBuilder, targetChainId: number): Promise { + let added = 0 + const addressBooks: AddressBookType[] = ['horizon', 'subgraph-service', 'issuance'] + for (const abType of addressBooks) { + const bookRegistry = CONTRACT_REGISTRY[abType] + const ab = getAddressBookForType(abType, targetChainId) + + for (const [name, metadata] of Object.entries(bookRegistry)) { + const meta = metadata as ContractMetadata + if (!meta.deployable || !meta.proxyType) continue + if (!ab.entryExists(name)) continue + const entry = ab.getEntry(name) + + // Skip contracts with no pending implementation unless they have a + // shared implementation that might have changed (auto-detected by buildUpgradeTxs) + if (!entry?.pendingImplementation?.address && !meta.sharedImplementation) continue + + // Derive implementationName from sharedImplementation (e.g. 'DirectAllocation_Implementation' → 'DirectAllocation') + const implementationName = meta.sharedImplementation?.replace(/_Implementation$/, '') + + const result = await buildUpgradeTxs( + env, + { + contractName: name, + proxyType: meta.proxyType, + proxyAdminName: meta.proxyAdminName, + addressBook: abType, + implementationName, + }, + builder, + ) + if (result.upgraded) added++ + } + } + return added +} + +// ============================================================================ +// Group 2 — Existing contract config (RC, RM) +// ============================================================================ + +/** + * Bundle the few governance-only configure items on contracts that already + * existed before this deployment (typically the deployer does not hold + * GOVERNOR_ROLE on them — true on networks where RM was deployed by separate + * horizon-Ignition infrastructure; the dynamic role check is the source of truth): + * + * - RC.setPauseGuardian + * - RM.setDefaultReclaimAddress (only when RM has been upgraded) + * - RM.setRevertOnIneligible (driven by config; only when RM has been upgraded) + */ +async function collectExistingContractConfig( + env: Environment, + builder: TxBuilder, + client: PublicClient, + pauseGuardian: string, + settings: ResolvedSettings, +): Promise { + let added = 0 + + // RC.setPauseGuardian + const rc = env.getOrNull(Contracts.horizon.RecurringCollector.name) + if (rc) { + const isGuardian = (await client.readContract({ + address: rc.address as `0x${string}`, + abi: RECURRING_COLLECTOR_PAUSE_ABI, + functionName: 'pauseGuardians', + args: [pauseGuardian as `0x${string}`], + })) as boolean + if (!isGuardian) { + builder.addTx({ + to: rc.address, + value: '0', + data: encodeFunctionData({ + abi: RECURRING_COLLECTOR_PAUSE_ABI, + functionName: 'setPauseGuardian', + args: [pauseGuardian as `0x${string}`, true], + }), + }) + env.showMessage(` + ${Contracts.horizon.RecurringCollector.name}.setPauseGuardian(${pauseGuardian})`) + added++ + } + } + + // RM.setDefaultReclaimAddress — only after RM upgrade lands in the same batch + const reclaim = env.getOrNull(Contracts.issuance.ReclaimedRewards.name) + const rm = env.getOrNull(Contracts.horizon.RewardsManager.name) + if (reclaim && rm) { + const reclaimRMCheck = await checkReclaimRMIntegration(client, rm.address, reclaim.address) + if (!reclaimRMCheck.done && reclaimRMCheck.reason !== 'RM not upgraded') { + builder.addTx({ + to: rm.address, + value: '0', + data: encodeFunctionData({ + abi: REWARDS_MANAGER_ABI, + functionName: 'setDefaultReclaimAddress', + args: [reclaim.address as `0x${string}`], + }), + }) + env.showMessage(` + ${Contracts.horizon.RewardsManager.name}.setDefaultReclaimAddress(${reclaim.address})`) + added++ + } + } + + // RM.setRevertOnIneligible — driven by config; only after RM upgrade lands + if (rm) { + const desiredRevert = settings.rewardsManager.revertOnIneligible + const revertCheck = await checkRMRevertOnIneligible(client, rm.address, desiredRevert) + if (!revertCheck.done && revertCheck.reason !== 'RM not upgraded') { + builder.addTx({ + to: rm.address, + value: '0', + data: encodeFunctionData({ + abi: REWARDS_MANAGER_ABI, + functionName: 'setRevertOnIneligible', + args: [desiredRevert], + }), + }) + env.showMessage(` + ${Contracts.horizon.RewardsManager.name}.setRevertOnIneligible(${desiredRevert})`) + added++ + } + } + + return added +} + +// ============================================================================ +// Group 3 — Deferred new-contract config (IA, DA, RAM, Reclaim, REO A/B) +// ============================================================================ + +/** + * Bundle the configure items on new contracts that the deployer couldn't + * perform during `02_configure` because it lacks `GOVERNOR_ROLE` on the + * proxy (typical when forking an existing deployment whose proxies were + * already transferred). + */ +async function collectDeferredNewContractConfig( + env: Environment, + builder: TxBuilder, + client: PublicClient, + targetChainId: number, + governor: string, + pauseGuardian: string, +): Promise { + const grantHelper = createRoleGrantHelper(env, builder, client) + let added = 0 + + // IA: rate + roles + const ia = env.getOrNull(Contracts.issuance.IssuanceAllocator.name) + const rm = env.getOrNull(Contracts.horizon.RewardsManager.name) + if (ia && rm) { + const iaCheck = await checkIAConfigured(client, ia.address, rm.address, governor, pauseGuardian) + if (!iaCheck.done && iaCheck.reason !== 'RM.issuancePerBlock is 0') { + const rmRate = (await client.readContract({ + address: rm.address as `0x${string}`, + abi: REWARDS_MANAGER_DEPRECATED_ABI, + functionName: 'issuancePerBlock', + })) as bigint + const iaRate = (await client.readContract({ + address: ia.address as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getIssuancePerBlock', + })) as bigint + // The outer iaCheck already returns when RM rate is 0, so rmRate > 0n here. + if (iaRate !== rmRate) { + builder.addTx({ + to: ia.address, + value: '0', + data: encodeFunctionData({ + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'setIssuancePerBlock', + args: [rmRate], + }), + }) + env.showMessage(` + IA.setIssuancePerBlock(${formatGRT(rmRate)})`) + added++ + } + added += await grantHelper(ia.address, 'IA', 'GOVERNOR_ROLE', governor, 'governor') + added += await grantHelper(ia.address, 'IA', 'PAUSE_ROLE', pauseGuardian, 'pauseGuardian') + } + } + + // DA: roles + const da = env.getOrNull(Contracts.issuance.DefaultAllocation.name) + if (da) { + const daCheck = await checkDefaultAllocationConfigured(client, da.address, governor, pauseGuardian) + if (!daCheck.done) { + added += await grantHelper(da.address, 'DA', 'GOVERNOR_ROLE', governor, 'governor') + added += await grantHelper(da.address, 'DA', 'PAUSE_ROLE', pauseGuardian, 'pauseGuardian') + } + } + + // RAM: roles + setIssuanceAllocator + const ram = env.getOrNull(Contracts.issuance.RecurringAgreementManager.name) + const rcDep = env.getOrNull(Contracts.horizon.RecurringCollector.name) + const ss = env.getOrNull(Contracts['subgraph-service'].SubgraphService.name) + if (ram && rcDep && ss) { + const ramCheck = await checkRAMConfigured( + client, + ram.address, + rcDep.address, + ss.address, + ia?.address ?? '', + governor, + pauseGuardian, + ) + if (!ramCheck.done) { + added += await grantHelper(ram.address, 'RAM', 'COLLECTOR_ROLE', rcDep.address, 'RC') + added += await grantHelper(ram.address, 'RAM', 'DATA_SERVICE_ROLE', ss.address, 'SS') + added += await grantHelper(ram.address, 'RAM', 'GOVERNOR_ROLE', governor, 'governor') + added += await grantHelper(ram.address, 'RAM', 'PAUSE_ROLE', pauseGuardian, 'pauseGuardian') + if (ia) { + try { + const currentIA = (await client.readContract({ + address: ram.address as `0x${string}`, + abi: ISSUANCE_TARGET_ABI, + functionName: 'getIssuanceAllocator', + })) as string + if (currentIA.toLowerCase() !== ia.address.toLowerCase()) { + builder.addTx({ + to: ram.address, + value: '0', + data: encodeFunctionData({ + abi: ISSUANCE_TARGET_ABI, + functionName: 'setIssuanceAllocator', + args: [ia.address as `0x${string}`], + }), + }) + env.showMessage(` + RAM.setIssuanceAllocator(${ia.address})`) + added++ + } + } catch { + /* getter not available */ + } + } + } + } + + // Reclaim: roles only — RM integration is handled by collectExistingContractConfig + const reclaim = env.getOrNull(Contracts.issuance.ReclaimedRewards.name) + if (reclaim) { + const reclaimRoles = await checkReclaimRoles(client, reclaim.address, governor, pauseGuardian) + if (!reclaimRoles.done) { + added += await grantHelper(reclaim.address, 'Reclaim', 'GOVERNOR_ROLE', governor, 'governor') + added += await grantHelper(reclaim.address, 'Reclaim', 'PAUSE_ROLE', pauseGuardian, 'pauseGuardian') + } + } + + // REO A/B: params + roles. Driven by the same condition list as `04_configure`. + const issuanceBook = graph.getIssuanceAddressBook(targetChainId) + if (issuanceBook.entryExists('NetworkOperator')) { + const reoConditions = await getREOConditions(env) + for (const [label, entry] of [ + ['REO-A', Contracts.issuance.RewardsEligibilityOracleA], + ['REO-B', Contracts.issuance.RewardsEligibilityOracleB], + ] as const) { + const reoDep = env.getOrNull(entry.name) + if (!reoDep) continue + const reoConfig = await checkConfigurationStatus(client, reoDep.address, reoConditions) + if (reoConfig.allOk) continue + for (let i = 0; i < reoConditions.length; i++) { + if (reoConfig.conditions[i].ok) continue + const cond = reoConditions[i] + if (cond.type === 'role') { + added += await grantHelper(reoDep.address, label, cond.roleGetter, cond.targetAccount, cond.description) + } else { + builder.addTx({ + to: reoDep.address, + value: '0', + data: encodeFunctionData({ + abi: cond.abi as readonly unknown[], + functionName: cond.setter, + args: [cond.target], + }), + }) + env.showMessage(` + ${label}.${cond.setter}(${cond.target})`) + added++ + } + } + } + } + + return added +} + +/** + * Returns a closure that, when called, adds a `grantRole` TX if the role is + * not already held. Returns 1 if a TX was added, 0 otherwise. + */ +function createRoleGrantHelper(env: Environment, builder: TxBuilder, client: PublicClient) { + return async function addRoleGrantIfNeeded( + contractAddr: string, + contractName: string, + roleName: string, + account: string, + accountLabel: string, + ): Promise { + try { + const role = (await client.readContract({ + address: contractAddr as `0x${string}`, + abi: [ + { inputs: [], name: roleName, outputs: [{ type: 'bytes32' }], stateMutability: 'view', type: 'function' }, + ], + functionName: roleName, + })) as `0x${string}` + const has = (await client.readContract({ + address: contractAddr as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [role, account as `0x${string}`], + })) as boolean + if (has) return 0 + builder.addTx({ + to: contractAddr, + value: '0', + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [role, account as `0x${string}`], + }), + }) + env.showMessage(` + ${contractName}.grantRole(${roleName}, ${accountLabel})`) + return 1 + } catch { + /* role getter not available — skip */ + return 0 + } + } +} diff --git a/packages/deployment/deploy/gip/0088/upgrade/10_status.ts b/packages/deployment/deploy/gip/0088/upgrade/10_status.ts new file mode 100644 index 000000000..fdf49394d --- /dev/null +++ b/packages/deployment/deploy/gip/0088/upgrade/10_status.ts @@ -0,0 +1,331 @@ +import { IISSUANCE_TARGET_INTERFACE_ID } from '@graphprotocol/deployment/lib/abis.js' +import { getAddressBookForType, getTargetChainIdFromEnv } from '@graphprotocol/deployment/lib/address-book-utils.js' +import { checkConfigurationStatus } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { + getREOConditions, + getREOTransferGovernanceConditions, + isRewardsManagerUpgraded, +} from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts, type RegistryEntry } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getGovernor, getPauseGuardian } from '@graphprotocol/deployment/lib/controller-utils.js' +import { getResolvedSettingsForEnv } from '@graphprotocol/deployment/lib/deployment-config.js' +import { ComponentTags, GoalTags, noTagsRequested } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { getDeployer, getProxyAdminAddress } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { + checkDefaultAllocationConfigured, + checkDeployerRevoked, + checkIAConfigured, + checkProxyAdminTransferred, + checkRAMConfigured, + checkReclaimRMIntegration, + checkReclaimRoles, + checkRMRevertOnIneligible, +} from '@graphprotocol/deployment/lib/preconditions.js' +import { showDetailedComponentStatus, showPendingGovernanceTxs } from '@graphprotocol/deployment/lib/status-detail.js' +import { checkAllProxyStates, getContractStatusLine, runFullSync } from '@graphprotocol/deployment/lib/sync-utils.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { DeployScriptModule } from '@rocketh/core/types' +import type { PublicClient } from 'viem' + +/** + * GIP-0088:upgrade status — full deployment state with next-step guidance + * + * Usage: + * pnpm hardhat deploy --tags GIP-0088:upgrade --network + */ +const func: DeployScriptModule = async (env) => { + if (noTagsRequested()) return + + // The upgrade status reads every contract in every address book — easier to + // run a full sync than to enumerate them. + await runFullSync(env) + + const client = graph.getPublicClient(env) as PublicClient + const targetChainId = await getTargetChainIdFromEnv(env) + + env.showMessage('\n========== GIP-0088 Upgrade ==========') + + // --- Proxy upgrades --- + env.showMessage('\nProxy upgrades:') + + const upgradeContracts: RegistryEntry[] = [ + Contracts.horizon.RewardsManager, + Contracts.horizon.HorizonStaking, + Contracts['subgraph-service'].SubgraphService, + Contracts['subgraph-service'].DisputeManager, + Contracts.horizon.PaymentsEscrow, + Contracts.horizon.L2Curation, + ] + + const rm = env.getOrNull('RewardsManager') + + for (const contract of upgradeContracts) { + const ab = getAddressBookForType(contract.addressBook, targetChainId) + + const result = await getContractStatusLine(client, contract.addressBook, ab, contract.name) + env.showMessage(` ${result.line}`) + + if (contract === Contracts.horizon.RewardsManager && result.exists && rm) { + const upgraded = await isRewardsManagerUpgraded(client, rm.address) + env.showMessage(` ${upgraded ? '✓' : '✗'} implements IIssuanceTarget (${IISSUANCE_TARGET_INTERFACE_ID})`) + } + } + + const { anyCodeChanged, anyPending } = checkAllProxyStates(targetChainId) + + // --- New contracts --- + env.showMessage('\nNew contracts:') + await showDetailedComponentStatus(env, Contracts.horizon.RecurringCollector, { showHints: false }) + await showDetailedComponentStatus(env, Contracts.issuance.IssuanceAllocator, { showHints: false }) + await showDetailedComponentStatus(env, Contracts.issuance.DefaultAllocation, { showHints: false }) + await showDetailedComponentStatus(env, Contracts.issuance.RecurringAgreementManager, { showHints: false }) + await showDetailedComponentStatus(env, Contracts.issuance.ReclaimedRewards, { showHints: false }) + await showDetailedComponentStatus(env, Contracts.issuance.RewardsEligibilityOracleA, { showHints: false }) + + // --- Next step --- + // Uses the same precondition checks as the action scripts (shared code, not copies) + const ia = env.getOrNull('IssuanceAllocator') + const da = env.getOrNull('DefaultAllocation') + const reoA = env.getOrNull('RewardsEligibilityOracleA') + const reoB = env.getOrNull('RewardsEligibilityOracleB') + const ram = env.getOrNull('RecurringAgreementManager') + const reclaim = env.getOrNull('ReclaimedRewards') + const rc = env.getOrNull('RecurringCollector') + const ss = env.getOrNull('SubgraphService') + + const anyNewContractMissing = !ia || !da || !reoA || !reoB || !ram || !reclaim + + if (anyNewContractMissing || !rm || (anyCodeChanged && !anyPending)) { + env.showMessage(`\n → Next: --tags GIP-0088:upgrade,deploy`) + const missing = [ + !ia && 'IssuanceAllocator', + !da && 'DefaultAllocation', + !reoA && 'REO-A', + !reoB && 'REO-B', + !ram && 'RAM', + !reclaim && 'Reclaim', + !rm && 'RM', + ].filter(Boolean) + if (missing.length > 0) env.showMessage(` Missing: ${missing.join(', ')}`) + if (anyCodeChanged && !anyPending) env.showMessage(` Code changed without pending implementation`) + } else { + const governor = await getGovernor(env) + const pauseGuardian = await getPauseGuardian(env) + + // Deployer address: from namedAccounts when key is loaded, otherwise infer + // from ProxyAdmin owner — if not governor, it's the deployer. + let deployer = getDeployer(env) + if (!deployer) { + try { + const proxyAdminAddr = await getProxyAdminAddress(client, ia.address) + const owner = (await client.readContract({ + address: proxyAdminAddr as `0x${string}`, + abi: [ + { inputs: [], name: 'owner', outputs: [{ type: 'address' }], stateMutability: 'view', type: 'function' }, + ], + functionName: 'owner', + })) as string + if (owner.toLowerCase() !== governor.toLowerCase()) deployer = owner + } catch { + // ProxyAdmin not readable — deployer stays undefined + } + } + + // Check configure state + // When deployer is available, classify issues as deployer-fixable vs deferred. + // When not (status-only run without deploy key), all issues are unclassified. + const configIssues: string[] = [] + const deferredIssues: string[] = [] + + // Helper: check if deployer has GOVERNOR_ROLE on a contract + // Returns false when deployer is not configured (status-only run without deploy key) + async function deployerHasGovernorRole(contractAddress: string): Promise { + if (!deployer) return false + try { + const role = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: [ + { + inputs: [], + name: 'GOVERNOR_ROLE', + outputs: [{ type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + ], + functionName: 'GOVERNOR_ROLE', + })) as `0x${string}` + return (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: [ + { + inputs: [{ type: 'bytes32' }, { type: 'address' }], + name: 'hasRole', + outputs: [{ type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + ], + functionName: 'hasRole', + args: [role, deployer as `0x${string}`], + })) as boolean + } catch { + return false + } + } + + // Helper: classify a failing config check + async function classifyConfigIssue(label: string, reason: string, contractAddress: string): Promise { + if (await deployerHasGovernorRole(contractAddress)) { + configIssues.push(`${label}: ${reason}`) + } else { + deferredIssues.push(`${label}: ${reason}`) + } + } + + // Check each new contract + const iaConfig = await checkIAConfigured(client, ia.address, rm.address, governor, pauseGuardian) + if (!iaConfig.done && iaConfig.reason !== 'RM.issuancePerBlock is 0') { + await classifyConfigIssue('IA', iaConfig.reason!, ia.address) + } + + const daConfig = await checkDefaultAllocationConfigured(client, da.address, governor, pauseGuardian) + if (!daConfig.done) { + await classifyConfigIssue('DA', daConfig.reason!, da.address) + } + + if (rc && ss) { + const ramConfig = await checkRAMConfigured( + client, + ram.address, + rc.address, + ss.address, + ia.address, + governor, + pauseGuardian, + ) + if (!ramConfig.done) { + await classifyConfigIssue('RAM', ramConfig.reason!, ram.address) + } + } + + const reclaimRolesCheck = await checkReclaimRoles(client, reclaim.address, governor, pauseGuardian) + if (!reclaimRolesCheck.done) { + await classifyConfigIssue('Reclaim', reclaimRolesCheck.reason!, reclaim.address) + } + + // RM.setDefaultReclaimAddress — governance-only (target is RM, not Reclaim). + // Always deferred to the upgrade governance batch, never blocks configure/transfer. + const reclaimRMCheck = await checkReclaimRMIntegration(client, rm.address, reclaim.address) + if (!reclaimRMCheck.done && reclaimRMCheck.reason !== 'RM not upgraded') { + deferredIssues.push(`Reclaim: ${reclaimRMCheck.reason}`) + } + + // RM.setRevertOnIneligible — config-driven; same deferred-only treatment as + // setDefaultReclaimAddress (target is RM, governance-only setter). + const settings = await getResolvedSettingsForEnv(env) + const revertCheck = await checkRMRevertOnIneligible(client, rm.address, settings.rewardsManager.revertOnIneligible) + if (!revertCheck.done && revertCheck.reason !== 'RM not upgraded') { + deferredIssues.push(`RM: ${revertCheck.reason}`) + } + + // REO configure + const issuanceBook = graph.getIssuanceAddressBook(targetChainId) + const hasNetworkOperator = issuanceBook.entryExists('NetworkOperator') + if (hasNetworkOperator) { + const reoConditions = await getREOConditions(env) + for (const [label, addr] of [ + ['REO-A', reoA.address], + ['REO-B', reoB.address], + ] as const) { + const reoConfig = await checkConfigurationStatus(client, addr, reoConditions) + if (!reoConfig.allOk) { + const failing = reoConfig.conditions.filter((c) => !c.ok).map((c) => c.name) + await classifyConfigIssue(label, failing.join(', '), addr) + } + } + } else { + deferredIssues.push('NetworkOperator not configured') + } + + const anyConfigIssues = configIssues.length > 0 || deferredIssues.length > 0 + + // Check transfer state + // ProxyAdmin ownership is deployer-independent (checks owner vs governor). + // Deployer GOVERNOR_ROLE revocation needs the deployer address — checked + // when available, skipped otherwise (ProxyAdmin transfer is the primary signal). + let proxyAdminsTransferred = true + + for (const contract of [ia, da, ram, reclaim, reoA, reoB]) { + try { + const proxyAdminAddr = await getProxyAdminAddress(client, contract.address) + const paCheck = await checkProxyAdminTransferred(client, proxyAdminAddr, governor) + if (!paCheck.done) proxyAdminsTransferred = false + } catch { + // ProxyAdmin not readable — skip + } + } + + let deployerRolesRevoked = true + if (deployer) { + for (const contract of [ia, da, ram, reclaim]) { + const revoked = await checkDeployerRevoked(client, contract.address, deployer) + if (!revoked.done) deployerRolesRevoked = false + } + if (hasNetworkOperator) { + const reoTransferConds = getREOTransferGovernanceConditions(deployer) + const reoATransfer = await checkConfigurationStatus(client, reoA.address, reoTransferConds) + if (!reoATransfer.allOk) deployerRolesRevoked = false + const reoBTransfer = await checkConfigurationStatus(client, reoB.address, reoTransferConds) + if (!reoBTransfer.allOk) deployerRolesRevoked = false + } + } + + const needsTransfer = !proxyAdminsTransferred || !deployerRolesRevoked + + // Next-step guidance + // Lifecycle: deploy → configure → transfer → upgrade + // ProxyAdmin not transferred ⇒ deployer still has control ⇒ configure/transfer phase + // ProxyAdmin transferred ⇒ remaining issues need governance ⇒ upgrade phase + if (anyConfigIssues && !proxyAdminsTransferred) { + env.showMessage(`\n → Next: --tags GIP-0088:upgrade,configure`) + for (const issue of configIssues) env.showMessage(` ${issue}`) + if (deferredIssues.length > 0) { + env.showMessage(` Deferred (governance TX):`) + for (const issue of deferredIssues) env.showMessage(` ${issue}`) + } + } else if (needsTransfer) { + env.showMessage(`\n → Next: --tags GIP-0088:upgrade,transfer`) + } else if (anyPending || anyConfigIssues) { + env.showMessage(`\n → Next: --tags GIP-0088:upgrade,upgrade`) + if (deferredIssues.length > 0) { + env.showMessage(` Deferred config (governance TX):`) + for (const issue of deferredIssues) env.showMessage(` ${issue}`) + } + } + } + + showPendingGovernanceTxs(env) + env.showMessage(`\n Actions: --tags GIP-0088:upgrade,`) + env.showMessage('') +} + +func.tags = [GoalTags.GIP_0088_UPGRADE] +func.dependencies = [ + // Upgrade contracts + ComponentTags.RECURRING_COLLECTOR, + ComponentTags.REWARDS_MANAGER, + ComponentTags.HORIZON_STAKING, + ComponentTags.SUBGRAPH_SERVICE, + ComponentTags.DISPUTE_MANAGER, + ComponentTags.PAYMENTS_ESCROW, + ComponentTags.L2_CURATION, + // New contracts (shown in status) + ComponentTags.ISSUANCE_ALLOCATOR, + ComponentTags.DEFAULT_ALLOCATION, + ComponentTags.RECURRING_AGREEMENT_MANAGER, + ComponentTags.REWARDS_ELIGIBILITY_A, +] +func.skip = async () => noTagsRequested() + +export default func diff --git a/packages/deployment/deploy/horizon/curation/01_deploy.ts b/packages/deployment/deploy/horizon/curation/01_deploy.ts new file mode 100644 index 000000000..1a0d9c9b0 --- /dev/null +++ b/packages/deployment/deploy/horizon/curation/01_deploy.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createImplementationDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createImplementationDeployModule(Contracts.horizon.L2Curation) diff --git a/packages/deployment/deploy/horizon/curation/02_upgrade.ts b/packages/deployment/deploy/horizon/curation/02_upgrade.ts new file mode 100644 index 000000000..efb44379c --- /dev/null +++ b/packages/deployment/deploy/horizon/curation/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.horizon.L2Curation) diff --git a/packages/deployment/deploy/horizon/curation/09_end.ts b/packages/deployment/deploy/horizon/curation/09_end.ts new file mode 100644 index 000000000..bd06ed9ad --- /dev/null +++ b/packages/deployment/deploy/horizon/curation/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.horizon.L2Curation) diff --git a/packages/deployment/deploy/horizon/curation/10_status.ts b/packages/deployment/deploy/horizon/curation/10_status.ts new file mode 100644 index 000000000..8a6d9f944 --- /dev/null +++ b/packages/deployment/deploy/horizon/curation/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.horizon.L2Curation) diff --git a/packages/deployment/deploy/horizon/payments-escrow/01_deploy.ts b/packages/deployment/deploy/horizon/payments-escrow/01_deploy.ts new file mode 100644 index 000000000..91d2db38b --- /dev/null +++ b/packages/deployment/deploy/horizon/payments-escrow/01_deploy.ts @@ -0,0 +1,58 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { deployImplementation, getImplementationConfig } from '@graphprotocol/deployment/lib/deploy-implementation.js' +import { ComponentTags, DeploymentActions, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +// PaymentsEscrow Implementation Deployment +// +// Deploys a new PaymentsEscrow implementation if artifact bytecode differs from on-chain. +// +// Workflow: +// 1. Read current immutable values from on-chain contract +// 2. Compare artifact bytecode with on-chain bytecode (accounting for immutables) +// 3. If different, deploy new implementation +// 4. Store as "pendingImplementation" in horizon/addresses.json +// 5. Upgrade task (separate) handles TX generation and execution + +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.DEPLOY)) return + await syncComponentsFromRegistry(env, [Contracts.horizon.Controller, Contracts.horizon.PaymentsEscrow]) + + const controllerDep = env.getOrNull('Controller') + const escrowDep = env.getOrNull('PaymentsEscrow') + + if (!controllerDep || !escrowDep) { + throw new Error('Missing required contract deployments (Controller, PaymentsEscrow) after sync.') + } + + // Read current immutable value from on-chain contract + const client = graph.getPublicClient(env) + const thawingPeriod = await client.readContract({ + address: escrowDep.address as `0x${string}`, + abi: [ + { + name: 'WITHDRAW_ESCROW_THAWING_PERIOD', + type: 'function', + inputs: [], + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + }, + ], + functionName: 'WITHDRAW_ESCROW_THAWING_PERIOD', + }) + + env.showMessage(` PaymentsEscrow WITHDRAW_ESCROW_THAWING_PERIOD: ${thawingPeriod}`) + + await deployImplementation( + env, + getImplementationConfig('horizon', 'PaymentsEscrow', { + constructorArgs: [controllerDep.address, thawingPeriod], + }), + ) +} + +func.tags = [ComponentTags.PAYMENTS_ESCROW] +func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) +export default func diff --git a/packages/deployment/deploy/horizon/payments-escrow/02_upgrade.ts b/packages/deployment/deploy/horizon/payments-escrow/02_upgrade.ts new file mode 100644 index 000000000..25c8f13e1 --- /dev/null +++ b/packages/deployment/deploy/horizon/payments-escrow/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.horizon.PaymentsEscrow) diff --git a/packages/deployment/deploy/horizon/payments-escrow/09_end.ts b/packages/deployment/deploy/horizon/payments-escrow/09_end.ts new file mode 100644 index 000000000..95272ed2d --- /dev/null +++ b/packages/deployment/deploy/horizon/payments-escrow/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.horizon.PaymentsEscrow) diff --git a/packages/deployment/deploy/horizon/payments-escrow/10_status.ts b/packages/deployment/deploy/horizon/payments-escrow/10_status.ts new file mode 100644 index 000000000..267692139 --- /dev/null +++ b/packages/deployment/deploy/horizon/payments-escrow/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.horizon.PaymentsEscrow) diff --git a/packages/deployment/deploy/horizon/recurring-collector/01_deploy.ts b/packages/deployment/deploy/horizon/recurring-collector/01_deploy.ts new file mode 100644 index 000000000..4f96b4c35 --- /dev/null +++ b/packages/deployment/deploy/horizon/recurring-collector/01_deploy.ts @@ -0,0 +1,48 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getResolvedSettingsForEnv } from '@graphprotocol/deployment/lib/deployment-config.js' +import { ComponentTags, DeploymentActions, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { deployProxyContract } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +/** + * Deploy RecurringCollector proxy and implementation + * + * Deploys OZ v5 TransparentUpgradeableProxy with atomic initialization. + * Deployer is the initial ProxyAdmin owner; ownership is transferred to + * the protocol governor in a separate governance step. + * + * RecurringCollector constructor takes (controller, revokeSignerThawingPeriod). + * initialize(eip712Name, eip712Version) sets up EIP-712 domain and pausability. + * + * On subsequent runs (proxy already deployed), deploys new implementation + * and stores it as pendingImplementation for governance upgrade. + * + * Usage: + * pnpm hardhat deploy --tags RecurringCollector:deploy --network + */ +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.DEPLOY)) return + await syncComponentsFromRegistry(env, [Contracts.horizon.Controller, Contracts.horizon.RecurringCollector]) + + const controllerDep = env.getOrNull('Controller') + if (!controllerDep) { + throw new Error('Missing Controller deployment after sync.') + } + + const settings = await getResolvedSettingsForEnv(env) + const { revokeSignerThawingPeriod, eip712Name, eip712Version } = settings.recurringCollector + + env.showMessage(`\n📦 Deploying ${Contracts.horizon.RecurringCollector.name}`) + + await deployProxyContract(env, { + contract: Contracts.horizon.RecurringCollector, + constructorArgs: [controllerDep.address, revokeSignerThawingPeriod], + initializeArgs: [eip712Name, eip712Version], + }) +} + +func.tags = [ComponentTags.RECURRING_COLLECTOR] +func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) + +export default func diff --git a/packages/deployment/deploy/horizon/recurring-collector/02_upgrade.ts b/packages/deployment/deploy/horizon/recurring-collector/02_upgrade.ts new file mode 100644 index 000000000..f58136aad --- /dev/null +++ b/packages/deployment/deploy/horizon/recurring-collector/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.horizon.RecurringCollector) diff --git a/packages/deployment/deploy/horizon/recurring-collector/04_configure.ts b/packages/deployment/deploy/horizon/recurring-collector/04_configure.ts new file mode 100644 index 000000000..023e95ef3 --- /dev/null +++ b/packages/deployment/deploy/horizon/recurring-collector/04_configure.ts @@ -0,0 +1,62 @@ +import { RECURRING_COLLECTOR_PAUSE_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { canSignAsGovernor, getPauseGuardian } from '@graphprotocol/deployment/lib/controller-utils.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContract } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph, tx } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +/** + * Configure RecurringCollector — set pause guardian + * + * RC uses Controller-based access control: setPauseGuardian requires + * msg.sender == Controller.getGovernor(). If the deployer is the + * Controller governor (e.g. testnet), this script sets it directly. + * Otherwise it reports the gap — the upgrade step (04_upgrade.ts) + * bundles it as a governance TX. + * + * Idempotent: checks on-chain state, skips if already set. + * + * Usage: + * pnpm hardhat deploy --tags RecurringCollector:configure --network + */ +export default createActionModule(Contracts.horizon.RecurringCollector, DeploymentActions.CONFIGURE, async (env) => { + const client = graph.getPublicClient(env) as PublicClient + const rc = requireContract(env, Contracts.horizon.RecurringCollector) + const pauseGuardian = await getPauseGuardian(env) + + env.showMessage(`\n========== Configure ${Contracts.horizon.RecurringCollector.name} ==========`) + + const isGuardian = (await client.readContract({ + address: rc.address as `0x${string}`, + abi: RECURRING_COLLECTOR_PAUSE_ABI, + functionName: 'pauseGuardians', + args: [pauseGuardian as `0x${string}`], + })) as boolean + + if (isGuardian) { + env.showMessage(` ✓ Pause guardian already set\n`) + return + } + + const { canSign } = await canSignAsGovernor(env) + if (!canSign) { + env.showMessage(` ○ Pause guardian not set — will be configured in upgrade step (governance TX)\n`) + return + } + + env.showMessage('\n🔨 Setting pause guardian as governor...\n') + const txFn = tx(env) + await txFn({ + account: 'governor', + to: rc.address as `0x${string}`, + data: encodeFunctionData({ + abi: RECURRING_COLLECTOR_PAUSE_ABI, + functionName: 'setPauseGuardian', + args: [pauseGuardian as `0x${string}`, true], + }), + }) + env.showMessage(` ✓ setPauseGuardian(${pauseGuardian})\n`) +}) diff --git a/packages/deployment/deploy/horizon/recurring-collector/05_transfer_governance.ts b/packages/deployment/deploy/horizon/recurring-collector/05_transfer_governance.ts new file mode 100644 index 000000000..672cc47d5 --- /dev/null +++ b/packages/deployment/deploy/horizon/recurring-collector/05_transfer_governance.ts @@ -0,0 +1,69 @@ +import { OZ_PROXY_ADMIN_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getGovernor } from '@graphprotocol/deployment/lib/controller-utils.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + getProxyAdminAddress, + requireContract, + requireDeployer, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph, tx } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +/** + * Transfer RecurringCollector ProxyAdmin to protocol governor + * + * RC doesn't use BaseUpgradeable GOVERNOR_ROLE — only ProxyAdmin needs transfer. + * + * Idempotent: checks current owner, skips if already governor. + * + * Usage: + * pnpm hardhat deploy --tags RecurringCollector,transfer --network + */ +export default createActionModule(Contracts.horizon.RecurringCollector, DeploymentActions.TRANSFER, async (env) => { + const client = graph.getPublicClient(env) as PublicClient + const deployer = requireDeployer(env) + const governor = await getGovernor(env) + const rc = requireContract(env, Contracts.horizon.RecurringCollector) + + env.showMessage(`\n========== Transfer ${Contracts.horizon.RecurringCollector.name} ==========`) + + // Read ProxyAdmin from ERC1967 slot + const proxyAdminAddress = await getProxyAdminAddress(client, rc.address) + + const currentOwner = (await client.readContract({ + address: proxyAdminAddress as `0x${string}`, + abi: OZ_PROXY_ADMIN_ABI, + functionName: 'owner', + })) as string + + if (currentOwner.toLowerCase() === governor.toLowerCase()) { + env.showMessage(` ✓ ProxyAdmin already owned by governor\n`) + return + } + + if (currentOwner.toLowerCase() !== deployer.toLowerCase()) { + env.showMessage(` ○ ProxyAdmin owned by ${currentOwner}, not deployer — skipping\n`) + return + } + + env.showMessage(` Transferring ProxyAdmin ownership to governor...`) + env.showMessage(` ProxyAdmin: ${proxyAdminAddress}`) + env.showMessage(` From: ${deployer}`) + env.showMessage(` To: ${governor}`) + + const txFn = tx(env) + await txFn({ + account: deployer, + to: proxyAdminAddress as `0x${string}`, + data: encodeFunctionData({ + abi: OZ_PROXY_ADMIN_ABI, + functionName: 'transferOwnership', + args: [governor as `0x${string}`], + }), + }) + + env.showMessage(` ✓ ProxyAdmin ownership transferred to governor\n`) +}) diff --git a/packages/deployment/deploy/horizon/recurring-collector/09_end.ts b/packages/deployment/deploy/horizon/recurring-collector/09_end.ts new file mode 100644 index 000000000..5240c729c --- /dev/null +++ b/packages/deployment/deploy/horizon/recurring-collector/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.horizon.RecurringCollector) diff --git a/packages/deployment/deploy/horizon/recurring-collector/10_status.ts b/packages/deployment/deploy/horizon/recurring-collector/10_status.ts new file mode 100644 index 000000000..da1ecafc3 --- /dev/null +++ b/packages/deployment/deploy/horizon/recurring-collector/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.horizon.RecurringCollector) diff --git a/packages/deployment/deploy/horizon/staking/01_deploy.ts b/packages/deployment/deploy/horizon/staking/01_deploy.ts new file mode 100644 index 000000000..3b9f1c9d4 --- /dev/null +++ b/packages/deployment/deploy/horizon/staking/01_deploy.ts @@ -0,0 +1,15 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createImplementationDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createImplementationDeployModule( + Contracts.horizon.HorizonStaking, + (env) => { + const controller = env.getOrNull('Controller') + const subgraphService = env.getOrNull('SubgraphService') + if (!controller || !subgraphService) { + throw new Error('Missing required contract deployments (Controller, SubgraphService) after sync.') + } + return [controller.address, subgraphService.address] + }, + { prerequisites: [Contracts.horizon.Controller, Contracts['subgraph-service'].SubgraphService] }, +) diff --git a/packages/deployment/deploy/horizon/staking/02_upgrade.ts b/packages/deployment/deploy/horizon/staking/02_upgrade.ts new file mode 100644 index 000000000..d7abe8bbe --- /dev/null +++ b/packages/deployment/deploy/horizon/staking/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.horizon.HorizonStaking) diff --git a/packages/deployment/deploy/horizon/staking/09_end.ts b/packages/deployment/deploy/horizon/staking/09_end.ts new file mode 100644 index 000000000..d374f7e79 --- /dev/null +++ b/packages/deployment/deploy/horizon/staking/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.horizon.HorizonStaking) diff --git a/packages/deployment/deploy/horizon/staking/10_status.ts b/packages/deployment/deploy/horizon/staking/10_status.ts new file mode 100644 index 000000000..22c2a940d --- /dev/null +++ b/packages/deployment/deploy/horizon/staking/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.horizon.HorizonStaking) diff --git a/packages/deployment/deploy/rewards/eligibility/a/01_deploy.ts b/packages/deployment/deploy/rewards/eligibility/a/01_deploy.ts new file mode 100644 index 000000000..1bde8305b --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/a/01_deploy.ts @@ -0,0 +1,12 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { requireDeployer, requireGraphToken } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createProxyDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createProxyDeployModule( + Contracts.issuance.RewardsEligibilityOracleA, + (env) => ({ + constructorArgs: [requireGraphToken(env).address], + initializeArgs: [requireDeployer(env)], + }), + { prerequisites: [Contracts.horizon.L2GraphToken] }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/a/02_upgrade.ts b/packages/deployment/deploy/rewards/eligibility/a/02_upgrade.ts new file mode 100644 index 000000000..063a33cae --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/a/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.issuance.RewardsEligibilityOracleA) diff --git a/packages/deployment/deploy/rewards/eligibility/a/04_configure.ts b/packages/deployment/deploy/rewards/eligibility/a/04_configure.ts new file mode 100644 index 000000000..26bb1e7c7 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/a/04_configure.ts @@ -0,0 +1,39 @@ +import { applyConfiguration } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { checkREORole, getREOConditions } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContracts, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Configure RewardsEligibilityOracleA (params + roles) + * + * Deployer executes directly (has GOVERNOR_ROLE from deploy). + * If deployer doesn't have the role, skips — upgrade step handles it. + */ +export default createActionModule( + Contracts.issuance.RewardsEligibilityOracleA, + DeploymentActions.CONFIGURE, + async (env) => { + const [reo] = requireContracts(env, [Contracts.issuance.RewardsEligibilityOracleA]) + const client = graph.getPublicClient(env) as PublicClient + const deployer = requireDeployer(env) + + const deployerRole = await checkREORole(client, reo.address, 'GOVERNOR_ROLE', deployer) + if (!deployerRole.hasRole) { + env.showMessage( + `\n ○ ${Contracts.issuance.RewardsEligibilityOracleA.name}: deployer does not have GOVERNOR_ROLE — skipping\n`, + ) + return + } + + await applyConfiguration(env, client, await getREOConditions(env), { + contractName: Contracts.issuance.RewardsEligibilityOracleA.name, + contractAddress: reo.address, + canExecuteDirectly: true, + executor: deployer, + }) + }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/a/05_transfer_governance.ts b/packages/deployment/deploy/rewards/eligibility/a/05_transfer_governance.ts new file mode 100644 index 000000000..e09593859 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/a/05_transfer_governance.ts @@ -0,0 +1,45 @@ +import { applyConfiguration, checkConfigurationStatus } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { getREOConditions, getREOTransferGovernanceConditions } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + requireContracts, + requireDeployer, + transferProxyAdminOwnership, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Transfer governance of RewardsEligibilityOracleA + */ +export default createActionModule( + Contracts.issuance.RewardsEligibilityOracleA, + DeploymentActions.TRANSFER, + async (env) => { + const deployer = requireDeployer(env) + const [reo] = requireContracts(env, [Contracts.issuance.RewardsEligibilityOracleA]) + const client = graph.getPublicClient(env) as PublicClient + + // 1. Verify preconditions (same conditions as step 4) + env.showMessage(`\n📋 Verifying ${Contracts.issuance.RewardsEligibilityOracleA.name} configuration...\n`) + const status = await checkConfigurationStatus(client, reo.address, await getREOConditions(env)) + for (const r of status.conditions) env.showMessage(` ${r.message}`) + if (!status.allOk) { + env.showMessage('\n ○ Configuration incomplete — skipping transfer\n') + return + } + + // 2. Apply: revoke deployer's GOVERNOR_ROLE + await applyConfiguration(env, client, getREOTransferGovernanceConditions(deployer), { + contractName: `${Contracts.issuance.RewardsEligibilityOracleA.name}-transfer-governance`, + contractAddress: reo.address, + canExecuteDirectly: true, + executor: deployer, + }) + + // 3. Transfer ProxyAdmin ownership to governor + await transferProxyAdminOwnership(env, Contracts.issuance.RewardsEligibilityOracleA) + }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/a/09_end.ts b/packages/deployment/deploy/rewards/eligibility/a/09_end.ts new file mode 100644 index 000000000..dd53f54ec --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/a/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.issuance.RewardsEligibilityOracleA) diff --git a/packages/deployment/deploy/rewards/eligibility/a/10_status.ts b/packages/deployment/deploy/rewards/eligibility/a/10_status.ts new file mode 100644 index 000000000..a42b58304 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/a/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.issuance.RewardsEligibilityOracleA) diff --git a/packages/deployment/deploy/rewards/eligibility/b/01_deploy.ts b/packages/deployment/deploy/rewards/eligibility/b/01_deploy.ts new file mode 100644 index 000000000..c360d882a --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/b/01_deploy.ts @@ -0,0 +1,12 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { requireDeployer, requireGraphToken } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createProxyDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createProxyDeployModule( + Contracts.issuance.RewardsEligibilityOracleB, + (env) => ({ + constructorArgs: [requireGraphToken(env).address], + initializeArgs: [requireDeployer(env)], + }), + { prerequisites: [Contracts.horizon.L2GraphToken] }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/b/02_upgrade.ts b/packages/deployment/deploy/rewards/eligibility/b/02_upgrade.ts new file mode 100644 index 000000000..1863d2847 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/b/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.issuance.RewardsEligibilityOracleB) diff --git a/packages/deployment/deploy/rewards/eligibility/b/04_configure.ts b/packages/deployment/deploy/rewards/eligibility/b/04_configure.ts new file mode 100644 index 000000000..e06307f45 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/b/04_configure.ts @@ -0,0 +1,39 @@ +import { applyConfiguration } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { checkREORole, getREOConditions } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContracts, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Configure RewardsEligibilityOracleB (params + roles) + * + * Deployer executes directly (has GOVERNOR_ROLE from deploy). + * If deployer doesn't have the role, skips — upgrade step handles it. + */ +export default createActionModule( + Contracts.issuance.RewardsEligibilityOracleB, + DeploymentActions.CONFIGURE, + async (env) => { + const [reo] = requireContracts(env, [Contracts.issuance.RewardsEligibilityOracleB]) + const client = graph.getPublicClient(env) as PublicClient + const deployer = requireDeployer(env) + + const deployerRole = await checkREORole(client, reo.address, 'GOVERNOR_ROLE', deployer) + if (!deployerRole.hasRole) { + env.showMessage( + `\n ○ ${Contracts.issuance.RewardsEligibilityOracleB.name}: deployer does not have GOVERNOR_ROLE — skipping\n`, + ) + return + } + + await applyConfiguration(env, client, await getREOConditions(env), { + contractName: Contracts.issuance.RewardsEligibilityOracleB.name, + contractAddress: reo.address, + canExecuteDirectly: true, + executor: deployer, + }) + }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/b/05_transfer_governance.ts b/packages/deployment/deploy/rewards/eligibility/b/05_transfer_governance.ts new file mode 100644 index 000000000..87bcb281e --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/b/05_transfer_governance.ts @@ -0,0 +1,45 @@ +import { applyConfiguration, checkConfigurationStatus } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { getREOConditions, getREOTransferGovernanceConditions } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + requireContracts, + requireDeployer, + transferProxyAdminOwnership, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Transfer governance of RewardsEligibilityOracleB + */ +export default createActionModule( + Contracts.issuance.RewardsEligibilityOracleB, + DeploymentActions.TRANSFER, + async (env) => { + const deployer = requireDeployer(env) + const [reo] = requireContracts(env, [Contracts.issuance.RewardsEligibilityOracleB]) + const client = graph.getPublicClient(env) as PublicClient + + // 1. Verify preconditions (same conditions as step 4) + env.showMessage(`\n📋 Verifying ${Contracts.issuance.RewardsEligibilityOracleB.name} configuration...\n`) + const status = await checkConfigurationStatus(client, reo.address, await getREOConditions(env)) + for (const r of status.conditions) env.showMessage(` ${r.message}`) + if (!status.allOk) { + env.showMessage('\n ○ Configuration incomplete — skipping transfer\n') + return + } + + // 2. Apply: revoke deployer's GOVERNOR_ROLE + await applyConfiguration(env, client, getREOTransferGovernanceConditions(deployer), { + contractName: `${Contracts.issuance.RewardsEligibilityOracleB.name}-transfer-governance`, + contractAddress: reo.address, + canExecuteDirectly: true, + executor: deployer, + }) + + // 3. Transfer ProxyAdmin ownership to governor + await transferProxyAdminOwnership(env, Contracts.issuance.RewardsEligibilityOracleB) + }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/b/09_end.ts b/packages/deployment/deploy/rewards/eligibility/b/09_end.ts new file mode 100644 index 000000000..3a11b891a --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/b/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.issuance.RewardsEligibilityOracleB) diff --git a/packages/deployment/deploy/rewards/eligibility/b/10_status.ts b/packages/deployment/deploy/rewards/eligibility/b/10_status.ts new file mode 100644 index 000000000..f8a4d48a8 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/b/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.issuance.RewardsEligibilityOracleB) diff --git a/packages/deployment/deploy/rewards/eligibility/mock/01_deploy.ts b/packages/deployment/deploy/rewards/eligibility/mock/01_deploy.ts new file mode 100644 index 000000000..0d687127c --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/mock/01_deploy.ts @@ -0,0 +1,12 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { requireDeployer, requireGraphToken } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createProxyDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createProxyDeployModule( + Contracts.issuance.RewardsEligibilityOracleMock, + (env) => ({ + constructorArgs: [requireGraphToken(env).address], + initializeArgs: [requireDeployer(env)], + }), + { prerequisites: [Contracts.horizon.L2GraphToken] }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/mock/02_upgrade.ts b/packages/deployment/deploy/rewards/eligibility/mock/02_upgrade.ts new file mode 100644 index 000000000..74e2374b8 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/mock/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.issuance.RewardsEligibilityOracleMock) diff --git a/packages/deployment/deploy/rewards/eligibility/mock/05_transfer_governance.ts b/packages/deployment/deploy/rewards/eligibility/mock/05_transfer_governance.ts new file mode 100644 index 000000000..6be92ce32 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/mock/05_transfer_governance.ts @@ -0,0 +1,39 @@ +import { applyConfiguration } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { getREOTransferGovernanceConditions } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + requireContracts, + requireDeployer, + transferProxyAdminOwnership, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Transfer governance of MockRewardsEligibilityOracle + * + * Revokes deployer's GOVERNOR_ROLE and transfers ProxyAdmin ownership + * to the protocol governor. + */ +export default createActionModule( + Contracts.issuance.RewardsEligibilityOracleMock, + DeploymentActions.TRANSFER, + async (env) => { + const deployer = requireDeployer(env) + const [reo] = requireContracts(env, [Contracts.issuance.RewardsEligibilityOracleMock]) + const client = graph.getPublicClient(env) as PublicClient + + // Revoke deployer's GOVERNOR_ROLE + await applyConfiguration(env, client, getREOTransferGovernanceConditions(deployer), { + contractName: `${Contracts.issuance.RewardsEligibilityOracleMock.name}-transfer-governance`, + contractAddress: reo.address, + canExecuteDirectly: true, + executor: deployer, + }) + + // Transfer ProxyAdmin ownership to governor + await transferProxyAdminOwnership(env, Contracts.issuance.RewardsEligibilityOracleMock) + }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/mock/06_integrate.ts b/packages/deployment/deploy/rewards/eligibility/mock/06_integrate.ts new file mode 100644 index 000000000..2bd1ed3ac --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/mock/06_integrate.ts @@ -0,0 +1,39 @@ +import { applyConfiguration } from '@graphprotocol/deployment/lib/apply-configuration.js' +import { createRMIntegrationCondition } from '@graphprotocol/deployment/lib/contract-checks.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js' +import { ComponentTags, DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContracts } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Integrate MockRewardsEligibilityOracle with RewardsManager (testnet only) + * + * Points RewardsManager at the mock so indexers can control their own eligibility. + */ +export default createActionModule( + Contracts.issuance.RewardsEligibilityOracleMock, + DeploymentActions.INTEGRATE, + async (env) => { + const [reo, rm] = requireContracts(env, [ + Contracts.issuance.RewardsEligibilityOracleMock, + Contracts.horizon.RewardsManager, + ]) + const client = graph.getPublicClient(env) as PublicClient + + const { governor, canSign } = await canSignAsGovernor(env) + + await applyConfiguration(env, client, [createRMIntegrationCondition(reo.address)], { + contractName: `${Contracts.horizon.RewardsManager.name}-MockREO`, + contractAddress: rm.address, + canExecuteDirectly: canSign, + executor: governor, + }) + }, + { + extraDependencies: [ComponentTags.REWARDS_MANAGER], + prerequisites: [Contracts.horizon.RewardsManager], + }, +) diff --git a/packages/deployment/deploy/rewards/eligibility/mock/09_end.ts b/packages/deployment/deploy/rewards/eligibility/mock/09_end.ts new file mode 100644 index 000000000..98cacd97f --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/mock/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.issuance.RewardsEligibilityOracleMock) diff --git a/packages/deployment/deploy/rewards/eligibility/mock/10_status.ts b/packages/deployment/deploy/rewards/eligibility/mock/10_status.ts new file mode 100644 index 000000000..611316b02 --- /dev/null +++ b/packages/deployment/deploy/rewards/eligibility/mock/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.issuance.RewardsEligibilityOracleMock) diff --git a/packages/deployment/deploy/rewards/manager/01_deploy.ts b/packages/deployment/deploy/rewards/manager/01_deploy.ts new file mode 100644 index 000000000..2223ce0ed --- /dev/null +++ b/packages/deployment/deploy/rewards/manager/01_deploy.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createImplementationDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createImplementationDeployModule(Contracts.horizon.RewardsManager) diff --git a/packages/deployment/deploy/rewards/manager/02_upgrade.ts b/packages/deployment/deploy/rewards/manager/02_upgrade.ts new file mode 100644 index 000000000..5c888723b --- /dev/null +++ b/packages/deployment/deploy/rewards/manager/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts.horizon.RewardsManager) diff --git a/packages/deployment/deploy/rewards/manager/09_end.ts b/packages/deployment/deploy/rewards/manager/09_end.ts new file mode 100644 index 000000000..ae4996ffd --- /dev/null +++ b/packages/deployment/deploy/rewards/manager/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.horizon.RewardsManager) diff --git a/packages/deployment/deploy/rewards/manager/10_status.ts b/packages/deployment/deploy/rewards/manager/10_status.ts new file mode 100644 index 000000000..4b47d40bb --- /dev/null +++ b/packages/deployment/deploy/rewards/manager/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts.horizon.RewardsManager) diff --git a/packages/deployment/deploy/rewards/reclaim/01_deploy.ts b/packages/deployment/deploy/rewards/reclaim/01_deploy.ts new file mode 100644 index 000000000..3ee161636 --- /dev/null +++ b/packages/deployment/deploy/rewards/reclaim/01_deploy.ts @@ -0,0 +1,45 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { ComponentTags, DeploymentActions, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { deployProxyContract, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +/** + * Deploy DirectAllocation proxy as default reclaim address + * + * This script deploys a single DirectAllocation proxy instance used as the + * default reclaim address on RewardsManager for all reclaim reasons. + * The proxy uses the DirectAllocation_Implementation deployed by direct-allocation-impl. + * + * Deployed contracts: + * - ReclaimedRewards + * + * Usage: + * pnpm hardhat deploy --tags RewardsReclaim:deploy --network + */ + +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.DEPLOY)) return + await syncComponentsFromRegistry(env, [ + Contracts.issuance.DirectAllocation_Implementation, + Contracts.horizon.RewardsManager, + Contracts.issuance.ReclaimedRewards, + ]) + + env.showMessage(`\n📦 Deploying DirectAllocation reclaim address proxy...`) + env.showMessage(` Shared implementation: ${Contracts.issuance.DirectAllocation_Implementation.name}`) + + await deployProxyContract(env, { + contract: Contracts.issuance.ReclaimedRewards, + sharedImplementation: Contracts.issuance.DirectAllocation_Implementation, + initializeArgs: [requireDeployer(env)], + }) + + env.showMessage('\n✓ Reclaim address deployment complete') +} + +func.tags = [ComponentTags.REWARDS_RECLAIM] +func.dependencies = [ComponentTags.DIRECT_ALLOCATION_IMPL, ComponentTags.REWARDS_MANAGER] +func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) + +export default func diff --git a/packages/deployment/deploy/rewards/reclaim/02_upgrade.ts b/packages/deployment/deploy/rewards/reclaim/02_upgrade.ts new file mode 100644 index 000000000..bc27987a0 --- /dev/null +++ b/packages/deployment/deploy/rewards/reclaim/02_upgrade.ts @@ -0,0 +1,36 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { ComponentTags, DeploymentActions, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { upgradeImplementation } from '@graphprotocol/deployment/lib/upgrade-implementation.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +// ReclaimedRewards Upgrade +// +// Upgrades ReclaimedRewards proxy to DirectAllocation implementation via per-proxy ProxyAdmin. +// +// Workflow: +// 1. Check for pending implementation in address book (set by direct-allocation-impl) +// 2. Generate governance TX (upgradeAndCall to per-proxy ProxyAdmin) +// 3. Fork mode: execute via governor impersonation +// 4. Production: output TX file for Safe execution +// +// Usage: +// FORK_NETWORK=arbitrumSepolia npx hardhat deploy --tags RewardsReclaim:upgrade --network localhost + +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.UPGRADE)) return + await syncComponentsFromRegistry(env, [ + Contracts.issuance.DirectAllocation_Implementation, + Contracts.issuance.ReclaimedRewards, + ]) + await upgradeImplementation(env, Contracts.issuance.ReclaimedRewards, { + implementationName: 'DirectAllocation', + }) + await syncComponentsFromRegistry(env, [Contracts.issuance.ReclaimedRewards]) +} + +func.tags = [ComponentTags.REWARDS_RECLAIM] +func.dependencies = [ComponentTags.DIRECT_ALLOCATION_IMPL] +func.skip = async () => shouldSkipAction(DeploymentActions.UPGRADE) + +export default func diff --git a/packages/deployment/deploy/rewards/reclaim/04_configure.ts b/packages/deployment/deploy/rewards/reclaim/04_configure.ts new file mode 100644 index 000000000..ad1afee4d --- /dev/null +++ b/packages/deployment/deploy/rewards/reclaim/04_configure.ts @@ -0,0 +1,144 @@ +import { ACCESS_CONTROL_ENUMERABLE_ABI, REWARDS_MANAGER_ABI } from '@graphprotocol/deployment/lib/abis.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { getGovernor, getPauseGuardian } from '@graphprotocol/deployment/lib/controller-utils.js' +import { ComponentTags, DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContract, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { checkReclaimConfigured } from '@graphprotocol/deployment/lib/preconditions.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { graph, read, tx } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +/** + * Configure ReclaimedRewards — role grants only + * + * Grants GOVERNOR_ROLE to protocol governor and PAUSE_ROLE to pause guardian. + * Deployer executes directly (has GOVERNOR_ROLE from deploy). + * If deployer doesn't have the role, skips — upgrade step handles it. + * + * RM.setDefaultReclaimAddress is a governance TX bundled in the upgrade step. + * + * Usage: + * pnpm hardhat deploy --tags RewardsReclaim:configure --network + */ +export default createActionModule( + Contracts.issuance.ReclaimedRewards, + DeploymentActions.CONFIGURE, + async (env) => { + const client = graph.getPublicClient(env) as PublicClient + const readFn = read(env) + const deployer = requireDeployer(env) + const governor = await getGovernor(env) + const pauseGuardian = await getPauseGuardian(env) + + const rewardsManager = requireContract(env, Contracts.horizon.RewardsManager) + const reclaimedRewards = requireContract(env, Contracts.issuance.ReclaimedRewards) + + env.showMessage(`\n========== Configure ${Contracts.issuance.ReclaimedRewards.name} ==========`) + env.showMessage(`ReclaimedRewards: ${reclaimedRewards.address}`) + + // Check if fully configured (shared precondition check) + const precondition = await checkReclaimConfigured( + client, + rewardsManager.address, + reclaimedRewards.address, + governor, + pauseGuardian, + ) + if (precondition.done) { + env.showMessage(`\n✅ ${Contracts.issuance.ReclaimedRewards.name} already configured\n`) + return + } + + // Check role grants + env.showMessage('\n📋 Checking configuration...\n') + + const GOVERNOR_ROLE = (await readFn(reclaimedRewards, { functionName: 'GOVERNOR_ROLE' })) as `0x${string}` + const PAUSE_ROLE = (await readFn(reclaimedRewards, { functionName: 'PAUSE_ROLE' })) as `0x${string}` + + const governorHasRole = (await client.readContract({ + address: reclaimedRewards.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [GOVERNOR_ROLE, governor as `0x${string}`], + })) as boolean + env.showMessage(` Governor GOVERNOR_ROLE: ${governorHasRole ? '✓' : '✗'}`) + + const pauseGuardianHasRole = (await client.readContract({ + address: reclaimedRewards.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [PAUSE_ROLE, pauseGuardian as `0x${string}`], + })) as boolean + env.showMessage(` PauseGuardian PAUSE_ROLE: ${pauseGuardianHasRole ? '✓' : '✗'}`) + + // RM integration status (informational — handled by upgrade step) + try { + const currentDefault = (await client.readContract({ + address: rewardsManager.address as `0x${string}`, + abi: REWARDS_MANAGER_ABI, + functionName: 'getDefaultReclaimAddress', + })) as string + const rmOk = currentDefault.toLowerCase() === reclaimedRewards.address.toLowerCase() + env.showMessage(` RM default reclaim: ${rmOk ? '✓' : '○ will be set in upgrade step (governance TX)'}`) + } catch { + env.showMessage(` RM default reclaim: ○ RM not upgraded — will be set in upgrade step`) + } + + // Execute role grants as deployer + const deployerHasRole = (await client.readContract({ + address: reclaimedRewards.address as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [GOVERNOR_ROLE, deployer as `0x${string}`], + })) as boolean + + if (!deployerHasRole) { + env.showMessage( + `\n ○ Deployer does not have GOVERNOR_ROLE — skipping role grants (governance TX in upgrade step)\n`, + ) + return + } + + const txs: Array<{ to: string; data: `0x${string}`; label: string }> = [] + + if (!governorHasRole) { + txs.push({ + to: reclaimedRewards.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [GOVERNOR_ROLE, governor as `0x${string}`], + }), + label: `grantRole(GOVERNOR_ROLE, ${governor})`, + }) + } + + if (!pauseGuardianHasRole) { + txs.push({ + to: reclaimedRewards.address, + data: encodeFunctionData({ + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'grantRole', + args: [PAUSE_ROLE, pauseGuardian as `0x${string}`], + }), + label: `grantRole(PAUSE_ROLE, ${pauseGuardian})`, + }) + } + + if (txs.length > 0) { + env.showMessage('\n🔨 Executing role grants as deployer...\n') + const txFn = tx(env) + for (const t of txs) { + await txFn({ account: deployer, to: t.to as `0x${string}`, data: t.data }) + env.showMessage(` ✓ ${t.label}`) + } + } + + env.showMessage(`\n✅ ${Contracts.issuance.ReclaimedRewards.name} role grants complete\n`) + }, + { + extraDependencies: [ComponentTags.REWARDS_MANAGER], + prerequisites: [Contracts.horizon.RewardsManager], + }, +) diff --git a/packages/deployment/deploy/rewards/reclaim/05_transfer_governance.ts b/packages/deployment/deploy/rewards/reclaim/05_transfer_governance.ts new file mode 100644 index 000000000..bdcd728b2 --- /dev/null +++ b/packages/deployment/deploy/rewards/reclaim/05_transfer_governance.ts @@ -0,0 +1,56 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { + requireContract, + requireDeployer, + transferProxyAdminOwnership, +} from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { checkDeployerRevoked } from '@graphprotocol/deployment/lib/preconditions.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { execute, graph, read } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { PublicClient } from 'viem' + +/** + * Transfer ReclaimedRewards governance from deployer + * + * - Revoke GOVERNOR_ROLE from deployment account + * - Transfer ProxyAdmin ownership to governor + * + * Role grants (GOVERNOR_ROLE, PAUSE_ROLE) happen in 04_configure.ts. + * This script only revokes deployer access. + * + * Idempotent: checks on-chain state, skips if already transferred. + * + * Usage: + * pnpm hardhat deploy --tags RewardsReclaim,transfer --network + */ +export default createActionModule(Contracts.issuance.ReclaimedRewards, DeploymentActions.TRANSFER, async (env) => { + const readFn = read(env) + const executeFn = execute(env) + const client = graph.getPublicClient(env) as PublicClient + const deployer = requireDeployer(env) + const reclaim = requireContract(env, Contracts.issuance.ReclaimedRewards) + + env.showMessage(`\n========== Transfer ${Contracts.issuance.ReclaimedRewards.name} ==========`) + + // Check if deployer GOVERNOR_ROLE already revoked (shared precondition check) + const precondition = await checkDeployerRevoked(client, reclaim.address, deployer) + if (precondition.done) { + env.showMessage(`✓ Deployer GOVERNOR_ROLE already revoked`) + } else { + const GOVERNOR_ROLE = (await readFn(reclaim, { functionName: 'GOVERNOR_ROLE' })) as `0x${string}` + + env.showMessage(`🔨 Revoking deployer GOVERNOR_ROLE...`) + await executeFn(reclaim, { + account: deployer, + functionName: 'revokeRole', + args: [GOVERNOR_ROLE, deployer], + }) + env.showMessage(` ✓ revokeRole(GOVERNOR_ROLE) executed`) + } + + // Transfer ProxyAdmin ownership to governor + await transferProxyAdminOwnership(env, Contracts.issuance.ReclaimedRewards) + + env.showMessage(`\n✅ ${Contracts.issuance.ReclaimedRewards.name} governance transferred!\n`) +}) diff --git a/packages/deployment/deploy/rewards/reclaim/09_end.ts b/packages/deployment/deploy/rewards/reclaim/09_end.ts new file mode 100644 index 000000000..46d6aa2dc --- /dev/null +++ b/packages/deployment/deploy/rewards/reclaim/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts.issuance.ReclaimedRewards) diff --git a/packages/deployment/deploy/rewards/reclaim/10_status.ts b/packages/deployment/deploy/rewards/reclaim/10_status.ts new file mode 100644 index 000000000..c5f778ac9 --- /dev/null +++ b/packages/deployment/deploy/rewards/reclaim/10_status.ts @@ -0,0 +1,14 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { ComponentTags } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' +import { showDetailedComponentStatus } from '@graphprotocol/deployment/lib/status-detail.js' + +/** + * RewardsReclaim status - show detailed state of reclaim contract + * + * Usage: + * pnpm hardhat deploy --tags RewardsReclaim --network + */ +export default createStatusModule(ComponentTags.REWARDS_RECLAIM, async (env) => { + await showDetailedComponentStatus(env, Contracts.issuance.ReclaimedRewards) +}) diff --git a/packages/deployment/deploy/service/dispute/01_deploy.ts b/packages/deployment/deploy/service/dispute/01_deploy.ts new file mode 100644 index 000000000..3158750b9 --- /dev/null +++ b/packages/deployment/deploy/service/dispute/01_deploy.ts @@ -0,0 +1,12 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createImplementationDeployModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createImplementationDeployModule( + Contracts['subgraph-service'].DisputeManager, + (env) => { + const controller = env.getOrNull('Controller') + if (!controller) throw new Error('Missing Controller deployment after sync.') + return [controller.address] + }, + { prerequisites: [Contracts.horizon.Controller] }, +) diff --git a/packages/deployment/deploy/service/dispute/02_upgrade.ts b/packages/deployment/deploy/service/dispute/02_upgrade.ts new file mode 100644 index 000000000..99c75d9e3 --- /dev/null +++ b/packages/deployment/deploy/service/dispute/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts['subgraph-service'].DisputeManager) diff --git a/packages/deployment/deploy/service/dispute/09_end.ts b/packages/deployment/deploy/service/dispute/09_end.ts new file mode 100644 index 000000000..5a1afb1a4 --- /dev/null +++ b/packages/deployment/deploy/service/dispute/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts['subgraph-service'].DisputeManager) diff --git a/packages/deployment/deploy/service/dispute/10_status.ts b/packages/deployment/deploy/service/dispute/10_status.ts new file mode 100644 index 000000000..1039074c0 --- /dev/null +++ b/packages/deployment/deploy/service/dispute/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts['subgraph-service'].DisputeManager) diff --git a/packages/deployment/deploy/service/subgraph/01_deploy.ts b/packages/deployment/deploy/service/subgraph/01_deploy.ts new file mode 100644 index 000000000..ff1b46b95 --- /dev/null +++ b/packages/deployment/deploy/service/subgraph/01_deploy.ts @@ -0,0 +1,146 @@ +import { linkArtifactLibraries } from '@graphprotocol/deployment/lib/artifact-loaders.js' +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { + deployImplementation, + getImplementationConfig, + loadArtifactFromSource, +} from '@graphprotocol/deployment/lib/deploy-implementation.js' +import { ComponentTags, DeploymentActions, shouldSkipAction } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { syncComponentsFromRegistry } from '@graphprotocol/deployment/lib/sync-utils.js' +import { deploy } from '@graphprotocol/deployment/rocketh/deploy.js' +import type { DeployScriptModule } from '@rocketh/core/types' + +// SubgraphService Implementation Deployment +// +// SubgraphService uses external Solidity libraries that must be deployed first +// and linked into the implementation bytecode before deployment. +// +// Library dependency order: +// 1. StakeClaims (standalone, from horizon) +// 2. AllocationHandler (standalone) +// 3. IndexingAgreementDecoderRaw (standalone) +// 4. IndexingAgreementDecoder (links IndexingAgreementDecoderRaw) +// 5. IndexingAgreement (links IndexingAgreementDecoder) +// 6. SubgraphService (links all above) +// +// Workflow: +// 1. Deploy libraries in dependency order +// 2. Deploy SS implementation with linked libraries +// 3. Store as "pendingImplementation" in subgraph-service/addresses.json +// 4. Upgrade task (separate) handles TX generation and execution + +const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.DEPLOY)) return + await syncComponentsFromRegistry(env, [ + Contracts.horizon.Controller, + Contracts['subgraph-service'].DisputeManager, + Contracts.horizon.GraphTallyCollector, + Contracts.horizon.L2Curation, + Contracts.horizon.RecurringCollector, + Contracts['subgraph-service'].SubgraphService, + ]) + + // Get constructor args from imported deployments + const controllerDep = env.getOrNull('Controller') + const disputeManagerDep = env.getOrNull('DisputeManager') + const graphTallyCollectorDep = env.getOrNull('GraphTallyCollector') + const curationDep = env.getOrNull('L2Curation') + const recurringCollectorDep = env.getOrNull('RecurringCollector') + + if (!controllerDep || !disputeManagerDep || !graphTallyCollectorDep || !curationDep || !recurringCollectorDep) { + throw new Error( + 'Missing required contract deployments after sync ' + + '(Controller, DisputeManager, GraphTallyCollector, L2Curation, RecurringCollector).', + ) + } + + // Deploy libraries in dependency order + const deployFn = deploy(env) + const deployer = env.namedAccounts.deployer + if (!deployer) throw new Error('No deployer account configured') + + env.showMessage('\n📚 Deploying SubgraphService libraries...') + + // 1. StakeClaims (from horizon, standalone) + const stakeClaimsArtifact = loadArtifactFromSource({ + type: 'horizon', + path: 'contracts/data-service/libraries/StakeClaims.sol/StakeClaims', + }) + const stakeClaims = await deployFn('StakeClaims', { + account: deployer, + artifact: stakeClaimsArtifact, + args: [], + }) + env.showMessage(` StakeClaims: ${stakeClaims.address}`) + + // 2. AllocationHandler (standalone) + const allocationHandlerArtifact = loadArtifactFromSource({ + type: 'subgraph-service', + name: 'libraries/AllocationHandler', + }) + const allocationHandler = await deployFn('AllocationHandler', { + account: deployer, + artifact: allocationHandlerArtifact, + args: [], + }) + env.showMessage(` AllocationHandler: ${allocationHandler.address}`) + + // 3. IndexingAgreementDecoderRaw (standalone) + const decoderRawArtifact = loadArtifactFromSource({ + type: 'subgraph-service', + name: 'libraries/IndexingAgreementDecoderRaw', + }) + const decoderRaw = await deployFn('IndexingAgreementDecoderRaw', { + account: deployer, + artifact: decoderRawArtifact, + args: [], + }) + env.showMessage(` IndexingAgreementDecoderRaw: ${decoderRaw.address}`) + + // 4. IndexingAgreementDecoder (links IndexingAgreementDecoderRaw) + // Pre-link libraries into artifact so rocketh stores linked bytecode + // (rocketh's bytecode comparison breaks for unlinked artifacts — see linkArtifactLibraries) + const decoderArtifact = linkArtifactLibraries( + loadArtifactFromSource({ type: 'subgraph-service', name: 'libraries/IndexingAgreementDecoder' }), + { IndexingAgreementDecoderRaw: decoderRaw.address as `0x${string}` }, + ) + const decoder = await deployFn('IndexingAgreementDecoder', { account: deployer, artifact: decoderArtifact, args: [] }) + env.showMessage(` IndexingAgreementDecoder: ${decoder.address}`) + + // 5. IndexingAgreement (links IndexingAgreementDecoder) + const indexingAgreementArtifact = linkArtifactLibraries( + loadArtifactFromSource({ type: 'subgraph-service', name: 'libraries/IndexingAgreement' }), + { IndexingAgreementDecoder: decoder.address as `0x${string}` }, + ) + const indexingAgreement = await deployFn('IndexingAgreement', { + account: deployer, + artifact: indexingAgreementArtifact, + args: [], + }) + env.showMessage(` IndexingAgreement: ${indexingAgreement.address}`) + + env.showMessage(' ✓ Libraries deployed\n') + + // 6. Deploy SubgraphService implementation with all libraries linked + const config = getImplementationConfig('subgraph-service', 'SubgraphService', { + constructorArgs: [ + controllerDep.address, + disputeManagerDep.address, + graphTallyCollectorDep.address, + curationDep.address, + recurringCollectorDep.address, + ], + }) + + await deployImplementation(env, config, { + StakeClaims: stakeClaims.address, + AllocationHandler: allocationHandler.address, + IndexingAgreement: indexingAgreement.address, + IndexingAgreementDecoder: decoder.address, + }) +} + +func.tags = [ComponentTags.SUBGRAPH_SERVICE] +func.dependencies = [ComponentTags.RECURRING_COLLECTOR] +func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) +export default func diff --git a/packages/deployment/deploy/service/subgraph/02_upgrade.ts b/packages/deployment/deploy/service/subgraph/02_upgrade.ts new file mode 100644 index 000000000..1395af76c --- /dev/null +++ b/packages/deployment/deploy/service/subgraph/02_upgrade.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createUpgradeModule(Contracts['subgraph-service'].SubgraphService) diff --git a/packages/deployment/deploy/service/subgraph/04_configure.ts b/packages/deployment/deploy/service/subgraph/04_configure.ts new file mode 100644 index 000000000..61dfc3f17 --- /dev/null +++ b/packages/deployment/deploy/service/subgraph/04_configure.ts @@ -0,0 +1,22 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { DeploymentActions } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { createActionModule } from '@graphprotocol/deployment/lib/script-factories.js' + +/** + * Configure SubgraphService + * + * In the current contract version, RecurringCollector is set as an immutable + * constructor argument — no runtime authorization is needed. + * + * This script is a no-op placeholder for future configuration needs. + * + * Usage: + * pnpm hardhat deploy --tags SubgraphService:configure --network + */ +export default createActionModule( + Contracts['subgraph-service'].SubgraphService, + DeploymentActions.CONFIGURE, + async (env) => { + env.showMessage(`\n✅ SubgraphService: RecurringCollector is set at construction time, no configuration needed\n`) + }, +) diff --git a/packages/deployment/deploy/service/subgraph/09_end.ts b/packages/deployment/deploy/service/subgraph/09_end.ts new file mode 100644 index 000000000..786490018 --- /dev/null +++ b/packages/deployment/deploy/service/subgraph/09_end.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createEndModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createEndModule(Contracts['subgraph-service'].SubgraphService) diff --git a/packages/deployment/deploy/service/subgraph/10_status.ts b/packages/deployment/deploy/service/subgraph/10_status.ts new file mode 100644 index 000000000..aa66de54e --- /dev/null +++ b/packages/deployment/deploy/service/subgraph/10_status.ts @@ -0,0 +1,4 @@ +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { createStatusModule } from '@graphprotocol/deployment/lib/script-factories.js' + +export default createStatusModule(Contracts['subgraph-service'].SubgraphService) diff --git a/packages/deployment/docs/Architecture.md b/packages/deployment/docs/Architecture.md new file mode 100644 index 000000000..2704a722f --- /dev/null +++ b/packages/deployment/docs/Architecture.md @@ -0,0 +1,61 @@ +# Deployment Package Architecture + +Unified deployment package for Graph Protocol contracts. + +## Design Principles + +- **No local Solidity sources** - Uses external artifacts from sibling packages +- **Single deployment system** - All protocol contracts deployed from one place +- **Component organization** - Deploy scripts organized by component (issuance, contracts, subgraph-service) + +## Structure + +``` +packages/deployment/ +├── deploy/ # hardhat-deploy / rocketh scripts +│ ├── common/ # 00_sync.ts +│ ├── horizon/ # RM, HS, PE, L2Curation, RC +│ ├── service/ # SubgraphService, DisputeManager +│ ├── allocate/ # IssuanceAllocator, DefaultAllocation, DirectAllocation +│ ├── agreement/ # RecurringAgreementManager +│ ├── rewards/ # RewardsEligibilityOracle (A/B/mock), Reclaim +│ └── gip/0088/ # GIP-0088 goal orchestration (upgrade phase + activation) +├── lib/ # Shared utilities (preconditions, contract registry, tags, ABIs, ...) +├── tasks/ # Hardhat tasks (deploy:*) +├── docs/ # This documentation +└── test/ # Unit tests (bytecode, registry, tx-builder, ...) +``` + +## Tags + +Two-dimensional tag model. See [`lib/deployment-tags.ts`](../lib/deployment-tags.ts) for the source of truth. + +| Kind | Examples | Purpose | +| --------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| Special | `sync` | Sync address books, import contracts | +| Component | `IssuanceAllocator`, `RewardsManager`, `RecurringAgreementManager`, `RewardsEligibilityOracleA`, ... | One per deployable contract | +| Action verb | `deploy`, `upgrade`, `configure`, `transfer`, `integrate`, `all` | Combined with a component or goal tag to gate work | +| Goal scope | `GIP-0088`, `GIP-0088:upgrade` | Multi-component orchestration for a deployment | +| Activation goal | `GIP-0088:eligibility-integrate`, `GIP-0088:issuance-connect`, `GIP-0088:issuance-allocate` | Per-step governance TX for the activation phases | +| Optional goal | `GIP-0088:eligibility-revert`, `GIP-0088:issuance-close-guard` | Excluded from `--tags ...,all` — must be requested explicitly | + +## External Artifacts + +Artifacts are loaded directly in deploy scripts via `require.resolve()`: + +```typescript +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) + +// Load artifact from sibling package +const artifactPath = + require.resolve('@graphprotocol/horizon/artifacts/contracts/RewardsManager.sol/RewardsManager.json') +const artifact = JSON.parse(readFileSync(artifactPath, 'utf-8')) +``` + +This approach (vs Hardhat v2's `external: {}` config) allows more control over which artifacts are loaded and when. + +## See Also + +- [GovernanceWorkflow.md](./GovernanceWorkflow.md) - Governance execution +- [Design.md](./Design.md) - Technical design documentation diff --git a/packages/deployment/docs/DeploymentSetup.md b/packages/deployment/docs/DeploymentSetup.md new file mode 100644 index 000000000..4b4fd4f4d --- /dev/null +++ b/packages/deployment/docs/DeploymentSetup.md @@ -0,0 +1,224 @@ +# Deployment Setup and Flow + +Quick reference for setting up and running deployments on testnet/mainnet. + +## Prerequisites + +- Node.js 18+ +- pnpm +- Foundry (for fork testing): `curl -L https://foundry.paradigm.xyz | bash && foundryup` + +## Initial Setup + +### 1. Install Dependencies + +```bash +pnpm install +pnpm build +``` + +### 2. Configure Secrets (Keystore) + +Use Hardhat's encrypted keystore for secure secret storage. +Keys are network-specific: + +```bash +# Deployer keys (required per network) +npx hardhat keystore set ARBITRUM_SEPOLIA_DEPLOYER_KEY +npx hardhat keystore set ARBITRUM_ONE_DEPLOYER_KEY + +# Governor keys for EOA execution (testnet only) +npx hardhat keystore set ARBITRUM_SEPOLIA_GOVERNOR_KEY +``` + +**Keystore commands:** + +```bash +npx hardhat keystore list # View stored keys +npx hardhat keystore get # Retrieve a value +npx hardhat keystore delete # Remove a secret +npx hardhat keystore path # Show keystore location +npx hardhat keystore change-password # Update password +``` + +**Development keystore** (no password, for non-sensitive values): + +```bash +npx hardhat keystore set --dev ARBITRUM_SEPOLIA_DEPLOYER_KEY +``` + +**Environment override** (CI/CD): + +```bash +export ARBITRUM_SEPOLIA_DEPLOYER_KEY=0x... +``` + +### 3. Verify Setup + +```bash +npx hardhat deploy:check-deployer --network arbitrumSepolia +``` + +## Deployment Flow (Testnet/Mainnet) + +### Step 1: Check Status + +```bash +npx hardhat deploy:status --network arbitrumSepolia +``` + +### Step 2: Sync Address Books + +Always sync first to ensure local state matches on-chain: + +```bash +npx hardhat deploy --skip-prompts --network arbitrumSepolia --tags sync +``` + +### Step 3: Deploy + +```bash +npx hardhat deploy --skip-prompts --network arbitrumSepolia --tags +``` + +If governance action is required, the deployment will: + +1. Generate TX batch in `txs/arbitrumSepolia/*.json` +2. Exit with code 1 (expected - waiting for governance) + +### Step 4: Execute Governance + +**EOA Governor (testnet):** + +```bash +# If stored in keystore, just run directly (prompts for password) +npx hardhat deploy:execute-governance --network arbitrumSepolia + +# Or via environment variable +ARBITRUM_SEPOLIA_GOVERNOR_KEY=0x... npx hardhat deploy:execute-governance --network arbitrumSepolia +``` + +**Safe Multisig (mainnet):** + +1. Go to [Safe Transaction Builder](https://app.safe.global/) +2. Connect governor Safe wallet +3. Apps > Transaction Builder > Upload JSON +4. Select `txs/arbitrumSepolia/*.json` +5. Create batch > Collect signatures > Execute + +### Step 5: Sync After Governance + +```bash +npx hardhat deploy --skip-prompts --network arbitrumSepolia --tags sync +``` + +### Step 6: Continue Deployment + +Re-run the deploy command - it will continue from where it left off: + +```bash +npx hardhat deploy --skip-prompts --network arbitrumSepolia --tags +``` + +## Quick Reference + +| Network | Chain ID | RPC (default) | +| --------------- | -------- | ---------------------------------------- | +| localNetwork | 1337 | `http://chain:8545` | +| arbitrumSepolia | 421614 | | +| arbitrumOne | 42161 | | + +| Key Pattern | Purpose | Storage | +| ------------------------ | ---------------------- | ------------------- | +| `_DEPLOYER_KEY` | Contract deployment | Keystore or env var | +| `_GOVERNOR_KEY` | EOA governor execution | Keystore or env var | +| `ARBISCAN_API_KEY` | Contract verification | Keystore or env var | +| `ARBITRUM_ONE_RPC` | Custom RPC URL | Environment | +| `ARBITRUM_SEPOLIA_RPC` | Custom RPC URL | Environment | + +`` = `ARBITRUM_SEPOLIA` or `ARBITRUM_ONE` + +## Contract Verification + +Since deployment uses external artifacts, **verify from the source package**: + +```bash +# Set API key (in source package or deployment package) +npx hardhat keystore set ARBISCAN_API_KEY + +# Verify from source package (has source code + compiler settings) +cd packages/horizon +npx hardhat verify --network arbitrumSepolia +``` + +For deploy scripts that run verification automatically, export the API key: + +```bash +export ARBISCAN_API_KEY=$(npx hardhat keystore get ARBISCAN_API_KEY) +npx hardhat deploy --skip-prompts --network arbitrumSepolia --tags +``` + +## Tagging Deployments (WIP) + +> This convention is a work in progress — feedback and changes welcome. + +After a deployment is committed, create an annotated git tag to record the deployment. +Tags use `deploy/{mainnet|testnet}/YYYY-MM-DD` format. The annotation is auto-generated +from address book diffs, listing which contracts changed. + +**Requires:** `jq` (`sudo apt install jq` / `brew install jq`) + +### Usage + +```bash +# Preview first +./scripts/tag-deployment.sh \ + --deployer "packages/deployment --tags RewardsManager" \ + --network arbitrumSepolia \ + --base main \ + --dry-run + +# Create the tag +./scripts/tag-deployment.sh \ + --deployer "packages/deployment --tags RewardsManager" \ + --network arbitrumSepolia \ + --base main + +# Push +git push origin deploy/testnet/2026-03-02 +``` + +The `--deployer` argument is free-form — describe what performed the deployment: + +- `"packages/deployment --tags RewardsManager,SubgraphService"` +- `"packages/horizon ignition migrate"` +- `"manual: forge script DeployFoo"` + +### Workflow + +1. Deploy contracts and update address books +2. Commit the address book changes +3. Run `tag-deployment.sh` (tag must point to a finalized commit) +4. Push branch and tag + +### Options + +| Option | Description | +| ------------------- | --------------------------------------------- | +| `--deployer ` | What performed the deployment (required) | +| `--network ` | `arbitrumOne` or `arbitrumSepolia` (required) | +| `--base ` | Git ref to diff against (default: `HEAD~1`) | +| `--dry-run` | Preview without creating tag | +| `--sign` | Force-sign the tag with `-s` | + +### Viewing tags + +```bash +git tag -l 'deploy/*' # List all deployment tags +git show --no-patch deploy/testnet/... # View tag annotation +``` + +## See Also + +- [LocalForkTesting.md](./LocalForkTesting.md) - Fork-based testing workflow +- [GovernanceWorkflow.md](./GovernanceWorkflow.md) - Detailed governance execution diff --git a/packages/deployment/docs/Design.md b/packages/deployment/docs/Design.md new file mode 100644 index 000000000..6eec92811 --- /dev/null +++ b/packages/deployment/docs/Design.md @@ -0,0 +1,244 @@ +# Deployment Package Design + +High-level architecture for the unified deployment system. + +**See also:** + +- [Architecture.md](./Architecture.md) - Package structure and organization +- [deploy/ImplementationPrinciples.md](./deploy/ImplementationPrinciples.md) - Deploy script patterns and conventions + +## Components + +**Deployed by this package:** + +- IssuanceAllocator - Upgradeable proxy managing issuance distribution +- RewardsEligibilityOracle - Upgradeable proxy for eligibility verification +- ReclaimedRewards (DirectAllocation) - Upgradeable proxy for default reclaim address +- RecurringAgreementManager - Upgradeable proxy for agreement-based payments + +**Referenced contracts** (already deployed): + +- RewardsManager (from @graphprotocol/contracts or @graphprotocol/horizon) +- GraphToken (from @graphprotocol/contracts) +- GraphProxyAdmin (from @graphprotocol/contracts or @graphprotocol/horizon) + +## Directory Structure + +``` +packages/deployment/ +├── deploy/ # Numbered deployment scripts (rocketh + hardhat-deploy) +│ ├── common/ # 00_sync.ts +│ ├── horizon/ # RewardsManager, HorizonStaking, PaymentsEscrow, L2Curation, RecurringCollector +│ ├── service/ # SubgraphService, DisputeManager +│ ├── allocate/ # IssuanceAllocator, DefaultAllocation, DirectAllocation impl +│ ├── agreement/ # RecurringAgreementManager +│ ├── rewards/ # RewardsEligibilityOracle (A/B/mock), Reclaim +│ └── gip/0088/ # GIP-0088 goal orchestration +├── lib/ # Shared utilities (preconditions, registry, tags, ABIs, governance) +├── tasks/ # Hardhat tasks (deploy:*) +├── docs/ # Architecture and operational documentation +│ └── deploy/ # Deploy-script principles and per-component design notes +└── test/ # Unit tests +``` + +## Governance Model + +### Three-Phase Workflow + +1. **Prepare** (permissionless) - Deploy new implementations, generate TX batches +2. **Execute** (governance) - Execute Safe TX batch for state transitions +3. **Verify** (permissionless) - Verify integration, sync address books + +### Proxy Administration + +Two distinct proxy patterns coexist: + +- **Legacy `GraphProxy`** (custom Graph Protocol pattern) — used by RewardsManager, HorizonStaking, L2Curation, EpochManager. A single shared `GraphProxyAdmin` (owned by governance) controls upgrades for all of them. +- **OZ v5 `TransparentUpgradeableProxy`** — used by every new contract this package deploys (IssuanceAllocator, DefaultAllocation, ReclaimedRewards, RecurringAgreementManager, RewardsEligibilityOracle A/B, RecurringCollector, SubgraphService, DisputeManager, PaymentsEscrow). Each proxy gets its own per-proxy `ProxyAdmin` created by the proxy constructor; ownership is transferred to governance in the transfer step. + +```mermaid +graph TB + Gov[Governance Multi-sig] + GraphAdmin[GraphProxyAdmin] + + subgraph "Legacy GraphProxy" + RM[RewardsManager] + HS[HorizonStaking] + L2C[L2Curation] + end + + subgraph "OZ v5 TransparentUpgradeableProxy
(per-proxy admin)" + IA[IssuanceAllocator] + DA[DefaultAllocation] + Reclaim[ReclaimedRewards] + RAM[RecurringAgreementManager] + REO[RewardsEligibilityOracle A/B] + RC[RecurringCollector] + end + + Gov -->|owns| GraphAdmin + GraphAdmin -->|upgrades| RM + GraphAdmin -->|upgrades| HS + GraphAdmin -->|upgrades| L2C + + Gov -.->|owns each per-proxy admin| IA + Gov -.->|owns each per-proxy admin| DA + Gov -.->|owns each per-proxy admin| Reclaim + Gov -.->|owns each per-proxy admin| RAM + Gov -.->|owns each per-proxy admin| REO + Gov -.->|owns each per-proxy admin| RC +``` + +**Key principle:** Every proxy admin is governance-owned. Legacy contracts share a single `GraphProxyAdmin`; new contracts each have their own per-proxy admin created at construction. + +## Contract Integration + +### RewardsEligibilityOracle Integration + +```mermaid +graph LR + REO[RewardsEligibilityOracle] + RM[RewardsManager] + Oracles[Off-chain Oracles] + + Oracles -->|set eligibility| REO + RM -->|check eligibility| REO +``` + +**Integration:** `RewardsManager.setProviderEligibilityOracle(REO)` via governance + +### IssuanceAllocator Integration + +```mermaid +graph TB + GT[GraphToken] + IA[IssuanceAllocator] + + subgraph "Allocator Minting" + RAM[RecurringAgreementManager] + end + + subgraph "Self Minting" + RM[RewardsManager] + end + + GT -->|minting authority| IA + IA -->|distributes to| RAM + IA -->|allocates to| RM +``` + +**Integration:** + +- `RewardsManager.setIssuanceAllocator(IA)` via governance +- `GraphToken.addMinter(IA)` via governance + +### Contract Dependencies + +```mermaid +graph TD + GraphToken[GraphToken] + RewardsManager[RewardsManager] + + RewardsEligibilityOracle[RewardsEligibilityOracle] + IssuanceAllocator[IssuanceAllocator] + RecurringAgreementManager[RecurringAgreementManager] + + RewardsManager -.->|queries| RewardsEligibilityOracle + IssuanceAllocator -.->|integrates with| RewardsManager + IssuanceAllocator -.->|mints from| GraphToken + IssuanceAllocator -.->|distributes to| RecurringAgreementManager + RecurringAgreementManager -.->|funds| PaymentsEscrow +``` + +## Address Book Management + +### Pending Implementation Pattern + +Deployment tracks both active and pending implementations: + +```json +{ + "IssuanceAllocator": { + "address": "0x9fE46...", + "implementation": { + "address": "0xe7f17..." + }, + "pendingImplementation": { + "address": "0x5FbDB...", + "readyForUpgrade": true + } + } +} +``` + +### Upgrade Workflow + +```mermaid +sequenceDiagram + participant Deployer + participant AB as Address Book + participant Proxy + participant Gov as Governance + + Note over Deployer,Gov: Phase 1: Prepare + Deployer->>AB: Deploy new implementation + AB->>AB: Set pendingImplementation + + Note over Deployer,Gov: Phase 2: Execute + Deployer->>Gov: Generate Safe TX batch + Gov->>Proxy: Execute upgrade + Proxy->>Proxy: Update implementation pointer + + Note over Deployer,Gov: Phase 3: Verify + Deployer->>AB: Sync (--tags sync) + AB->>AB: Move pending → active +``` + +## Deployment Workflow + +### Proxy Deployment and Upgrade + +```mermaid +sequenceDiagram + participant Deployer + participant Deploy as rocketh + participant Admin as ProxyAdmin (per-proxy) + participant Impl as Implementation + participant Proxy as TransparentUpgradeableProxy + participant Gov as Governance + + Note over Deployer,Gov: Initial Deployment + Deployer->>Deploy: --tags Component,deploy + Deploy->>Impl: Deploy implementation + Deploy->>Proxy: Deploy proxy (constructor creates per-proxy Admin) + Proxy->>Impl: Initialize with deployer as governor + + Note over Deployer,Gov: Configure + Deployer->>Deploy: --tags Component,configure + Deploy->>Proxy: Set params, grant roles to gov + pause guardian + + Note over Deployer,Gov: Transfer + Deployer->>Deploy: --tags Component,transfer + Deploy->>Proxy: Revoke deployer GOVERNOR_ROLE + Deploy->>Admin: Transfer ProxyAdmin ownership to Gov + + Note over Deployer,Gov: Implementation Upgrade + Deployer->>Deploy: --tags Component,upgrade + Deploy->>Impl: Deploy new implementation + Deploy->>Deploy: Save governance TX batch + Gov->>Admin: Execute upgrade TX + Admin->>Proxy: upgradeAndCall(newImpl) + + Note over Deployer,Gov: Sync + Deployer->>Deploy: --tags sync + Deploy->>Proxy: Read current implementation + Deploy->>Deploy: Update address book (pending → active) +``` + +## Conventions + +- TypeScript throughout (.ts) +- TitleCase for documentation +- Deploy script patterns: [ImplementationPrinciples.md](./deploy/ImplementationPrinciples.md) +- Deploy scripts sync the contracts they touch immediately before/after their action via `syncComponentFromRegistry`/`syncComponentsFromRegistry`. The full + global sync is opt-in via `npx hardhat deploy:sync` and is no longer an automatic dependency of every component script. diff --git a/packages/deployment/docs/Gip0088.md b/packages/deployment/docs/Gip0088.md new file mode 100644 index 000000000..3afd7d815 --- /dev/null +++ b/packages/deployment/docs/Gip0088.md @@ -0,0 +1,241 @@ +# GIP-0088: Deployment Guide + +Protocol upgrade deploying the Issuance Allocator, Rewards Eligibility Oracle, and on-chain indexing agreements, as specified by [GIP-0088](https://github.com/graphprotocol/graph-improvement-proposals/blob/main/gips/0088.md). + +## Related GIPs + +| GIP | Title | What it specifies | +| ----------------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| [GIP-0076](https://github.com/graphprotocol/graph-improvement-proposals/blob/main/gips/0076.md) | Issuance Allocator | Contract spec: governance-controlled issuance distribution across self-minting and allocator-minting targets | +| [GIP-0079](https://github.com/graphprotocol/graph-improvement-proposals/blob/main/gips/0079.md) | Rewards Eligibility Oracle | Contract spec: quality-of-service gating on indexing rewards via authorized oracle | +| [GIP-0086](https://github.com/graphprotocol/graph-improvement-proposals/blob/main/gips/0086.md) | RM and SS Upgrade | Contract upgrades: RM gains eligibility oracle hook + issuance allocator integration; SS gains agreement support | +| [GIP-0087](https://github.com/graphprotocol/graph-improvement-proposals/blob/main/gips/0087.md) | On-Chain Indexing Agreements | Contract spec: RecurringCollector, RecurringAgreementManager, indexing agreement lifecycle in SubgraphService | +| [GIP-0088](https://github.com/graphprotocol/graph-improvement-proposals/blob/main/gips/0088.md) | IA Deployment and IP Config | **Deployment proposal**: deploy IA (0076), connect to upgraded RM (0086), allocate to RAM (0087) | + +## Contracts + +### New contracts (deploy) + +| Contract | Package | GIP | Purpose | +| ------------------------------ | -------- | ---- | ----------------------------------------------------------- | +| IssuanceAllocator | issuance | 0076 | Governance-managed issuance distribution across targets | +| DefaultAllocation | issuance | 0076 | Default target safety net for unallocated issuance | +| ReclaimedRewards | issuance | 0076 | Default reclaim destination for reclaimed rewards | +| RecurringCollector | horizon | 0087 | EIP-712 collector for recurring payment agreement lifecycle | +| RecurringAgreementManager | issuance | 0087 | Protocol-funded indexing agreements and escrow management | +| RewardsEligibilityOracle (A/B) | issuance | 0079 | Quality-of-service gating on indexing rewards | + +### Existing contracts (upgrade implementation) + +| Contract | Package | GIP | Key changes | +| --------------- | ---------------- | --------- | ------------------------------------------------------------------------------------------------- | +| RewardsManager | contracts | 0086 | `setIssuanceAllocator()`, `IProviderEligibility` integration, `revertOnIneligible`, reclaim infra | +| SubgraphService | subgraph-service | 0086/0087 | Indexing agreement lifecycle, `enforceService`, `recurringCollector` integration | +| DisputeManager | subgraph-service | 0086/0087 | `createIndexingFeeDisputeV1()`, removes legacy dispute creation | +| HorizonStaking | horizon | 0086 | Removes HorizonStakingExtension, consolidates functionality | +| PaymentsEscrow | horizon | 0087 | `adjustThaw()` for payer thaw modification | +| L2Curation | contracts | 0086 | Removes staking as authorized `collect()` caller | + +## Deploy Scripts + +### GIP-0088 scripts (`deploy/gip/0088/`) + +**Upgrade phase** (`upgrade/`) — deploys, configures, transfers, and upgrades ALL contracts: + +| Script | `--tags` | What it does | +| -------------- | ---------------------------- | ------------------------------------------------------------------------------------- | +| `01_deploy` | `GIP-0088:upgrade,deploy` | Deploy all new contracts + implementations | +| `02_configure` | `GIP-0088:upgrade,configure` | Deployer-only configure: role grants and params on contracts where deployer is gov | +| `03_transfer` | `GIP-0088:upgrade,transfer` | Transfer governance of new contracts (revoke deployer role + ProxyAdmin to gov) | +| `04_upgrade` | `GIP-0088:upgrade,upgrade` | Bundle proxy upgrades + all deferred configure into one governance TX batch (details) | +| `10_status` | `GIP-0088:upgrade` | Show upgrade state and next step | + +`04_upgrade` builds a single governance TX batch containing: + +| Group | Items | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Proxy upgrades | Iterates registry; for any deployable proxy with `pendingImplementation`, adds the proxy upgrade TX | +| Existing-contract config | `RC.setPauseGuardian`, `RM.setDefaultReclaimAddress` | +| Deferred new-contract config | IA: `setIssuancePerBlock`, role grants. DA: role grants. RAM: role grants + `setIssuanceAllocator`. Reclaim: role grants. REO A/B: params + role grants | + +Items in groups 2 and 3 are added only when not already on-chain. The bundle exists because configure runs as the deployer and skips anything that requires `GOVERNOR_ROLE` on contracts the deployer doesn't yet control (or that depend on RM being upgraded). + +**Activation goals** — governance TXs that change protocol behaviour (after upgrade complete): + +| Script | `--tags` | What it does | +| ----------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `eligibility_integrate` | `GIP-0088:eligibility-integrate` | `RM.setProviderEligibilityOracle(REO_A)` | +| `issuance_connect` | `GIP-0088:issuance-connect` | `GraphToken.addMinter(IA)` → `RM.setIssuanceAllocator(IA)` → `IA.setTargetAllocation(RM, 0, rate)` (RM as 100% self-minting target) → `IA.setDefaultTarget(DA)` (safety net) | +| `issuance_allocate` | `GIP-0088:issuance-allocate` | `IA.setTargetAllocation(RAM, allocatorRate, selfRate)` (rates from `config/.json5`) | + +**Optional goals** — not planned for initial deployment: + +| Script | `--tags` | What it does | +| ---------------------- | ------------------------------- | ------------------------------------------------------- | +| `eligibility_revert` | `GIP-0088:eligibility-revert` | `RM.setRevertOnIneligible(true)` | +| `issuance_close_guard` | `GIP-0088:issuance-close-guard` | `SS.setBlockClosingAllocationWithActiveAgreement(true)` | + +**Overall** — `09_end` (`GIP-0088,all`) verifies all non-optional goals. `10_status` (`GIP-0088`) shows full deployment state. + +### Component lifecycle scripts + +Each contract has its own lifecycle scripts under `deploy/`. The GIP-0088 upgrade phase depends on component tags — it orchestrates the component scripts rather than duplicating their logic. + +## Deployment Process + +### How `--tags` drives the deployment + +The upgrade phase tag (`GIP-0088:upgrade`) combined with an action verb (`deploy`, `configure`, `transfer`, `upgrade`) selects which lifecycle step runs. Activation goals have their own tags. + +- `--tags GIP-0088:upgrade,deploy` — deploy all contracts +- `--tags GIP-0088:upgrade,configure` — configure all contracts +- `--tags GIP-0088:upgrade,transfer` — transfer to governance control +- `--tags GIP-0088:upgrade,upgrade` — generate proxy upgrade TX batch +- `--tags GIP-0088:upgrade` — show status and next step +- `--tags GIP-0088:eligibility-integrate` — integrate REO with RM (governance TX) +- `--tags GIP-0088:issuance-connect` — connect IA to RM + minter role (governance TX) +- `--tags GIP-0088:issuance-allocate` — allocate issuance to RAM (governance TX) +- `--tags GIP-0088` — overall status + +All scripts are idempotent — they check on-chain state and skip if already done. Scripts do not presume a particular starting state. + +Sync runs automatically as a dependency of all scripts. + +### Deployment sequence + +```bash +# Deploy and configure all contracts +pnpm hardhat deploy --tags GIP-0088:upgrade,deploy --network +pnpm hardhat deploy --tags GIP-0088:upgrade,configure --network + +# Check status before transferring governance +pnpm hardhat deploy --tags GIP-0088:upgrade --network + +# Transfer governance — after this, deployer has no special access +pnpm hardhat deploy --tags GIP-0088:upgrade,transfer --network + +# Generate proxy upgrade governance TX batch +pnpm hardhat deploy --tags GIP-0088:upgrade,upgrade --network +# → execute governance TXs (see Environments below) + +# Activation goals (each generates governance TXs independently) +pnpm hardhat deploy --tags GIP-0088:eligibility-integrate --network +pnpm hardhat deploy --tags GIP-0088:issuance-connect --network +pnpm hardhat deploy --tags GIP-0088:issuance-allocate --network +# → execute governance TXs + +# Verify +pnpm hardhat deploy --tags GIP-0088 --network +``` + +### Preconditions + +Each script checks its own preconditions and skips if not met. Scripts do not presume a particular starting state — they are goal-seeking, not sequential steps. + +#### Deploy (`GIP-0088:upgrade,deploy`) + +| Contract | Precondition | Notes | +| ------------------------------------------ | ------------ | ----------------------------------------------------- | +| RC | — | No dependencies | +| SS implementation | RC deployed | SS has RC address baked into bytecode via `Directory` | +| RM, HS, DM, PE, L2Curation implementations | — | No deploy-time dependencies | +| IA, DefaultAllocation, Reclaim | — | Independent | +| RAM | — | Independent | +| REO A, REO B | — | Independent | + +#### Configure (`GIP-0088:upgrade,configure`) + +| Contract | Precondition | Notes | +| -------- | --------------------------------- | ---------------------------------------------------------------------------------------------- | +| RC | Deployed | setPauseGuardian | +| IA | Deployed, 0 < RM.issuancePerBlock | Rates, RM as 100% self-minting target, grant governor/pause roles | +| DA | Deployed (+ IA deployed) | Grant governor/pause roles, set as IA default target | +| REO A/B | Deployed | Grant governor/pause/operator roles. Validation enabled by operator post-deploy. | +| RAM | Deployed (+ RC, SS, IA deployed) | Grant governor/pause/collector/data-service roles, set issuance allocator | +| Reclaim | Deployed | Grant governor/pause roles | +| Reclaim | RM upgraded | Sets RM.defaultReclaimAddress — skips if RM not yet upgraded (handled by `04_upgrade` instead) | + +#### Transfer (`GIP-0088:upgrade,transfer`) + +| Contract | Precondition | Notes | +| -------- | ------------------------------- | --------------------------------------------------------------------------- | +| RC | Deployed | ProxyAdmin only — RC has no `GOVERNOR_ROLE`. Skips if owner is not deployer | +| IA | Configured | Revokes deployer GOVERNOR_ROLE, transfers ProxyAdmin | +| DA | Configured | Revokes deployer GOVERNOR_ROLE, transfers ProxyAdmin | +| RAM | Configured | Revokes deployer GOVERNOR_ROLE, transfers ProxyAdmin | +| Reclaim | Configured | Revokes deployer GOVERNOR_ROLE, transfers ProxyAdmin | +| REO A | Configured (all conditions met) | Revokes deployer GOVERNOR_ROLE, transfers ProxyAdmin | +| REO B | Configured (all conditions met) | Revokes deployer GOVERNOR_ROLE, transfers ProxyAdmin | + +#### Upgrade (`GIP-0088:upgrade,upgrade`) + +State-driven: builds a single governance TX batch from three groups. Each group skips items already on-chain. + +| Group | Items | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Proxy upgrades | Iterates registry; for any deployable proxy with `pendingImplementation`, adds proxy upgrade TX | +| Existing-contract config | `RC.setPauseGuardian(pauseGuardian)`; `RM.setDefaultReclaimAddress(reclaim)` (only after RM upgrade — bundle order means RM upgrade executes first in the same batch) | +| Deferred new-contract config | IA: `setIssuancePerBlock`, `grantRole(GOVERNOR/PAUSE)`. DA: `grantRole(GOVERNOR/PAUSE)`. RAM: `grantRole(COLLECTOR/DATA_SERVICE/GOVERNOR/PAUSE)` + `setIssuanceAllocator`. Reclaim: `grantRole(GOVERNOR/PAUSE)`. REO A/B: param setters + role grants. | + +These deferred items exist because configure runs as the deployer and skips items requiring `GOVERNOR_ROLE` on contracts the deployer doesn't yet control, or items that depend on RM being upgraded. + +#### Activation goals + +| Goal | Precondition | Notes | +| ----------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `eligibility-integrate` | RM upgraded, REO A deployed, oracle not already set | `RM.setProviderEligibilityOracle(REO_A)`. Skips if any oracle already set (does not override). | +| `issuance-connect` | RM upgraded, IA deployed + configured (rate matches RM) | Builds TX batch in order: `GraphToken.addMinter(IA)` → `RM.setIssuanceAllocator(IA)` → `IA.setTargetAllocation(RM, 0, rate)` → `IA.setDefaultTarget(DA)`. Order matters: `setTargetAllocation` calls `RM.onIssuanceChange` which requires the allocator already be set. **Exits on invariant failure** (IA rate ≠ RM rate). | +| `issuance-allocate` | IA deployed, RAM deployed, issuance-connect done | `IA.setTargetAllocation(RAM, allocatorMintingRate, selfMintingRate)`. Rates from `config/.json5`, skips if both are 0. | + +#### Optional goals + +| Goal | Precondition | Notes | +| ---------------------- | -------------------------------------- | ----------------------------------------------------- | +| `eligibility-revert` | RM upgraded (supports IRewardsManager) | RM.setRevertOnIneligible(true) | +| `issuance-close-guard` | SS upgraded | SS.setBlockClosingAllocationWithActiveAgreement(true) | + +### Environments + +The same commands apply to all environments. What differs is how governance TXs are executed. + +| Environment | Governance execution | Speed | +| ----------------- | ------------------------------------------------- | -------- | +| Fork (localhost) | `deploy:execute-governance` impersonates governor | Instant | +| Testnet (Sepolia) | `deploy:execute-governance` signs with EOA key | ~minutes | +| Mainnet (Arb One) | TX batch uploaded to Safe for council multisig | ~days | + +#### Fork testing + +Validates the full flow using account impersonation. See [LocalForkTesting.md](LocalForkTesting.md). + +```bash +anvil --fork-url --chain-id 31337 +pnpm hardhat deploy:reset-fork --network localhost + +# Deploy, configure, transfer +pnpm hardhat deploy --tags GIP-0088:upgrade,deploy --network localhost --skip-prompts +pnpm hardhat deploy --tags GIP-0088:upgrade,configure --network localhost --skip-prompts +pnpm hardhat deploy --tags GIP-0088:upgrade,transfer --network localhost --skip-prompts + +# Proxy upgrades +pnpm hardhat deploy --tags GIP-0088:upgrade,upgrade --network localhost --skip-prompts +pnpm hardhat deploy:execute-governance --network localhost + +# Activation +pnpm hardhat deploy --tags GIP-0088:eligibility-integrate --network localhost --skip-prompts +pnpm hardhat deploy:execute-governance --network localhost +pnpm hardhat deploy --tags GIP-0088:issuance-connect --network localhost --skip-prompts +pnpm hardhat deploy:execute-governance --network localhost +pnpm hardhat deploy --tags GIP-0088:issuance-allocate --network localhost --skip-prompts +pnpm hardhat deploy:execute-governance --network localhost + +# Verify +pnpm hardhat deploy --tags GIP-0088 --network localhost --skip-prompts +``` + +## See Also + +- [GovernanceWorkflow.md](GovernanceWorkflow.md) — governance TX generation and execution across environments +- [LocalForkTesting.md](LocalForkTesting.md) — fork mode testing setup and workflow +- [Architecture.md](Architecture.md) — deployment package architecture +- [deploy/ImplementationPrinciples.md](deploy/ImplementationPrinciples.md) — patterns and rules for deploy scripts diff --git a/packages/deployment/docs/GovernanceWorkflow.md b/packages/deployment/docs/GovernanceWorkflow.md new file mode 100644 index 000000000..7b4ade2ed --- /dev/null +++ b/packages/deployment/docs/GovernanceWorkflow.md @@ -0,0 +1,373 @@ +# Governance Transaction Workflow + +This document explains how governance transactions are executed in different deployment modes. + +## Overview + +Graph Protocol uses a Governor (typically a Safe multisig) to control protocol upgrades and configuration. The deployment system generates transaction batches that must be executed by the Governor. + +## Fork Mode (Testing) + +In fork mode, governance transactions can be executed automatically via account impersonation for testing purposes. + +### Setup + +```bash +# Ephemeral: run deployment directly (state lost on exit) +FORK_NETWORK=arbitrumSepolia npx hardhat deploy --tags IssuanceAllocator:deploy --network fork + +# Or persistent: start anvil in Terminal 1, run deploys in Terminal 2 +# See LocalForkTesting.md for persistent fork setup +``` + +### Execution + +When a deployment generates a governance TX batch: + +1. The TX batch is saved to `fork/fork/arbitrumSepolia/txs/*.json` +2. The script returns (it does **not** exit) — subsequent scripts in the run keep going and check their own preconditions, so a single command can produce several TX batches +3. Execute the saved governance TXs: + + ```bash + npx hardhat deploy:execute-governance --network fork + ``` + +4. This uses `hardhat_impersonateAccount` to execute as the governor +5. Re-run the deployment command to continue past the governance boundary + +## Testnet Mode with EOA Governor + +**Note:** Safe Transaction Builder may not be available on all testnets (e.g., Arbitrum Sepolia may not be supported). For testnet deployments, use an EOA governor or fork mode for testing. + +If your testnet governor is an EOA (regular wallet) rather than a Safe multisig, you can execute governance transactions directly using the governor's private key. + +### Setup + +```bash +export DEPLOYER_PRIVATE_KEY=0xYOUR_DEPLOYER_KEY +export GOVERNOR_PRIVATE_KEY=0xYOUR_GOVERNOR_KEY +``` + +### Execution + +When a deployment generates a governance TX batch: + +1. The TX batch is saved to `txs/arbitrumSepolia/*.json` +2. Execute directly with the governor private key: + + ```bash + npx hardhat deploy:execute-governance --network arbitrumSepolia + ``` + +3. The system will: + - Detect that governor is an EOA + - Use GOVERNOR_PRIVATE_KEY to sign and send transactions + - Move executed batches to `executed/` subdirectory +4. Continue with deployments + +**Note:** This only works when the governor is an EOA. If the governor is a Safe multisig, you must use the Safe UI workflow below. + +### Testing Safe Transaction Builder Format + +Even with an EOA governor, you can validate the Safe Transaction Builder JSON format: + +1. Transaction batch files are always created in `txs//*.json` +2. These files use Safe Transaction Builder format (work with both EOA and Safe) +3. To test the format before mainnet: + - Go to + - Apps → Transaction Builder + - Upload the JSON file + - Review decoded transactions + - (Don't execute - this is just format validation) + +## Mainnet/Production Mode with Safe Multisig + +On mainnet (and testnets where Safe is deployed), governance transactions with Safe multisig governors MUST be executed via Safe UI. + +**Important:** Safe Transaction Builder is not available on all networks. Check to verify your network is supported. For testnets without Safe support (like Arbitrum Sepolia), use an EOA governor or fork mode for testing. + +### Workflow + +#### 1. Deploy and Generate TX Batches + +```bash +export DEPLOYER_PRIVATE_KEY=0xYOUR_PRIVATE_KEY +npx hardhat deploy --tags IssuanceAllocator:deploy --network arbitrumSepolia +``` + +When governance action is required, the deployment will: + +- Generate a TX batch file in `txs/arbitrumSepolia/*.json` +- Display the file path +- Return (not exit) — the run continues and other scripts check their own preconditions + +#### 2. Review the TX Batch + +The generated JSON file contains all transaction details: + +```json +{ + "version": "1.0", + "chainId": "421614", + "createdAt": 1234567890, + "meta": { + "name": "IssuanceAllocator activation", + "description": "..." + }, + "transactions": [ + { + "to": "0x...", + "value": "0", + "data": "0x...", + "contractMethod": {...}, + "contractInputsValues": {...} + } + ] +} +``` + +#### 3. Execute via Safe Transaction Builder + +1. Go to [Safe Transaction Builder](https://app.safe.global/) +2. Connect to your Safe wallet (the one configured as Governor) +3. Navigate to "Transaction Builder" in the Safe UI +4. Click "Upload a JSON" and select the governance TX batch file +5. Review all transactions: + - Verify target addresses + - Check function calls and parameters + - Ensure chain ID matches your network +6. Create the transaction batch +7. Collect required signatures from Safe signers +8. Execute the transaction batch + +#### 4. Sync After Execution + +After the transactions are executed on-chain, sync the address books: + +```bash +npx hardhat deploy --tags sync --network arbitrumSepolia +``` + +This updates the address books with the new on-chain state. + +#### 5. Continue Deployment + +Re-run the original deployment command: + +```bash +npx hardhat deploy --tags IssuanceAllocator:deploy --network arbitrumSepolia +``` + +The deployment will detect that governance has executed and continue to the next steps. + +## Common Governance Operations + +### Contract Upgrades + +```bash +# 1. Deploy new implementation +npx hardhat deploy --tags RewardsManager:deploy --network arbitrumSepolia + +# This generates: txs/arbitrumSepolia/upgrade-RewardsManager.json + +# 2. Execute via Safe UI (see workflow above) + +# 3. Sync and verify +npx hardhat deploy --tags sync --network arbitrumSepolia +``` + +### Configuration Changes + +```bash +# Deploy and configure (generates governance TX if needed) +npx hardhat deploy --tags IssuanceActivation --network arbitrumSepolia + +# Execute via Safe UI + +# Sync and continue +npx hardhat deploy --tags sync --network arbitrumSepolia +``` + +## Governance TX File Locations + +The location of governance TX files depends on the deployment mode: + +### Fork Mode + +``` +fork///txs/*.json +``` + +Example: `fork/fork/arbitrumSepolia/txs/upgrade-RewardsManager.json` + +### Testnet/Mainnet + +``` +txs//*.json +``` + +Example: `txs/arbitrumSepolia/upgrade-RewardsManager.json` + +After execution, files are moved to: + +``` +txs//executed/*.json +``` + +## Execution Modes + +| Mode | When Used | Execution Method | Environment Variables | +| ---------------------- | ------------------------- | ---------------------------------------- | ------------------------------ | +| **Fork Impersonation** | Local testing | Automatic via hardhat_impersonateAccount | `FORK_NETWORK=arbitrumSepolia` | +| **EOA Direct** | Testnet with EOA governor | Automatic with private key | `GOVERNOR_PRIVATE_KEY=0x...` | +| **Safe Multisig** | Production/mainnet | Manual via Safe Transaction Builder | None (auto-detected) | + +**Fork mode is network-aware**: `FORK_NETWORK` is automatically ignored on real networks (arbitrumSepolia, arbitrumOne). Fork mode only activates on local networks (localhost, fork, hardhat), so you don't need to unset it when switching to real deployments. + +**Transaction batch files** (Safe Transaction Builder JSON format) are always created in `txs//*.json` regardless of execution mode. + +### Usage Examples + +**Local fork testing (ephemeral):** + +```bash +FORK_NETWORK=arbitrumSepolia npx hardhat deploy:execute-governance --network fork +``` + +**Fast testnet iteration (EOA):** + +```bash +export GOVERNOR_PRIVATE_KEY=0xYOUR_KEY +npx hardhat deploy:execute-governance --network arbitrumSepolia +``` + +**Production deployment (Safe):** + +```bash +npx hardhat deploy:execute-governance --network arbitrumOne +# Follow Safe Transaction Builder instructions in output +``` + +## Safety Features + +### Automatic Governor Detection + +The `deploy:execute-governance` command automatically detects the governor type: + +**For Safe Multisig Governors:** + +```bash +npx hardhat deploy:execute-governance --network arbitrumSepolia + +# Output: +# ❌ Cannot execute governance TXs on arbitrumSepolia (governor is a Safe multisig) +# Governor address: 0x... +# Governance transactions must be executed via Safe UI +``` + +**For EOA Governors (without private key):** + +```bash +npx hardhat deploy:execute-governance --network arbitrumSepolia + +# Output: +# ❌ Cannot execute governance TXs on arbitrumSepolia +# Governor address: 0x... (EOA) +# To execute governance TXs as EOA governor, set GOVERNOR_PRIVATE_KEY +``` + +**For EOA Governors (with private key):** + +```bash +export GOVERNOR_PRIVATE_KEY=0xYOUR_GOVERNOR_KEY +npx hardhat deploy:execute-governance --network arbitrumSepolia + +# Output: +# 🔓 Executing 1 governance TX batch(es)... +# Governor: 0x... (EOA) +``` + +### No Exit on Governance Save + +When a script generates a governance TX batch, it **returns** rather than exiting. This: + +- Lets a single command produce multiple governance TX batches in one run (one per script that needs governance authority) +- Avoids implicit ordering coupling — every script checks its own on-chain preconditions and skips if they aren't met +- Is normal flow, not an error condition + +To detect "needs governance" in CI/CD, check whether any files exist under `txs//` after a run, or use the goal status scripts (`--tags GIP-0088`). + +## Troubleshooting + +### "No deployer account configured" + +You need to set `DEPLOYER_PRIVATE_KEY`: + +```bash +export DEPLOYER_PRIVATE_KEY=0xYOUR_PRIVATE_KEY +npx hardhat deploy --network arbitrumSepolia +``` + +### "Cannot execute governance TXs" with Safe multisig + +This is correct behavior for Safe multisig governors. Execute the TXs via Safe UI instead of the CLI command. + +### "Cannot execute governance TXs" with EOA governor + +Set the `GOVERNOR_PRIVATE_KEY` environment variable: + +```bash +export GOVERNOR_PRIVATE_KEY=0xYOUR_GOVERNOR_KEY +npx hardhat deploy:execute-governance --network arbitrumSepolia +``` + +### "Chain ID mismatch" + +The TX batch file's `chainId` must match the network you're executing on: + +- arbitrumSepolia: 421614 +- arbitrumOne: 42161 + +Regenerate the TX batch if you deployed to the wrong network. + +### TX Batch Already Exists + +If you re-run a deployment, it will overwrite the existing TX batch file with the same name. This is by design - the latest deployment's TX batch is always canonical. + +### "Safe not available on this network" + +Safe Transaction Builder is not deployed on all networks. If your network isn't supported: + +**For testnet deployments:** + +- Use an EOA governor with `GOVERNOR_PRIVATE_KEY` +- Or test in fork mode: `FORK_NETWORK=arbitrumOne` (fork mainnet instead) + +**Supported networks:** Check and select your network from the dropdown. If it's not listed, Safe is not available. + +**Example - Arbitrum Sepolia:** Safe may not be available. Use EOA governor: + +```bash +export GOVERNOR_PRIVATE_KEY=0xYOUR_TESTNET_GOVERNOR_KEY +npx hardhat deploy:execute-governance --network arbitrumSepolia +``` + +## Testing Governance Workflows + +Before executing on mainnet, always test in fork mode: + +```bash +# 1. Deploy (generates governance TXs) +export FORK_NETWORK=arbitrumOne +npx hardhat deploy --tags IssuanceAllocator:deploy --network fork + +# 2. Execute governance TXs automatically +npx hardhat deploy:execute-governance --network fork + +# 3. Verify state +npx hardhat deploy:status --network fork +``` + +For persistent fork testing (state survives across commands), see [LocalForkTesting.md](./LocalForkTesting.md). + +This tests the full governance workflow without touching real funds or requiring actual Safe signatures. diff --git a/packages/deployment/docs/LocalForkTesting.md b/packages/deployment/docs/LocalForkTesting.md new file mode 100644 index 000000000..f7247fba4 --- /dev/null +++ b/packages/deployment/docs/LocalForkTesting.md @@ -0,0 +1,170 @@ +# Local Fork Testing + +Fork testing allows simulating deployments against real network state without spending gas or requiring governance permissions. + +## Ephemeral Fork (single session) + +State is lost when the command exits. Good for quick testing. + +```bash +# Run full deployment flow against forked arbitrumSepolia +FORK_NETWORK=arbitrumSepolia npx hardhat deploy --tags sync,RewardsManager:deploy --network fork +``` + +## Persistent Fork (multiple sessions) + +State persists between commands. Good for iterative testing. + +```bash +# Terminal 1 - start persistent forked node using anvil (Foundry) +# Use --chain-id 31337 so hardhat's localhost network can connect +anvil --fork-url https://sepolia-rollup.arbitrum.io/rpc --chain-id 31337 +``` + +```bash +# Terminal 2 - run deploys against it +npx hardhat deploy:reset-fork --network localhost +npx hardhat deploy:status --network localhost +npx hardhat deploy --network localhost --skip-prompts --tags sync +npx hardhat deploy --network localhost --skip-prompts --tags RewardsManager +npx hardhat deploy:execute-governance --network localhost +``` + +Or for Arbitrum One: + +```bash +anvil --fork-url https://arb1.arbitrum.io/rpc --chain-id 31337 +``` + +**Important**: + +- Terminal 1: Use anvil (from Foundry) instead of `hardhat node` - Hardhat v3's node command doesn't properly support the `--fork` flag +- Terminal 1: Use `--chain-id 31337` - anvil defaults to the forked chain's ID (421614) but hardhat's localhost expects 31337 + +### Fork Network Detection + +The fork network (which chain is being forked) is **auto-detected** from anvil's RPC metadata. When you run against localhost, deploy scripts query `anvil_nodeInfo` to get the fork URL and match it against known network RPC hostnames. + +You can also set `FORK_NETWORK` explicitly to override auto-detection: + +```bash +export FORK_NETWORK=arbitrumSepolia +``` + +**Safe on real networks**: `FORK_NETWORK` is automatically ignored when running against real networks (`--network arbitrumSepolia`, `--network arbitrumOne`). Fork mode only activates on local networks (localhost, fork, hardhat), so you don't need to unset `FORK_NETWORK` when switching between fork testing and real deployments. + +## Architecture + +``` +fork/ # Fork state (outside deployments/ to avoid rocketh conflicts) +└── / # Rocketh environment (fork, localhost) + └── / # Fork source network + ├── horizon-addresses.json + ├── subgraph-service-addresses.json + ├── issuance-addresses.json + └── txs/ + └── upgrade-*.json + +deployments/ # Managed by rocketh (deployment records, .chain files) +└── / + └── ... +``` + +**Fork state organization:** + +- Fork state is stored under `fork///` + - Separate from `deployments/` so rocketh manages its own directory cleanly + - `` is the rocketh environment (fork, localhost) + - `` is the source network being forked (arbitrumSepolia, arbitrumOne) +- This prevents addresses from wrong network being used if fork target changes +- Address books and governance TXs are stored together +- State persists across fork sessions (rocketh's data is ephemeral, this is not) + +## Key Points + +| Setting | Value | Purpose | +| --------------------- | ---------------------------------- | -------------------------------------------------------------- | +| `FORK_NETWORK` | `arbitrumSepolia` or `arbitrumOne` | Override auto-detected fork network (ignored on real networks) | +| `SHOW_ADDRESSES` | `0`, `1` (default), or `2` | Address display: none/short/full | +| `--network fork` | in-process EDR | Ephemeral, fast startup | +| `--network localhost` | external node | Persistent state | + +## Configuration + +### Address Display + +Control how addresses are shown in sync output with `SHOW_ADDRESSES`: + +```bash +# Show full addresses (default) +SHOW_ADDRESSES=2 npx hardhat deploy --tags sync --network fork + +# Show truncated addresses (0x1234567890...) +SHOW_ADDRESSES=1 npx hardhat deploy --tags sync --network fork + +# Hide addresses completely +SHOW_ADDRESSES=0 npx hardhat deploy --tags sync --network fork +``` + +**Output examples:** + +``` +# SHOW_ADDRESSES=2 (default - full addresses) +✓ SubgraphService @ 0xc24A3dAC5d06d771f657A48B20cE1a671B78f26b → 0xEc11f71070503D29098149195f95FEb1B1CeF93E + +# SHOW_ADDRESSES=1 (truncated) +✓ SubgraphService @ 0xc24A3dAC... → 0xEc11f710... + +# SHOW_ADDRESSES=0 (hidden) +✓ SubgraphService +``` + +## Reset Fork State + +```bash +# Use the reset task (deletes entire network directory) +npx hardhat deploy:reset-fork --network localhost +# Or for ephemeral fork: +npx hardhat deploy:reset-fork --network fork +``` + +## Limitations + +- **On-chain state**: Only persists with persistent node (anvil) +- **rocketh deployment files**: Don't persist for forks (by design) +- **Contract size**: Fork allows unlimited contract size (Arbitrum supports >24KB) + +## Prerequisites + +- **Foundry**: Install via `curl -L https://foundry.paradigm.xyz | bash && foundryup` + +## Local Network + +The `localNetwork` network targets a Graph local network at chain ID 1337. +Unlike fork mode, contracts are deployed fresh from scratch. + +```bash +# Deploy a single contract via its component lifecycle +npx hardhat deploy --tags IssuanceAllocator,deploy --network localNetwork + +# Or run the full GIP-0088 upgrade phase +npx hardhat deploy --tags GIP-0088:upgrade,deploy --network localNetwork +``` + +**Key differences from fork mode:** + +- Chain ID 1337 (not 31337) +- No `FORK_NETWORK` env var needed +- Address books use `addresses-local-network.json` files that the dev environment must provide +- Deployer is also governor (direct execution, no governance batch files) +- Uses standard test mnemonic (`test test test ... junk`) + +**Environment:** + +- RPC: `http://chain:8545` (override with `LOCAL_NETWORK_RPC`) +- Address books must be populated by an upstream step that deploys Horizon + SubgraphService +- This package then deploys contracts on top (e.g., issuance) + +## See Also + +- [GovernanceWorkflow.md](./GovernanceWorkflow.md) - Production deployment flow diff --git a/packages/deployment/docs/SyncBytecodeDetectionFix.md b/packages/deployment/docs/SyncBytecodeDetectionFix.md new file mode 100644 index 000000000..5c4498fd1 --- /dev/null +++ b/packages/deployment/docs/SyncBytecodeDetectionFix.md @@ -0,0 +1,149 @@ +# Sync Bytecode Detection Fix + +## Issues Identified + +### Issue 1: Local Bytecode Changes Ignored + +**Problem**: Deploy incorrectly reported "implementation unchanged" when local bytecode had actually changed. + +**Evidence**: + +``` +Local artifact: 0x9c25d2f93e6a2a34cc19d00224872e288a8392d5d99b2df680b7e978d148d450 +On-chain: 0xfafdeb48fae37e277e007e7b977f3cd124065ac1c27ed5208982c2965cf07008 +Address book: 0x4805a902756c8f4421c2a2710dcc76885ffd01d7777bbe6cab010fe9748b7efa +``` + +All three hashes are different, yet deploy said "unchanged", meaning local changes would be ignored. + +### Issue 2: Confusing Sync Behavior + +**Problem**: Sync showed "code changed" but didn't handle the state appropriately: + +1. Showed △ (code changed) indicator +2. But didn't sync implementation to rocketh +3. Saved proxy record with wrong bytecode +4. This confused rocketh's change detection + +## Root Causes + +### Cause 1: Missing/Stale Bytecode Hash + +When the address book had no bytecode hash (or wrong hash): + +- Sync detected "code changed" ([sync-utils.ts:475-477](../lib/sync-utils.ts#L475-L477)) +- But only synced to rocketh if hash matched ([sync-utils.ts:653](../lib/sync-utils.ts#L653)) +- This left rocketh with incomplete/wrong state + +### Cause 2: Wrong Bytecode Stored for Proxy + +The sync step saved the **implementation's bytecode** under the **proxy's deployment record**: + +- Lines 508-532: Created proxy record with implementation artifact bytecode +- This is wrong - proxy should have its own bytecode (or none) +- Rocketh then compared wrong bytecode and gave incorrect results + +## Fixes Applied + +### Fix 1: Hash Comparison and Stale Record Cleanup ([sync-utils.ts:645-679](../lib/sync-utils.ts#L645-L679)) + +When sync processes an implementation: + +1. **Compare local artifact hash to address-book-stored hash** +2. **If hashes match**: sync the implementation record to rocketh normally +3. **If hashes don't match**: overwrite any stale rocketh record with empty bytecode, forcing a fresh deployment + + ```typescript + if (storedHash && localHash) { + hashMatches = storedHash === localHash + } + + // Clean up stale rocketh record if hash doesn't match + if (!hashMatches && existingImpl) { + // Overwrite stale record with empty bytecode - forces fresh deployment + await env.save(`${spec.name}_Implementation`, { + address: existingImpl.address, + bytecode: '0x', + deployedBytecode: undefined, + ... + }) + } + ``` + +This ensures rocketh correctly detects when local code has changed and triggers a new deployment. + +### Fix 2: Don't Store Wrong Bytecode for Proxy ([sync-utils.ts:508-532](../lib/sync-utils.ts#L508-L532)) + +Changed proxy record creation to **NOT include implementation bytecode**: + +```typescript +// Before: +bytecode: artifact.bytecode // ← Wrong! This is implementation bytecode +deployedBytecode: artifact.deployedBytecode + +// After: +bytecode: '0x' // ← Correct! Proxy record doesn't need bytecode +deployedBytecode: undefined +``` + +This ensures rocketh only uses implementation bytecode for the actual implementation record. + +## Expected Behavior After Fix + +### Scenario 1: Local Matches Address Book + +When local artifact hash matches the stored hash, sync proceeds normally and rocketh +correctly reports the implementation as unchanged. + +### Scenario 2: Local Code Changed + +**Before**: + +``` +△ SubgraphService @ 0xc24A3dAC... → 0x2af1b0ed... (code changed) +✓ SubgraphService implementation unchanged ← WRONG! +``` + +**After**: + +``` +△ SubgraphService @ 0xc24A3dAC... → 0x2af1b0ed... (local code changed) +📋 New SubgraphService implementation deployed: 0x... ← NEW! + Storing as pending implementation... +``` + +Deploy correctly detects the change and deploys new implementation. + +### Scenario 3: Stale Rocketh Record + +When the hash doesn't match and a stale rocketh record exists, sync overwrites it +with empty bytecode. This forces the next deploy to create a fresh implementation +record rather than incorrectly reporting "unchanged". + +## Testing + +To verify the fix works: + +```bash +# Clean build +cd packages/deployment +pnpm build + +# Run sync - should now show clearer messages +npx hardhat deploy --skip-prompts --network arbitrumSepolia --tags sync + +# Run deploy - should correctly detect local changes +npx hardhat deploy --skip-prompts --network arbitrumSepolia --tags SubgraphService +``` + +## Migration Notes + +- **No manual migration needed** - stale rocketh records are cleaned up automatically +- First sync after fix will detect hash mismatches and clear stale records +- Subsequent deploys will create fresh implementation records + +## Related Files + +- [sync-utils.ts](../lib/sync-utils.ts) - Main fix implementation +- [deploy-implementation.ts](../lib/deploy-implementation.ts) - Deploy logic (unchanged, now works correctly) +- [check-bytecode.ts](../scripts/check-bytecode.ts) - Diagnostic script for manual verification diff --git a/packages/deployment/docs/SyncSpecification.md b/packages/deployment/docs/SyncSpecification.md new file mode 100644 index 000000000..92e146636 --- /dev/null +++ b/packages/deployment/docs/SyncSpecification.md @@ -0,0 +1,285 @@ +# Sync Specification + +This document defines the bidirectional sync behavior between address books and rocketh deployment records. + +## Data Structures + +### Address Book Entry (Proxied Contract) + +```json +{ + "ContractName": { + "address": "0x...", // Proxy address + "proxy": "graph|transparent", + "proxyAdmin": "0x...", // Inline or via separate entry + "implementation": "0x...", // Current on-chain implementation + "implementationDeployment": { + "txHash": "0x...", + "argsData": "0x...", + "bytecodeHash": "0x...", // Hash of deployed bytecode (metadata stripped) + "blockNumber": 12345 + }, + "pendingImplementation": { + // Optional: deployed but not yet upgraded + "address": "0x...", + "deployment": { + // Same structure as implementationDeployment + "txHash": "0x...", + "argsData": "0x...", + "bytecodeHash": "0x...", + "blockNumber": 12346 + } + } + } +} +``` + +### Rocketh Deployment Record + +```typescript +{ + address: "0x...", + abi: [...], + bytecode: "0x...", // Creation bytecode + deployedBytecode: "0x...", // Runtime bytecode (for change detection) + argsData: "0x...", // Encoded constructor args + metadata: "...", + transaction?: { hash: "0x..." }, + receipt?: { blockNumber: 12345 } +} +``` + +### Rocketh Record Names + +For a proxied contract `ContractName`: + +- `ContractName` - The proxy contract +- `ContractName_Proxy` - Alias for proxy (some patterns use this) +- `ContractName_Implementation` - The implementation contract +- `ContractName_ProxyAdmin` - The proxy admin + +## Sync Direction Rules + +### Address Book → Rocketh + +**When**: Sync step runs, address book has data rocketh doesn't have. + +**What syncs**: + +- Proxy address → `ContractName` and `ContractName_Proxy` +- Proxy admin address → `ContractName_ProxyAdmin` +- Implementation address → `ContractName_Implementation` + +**Implementation address selection**: + +1. If `pendingImplementation.address` exists → use pending address +2. Else → use `implementation` address + +**Bytecode hash gating**: + +- **Only sync implementation if `bytecodeHash` matches local artifact** +- No stored hash → don't sync (can't verify consistency) +- Hash mismatch → don't sync, add "impl outdated" note + +**Rationale**: Syncing stale bytecode to rocketh would make rocketh think the deployed code matches local, preventing necessary redeployment. + +### Rocketh → Address Book (Backfill) + +**When**: Rocketh has deployment metadata that address book lacks. + +**What backfills**: + +- `txHash`, `argsData`, `bytecodeHash`, `blockNumber` + +**Determining "newer"** (blockNumber comparison): + +1. Address book has no metadata → rocketh is newer +2. Rocketh has blockNumber, address book doesn't → rocketh is newer +3. Rocketh blockNumber > address book blockNumber → rocketh is newer + +**Where to write**: + +- For current implementation → `implementationDeployment` +- For pending implementation → `pendingImplementation.deployment` + +## Implementation Lifecycle + +### State Transitions + +``` +┌─────────────────────────────────────────┐ +│ Initial Deployment │ +│ (deploy creates implementation) │ +└──────────────────┬──────────────────────┘ + │ deploy script + ▼ +┌─────────────────────────────────────────┐ +│ implementation: 0xIMPL │ +│ implementationDeployment: {...} │ +└──────────────────┬──────────────────────┘ + │ code changes, deploy new impl + ▼ +┌─────────────────────────────────────────┐ +│ implementation: 0xIMPL │ (unchanged until upgrade) +│ implementationDeployment: {...} │ +│ pendingImplementation: { │ (new impl awaiting governance) +│ address: 0xNEW, │ +│ deployment: {...} │ +│ } │ +└──────────────────┬──────────────────────┘ + │ governance upgrade TX executed + ▼ +┌─────────────────────────────────────────┐ +│ implementation: 0xNEW │ (promoted from pending) +│ implementationDeployment: {...} │ (metadata from pending) +│ (pendingImplementation cleared) │ +└─────────────────────────────────────────┘ +``` + +### Sync Sequence (Logical Order) + +When sync runs, execute in this order: + +#### Step 1: Reconcile on-chain address + +``` +IF on-chain impl != address book impl: + → Update address book impl to match on-chain + → Wipe stale implementationDeployment (address changed, metadata invalid) + → Note: This handles external upgrades (from other deployment systems) +``` + +#### Step 2: Promote pending if upgraded + +``` +IF pendingImplementation.address == implementation (on-chain): + → Move pendingImplementation.deployment → implementationDeployment + → Clear pendingImplementation + → Add "upgraded" sync note +``` + +#### Step 3: Sync rocketh ↔ address book + +After steps 1-2, address book has correct addresses. Now sync: + +- Pick implementation to sync (pending if exists, else current) +- If bytecodeHash matches local → sync to rocketh +- If rocketh has newer metadata → backfill to address book + +This sequence ensures: + +- Address book always reflects on-chain reality first +- Pending metadata is preserved when promoted +- Rocketh sync naturally goes to the correct location + +## Implementation Sync Decision Tree + +``` + ┌─────────────────┐ + │ Has implAddress?│ + └────────┬────────┘ + │ + ┌─────────────┴─────────────┐ + │ No │ Yes + ▼ ▼ + ┌──────────┐ ┌─────────────────┐ + │ Skip │ │ Get storedHash │ + │ (no impl)│ │ from deployment │ + └──────────┘ └────────┬────────┘ + │ + ┌────────────┴────────────┐ + │ storedHash exists? │ + └────────────┬────────────┘ + │ + ┌────────────────────┴────────────────────┐ + │ No │ Yes + ▼ ▼ + ┌──────────────┐ ┌─────────────────────┐ + │ Don't sync │ │ Compare to local │ + │ (unverified) │ │ artifact hash │ + └──────────────┘ └──────────┬──────────┘ + │ + ┌────────────────────┴────────────────────┐ + │ Match? │ + └────────────────────┬────────────────────┘ + │ + ┌──────────────────────────────┴──────────────────────────────┐ + │ Yes │ No + ▼ ▼ + ┌────────────────────┐ ┌─────────────────────┐ + │ Sync to rocketh │ │ Don't sync │ + │ + backfill if newer│ │ Add "impl outdated" │ + └────────────────────┘ └─────────────────────┘ +``` + +## Backfill Decision (Rocketh → Address Book) + +Only runs after successful sync (hash matched). Determines which direction has newer data: + +``` + ┌────────────────────────────────┐ + │ Rocketh has argsData != '0x'? │ + └───────────────┬────────────────┘ + │ + ┌─────────────┴─────────────┐ + │ No │ Yes + ▼ ▼ + ┌──────────┐ ┌───────────────────────────┐ + │ No │ │ Address book has metadata?│ + │ backfill │ └─────────────┬─────────────┘ + └──────────┘ │ + ┌────────────────┴────────────────┐ + │ No │ Yes + ▼ ▼ + ┌─────────────────┐ ┌─────────────────────────────┐ + │ Backfill │ │ Compare blockNumbers │ + │ (book is empty) │ └──────────────┬──────────────┘ + └─────────────────┘ │ + ┌─────────────────┴─────────────────┐ + │ rocketh.blockNumber > │ + │ book.blockNumber? │ + └─────────────────┬─────────────────┘ + │ + ┌──────────────────────┴──────────────────────┐ + │ Yes │ No + ▼ ▼ + ┌─────────────────┐ ┌──────────────────┐ + │ Backfill │ │ No backfill │ + │ (rocketh newer) │ │ (book is newer) │ + └─────────────────┘ └──────────────────┘ +``` + +## Summary + +| Scenario | Action | +| --------------------------- | ----------------------------------------- | +| No impl address | Skip | +| Impl exists, no stored hash | Don't sync (unverified) | +| Impl exists, hash mismatch | Don't sync, note "impl outdated" | +| Impl exists, hash matches | Sync to rocketh | +| After sync, rocketh newer | Backfill to address book | +| Pending upgraded on-chain | Promote pending to current, clear pending | + +## Key Invariants + +1. **Bytecode hash is required for sync** - Without it, we can't verify the implementation matches local artifacts +2. **Pending takes precedence** - If pending exists with matching hash, sync pending (not current) +3. **On-chain is authoritative for addresses** - Sync reads actual implementation from chain +4. **BlockNumber determines recency** - Higher block number = newer deployment +5. **Backfill goes to correct location** - Current impl → `implementationDeployment`, pending → `pendingImplementation.deployment` + +## Future Enhancements + +### Upgrade Timing Tracking + +Currently, deployment metadata tracks when the implementation was _deployed_ (`blockNumber`, `timestamp`), but not when the proxy was _upgraded_ to use it. These are separate events: + +1. **Deploy** - New implementation contract created (currently tracked) +2. **Upgrade** - Proxy switched to use the new implementation (not tracked) + +A future enhancement could add `upgradedAt: { blockNumber, timestamp }` to `implementationDeployment` to capture when the proxy actually started using the implementation. This would require either: + +- Querying the chain for the upgrade transaction when promoting pending +- Recording detection time (less accurate but simpler) + +This information would be useful for audit trails and understanding the timeline between deployment and activation. diff --git a/packages/deployment/docs/address-book/LayerAnalysis.md b/packages/deployment/docs/address-book/LayerAnalysis.md new file mode 100644 index 000000000..e1bd8399a --- /dev/null +++ b/packages/deployment/docs/address-book/LayerAnalysis.md @@ -0,0 +1,47 @@ +# Layer Analysis: Future Work + +## Current State + +**Layer 1 (AddressBookOps)**: ✅ Complete - pure local storage operations. + +## Potential Future Layers + +### Layer 2: Network-Linked Operations + +Combine on-chain queries with address book updates. Currently scattered in `sync-utils.ts`. + +```typescript +class NetworkAddressBookOps { + constructor( + private ops: AddressBookOps, + private client: PublicClient, + ) {} + + async syncImplementationFromChain(name, proxyAddress, proxyType): Promise { + const impl = await getOnChainImplementation(this.client, proxyAddress, proxyType) + this.ops.setImplementationAndClearIfMatches(name, impl) + } + + async syncProxyAdminFromChain(name, proxyAddress): Promise { + const admin = await getOnChainProxyAdmin(this.client, proxyAddress) + this.ops.setProxyAdmin(name, admin) + } +} +``` + +### Layer 3+: Higher-Level Abstractions + +| Layer | Purpose | Status | +| ------- | ----------------------------- | ----------------------------- | +| Layer 3 | Rocketh state sync | Exists in `sync-utils.ts` | +| Layer 4 | Deploy + address book update | Scattered in deploy scripts | +| Layer 5 | Integrated deploy-and-sync | Does not exist | +| Layer 6 | State assessment + governance | Partial in `upgrade-utils.ts` | + +## Design Rationale + +Layer 1 is pure local storage because: + +- **Testability**: No mocked RPC clients needed +- **Flexibility**: Callers choose when/how to fetch on-chain data +- **Composability**: Higher layers can wrap Layer 1 diff --git a/packages/deployment/docs/address-book/README.md b/packages/deployment/docs/address-book/README.md new file mode 100644 index 000000000..c7ffe7616 --- /dev/null +++ b/packages/deployment/docs/address-book/README.md @@ -0,0 +1,58 @@ +# AddressBook Operations + +## Overview + +`AddressBookOps` wraps the base `AddressBook` class from toolshed, providing data-centric operations for managing contract addresses. Deployment code only sees `AddressBookOps` - the base class is internal. + +**Layer 1 only**: Pure local storage operations with no on-chain interactions. + +## Usage + +```typescript +import { graph } from '../rocketh/deploy.js' + +// Get AddressBookOps directly from factory functions +const addressBook = graph.getIssuanceAddressBook(chainId) + +// Write operations +addressBook.setProxy('RewardsManager', proxyAddr, implAddr, adminAddr, 'transparent') +addressBook.setPendingImplementationWithMetadata('RewardsManager', newImplAddr, deploymentMetadata) + +// Read operations +const entry = addressBook.getEntry('RewardsManager') +``` + +## API + +### Write Operations + +| Method | Purpose | +| ------------------------------------------------------------ | ---------------------------------------- | +| `setContract(name, address)` | Non-proxied contract | +| `setProxy(name, proxy, impl, admin, type)` | All proxy fields | +| `setImplementation(name, impl)` | Active implementation | +| `setProxyAdmin(name, admin)` | Proxy admin | +| `setPendingImplementationWithMetadata(name, impl, metadata)` | Pending implementation | +| `promotePendingImplementation(name)` | Move pending → active | +| `clearPendingImplementation(name)` | Clear pending | +| `setImplementationAndClearIfMatches(name, impl)` | Set impl + auto-clear pending if matches | + +### Read Operations + +| Method | Purpose | +| ------------------------------ | ------------------------------------ | +| `getEntry(name)` | Get address book entry | +| `entryExists(name)` | Check if entry exists | +| `listPendingImplementations()` | List contracts with pending upgrades | +| `isContractName(name)` | Type predicate for contract names | + +### Types + +```typescript +// For union types where contract name would be inferred as `never` +type AnyAddressBookOps = AddressBookOps +``` + +## Next Steps + +See [LayerAnalysis.md](./LayerAnalysis.md) for potential Layer 2 (network-linked operations) design. diff --git a/packages/deployment/docs/deploy/ImplementationPrinciples.md b/packages/deployment/docs/deploy/ImplementationPrinciples.md new file mode 100644 index 000000000..9226611a9 --- /dev/null +++ b/packages/deployment/docs/deploy/ImplementationPrinciples.md @@ -0,0 +1,610 @@ +# Deployment Script Implementation Principles + +This document defines the core principles and patterns for writing deployment scripts. Found in the `deploy/` directory where you work on these scripts. + +## Script Numbering and Structure + +### Principle: Numbered Scripts Follow Standard Objectives + +**Rule**: Component deployments use numbered scripts (`01_*.ts`, `02_*.ts`, etc.) with standardized objectives. + +**Numbering principles:** + +1. **Script names describe what is done** - Filename indicates the action (e.g., `01_deploy.ts`, `02_upgrade.ts`, `03_configure.ts`) +2. **Avoid redundant naming** - Don't repeat information in number and name (use `01_deploy.ts`, not `01_deploy_contract.ts`) +3. **Final script is always 09_end.ts** - Standardized end state aggregate provides completion tag, intermediate steps (01-08) vary by component complexity + +**Standard step objectives:** + +- **01_deploy.ts** - Deploy proxy + implementation, initialize with deployer + - Sync the contract being deployed (and any contracts it reads) immediately + before acting via `syncComponentFromRegistry` / + `syncComponentsFromRegistry`. The script factories + (`createProxyDeployModule`, `createImplementationDeployModule`, + `createUpgradeModule`, etc.) handle this automatically. + - For a global pre-deploy reconciliation, use `npx hardhat deploy:sync` + explicitly — it is no longer pulled in as an automatic dependency. + - Each script should declare its own prerequisites explicitly, not rely on transitive dependencies +- **02_upgrade.ts** - Handle proxy upgrades via governance (generates TX batch) +- **04_configure.ts** - Deployer-only configure: role grants and params on contracts where the deployer is governor +- **05_transfer_governance.ts** - Revoke deployer GOVERNOR_ROLE; transfer ProxyAdmin to protocol governor +- **06_integrate.ts** (optional) - Wire the contract into the rest of the protocol +- **09_end.ts** - End state aggregate (only has dependencies and verification, no execution) +- **10_status.ts** - Read-only status display (see below) + +The `03_*` slot is intentionally left empty so that `02_upgrade` can be inserted as a clearly distinct phase without renumbering. The `04_configure` numbering is the actual convention used throughout the tree. + +### Principle: Status Scripts Are Read-Only + +**Rule**: `10_status.ts` scripts MUST be purely read-only. They MUST NOT make on-chain changes, write transactions, or modify any state. + +**Why**: When `--tags ` is run without an action verb, only status scripts execute. Users rely on this for safe inspection of deployment state at any time — during planning, mid-deployment, and in production. Any mutation in a status script would violate this trust and could cause unintended state changes. + +**How it works**: + +1. Status scripts use `createStatusModule()`, which gates on `noTagsRequested()` — they only run when tags are present but no action verb is included +2. Stage scripts (01-08) use `shouldSkipAction(verb)` — they skip when their action verb is absent from `--tags` +3. Combined: `--tags GIP-0088` alone runs only `10_status.ts` (status reads on-chain directly and does not need a global sync first) + +**Pattern**: + +```typescript +// Component status — delegates to showDetailedComponentStatus (reads only) +export default createStatusModule(Contracts.issuance.IssuanceAllocator) + +// Goal status — custom handler, must only use readContract/getCode +export default createStatusModule(GoalTags.GIP_0088, async (env) => { + const client = graph.getPublicClient(env) as PublicClient + // ✅ Read on-chain state and display + const value = await client.readContract({ ... }) + env.showMessage(` ${value ? '✓' : '✗'} check description`) + // ❌ NEVER: execute(), tx(), deploy(), process.exit(1), TxBuilder +}) +``` + +**Invariant**: If a script is named `10_status.ts`, it contains zero writes. No exceptions. + +#### Example: RewardsEligibilityOracle (simple - 4 steps) + +``` +01_deploy.ts - Deploy proxy + implementation +02_upgrade.ts - Handle proxy upgrades (governance TX batch) +04_configure.ts - Deployer-only configure (params, role grants) +09_end.ts - End state aggregate +10_status.ts - Read-only status display +``` + +#### Example: RewardsEligibilityOracle (full lifecycle) + +``` +01_deploy.ts - Deploy proxy + implementation +02_upgrade.ts - Handle proxy upgrades +04_configure.ts - Configure params + role grants +05_transfer_governance.ts - Revoke deployer role + transfer ProxyAdmin +06_integrate.ts - Wire into RewardsManager (governance TX) +09_end.ts - End state aggregate +10_status.ts - Read-only status display +``` + +**Note:** Step `03_*` is intentionally left empty so `02_upgrade` stays a clearly separate phase. Steps 04-08 are flexible and vary by component. Always use `09_end.ts` for the aggregate and `10_status.ts` for read-only status. + +#### Tag structure in deployment-tags.ts + +```typescript +// Component tags are PascalCase contract names matching the registry +ComponentTags = { + REWARDS_ELIGIBILITY_A: 'RewardsEligibilityOracleA', + // ... +} + +// Action verbs are appended via --tags Component,verb +// e.g. --tags RewardsEligibilityOracleA,deploy +``` + +## Exit Codes and Flow Control + +### Principle: Scripts Are Goal-Seeking, Not Sequential Steps + +**Rule**: Each script checks its own preconditions and skips if not met. Scripts return (not exit) when work cannot proceed — subsequent scripts check their own state independently. + +**Rationale**: Scripts run in sequence but must not assume a particular starting state. Each script is idempotent and goal-seeking: it checks on-chain state, does what's needed, and returns. + +**Examples**: + +```typescript +// CORRECT: Save governance TX and return (allows subsequent scripts to run) +saveGovernanceTx(env, builder, `ContractName activation`) +// Returns — subsequent scripts check their own preconditions + +// CORRECT: Skip when precondition not met +if (!prerequisiteMet) { + env.showMessage(' ○ Prerequisite not met — skipping') + return +} + +// CORRECT: Use shared precondition check to skip if done +const precondition = await checkIAConfigured(client, ia.address, rm.address) +if (precondition.done) { + env.showMessage('✅ Already configured') + return +} +``` + +### When to Use Exit Code 1 + +Use `process.exit(1)` only for: + +- **Migration invariant violations** (data corruption risk, e.g. IA rate != RM rate before connection) +- **Verification failures** in `09_end` scripts +- **Sync failures** (can't proceed without address books) + +Do NOT use `process.exit(1)` for: + +- Governance TX generation (use `saveGovernanceTx` which returns) +- Preconditions not met (return/skip, let subsequent scripts check their own preconditions) +- Configuration already correct (idempotent check passed) +- Script successfully completed its work + +### When to Throw Exceptions + +Throw exceptions for: + +- Unexpected errors (network failures, contract not found) +- Invalid configuration +- Programming errors +- Truly exceptional conditions + +```typescript +// Exception for unexpected error +if (!deployer) { + throw new Error('No deployer account configured') +} + +// Clean exit for expected state +if (!upgraded) { + env.showMessage('Prerequisite not met') + process.exit(1) +} +``` + +## Idempotency + +### Principle: All Deployment Steps Must Be Idempotent + +**Rule**: Every deployment script MUST check current on-chain state and skip actions already completed. + +**Pattern**: + +```typescript +const func: DeployScriptModule = async (env) => { + // 1. Check current state + const checks = { + configA: false, + configB: false, + } + + // Read on-chain state + checks.configA = await readCurrentStateA() + checks.configB = await readCurrentStateB() + + // 2. If all checks pass, exit early + if (Object.values(checks).every(Boolean)) { + env.showMessage('✅ Already configured\n') + return + } + + // 3. Execute only missing steps + if (!checks.configA) { + await executeConfigA() + } + if (!checks.configB) { + await executeConfigB() + } +} +``` + +## Import Patterns + +### Principle: Use Package Imports for Shared Utilities + +**Rule**: Import shared utilities from `@graphprotocol/deployment` package, not relative paths. + +**Why**: Package imports are clearer, more maintainable, and work correctly with TypeScript path mapping. + +**Pattern**: + +```typescript +import type { DeployScriptModule } from '@rocketh/core/types' +import type { PublicClient } from 'viem' + +// Deployment helpers (rocketh specific) +import { deploy, execute, read, tx, graph } from '@graphprotocol/deployment/rocketh/deploy.js' + +// Contract utilities +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { requireContract, requireContracts } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' + +// Governance utilities +import { getGovernor, getPauseGuardian } from '@graphprotocol/deployment/lib/controller-utils.js' +import { TxBuilder } from '@graphprotocol/deployment/lib/tx-builder.js' +import { getGovernanceTxDir } from '@graphprotocol/deployment/lib/execute-governance.js' + +// Contract checks +import { requireRewardsManagerUpgraded } from '@graphprotocol/deployment/lib/contract-checks.js' + +// ABIs +import { REWARDS_MANAGER_ABI, GRAPH_TOKEN_ABI } from '@graphprotocol/deployment/lib/abis.js' + +// Tags +import { Tags, ComponentTags, actionTag } from '@graphprotocol/deployment/lib/deployment-tags.js' +``` + +**Anti-pattern** (don't do this): + +```typescript +// ❌ Relative imports make code hard to move and unclear about package boundaries +import { Contracts } from '../../lib/contract-registry.js' +import { TxBuilder } from '../../lib/tx-builder.js' +``` + +## Shared Utilities + +### Principle: Use Shared Functions for Common Patterns + +**Rule**: Always use shared utilities instead of duplicating code. + +### Deployer Pattern + +```typescript +import { requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' + +// ✅ GOOD: Use utility +const deployer = requireDeployer(env) + +// ❌ BAD: Manual check repeated everywhere +const deployer = env.namedAccounts.deployer +if (!deployer) { + throw new Error('No deployer account configured') +} +``` + +### Address Book Pattern + +```typescript +// Get target chain ID (handles fork mode) +const targetChainId = graph.getTargetChainId() + +// Get address books (fork-aware) +const horizonAddressBook = graph.getHorizonAddressBook(targetChainId) +const issuanceAddressBook = graph.getIssuanceAddressBook(targetChainId) + +// Get contract from registry +const contract = requireContract(env, Contracts.RewardsManager) +``` + +### Viem Client Pattern + +```typescript +// Get viem public client +const client = graph.getPublicClient(env) as PublicClient + +// Read contract state +const value = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: CONTRACT_ABI, + functionName: 'someFunction', + args: [arg1, arg2], +})) as ReturnType +``` + +## Governance Transaction Generation + +### Principle: Standard Pattern for Governance TXs + +**Pattern**: + +```typescript +import { + createGovernanceTxBuilder, + executeTxBatchDirect, + saveGovernanceTx, +} from '@graphprotocol/deployment/lib/execute-governance.js' +import { canSignAsGovernor } from '@graphprotocol/deployment/lib/controller-utils.js' + +const { governor, canSign } = await canSignAsGovernor(env) + +// Create TX builder (handles chainId, outputDir, template automatically) +const builder = await createGovernanceTxBuilder(env, `action-${contractName}`, { + name: 'Human Readable Name', + description: 'What this TX batch does', +}) + +// Add transactions +builder.addTx({ to: contractAddress, value: '0', data: encodedCalldata }) +env.showMessage(` + ContractName.functionName(args)`) + +// Execute directly if possible, otherwise save for governance +if (canSign) { + await executeTxBatchDirect(env, builder, governor) +} else { + saveGovernanceTx(env, builder, `${contractName} activation`) +} +// Returns — does NOT exit. Subsequent scripts check their own preconditions. +``` + +### Metadata Standards + +All governance TX batches should include descriptive metadata: + +```typescript +meta: { + name: 'Contract Upgrade', // Short, human-readable title + description: 'Upgrade ContractName proxy to new implementation', // What it does +} +``` + +## Fork Mode Patterns + +### Principle: Scripts Must Work in Both Fork and Production Modes + +**Pattern**: + +```typescript +// Use target chain ID (handles fork) +const targetChainId = graph.getTargetChainId() + +// Use fork-aware address books +const addressBook = graph.getIssuanceAddressBook(targetChainId) + +// Check if in fork mode (optional - for conditional behavior) +const isFork = graph.isForkMode() + +// Governance TX directory is fork-aware +const outputDir = getGovernanceTxDir(env.name) +// Returns: fork/localhost/arbitrumOne/txs/ (fork) +// or txs/arbitrumOne/ (production) +``` + +## Script Structure + +### Standard Script Template + +```typescript +import type { DeployScriptModule } from '@rocketh/core/types' +import type { PublicClient } from 'viem' + +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { Tags, ComponentTags } from '@graphprotocol/deployment/lib/deployment-tags.js' +import { requireContracts, requireDeployer } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' +import { graph } from '@graphprotocol/deployment/rocketh/deploy.js' + +/** + * Script purpose and description + * + * Details about what this script does. + * Prerequisites if any. + * + * Usage: + * npx hardhat deploy --tags script-tag --network + */ +const func: DeployScriptModule = async (env) => { + // 1. Get named accounts + const deployer = requireDeployer(env) + + // 2. Get required contracts + const [contractA, contractB] = requireContracts(env, [Contracts.ContractA, Contracts.ContractB]) + + // 3. Get viem client + const client = graph.getPublicClient(env) as PublicClient + + // 4. Check prerequisites + await requireSomePrerequisite(env) + + // 5. Show script header + env.showMessage('\n========== Script Name ==========') + env.showMessage(`Contract: ${contractA.address}\n`) + + // 6. Check current state (idempotency) + const checks = { + checkA: await checkStateA(), + checkB: await checkStateB(), + } + + if (Object.values(checks).every(Boolean)) { + env.showMessage('✅ Already configured\n') + return + } + + // 7. Execute missing steps + if (!checks.checkA) { + await executeA() + } + + // 8. Show completion + env.showMessage('\n✅ Complete!\n') +} + +// 9. Configure tags and dependencies +func.tags = Tags.scriptTag +func.dependencies = [ComponentTags.PREREQUISITE] + +export default func +``` + +## Error Messages + +### Principle: Clear, Actionable Error Messages with Dynamic Values + +**Rule**: Use contract names from registry and tag constants - never hardcode them in messages. + +**Why**: Hardcoded values break when contracts are renamed or tags change, and make code harder to maintain. + +**Pattern**: + +```typescript +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +// ✅ GOOD: Uses contract name from registry +const contract = Contracts.RewardsManager +env.showMessage(`\n❌ ${contract.name} has not been upgraded yet`) +env.showMessage(` The on-chain ${contract.name} does not support IERC165/IIssuanceTarget`) +env.showMessage(` Run: npx hardhat deploy:execute-governance --network ${env.name}`) +env.showMessage(` (This will execute the pending ${contract.name} upgrade TX)\n`) + +// ❌ BAD: Hardcoded contract name +env.showMessage(`\n❌ RewardsManager has not been upgraded yet`) +env.showMessage(` Run: npx hardhat deploy:execute-governance --network ${env.name}`) + +// ✅ GOOD: Shows what was found vs expected +env.showMessage(` IA integrated: ${checks.iaIntegrated ? '✓' : '✗'} (current: ${currentIA})`) + +// ❌ BAD: Vague error without context +env.showMessage('⚠️ Something is not ready') + +// ❌ BAD: Just shows boolean without explanation +env.showMessage(` IA integrated: ${checks.iaIntegrated}`) +``` + +## Contract Registry + +### Principle: Use Contract Registry for Type Safety + +**Pattern**: + +```typescript +import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' +import { requireContract } from '@graphprotocol/deployment/lib/issuance-deploy-utils.js' + +// GOOD: Type-safe, refactorable, discoverable +const contract = requireContract(env, Contracts.RewardsManager) + +// BAD: String literal (typos, hard to refactor) +const contract = requireContract(env, 'RewardsManager') + +// Registry provides: +// - Type safety +// - Metadata (proxy type, address book, proxy admin) +// - Discoverability (IDE autocomplete) +``` + +## Documentation + +### Principle: Every Script Has Clear Documentation + +**Requirements**: + +```typescript +/** + * Brief description of what this script does + * + * Longer description with: + * - Prerequisites + * - What actions it performs + * - Whether it's idempotent + * - Whether it generates governance TXs + * + * Corresponds to: IssuanceAllocatorDeployment.md step X (if applicable) + * + * Usage: + * npx hardhat deploy --tags script-tag --network + * FORK_NETWORK=arbitrumOne npx hardhat deploy --tags script-tag --network localhost + */ +``` + +### Principle: Deployment Documentation in docs/deploy/ + +**Rule**: Deployment documentation should be placed in `docs/deploy/`, mirroring the deploy script structure. + +**Why not colocate?** The rocketh/hardhat-deploy script loader auto-loads all files in the `deploy/` directory. Placing `.md` files there causes loader errors. There's no extension filtering option available. + +**Structure**: + +``` +deploy/ docs/deploy/ + allocate/ IssuanceAllocatorDeployment.md + allocator/ DirectAllocationDeployment.md + 01_deploy.ts rewards/ + 02_upgrade.ts RewardsEligibilityOracleDeployment.md + 09_end.ts + rewards/ + eligibility/ + 01_deploy.ts + 02_upgrade.ts + 09_end.ts +``` + +**Cross-referencing**: + +- Contract documentation (in `packages/issuance/contracts/`) should link to deployment documentation +- Deployment documentation should link back to contract documentation +- General framework documentation stays in `packages/deployment/docs/` + +**Example references**: + +```markdown + + +For deployment instructions, see [IssuanceAllocatorDeployment.md](../../../deployment/docs/deploy/IssuanceAllocatorDeployment.md). + + + +For contract architecture and technical details, see [IssuanceAllocator.md](../../../issuance/contracts/allocate/IssuanceAllocator.md). +``` + +**Rationale**: While colocation would be ideal, the deploy loader limitation requires this separation. The `docs/deploy/` structure mirrors deployment organization to maintain logical association. + +## Testing + +### Principle: Scripts Should Be Testable + +**Pattern**: + +```typescript +// Make scripts testable by: +// 1. Using shared utilities (mockable) +// 2. Checking state before executing +// 3. Being idempotent +// 4. Providing clear output + +// Example test flow: +// 1. Run script first time -> executes actions +// 2. Run script second time -> skips (idempotent) +// 3. Check on-chain state matches expected +``` + +## Summary + +### Key Principles Checklist + +For every deployment script: + +- [ ] Uses `return` (not `process.exit`) for precondition skips and governance TX saves +- [ ] Throws exceptions only for unexpected errors +- [ ] Is idempotent (checks state, skips if done) +- [ ] Uses package imports (`@graphprotocol/deployment`) not relative paths +- [ ] Uses shared utilities from `lib/` +- [ ] Uses `Contracts` registry for type safety and dynamic contract names +- [ ] Uses tag constants (never hardcodes tag strings) +- [ ] Works in both fork and production modes +- [ ] Has clear, actionable error messages with dynamic values +- [ ] Includes comprehensive documentation +- [ ] Follows standard script structure (01_deploy, 02_upgrade, ..., 09_end, 10_status) +- [ ] Properly configures tags and dependencies +- [ ] End state script is always `09_end.ts` with only dependencies +- [ ] `10_status.ts` is purely read-only (zero writes, zero TXs, zero exits) + +### Anti-Patterns to Avoid + +❌ Using `process.exit(1)` for precondition skips or governance TX saves (use `return`) +❌ Duplicating precondition checks instead of using shared functions from `lib/preconditions.ts` +❌ Duplicating code instead of using shared utilities +❌ Using relative imports (`../../lib/`) instead of package imports +❌ Using string literals instead of `Contracts` registry +❌ Hardcoding contract names in error messages (use `Contracts.X.name`) +❌ Hardcoding contract names in TX batch filenames (use `Contracts.X.name`) +❌ Hardcoding tag strings in messages (use tag constants) +❌ Hardcoding chain IDs instead of using `getTargetChainId()` +❌ Direct address book imports instead of `graph.get*AddressBook()` +❌ Vague error messages without actionable next steps +❌ Non-idempotent scripts that fail on re-run +❌ Using non-standard end script numbering (use `09_end.ts` always) +❌ Any mutation (write, TX, deploy, exit) in a `10_status.ts` script diff --git a/packages/deployment/docs/deploy/IssuanceAllocatorDeployment.md b/packages/deployment/docs/deploy/IssuanceAllocatorDeployment.md new file mode 100644 index 000000000..60a110de5 --- /dev/null +++ b/packages/deployment/docs/deploy/IssuanceAllocatorDeployment.md @@ -0,0 +1,82 @@ +# IssuanceAllocator Deployment + +This document describes how `IssuanceAllocator` is deployed by this package. For contract architecture, behaviour, and technical details, see [IssuanceAllocator.md](../../../issuance/contracts/allocate/IssuanceAllocator.md). + +For the goal-level GIP-0088 workflow that orchestrates IA together with the rest of the upgrade, see [Gip0088.md](../Gip0088.md). + +## Component overview + +`IssuanceAllocator` is a deployable proxy in the `issuance` address book: + +- Pattern: OpenZeppelin v5 `TransparentUpgradeableProxy` with a per-proxy `ProxyAdmin` created in the constructor. +- Access control: `BaseUpgradeable` (`GOVERNOR_ROLE`, `PAUSE_ROLE`). +- Component tag: `IssuanceAllocator`. Lifecycle actions: `deploy`, `upgrade`, `configure`, `transfer`. +- Default target: a separate `DefaultAllocation` proxy ([../../deploy/allocate/default/](../../deploy/allocate/default/)) that holds any unallocated issuance as a safety net. + +## Lifecycle scripts + +| Script | Tag | Actor | Purpose | +| -------------------------------------------------------------------------------------- | ----------------------------- | ---------- | -------------------------------------------------------------------------- | +| [01_deploy.ts](../../deploy/allocate/allocator/01_deploy.ts) | `IssuanceAllocator,deploy` | Deployer | Deploy proxy + implementation, initialize with deployer as governor | +| [02_upgrade.ts](../../deploy/allocate/allocator/02_upgrade.ts) | `IssuanceAllocator,upgrade` | Governance | Build governance TX batch upgrading the proxy to its pendingImplementation | +| [04_configure.ts](../../deploy/allocate/allocator/04_configure.ts) | `IssuanceAllocator,configure` | Deployer | Set issuance rate (matches RM), grant `GOVERNOR_ROLE` and `PAUSE_ROLE` | +| [06_transfer_governance.ts](../../deploy/allocate/allocator/06_transfer_governance.ts) | `IssuanceAllocator,transfer` | Deployer | Revoke deployer `GOVERNOR_ROLE`, transfer per-proxy ProxyAdmin to gov | +| [09_end.ts](../../deploy/allocate/allocator/09_end.ts) | `IssuanceAllocator,all` | - | Aggregate end state — verifies upgrade has been executed | +| [10_status.ts](../../deploy/allocate/allocator/10_status.ts) | `IssuanceAllocator` | - | Read-only status display | + +`03_*`, `05_*`, and `07_08_*` slots are intentionally empty (per [ImplementationPrinciples.md](ImplementationPrinciples.md)). + +## What does NOT happen here + +The following operations are part of GIP-0088 activation, not the IA component lifecycle. They live in [../../deploy/gip/0088/](../../deploy/gip/0088/) and are governance TXs: + +- `IA.setTargetAllocation(RM, 0, rate)` — registers RM as the 100% self-minting target +- `IA.setDefaultTarget(DA)` — wires the safety net +- `RM.setIssuanceAllocator(IA)` — RM starts querying IA for its issuance rate +- `GraphToken.addMinter(IA)` — gives IA minter authority (only needed for allocator-minting targets) +- `IA.setTargetAllocation(RAM, allocatorRate, selfRate)` — distributes issuance to `RecurringAgreementManager` + +These are bundled into the `GIP-0088:upgrade,upgrade` and `GIP-0088:issuance-connect` / `GIP-0088:issuance-allocate` governance batches. See [Gip0088.md](../Gip0088.md) for the full picture. + +## Single-component usage + +```bash +# Read-only status +pnpm hardhat deploy --tags IssuanceAllocator --network + +# Lifecycle steps +pnpm hardhat deploy --tags IssuanceAllocator,deploy --network +pnpm hardhat deploy --tags IssuanceAllocator,configure --network +pnpm hardhat deploy --tags IssuanceAllocator,transfer --network +pnpm hardhat deploy --tags IssuanceAllocator,upgrade --network +``` + +The same scripts run as part of the goal-level GIP-0088 flow when invoked via `--tags GIP-0088:upgrade,`. + +## Verification checklist + +Run `--tags IssuanceAllocator` (component status) or `--tags GIP-0088:upgrade` (goal status) to inspect on-chain state. The status output already covers everything below — this list is for reviewing a finished deployment by hand. + +### Bytecode + +- Implementation bytecode matches the expected `IssuanceAllocator` contract + +### Access control + +- Protocol governor holds `GOVERNOR_ROLE` +- Pause guardian holds `PAUSE_ROLE` +- Deployer does **not** hold `GOVERNOR_ROLE` (asserted by `checkDeployerRevoked` in the transfer step) +- Per-proxy `ProxyAdmin` is owned by the protocol governor + +### Configuration + +- `getIssuancePerBlock()` matches `RewardsManager.issuancePerBlock()` +- `paused()` is `false` + +### Activation (GIP-0088) + +- `RewardsManager.getIssuanceAllocator()` returns the IA address +- `GraphToken.isMinter(IA)` is `true` (only when allocator-minting targets exist) +- `getTargetAllocation(RM)` shows `selfMintingRate == issuancePerBlock`, `allocatorMintingRate == 0` +- `getTargetAllocation(RAM)` matches `config/.json5` rates +- Default target points at `DefaultAllocation` diff --git a/packages/deployment/docs/deploy/RewardsEligibilityOracleDeployment.md b/packages/deployment/docs/deploy/RewardsEligibilityOracleDeployment.md new file mode 100644 index 000000000..e8e9d2968 --- /dev/null +++ b/packages/deployment/docs/deploy/RewardsEligibilityOracleDeployment.md @@ -0,0 +1,105 @@ +# RewardsEligibilityOracle Deployment + +Deployment guide for RewardsEligibilityOracle (REO). + +**Related:** + +- [Contract specification](../../../issuance/contracts/eligibility/RewardsEligibilityOracle.md) - architecture, operations, troubleshooting +- [GovernanceWorkflow.md](../GovernanceWorkflow.md) - Safe TX execution + +## Prerequisites + +- GraphToken deployed +- Controller deployed (provides governor, pause guardian addresses) +- `NetworkOperator` entry in issuance address book (for OPERATOR_ROLE) + +## Deployment Scripts + +All scripts are idempotent. + +| Script | Tag | Actor | Purpose | +| --------------------------------------------------------------------------------------- | ----------------------------------------- | ------------------- | ----------------------------------------- | +| [01_deploy.ts](../../deploy/rewards/eligibility/01_deploy.ts) | `RewardsEligibilityOracle{A,B}:deploy` | Deployer | Deploy proxy + implementation | +| [02_upgrade.ts](../../deploy/rewards/eligibility/02_upgrade.ts) | `RewardsEligibilityOracle{A,B}:upgrade` | Governance | Upgrade implementation | +| [04_configure.ts](../../deploy/rewards/eligibility/04_configure.ts) | `RewardsEligibilityOracle{A,B}:configure` | Deployer/Governance | Set parameters | +| [05_transfer_governance.ts](../../deploy/rewards/eligibility/05_transfer_governance.ts) | `RewardsEligibilityOracle{A,B}:transfer` | Deployer | Revoke deployer role, transfer ProxyAdmin | +| [09_end.ts](../../deploy/rewards/eligibility/09_end.ts) | `RewardsEligibilityOracle{A,B}` | - | Aggregate (deploy, upgrade, configure) | + +Integration with `RewardsManager` is **not** a per-component lifecycle action. Only one of REO-A or REO-B is integrated at a time, which is a goal-level decision. Use the GIP-0088 activation tag instead: + +```bash +pnpm hardhat deploy --tags GIP-0088:eligibility-integrate --network +``` + +The testnet-only `MockRewardsEligibilityOracle` is a separate, opt-in path with its own per-component [`06_integrate.ts`](../../deploy/rewards/eligibility/mock/06_integrate.ts). It is **not** part of any GIP-0088 phase tag, so `--tags all` will not pull it in — it runs only when `RewardsEligibilityOracleMock` is named explicitly: + +```bash +pnpm hardhat deploy --tags RewardsEligibilityOracleMock,integrate --network +``` + +Intentionally kept off the default deployment path and outside the governance-tx flow; not intended for mainnet. Its governance-tx batch name is `RewardsManager-MockREO` (vs `RewardsManager-REO` for the GIP-0088 A/B activation) so the two cannot collide on the same filesystem. + +### Quick Start + +```bash +# Read-only status (no --tags = no mutations) +pnpm hardhat deploy --tags RewardsEligibilityOracleA --network + +# Individual steps +pnpm hardhat deploy --tags RewardsEligibilityOracleA,deploy --network +pnpm hardhat deploy --tags RewardsEligibilityOracleA,configure --network +pnpm hardhat deploy --tags RewardsEligibilityOracleA,transfer --network + +# Integrate (only one of A/B at a time — goal-level) +pnpm hardhat deploy --tags GIP-0088:eligibility-integrate --network +``` + +## Verification Checklist + +### Deployment + +- [ ] Contract deployed via transparent proxy +- [ ] Implementation verified on block explorer + +### Access Control + +- [ ] Governor has GOVERNOR_ROLE +- [ ] Deployer does NOT have GOVERNOR_ROLE +- [ ] Pause guardian has PAUSE_ROLE +- [ ] Operator has OPERATOR_ROLE + +### Configuration + +- [ ] `eligibilityPeriod` = 14 days (1,209,600 seconds) +- [ ] `oracleUpdateTimeout` = 7 days (604,800 seconds) + +### Integration + +- [ ] `RewardsManager.getProviderEligibilityOracle()` returns REO address + +## Configuration Parameters + +| Parameter | Default | Purpose | +| ------------------------------ | ------- | --------------------------------------- | +| `eligibilityPeriod` | 14 days | How long indexer eligibility lasts | +| `oracleUpdateTimeout` | 7 days | Failsafe timeout for oracle updates | +| `eligibilityValidationEnabled` | false | Global enable/disable (set by operator) | + +## Roles + +| Role | Purpose | Assigned To | +| ------------- | ----------------------------------------- | -------------------------- | +| GOVERNOR_ROLE | Grant/revoke operator, governance actions | Protocol governance | +| OPERATOR_ROLE | Configure parameters, manage oracle roles | Network operator | +| ORACLE_ROLE | Renew indexer eligibility | Oracle services (multiple) | +| PAUSE_ROLE | Pause contract | Pause guardian | + +## Post-Deployment + +After deployment completes, the **operator** must: + +1. Grant ORACLE_ROLE to oracle services +2. Verify oracles are renewing eligibility +3. Enable eligibility validation when ready + +See [Contract specification - Operations](../../../issuance/contracts/eligibility/RewardsEligibilityOracle.md#operations) for detailed operational guidance, monitoring, and troubleshooting. diff --git a/packages/deployment/docs/plans/AddressBookEnhancement.md b/packages/deployment/docs/plans/AddressBookEnhancement.md new file mode 100644 index 000000000..de5ca17f7 --- /dev/null +++ b/packages/deployment/docs/plans/AddressBookEnhancement.md @@ -0,0 +1,448 @@ +# Address Book Enhancement Plan + +## Overview + +Extend the address book to store minimal deployment metadata that enables: + +1. Complete rocketh record reconstruction during sync +2. Contract verification without original deployment records +3. Deterministic change detection (has local bytecode changed since deployment?) +4. Pre-flight validation of deployment state +5. Bidirectional sync with conflict detection (using blockNumber comparison) + +## Current State + +### AddressBookEntry (toolshed) + +```ts +type AddressBookEntry = { + address: string + proxy?: 'graph' | 'transparent' + proxyAdmin?: string + implementation?: string + pendingImplementation?: PendingImplementation +} + +type PendingImplementation = { + address: string + deployedAt: string // ISO 8601 timestamp + txHash?: string // already has txHash! + readyForUpgrade?: boolean +} +``` + +### Problem + +- Sync creates minimal rocketh records with `argsData: '0x'`, `metadata: ''` +- Verification fails because constructor args are lost +- Bytecode comparison gymnastics required to detect changes +- No audit trail (txHash) for main contract/implementation deployments +- `pendingImplementation` has partial metadata but missing argsData/bytecodeHash + +## Proposed Changes + +### 1. Extend AddressBookEntry Type + +**File:** `packages/toolshed/src/deployments/address-book.ts` + +```ts +type DeploymentMetadata = { + /** Deployment transaction hash - enables recovery of all tx details */ + txHash: string + /** ABI-encoded constructor arguments */ + argsData: string + /** keccak256 of deployed bytecode (sans CBOR) for change detection */ + bytecodeHash: string + /** Block number of deployment - useful for sync conflict detection */ + blockNumber?: number + /** Block timestamp (ISO 8601) - human readable deployment time */ + timestamp?: string +} + +type AddressBookEntry = { + address: string + proxy?: 'graph' | 'transparent' + proxyAdmin?: string + implementation?: string + pendingImplementation?: PendingImplementation + /** Deployment metadata for non-proxied contracts */ + deployment?: DeploymentMetadata + /** Deployment metadata for proxy contract (proxied contracts only) */ + proxyDeployment?: DeploymentMetadata + /** Deployment metadata for implementation (proxied contracts only) */ + implementationDeployment?: DeploymentMetadata +} + +type PendingImplementation = { + address: string + deployedAt: string // keep for backwards compat + txHash?: string // already exists + readyForUpgrade?: boolean + /** Full deployment metadata (new) */ + deployment?: DeploymentMetadata +} +``` + +**Field usage:** + +- Non-proxied contract: `deployment` +- Proxied contract: `proxyDeployment` + `implementationDeployment` +- Pending upgrade: `pendingImplementation.deployment` + +### 2. Update Address Book Validation + +**File:** `packages/toolshed/src/deployments/address-book.ts` + +Update `_assertAddressBookEntry` to allow new fields: + +```ts +const allowedFields = [ + 'address', + 'implementation', + 'proxyAdmin', + 'proxy', + 'pendingImplementation', + 'deployment', + 'proxyDeployment', + 'implementationDeployment', // new +] +``` + +### 3. Add AddressBookOps Methods + +**File:** `packages/deployment/lib/address-book-ops.ts` + +```ts +/** + * Set deployment metadata for a contract + */ +setDeploymentMetadata( + name: ContractName, + metadata: DeploymentMetadata +): void + +/** + * Set implementation deployment metadata (for proxied contracts) + */ +setImplementationDeploymentMetadata( + name: ContractName, + metadata: DeploymentMetadata +): void + +/** + * Get deployment metadata + */ +getDeploymentMetadata(name: ContractName): DeploymentMetadata | undefined + +/** + * Check if deployment metadata exists and is complete + */ +hasCompleteDeploymentMetadata(name: ContractName): boolean +``` + +### 4. Bytecode Hash Utility + +**File:** `packages/deployment/lib/bytecode-utils.ts` (extend existing) + +Existing utilities to leverage: + +- `stripMetadata(bytecode)` - already strips CBOR suffix +- `bytecodeMatches(artifact, onChain)` - compares with immutable masking +- `findImmutablePositions(bytecode)` - finds PUSH32 zero placeholders + +Add new utility: + +```ts +import { keccak256 } from 'ethers' +import { stripMetadata } from './bytecode-utils.js' + +/** + * Compute bytecode hash for change detection + * Strips CBOR metadata suffix for stable comparison across recompilations + */ +export function computeBytecodeHash(bytecode: string): string { + const stripped = stripMetadata(bytecode) + return keccak256(stripped) +} +``` + +### 5. Enhanced Sync Process + +**File:** `packages/deployment/lib/sync-utils.ts` + +#### 5.1 Change Detection Before Sync (Bidirectional) + +Sync can flow in two directions: + +1. **Chain → Address Book**: On-chain state is newer (e.g., deployed via this package) +2. **Address Book → Rocketh**: Address book has metadata to reconstruct records + +Use `blockNumber` to determine which is authoritative when both exist. + +```ts +async function shouldSyncContract( + env: Environment, + spec: ContractSpec, + addressBook: AddressBookOps, + direction: 'toAddressBook' | 'toRocketh', +): Promise<{ sync: boolean; reason: string }> { + const existing = addressBook.getEntry(spec.name) + + // No existing entry - must sync + if (!existing) { + return { sync: true, reason: 'new contract' } + } + + // Address changed - must sync + if (existing.address.toLowerCase() !== spec.address.toLowerCase()) { + return { sync: true, reason: 'address changed' } + } + + // Check bytecode hash if available + const deployment = existing.deployment ?? existing.implementationDeployment + if (deployment?.bytecodeHash) { + const artifact = loadArtifact(spec.name) + const localHash = computeBytecodeHash(artifact.deployedBytecode) + if (deployment.bytecodeHash !== localHash) { + return { sync: false, reason: 'local bytecode changed - manual intervention required' } + } + } + + // For bidirectional sync, compare blockNumbers if both exist + if (direction === 'toAddressBook' && deployment?.blockNumber) { + const rockethRecord = env.getOrNull(spec.name) + if (rockethRecord?.receipt?.blockNumber) { + const rockethBlock = parseInt(rockethRecord.receipt.blockNumber) + if (deployment.blockNumber >= rockethBlock) { + return { sync: false, reason: 'address book is current or newer' } + } + } + } + + // No changes detected + return { sync: false, reason: 'unchanged' } +} +``` + +#### 5.2 Complete Record Reconstruction + +```ts +async function reconstructRockethRecord( + env: Environment, + spec: ContractSpec, + addressBook: AddressBookOps, +): Promise { + const entry = addressBook.getEntry(spec.name) + const artifact = loadArtifact(spec.name) + const deployment = entry.deployment + + // Verify we can reconstruct + if (!deployment) { + throw new Error(`Missing deployment metadata for ${spec.name}`) + } + + // Verify bytecode hasn't changed + const localHash = computeBytecodeHash(artifact.deployedBytecode) + if (deployment.bytecodeHash !== localHash) { + throw new Error(`Local bytecode differs from deployed for ${spec.name}`) + } + + // Optionally fetch tx details for complete record + const tx = deployment.txHash ? await env.network.provider.getTransaction(deployment.txHash) : undefined + + return { + address: entry.address, + abi: artifact.abi, + bytecode: artifact.bytecode, + deployedBytecode: artifact.deployedBytecode, + argsData: deployment.argsData, + metadata: artifact.metadata ?? '', + transaction: tx + ? { + hash: deployment.txHash, + nonce: tx.nonce.toString(), + origin: tx.from, + } + : undefined, + receipt: deployment.blockNumber + ? { + blockNumber: deployment.blockNumber.toString(), + } + : undefined, + } +} +``` + +### 6. Pre-flight Validation + +**File:** `packages/deployment/lib/deployment-validation.ts` (new) + +```ts +export interface ValidationResult { + contract: string + status: 'valid' | 'warning' | 'error' + message: string +} + +/** + * Validate deployment records can be reconstructed + * Run before any deployment to catch issues early + */ +export async function validateDeploymentRecords( + env: Environment, + addressBook: AddressBookOps, + contracts: string[], +): Promise { + const results: ValidationResult[] = [] + + for (const name of contracts) { + if (!addressBook.entryExists(name)) { + results.push({ contract: name, status: 'valid', message: 'not deployed' }) + continue + } + + const entry = addressBook.getEntry(name) + + // Check address has code + const code = await env.network.provider.getCode(entry.address) + if (code === '0x') { + results.push({ + contract: name, + status: 'error', + message: `no code at ${entry.address}`, + }) + continue + } + + // Check deployment metadata exists + if (!entry.deployment) { + results.push({ + contract: name, + status: 'warning', + message: 'missing deployment metadata (legacy entry)', + }) + continue + } + + // Verify bytecode hash + const artifact = loadArtifact(name) + const localHash = computeBytecodeHash(artifact.deployedBytecode) + if (entry.deployment.bytecodeHash !== localHash) { + results.push({ + contract: name, + status: 'warning', + message: 'local bytecode differs from deployed', + }) + continue + } + + // Verify argsData matches tx (optional, requires chain lookup) + if (entry.deployment.txHash) { + const tx = await env.network.provider.getTransaction(entry.deployment.txHash) + if (tx) { + const extractedArgs = tx.data.slice(artifact.bytecode.length) + if (extractedArgs !== entry.deployment.argsData) { + results.push({ + contract: name, + status: 'error', + message: 'argsData mismatch with deployment tx', + }) + continue + } + } + } + + results.push({ contract: name, status: 'valid', message: 'ok' }) + } + + return results +} +``` + +### 7. Update Deploy Scripts + +**File:** `packages/deployment/rocketh/deploy.ts` and deploy scripts + +After successful deployment, persist metadata to address book: + +```ts +// In deployment helper after successful deploy +const deploymentMetadata: DeploymentMetadata = { + txHash: result.transaction.hash, + argsData: result.argsData, + bytecodeHash: computeBytecodeHash(artifact.deployedBytecode), + blockNumber: result.receipt.blockNumber, +} + +addressBook.setDeploymentMetadata(contractName, deploymentMetadata) +``` + +## Implementation Order + +1. **Phase 1: Types & Utilities** + - Extend `AddressBookEntry` type in toolshed + - Add `DeploymentMetadata` type + - Extend `PendingImplementation` with deployment field + - Add `computeBytecodeHash` utility (uses existing `stripMetadata`) + - Update address book validation for new fields + +2. **Phase 2: AddressBookOps** + - Add new methods for deployment metadata + - Unit tests for new methods + +3. **Phase 3: Sync Enhancement** + - Change detection before sync (bidirectional) + - Record reconstruction from metadata + - Preserve existing metadata (don't overwrite without change) + - Use blockNumber for conflict resolution + +4. **Phase 4: Validation** + - Implement pre-flight validation + - Add validation task/command + - Integrate into deploy flow + +5. **Phase 5: Deploy Integration** + - Update deploy helpers to persist metadata + - Capture block timestamp for human readability + - Test end-to-end deploy → sync → verify flow + +**Note on existing entries:** Contracts already deployed without metadata will simply not have the new fields. They cannot be reconstructed anyway if bytecode has changed. New deployments will automatically capture full metadata going forward. + +## Size Impact + +Per-contract addition to address book: + +- `txHash`: 66 chars +- `argsData`: variable (typically 66-200 chars) +- `bytecodeHash`: 66 chars +- `blockNumber`: ~10 chars (optional) +- `timestamp`: ~24 chars (optional, ISO 8601) + +**Total: ~250-400 bytes per contract** (vs 40-60KB for full rocketh records) + +## Testing Strategy + +1. Unit tests for bytecode hash computation +2. Unit tests for record reconstruction +3. Integration tests for sync with metadata +4. E2E tests for deploy → validate → verify flow +5. Test handling of legacy entries (without metadata) + +## Open Questions + +1. Should `bytecodeHash` include or exclude CBOR metadata? + - **Recommendation: exclude** (stable across recompilations) + - Use existing `stripMetadata()` before hashing + +2. Should validation be blocking or warning-only? + - **Recommendation: configurable**, default to warning + - Critical errors (no code at address) should block + +3. Should `timestamp` use block timestamp or deployment time? + - **Recommendation: block timestamp** (deterministic, from chain) + - Format: ISO 8601 for human readability + +4. How to handle immutables in bytecodeHash? + - **Recommendation: hash artifact bytecode** (with zeros for immutables) + - This detects source changes, not deployment-time value changes + - Use `bytecodeMatches()` for full comparison when needed diff --git a/packages/deployment/hardhat.config.ts b/packages/deployment/hardhat.config.ts new file mode 100644 index 000000000..43256ddca --- /dev/null +++ b/packages/deployment/hardhat.config.ts @@ -0,0 +1,321 @@ +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' + +import hardhatEthers from '@nomicfoundation/hardhat-ethers' +import hardhatKeystore from '@nomicfoundation/hardhat-keystore' +import hardhatVerify from '@nomicfoundation/hardhat-verify' +import type { HardhatUserConfig } from 'hardhat/config' +import { configVariable } from 'hardhat/config' +import hardhatDeploy from 'hardhat-deploy' + +import checkDeployerTask from './tasks/check-deployer.js' +// Import tasks (HH v3 task API) +import deploymentStatusTask from './tasks/deployment-status.js' +import { ethBalanceTask, ethCheckKeyTask, ethFundTask } from './tasks/eth-tasks.js' +import executeGovernanceTask from './tasks/execute-governance.js' +import grantRoleTask from './tasks/grant-role.js' +import { grtBalanceTask, grtMintTask, grtStatusTask, grtTransferTask } from './tasks/grt-tasks.js' +import listPendingTask from './tasks/list-pending-implementations.js' +import listRolesTask from './tasks/list-roles.js' +import { reoDisableTask, reoEnableTask, reoIndexersTask, reoStatusTask } from './tasks/reo-tasks.js' +import resetForkTask from './tasks/reset-fork.js' +import revokeRoleTask from './tasks/revoke-role.js' +import { ssStatusTask } from './tasks/ss-tasks.js' +import syncTask from './tasks/sync.js' +import verifyContractTask from './tasks/verify-contract.js' + +// ESM compatibility +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// Package paths +const packageRoot = __dirname + +// Hardhat v3 does not auto-set HARDHAT_NETWORK (v2 did). +// isLocalNetworkMode() in address-book-utils.ts relies on this env var to +// select addresses-local-network.json over addresses.json. +const networkArg = process.argv.find((_, i, a) => a[i - 1] === '--network') +if (networkArg === 'localNetwork') { + process.env.HARDHAT_NETWORK = 'localNetwork' +} + +// RPC URLs with defaults +const ARBITRUM_ONE_RPC = process.env.ARBITRUM_ONE_RPC || 'https://arb1.arbitrum.io/rpc' +const ARBITRUM_SEPOLIA_RPC = process.env.ARBITRUM_SEPOLIA_RPC || 'https://sepolia-rollup.arbitrum.io/rpc' + +/** + * Convert network name to env var prefix: arbitrumSepolia → ARBITRUM_SEPOLIA + */ +function networkToEnvPrefix(networkName: string): string { + return networkName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase() +} + +/** + * Get deployer key name for a network. + * Always uses network-specific key (e.g., ARBITRUM_SEPOLIA_DEPLOYER_KEY). + * + * Keystore: npx hardhat keystore set ARBITRUM_SEPOLIA_DEPLOYER_KEY + * Env var: export ARBITRUM_SEPOLIA_DEPLOYER_KEY=0x... + */ +function getDeployerKeyName(networkName: string): string { + const prefix = networkToEnvPrefix(networkName) + return `${prefix}_DEPLOYER_KEY` +} + +/** + * Parse --tags from process.argv. + * Returns null when --tags is not present. + */ +function parseTagsFromArgv(): string[] | null { + const argv = process.argv + for (let i = 0; i < argv.length; i++) { + const a = argv[i] + if (a === '--tags') { + if (i + 1 >= argv.length) return null + return argv[i + 1].split(',') + } + if (a.startsWith('--tags=')) { + return a.slice('--tags='.length).split(',') + } + } + return null +} + +/** + * Detect whether the current invocation needs a deployer account. + * + * The deployer key is only needed when the `deploy` task is invoked with + * action verbs in `--tags` that perform mutations (deploy, upgrade, configure, + * transfer, integrate, all). Status-only runs (`--tags Component` without + * action verbs) are read-only and don't need the deployer key. + * + * Other tasks (reo:enable, grant-role, eth:fund, ...) resolve keys at + * execution time via resolveConfigVar(), and read-only tasks need no key + * at all. + * + * Gating configVariable() on this lets the hardhat-keystore plugin prompt for + * the password only when the user actually runs a mutating deploy action, + * instead of on every `deploy` invocation. + */ +function getTaskName(): string | null { + for (const arg of process.argv.slice(2)) { + if (arg.startsWith('-')) continue + return arg + } + return null +} + +function needsDeployerAccount(): boolean { + // Non-deploy tasks resolve keys at runtime; deploy:sync is read-only + if (getTaskName() !== 'deploy') return false + + // Status-only runs (no action verbs in --tags) don't need a signer + const tags = parseTagsFromArgv() + if (!tags) return false + + const ACTION_VERBS = ['deploy', 'upgrade', 'configure', 'transfer', 'integrate', 'all'] + return tags.some((tag) => ACTION_VERBS.includes(tag)) +} + +/** + * Dummy private key used when no real deployer key is needed. + * + * Rocketh requires at least one account to resolve namedAccounts.deployer. + * For status-only runs we provide this throwaway key so environment creation + * succeeds without prompting the keystore. The resulting address + * (0x7E5F...95Bdf) is filtered out by getDeployer() — status scripts infer + * the real deployer from the ProxyAdmin owner on-chain. + */ +const DUMMY_DEPLOYER_KEY = '0x0000000000000000000000000000000000000000000000000000000000000001' + +/** + * Get accounts config for a network. + * + * When the deploy task is invoked with action verbs (deploy, upgrade, etc.), + * returns a configVariable so the hardhat-keystore plugin resolves the + * deployer key from the keystore (with env-var fallback). + * + * For status-only deploy runs and all other tasks, returns a dummy key so + * rocketh can initialise namedAccounts without a keystore prompt. Signing + * tasks resolve keys themselves via resolveConfigVar(). + * + * Set the key via either: + * npx hardhat keystore set ARBITRUM_SEPOLIA_DEPLOYER_KEY + * export ARBITRUM_SEPOLIA_DEPLOYER_KEY=0x... + */ +const getNetworkAccounts = (networkName: string) => { + if (!needsDeployerAccount()) return [DUMMY_DEPLOYER_KEY] + const keyName = getDeployerKeyName(networkName) + if (networkName === networkArg && !process.env[keyName]) { + console.log(`\n Deployer key: ${keyName}`) + console.log(` Set via: npx hardhat keystore set ${keyName}\n`) + } + return [configVariable(keyName)] +} + +// Fork network detection (HARDHAT_FORK is the standard for hardhat-deploy v2) +const FORK_NETWORK = process.env.HARDHAT_FORK || process.env.FORK_NETWORK + +const config: HardhatUserConfig = { + // Register HH v3 plugins + plugins: [hardhatEthers, hardhatKeystore, hardhatVerify, hardhatDeploy], + + // Register tasks + tasks: [ + checkDeployerTask, + deploymentStatusTask, + ethBalanceTask, + ethCheckKeyTask, + ethFundTask, + executeGovernanceTask, + grantRoleTask, + grtBalanceTask, + grtMintTask, + grtStatusTask, + grtTransferTask, + listPendingTask, + listRolesTask, + reoDisableTask, + reoEnableTask, + reoIndexersTask, + reoStatusTask, + ssStatusTask, + syncTask, + resetForkTask, + revokeRoleTask, + verifyContractTask, + ], + + // Chain descriptors for fork execution and local development + chainDescriptors: { + // Graph Local Network (chainId 1337) + 1337: { + name: 'Graph Local Network', + hardforkHistory: { + berlin: { blockNumber: 0 }, + london: { blockNumber: 0 }, + merge: { blockNumber: 0 }, + shanghai: { blockNumber: 0 }, + cancun: { blockNumber: 0 }, + }, + }, + // Local hardhat network (for non-fork runs) + 31337: { + name: 'Hardhat Local', + hardforkHistory: { + berlin: { blockNumber: 0 }, + london: { blockNumber: 0 }, + merge: { blockNumber: 0 }, + shanghai: { blockNumber: 0 }, + cancun: { blockNumber: 0 }, + }, + }, + // Arbitrum Sepolia + 421614: { + name: 'Arbitrum Sepolia', + hardforkHistory: { + berlin: { blockNumber: 0 }, + london: { blockNumber: 0 }, + merge: { blockNumber: 0 }, + shanghai: { blockNumber: 0 }, + cancun: { blockNumber: 0 }, + }, + }, + // Arbitrum One + 42161: { + name: 'Arbitrum One', + hardforkHistory: { + berlin: { blockNumber: 0 }, + london: { blockNumber: 0 }, + merge: { blockNumber: 0 }, + shanghai: { blockNumber: 0 }, + cancun: { blockNumber: 0 }, + }, + }, + }, + + // No local solidity sources - deployment uses external artifacts only + // Verification should be done from the source package (e.g., packages/horizon) + paths: { + tests: path.join(packageRoot, 'test'), + artifacts: path.join(packageRoot, 'artifacts'), + cache: path.join(packageRoot, 'cache'), + }, + networks: { + // Hardhat network - uses chainId 31337 even when forking (rocketh/hardhat-deploy v2 expects this) + // The FORK_NETWORK env var determines which network to fork, but chainId stays 31337 + hardhat: { + type: 'edr-simulated', + chainId: 31337, + accounts: { + mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect', + }, + forking: FORK_NETWORK + ? { + url: FORK_NETWORK === 'arbitrumSepolia' ? ARBITRUM_SEPOLIA_RPC : ARBITRUM_ONE_RPC, + enabled: true, + } + : undefined, + }, + localhost: { + type: 'http', + url: 'http://127.0.0.1:8545', + chainId: 31337, + }, + // Fork network for hardhat-deploy v2 (HARDHAT_FORK env var) + fork: { + type: 'edr-simulated', + chainId: 31337, + accounts: { + mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect', + }, + forking: FORK_NETWORK + ? { + url: FORK_NETWORK === 'arbitrumSepolia' ? ARBITRUM_SEPOLIA_RPC : ARBITRUM_ONE_RPC, + enabled: true, + } + : undefined, + }, + // Graph Local Network — chainId 1337, contracts deployed fresh by an + // upstream step that populates addresses-local-network.json files. + localNetwork: { + type: 'http', + url: process.env.LOCAL_NETWORK_RPC || 'http://chain:8545', + chainId: 1337, + accounts: { + mnemonic: 'test test test test test test test test test test test junk', + }, + }, + arbitrumOne: { + type: 'http', + chainId: 42161, + url: ARBITRUM_ONE_RPC, + accounts: getNetworkAccounts('arbitrumOne'), + }, + arbitrumSepolia: { + type: 'http', + chainId: 421614, + url: ARBITRUM_SEPOLIA_RPC, + accounts: getNetworkAccounts('arbitrumSepolia'), + }, + }, + // Named accounts are configured in rocketh/config.ts for hardhat-deploy v2 + // External artifacts are loaded via direct imports in deploy scripts + + // Contract verification config (hardhat-verify v3) + // API key from keystore, gated to deploy:verify to avoid prompting on every task. + // Set via: npx hardhat keystore set ARBISCAN_API_KEY + verify: { + etherscan: { + apiKey: getTaskName() === 'deploy:verify' ? configVariable('ARBISCAN_API_KEY') : '', + }, + sourcify: { + enabled: false, + }, + blockscout: { + enabled: false, + }, + }, +} + +export default config diff --git a/packages/deployment/lib/abis.ts b/packages/deployment/lib/abis.ts new file mode 100644 index 000000000..ece524796 --- /dev/null +++ b/packages/deployment/lib/abis.ts @@ -0,0 +1,150 @@ +/** + * Shared ABI definitions for contract interactions + * + * Generated ABIs are produced by `pnpm generate:abis` from contract artifacts. + * The contract registry drives which ABIs and interface IDs are generated. + * Only ACCESS_CONTROL_ENUMERABLE_ABI is hand-maintained (generic role queries). + */ + +// Re-export all generated typed ABIs, aliases, and interface IDs +export { + CONTROLLER_ABI, + DIRECT_ALLOCATION_ABI, + GRAPH_PROXY_ADMIN_ABI, + GRAPH_TOKEN_ABI, + IERC165_ABI, + IERC165_INTERFACE_ID, + IISSUANCE_TARGET_INTERFACE_ID, + INITIALIZE_GOVERNOR_ABI, + IREWARDS_MANAGER_INTERFACE_ID, + ISSUANCE_ALLOCATOR_ABI, + ISSUANCE_TARGET_ABI, + OZ_PROXY_ADMIN_ABI, + PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + REWARDS_ELIGIBILITY_ORACLE_ABI, + REWARDS_MANAGER_ABI, + REWARDS_MANAGER_DEPRECATED_ABI, + SET_TARGET_ALLOCATION_ABI, +} from './generated/abis.js' + +// ============================================================================ +// Hand-rolled minimal ABIs (not in @graphprotocol/interfaces) +// ============================================================================ + +/** + * Minimal ABI for RecurringCollector pause guardian management + * + * RC's pause guardian functions are not part of an interface in + * @graphprotocol/interfaces. Used by RC configure and the GIP-0088 upgrade + * batch to manage `setPauseGuardian` / `pauseGuardians`. + */ +export const RECURRING_COLLECTOR_PAUSE_ABI = [ + { + inputs: [{ name: '_pauseGuardian', type: 'address' }], + name: 'pauseGuardians', + outputs: [{ type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { name: '_pauseGuardian', type: 'address' }, + { name: '_allowed', type: 'bool' }, + ], + name: 'setPauseGuardian', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const + +/** + * Minimal ABI for SubgraphService allocation close guard + * + * `blockClosingAllocationWithActiveAgreement` is part of the SS interface but + * not generated yet. Used by `GIP-0088:issuance-close-guard` and the goal + * status display. + */ +export const SUBGRAPH_SERVICE_CLOSE_GUARD_ABI = [ + { + inputs: [], + name: 'getBlockClosingAllocationWithActiveAgreement', + outputs: [{ type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ name: 'enabled', type: 'bool' }], + name: 'setBlockClosingAllocationWithActiveAgreement', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const + +// ============================================================================ +// Generic ABIs for role enumeration +// ============================================================================ + +/** + * Minimal ABI for AccessControlEnumerable role queries and management + * Works with any contract inheriting from OZ AccessControlEnumerableUpgradeable + */ +export const ACCESS_CONTROL_ENUMERABLE_ABI = [ + // View functions + { + inputs: [{ name: 'role', type: 'bytes32' }], + name: 'getRoleMemberCount', + outputs: [{ type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { name: 'role', type: 'bytes32' }, + { name: 'index', type: 'uint256' }, + ], + name: 'getRoleMember', + outputs: [{ type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { name: 'role', type: 'bytes32' }, + { name: 'account', type: 'address' }, + ], + name: 'hasRole', + outputs: [{ type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ name: 'role', type: 'bytes32' }], + name: 'getRoleAdmin', + outputs: [{ type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + // Write functions (require admin role) + { + inputs: [ + { name: 'role', type: 'bytes32' }, + { name: 'account', type: 'address' }, + ], + name: 'grantRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { name: 'role', type: 'bytes32' }, + { name: 'account', type: 'address' }, + ], + name: 'revokeRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const diff --git a/packages/deployment/lib/address-book-ops.ts b/packages/deployment/lib/address-book-ops.ts new file mode 100644 index 000000000..9f6f506f3 --- /dev/null +++ b/packages/deployment/lib/address-book-ops.ts @@ -0,0 +1,554 @@ +/** + * Data operations for managing address book entries + * + * This module provides a Layer 1 interface for address book operations. + * It focuses on WHAT data is being set, not WHY (deployment, sync, etc.). + * + * @example + * ```typescript + * import { graph } from '../rocketh/deploy.js' + * + * // Get AddressBookOps directly - never see the base AddressBook class + * const addressBook = graph.getIssuanceAddressBook(chainId) + * + * // Read operations + * const entry = addressBook.getEntry('RewardsManager') + * if (addressBook.entryExists('RewardsManager')) { ... } + * + * // Write operations + * addressBook.setProxy('RewardsManager', proxyAddr, implAddr, adminAddr, 'transparent') + * addressBook.setPendingImplementationWithMetadata('RewardsManager', newImplAddr, metadata) + * ``` + */ + +import type { + AddressBook, + AddressBookEntry, + DeploymentMetadata, + PendingImplementation, +} from '@graphprotocol/toolshed/deployments' + +// Re-export types that callers may need +export type { AddressBookEntry, DeploymentMetadata, PendingImplementation } + +/** + * Type alias for AddressBookOps with any contract name + * + * Use this when working with a union of different address book types, + * where TypeScript would otherwise infer the contract name as `never`. + * + * @example + * ```typescript + * const addressBook: AnyAddressBookOps = + * type === 'horizon' ? getHorizonAddressBook() : getIssuanceAddressBook() + * + * // Now methods work without type errors + * addressBook.getEntry(contractName) + * ``` + */ +export type AnyAddressBookOps = AddressBookOps + +/** + * Data operations for address book management + * + * Wraps a base AddressBook instance with structured data operations that: + * - Use data-centric naming (set/clear, not record/sync) + * - Encapsulate field-level business logic + * - Enforce type safety + * - Maintain consistency + * + * This is Layer 1 - pure local storage operations with no on-chain interactions. + */ +export class AddressBookOps { + constructor(private readonly addressBook: AddressBook) {} + + /** + * Set contract address + * + * Use for non-proxied contracts: Controller, EpochManager, GraphToken, etc. + * + * @example + * ```typescript + * ops.setContract('Controller', '0x123...') + * ``` + */ + setContract(name: ContractName, address: string): void { + this.addressBook.setEntry(name, { address }) + } + + /** + * Set all proxy-related fields at once + * + * Sets: address (proxy), proxy type, implementation, and proxyAdmin + * + * @example + * ```typescript + * ops.setProxy( + * 'RewardsManager', + * '0xProxy...', + * '0xImpl...', + * '0xAdmin...', + * 'transparent' + * ) + * ``` + */ + setProxy( + name: ContractName, + proxyAddress: string, + implementationAddress: string, + proxyAdminAddress: string, + proxyType: 'graph' | 'transparent', + ): void { + this.addressBook.setEntry(name, { + address: proxyAddress, + proxy: proxyType, + proxyAdmin: proxyAdminAddress, + implementation: implementationAddress, + }) + } + + /** + * Set implementation address (active implementation) + * + * Updates the active implementation field. Does not affect pendingImplementation. + * + * @example + * ```typescript + * ops.setImplementation('RewardsManager', '0xNewImpl...') + * ``` + */ + setImplementation(name: ContractName, implementationAddress: string): void { + const entry = this.addressBook.getEntry(name as string) + + this.addressBook.setEntry(name, { + ...entry, + implementation: implementationAddress, + }) + } + + /** + * Set proxy admin address + * + * @example + * ```typescript + * ops.setProxyAdmin('RewardsManager', '0xAdmin...') + * ``` + */ + setProxyAdmin(name: ContractName, proxyAdminAddress: string): void { + const entry = this.addressBook.getEntry(name as string) + + this.addressBook.setEntry(name, { + ...entry, + proxyAdmin: proxyAdminAddress, + }) + } + + /** + * Promote pending implementation to active + * + * Moves pendingImplementation.address → implementation and clears pendingImplementation. + * + * @example + * ```typescript + * ops.promotePendingImplementation('RewardsManager') + * ``` + * + * @throws Error if contract not found + * @throws Error if no pending implementation exists + */ + promotePendingImplementation(name: ContractName): void { + const entry = this.addressBook.getEntry(name as string) + + if (!entry) { + throw new Error(`Contract ${name} not found in address book`) + } + + if (!entry.pendingImplementation) { + throw new Error(`No pending implementation found for ${name}`) + } + + this.addressBook.setEntry(name, { + ...entry, + implementation: entry.pendingImplementation.address, + pendingImplementation: undefined, + }) + } + + /** + * Clear pending implementation + * + * Sets pendingImplementation to undefined. + * + * @example + * ```typescript + * ops.clearPendingImplementation('RewardsManager') + * ``` + */ + clearPendingImplementation(name: ContractName): void { + const entry = this.addressBook.getEntry(name as string) + + if (!entry) { + throw new Error(`Contract ${name} not found in address book`) + } + + this.addressBook.setEntry(name, { + ...entry, + pendingImplementation: undefined, + }) + } + + /** + * Set implementation and auto-clear pending if it matches + * + * This is a convenience method that: + * 1. Sets the implementation field to the provided address + * 2. If pendingImplementation matches the new implementation, clears it + * + * This encapsulates the common pattern: "set implementation from on-chain state, + * and if pending was applied, clear it." + * + * @example + * ```typescript + * // Caller fetches from chain, then updates address book + * const onChainImpl = await getImplementationAddress(proxyAddress) + * ops.setImplementationAndClearIfMatches('RewardsManager', onChainImpl) + * ``` + */ + setImplementationAndClearIfMatches(name: ContractName, implementationAddress: string): void { + const entry = this.addressBook.getEntry(name as string) + + // Check if pending matches the new implementation + const pendingMatches = entry.pendingImplementation?.address.toLowerCase() === implementationAddress.toLowerCase() + + // Update implementation and clear pending if it matches + this.addressBook.setEntry(name, { + ...entry, + implementation: implementationAddress, + ...(pendingMatches && { pendingImplementation: undefined }), + }) + } + + // ============================================================================ + // Deployment Metadata Operations + // ============================================================================ + + /** + * Set deployment metadata for a non-proxied contract + * + * @example + * ```typescript + * ops.setDeploymentMetadata('Controller', { + * txHash: '0xabc...', + * argsData: '0x...', + * bytecodeHash: '0x...', + * blockNumber: 12345678, + * timestamp: '2024-01-15T10:30:00Z', + * }) + * ``` + */ + setDeploymentMetadata(name: ContractName, metadata: DeploymentMetadata): void { + const entry = this.addressBook.getEntry(name as string) + + this.addressBook.setEntry(name, { + ...entry, + deployment: metadata, + }) + } + + /** + * Set proxy deployment metadata (for proxied contracts) + * + * @example + * ```typescript + * ops.setProxyDeploymentMetadata('RewardsManager', { + * txHash: '0xabc...', + * argsData: '0x...', + * bytecodeHash: '0x...', + * }) + * ``` + */ + setProxyDeploymentMetadata(name: ContractName, metadata: DeploymentMetadata): void { + const entry = this.addressBook.getEntry(name as string) + + this.addressBook.setEntry(name, { + ...entry, + proxyDeployment: metadata, + }) + } + + /** + * Set implementation deployment metadata (for proxied contracts) + * + * @example + * ```typescript + * ops.setImplementationDeploymentMetadata('RewardsManager', { + * txHash: '0xabc...', + * argsData: '0x...', + * bytecodeHash: '0x...', + * }) + * ``` + */ + setImplementationDeploymentMetadata(name: ContractName, metadata: DeploymentMetadata): void { + const entry = this.addressBook.getEntry(name as string) + + this.addressBook.setEntry(name, { + ...entry, + implementationDeployment: metadata, + }) + } + + /** + * Set pending implementation deployment metadata + * + * Updates only the deployment metadata for an existing pending implementation. + * Use this for backfilling metadata when rocketh has newer data than address book. + * + * @example + * ```typescript + * ops.setPendingDeploymentMetadata('RewardsManager', { + * txHash: '0xabc...', + * argsData: '0x...', + * bytecodeHash: '0x...', + * }) + * ``` + */ + setPendingDeploymentMetadata(name: ContractName, metadata: DeploymentMetadata): void { + const entry = this.addressBook.getEntry(name as string) + + if (!entry?.pendingImplementation) { + throw new Error(`No pending implementation found for ${name}`) + } + + this.addressBook.setEntry(name, { + ...entry, + pendingImplementation: { + ...entry.pendingImplementation, + deployment: metadata, + }, + }) + } + + /** + * Set pending implementation with full deployment metadata for verification + * and record reconstruction. + * + * @example + * ```typescript + * ops.setPendingImplementationWithMetadata('RewardsManager', '0xNewImpl...', { + * txHash: '0xabc...', + * argsData: '0x...', + * bytecodeHash: '0x...', + * blockNumber: 12345678, + * }) + * ``` + */ + setPendingImplementationWithMetadata( + name: ContractName, + implementationAddress: string, + metadata: DeploymentMetadata, + ): void { + const entry = this.addressBook.getEntry(name as string) + + if (!entry) { + throw new Error(`Contract ${name} not found in address book`) + } + + if (!entry.proxy) { + throw new Error(`Contract ${name} is not a proxy contract`) + } + + const pendingImplementation: PendingImplementation = { + address: implementationAddress, + deployment: metadata, + } + + this.addressBook.setEntry(name, { + ...entry, + pendingImplementation, + }) + } + + /** + * Promote pending implementation to active, preserving deployment metadata + * + * Moves pendingImplementation to active and transfers deployment metadata + * to implementationDeployment. + * + * @example + * ```typescript + * ops.promotePendingImplementationWithMetadata('RewardsManager') + * ``` + */ + promotePendingImplementationWithMetadata(name: ContractName): void { + const entry = this.addressBook.getEntry(name as string) + + if (!entry) { + throw new Error(`Contract ${name} not found in address book`) + } + + if (!entry.pendingImplementation) { + throw new Error(`No pending implementation found for ${name}`) + } + + this.addressBook.setEntry(name, { + ...entry, + implementation: entry.pendingImplementation.address, + implementationDeployment: entry.pendingImplementation.deployment, + pendingImplementation: undefined, + }) + } + + // ============================================================================ + // Read Operations + // ============================================================================ + + /** + * Get deployment metadata for a contract + * + * Returns the appropriate deployment metadata based on contract type: + * - Non-proxied: returns `deployment` + * - Proxied: returns `implementationDeployment` (the active implementation) + * + * @example + * ```typescript + * const metadata = addressBook.getDeploymentMetadata('RewardsManager') + * if (metadata) { + * console.log(`Deployed at block ${metadata.blockNumber}`) + * } + * ``` + */ + getDeploymentMetadata(name: ContractName): DeploymentMetadata | undefined { + const entry = this.addressBook.getEntry(name as string) + // For proxied contracts, return implementation metadata; for non-proxied, return deployment + return entry.proxy ? entry.implementationDeployment : entry.deployment + } + + /** + * Check if deployment metadata exists and has required fields + * + * @example + * ```typescript + * if (addressBook.hasCompleteDeploymentMetadata('RewardsManager')) { + * // Safe to reconstruct rocketh record + * } + * ``` + */ + hasCompleteDeploymentMetadata(name: ContractName): boolean { + const metadata = this.getDeploymentMetadata(name) + if (!metadata) return false + return Boolean(metadata.txHash && metadata.argsData && metadata.bytecodeHash) + } + + /** + * Get an entry from the address book + * + * @example + * ```typescript + * const entry = addressBook.getEntry('RewardsManager') + * console.log(entry.address, entry.implementation) + * ``` + */ + getEntry(name: ContractName): AddressBookEntry { + return this.addressBook.getEntry(name as string) + } + + /** + * Check if an entry exists in the address book + * + * @example + * ```typescript + * if (addressBook.entryExists('RewardsManager')) { + * const entry = addressBook.getEntry('RewardsManager') + * } + * ``` + */ + entryExists(name: ContractName): boolean { + return this.addressBook.entryExists(name as string) + } + + /** + * List all contract names with pending implementations + * + * @example + * ```typescript + * const pending = addressBook.listPendingImplementations() + * for (const contractName of pending) { + * const entry = addressBook.getEntry(contractName) + * console.log(`${contractName}: ${entry.pendingImplementation?.address}`) + * } + * ``` + */ + listPendingImplementations(): ContractName[] { + const contractsWithPending: ContractName[] = [] + + for (const contractName of this.addressBook.listEntries()) { + const entry = this.addressBook.getEntry(contractName) + if (entry?.pendingImplementation) { + contractsWithPending.push(contractName) + } + } + + return contractsWithPending + } + + /** + * Check if a name is a valid contract name for this address book + * + * @example + * ```typescript + * if (addressBook.isContractName('RewardsManager')) { + * // TypeScript knows this is a valid contract name + * } + * ``` + */ + isContractName(name: string): name is ContractName { + return this.addressBook.isContractName(name) + } + + /** + * Set verification URL for a contract's deployment metadata. + * For non-proxied contracts, updates `deployment.verified`. + * For proxied contracts, updates `proxyDeployment.verified`. + * + * @example + * ```typescript + * ops.setVerified('RewardsManager', 'https://arbiscan.io/address/0x123#code') + * ``` + */ + setVerified(name: ContractName, verificationUrl: string): void { + const entry = this.addressBook.getEntry(name as string) + if (entry.proxy) { + // Proxied contract - set on proxyDeployment + this.addressBook.setEntry(name, { + ...entry, + proxyDeployment: { ...entry.proxyDeployment, verified: verificationUrl } as typeof entry.proxyDeployment, + }) + } else { + // Non-proxied contract - set on deployment + this.addressBook.setEntry(name, { + ...entry, + deployment: { ...entry.deployment, verified: verificationUrl } as typeof entry.deployment, + }) + } + } + + /** + * Set implementation verification URL (for proxied contracts) + * Updates `implementationDeployment.verified`. + * + * @example + * ```typescript + * ops.setImplementationVerified('RewardsManager', 'https://arbiscan.io/address/0x456#code') + * ``` + */ + setImplementationVerified(name: ContractName, verificationUrl: string): void { + const entry = this.addressBook.getEntry(name as string) + this.addressBook.setEntry(name, { + ...entry, + implementationDeployment: { + ...entry.implementationDeployment, + verified: verificationUrl, + } as typeof entry.implementationDeployment, + }) + } +} diff --git a/packages/deployment/lib/address-book-utils.ts b/packages/deployment/lib/address-book-utils.ts new file mode 100644 index 000000000..086b5eb34 --- /dev/null +++ b/packages/deployment/lib/address-book-utils.ts @@ -0,0 +1,461 @@ +/** + * Address Book Utilities + * + * This module provides utilities for working with address books in deployment scripts. + * It handles fork mode detection, chain ID resolution, and address book instantiation. + * + * Structure: + * 1. Fork Mode Detection - Check if running in fork mode and get network info + * 2. Chain ID Resolution - Get target chain IDs for address book lookups + * 3. Fork State Management - Copy address books for fork-local modifications + * 4. Address Book Factories - Create AddressBookOps instances for each package + */ + +import { existsSync, mkdirSync, copyFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + +import type { Environment } from '@rocketh/core/types' +import type { + GraphHorizonContractName, + GraphIssuanceContractName, + SubgraphServiceContractName, +} from '@graphprotocol/toolshed/deployments' +import { + GraphHorizonAddressBook, + GraphIssuanceAddressBook, + SubgraphServiceAddressBook, +} from '@graphprotocol/toolshed/deployments' + +import { config as rockethConfig } from '../rocketh/config.js' +import type { AnyAddressBookOps } from './address-book-ops.js' +import { AddressBookOps } from './address-book-ops.js' +import type { AddressBookType } from './contract-registry.js' + +const require = createRequire(import.meta.url) + +// ============================================================================ +// Fork Auto-Detection +// ============================================================================ + +/** + * Build a map from RPC URL hostname to network name using rocketh config. + * Used by autoDetectForkNetwork() to match anvil's forkUrl. + */ +function buildRpcHostToNetworkMap(): Map { + const map = new Map() + const environments = rockethConfig.environments + const chains = rockethConfig.chains + if (!environments || !chains) return map + + for (const [envName, envConfig] of Object.entries(environments)) { + const chainId = (envConfig as { chain: number }).chain + const chainConfig = (chains as Record)[chainId] as + | { info?: { rpcUrls?: { default?: { http?: readonly string[] } } } } + | undefined + const rpcUrls = chainConfig?.info?.rpcUrls?.default?.http + if (!rpcUrls) continue + + for (const rpcUrl of rpcUrls) { + try { + const hostname = new URL(rpcUrl).hostname + map.set(hostname, { name: envName, chainId }) + } catch { + // Skip invalid URLs + } + } + } + return map +} + +/** + * Auto-detect the fork network by querying anvil's `anvil_nodeInfo` RPC method. + * + * If FORK_NETWORK is already set, this is a no-op. + * If the provider is an anvil fork, extracts the fork URL and matches it + * against known network RPC hostnames from rocketh config. + * + * On success, sets process.env.FORK_NETWORK so all downstream synchronous + * functions (isForkMode, getForkNetwork, etc.) work without changes. + * + * @param rpcUrl - The RPC URL to query (default: http://127.0.0.1:8545) + * @returns The detected network name, or null if not a fork / not detectable + */ +export async function autoDetectForkNetwork(rpcUrl = 'http://127.0.0.1:8545'): Promise { + // Already set — nothing to do + if (process.env.FORK_NETWORK || process.env.HARDHAT_FORK) { + return process.env.FORK_NETWORK || process.env.HARDHAT_FORK || null + } + + try { + const response = await fetch(rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'anvil_nodeInfo', params: [], id: 1 }), + }) + const json = (await response.json()) as { + result?: { forkConfig?: { forkUrl?: string } } + } + const forkUrl = json.result?.forkConfig?.forkUrl + if (!forkUrl) return null + + // Match fork URL hostname against known networks + const hostMap = buildRpcHostToNetworkMap() + const forkHostname = new URL(forkUrl).hostname + const match = hostMap.get(forkHostname) + if (!match) return null + + // Set env var so all synchronous fork detection works downstream + process.env.FORK_NETWORK = match.name + return match.name + } catch { + // Not reachable or not anvil — not a fork + return null + } +} + +// ============================================================================ +// Fork Mode Detection +// ============================================================================ + +/** Network names that are local/test and support fork mode */ +const LOCAL_NETWORKS = new Set(['localhost', 'fork', 'hardhat']) + +/** + * Check if the current network is a local network. + * Uses explicit networkName if provided, falls back to HARDHAT_NETWORK env var. + * Returns true if network is unknown (preserves existing behavior for callers + * that don't pass context). + */ +function isLocalNetwork(networkName?: string): boolean { + const name = networkName ?? process.env.HARDHAT_NETWORK + if (name === undefined) return true + return LOCAL_NETWORKS.has(name) +} + +/** + * Check if running in fork mode. + * + * Fork mode requires both: + * 1. FORK_NETWORK or HARDHAT_FORK env var is set + * 2. The current network is local (localhost, fork, hardhat) + * + * This prevents fork mode from activating when running against real networks + * even if FORK_NETWORK is still set in the environment. + * + * @param networkName - Optional network name for explicit check (e.g., env.name). + * Falls back to HARDHAT_NETWORK env var if not provided. + */ +export function isForkMode(networkName?: string): boolean { + if (!isLocalNetwork(networkName)) return false + return !!(process.env.HARDHAT_FORK || process.env.FORK_NETWORK) +} + +/** + * Get the fork network name from environment. + * Returns null if not in fork mode or if running on a real network. + * + * @param networkName - Optional network name for explicit check. + * Falls back to HARDHAT_NETWORK env var if not provided. + */ +export function getForkNetwork(networkName?: string): string | null { + if (!isLocalNetwork(networkName)) return null + return process.env.HARDHAT_FORK || process.env.FORK_NETWORK || null +} + +// ============================================================================ +// Local Network Detection +// ============================================================================ + +/** + * Check if running against the Graph local network (chainId 1337). + * + * The local network deploys contracts from scratch. Address books use + * addresses-local-network.json files that the orchestrating dev environment + * is expected to symlink (or otherwise create) before this code path runs. + */ +export function isLocalNetworkMode(): boolean { + return process.env.HARDHAT_NETWORK === 'localNetwork' +} + +/** + * Get the fork state directory for a given network. + * All fork-related state (address books, governance TXs) is stored here. + * + * Returns: fork/// + * + * Stored outside deployments/ so rocketh manages its own directory cleanly. + * + * @param envName - Hardhat network name (e.g., 'fork', 'localhost') + * @param forkNetwork - Fork network name (e.g., 'arbitrumSepolia', 'arbitrumOne') + */ +export function getForkStateDir(envName: string, forkNetwork: string): string { + return path.resolve(process.cwd(), 'fork', envName, forkNetwork) +} + +/** + * Get the target chain ID for fork mode address book lookups. + * Uses rocketh config to map FORK_NETWORK environment variable to actual chain IDs. + * + * Returns null if not in fork mode - callers should use provider chain ID instead. + * + * @example + * const forkChainId = getForkTargetChainId() + * const targetChainId = forkChainId ?? providerChainId + */ +export function getForkTargetChainId(networkName?: string): number | null { + const forkNetwork = getForkNetwork(networkName) + if (!forkNetwork) return null + + // Look up chain ID from rocketh config environments + const environments = rockethConfig.environments + if (!environments) { + throw new Error('rocketh config missing environments') + } + + const environment = environments[forkNetwork as keyof typeof environments] + if (!environment) { + throw new Error(`Unknown fork network: ${forkNetwork}. Not found in rocketh config.`) + } + + const chainId = environment.chain + if (typeof chainId !== 'number') { + throw new Error(`Invalid chain ID for fork network ${forkNetwork}`) + } + + return chainId +} + +// ============================================================================ +// Chain ID Resolution +// ============================================================================ + +/** + * Get the target chain ID for address book and transaction operations. + * This is the single canonical function for resolving chain IDs. + * + * In fork mode: Returns the fork target chain ID (e.g., 42161 for arbitrumOne fork) + * In non-fork mode: Returns the provider's actual chain ID + * + * @param env - Rocketh environment (used to query provider) + * @returns The target chain ID to use for address book lookups and transactions + * + * @example + * const targetChainId = await getTargetChainIdFromEnv(env) + * const addressBook = getIssuanceAddressBook(targetChainId) + */ +export async function getTargetChainIdFromEnv(env: Environment): Promise { + const forkChainId = getForkTargetChainId(env.name) + if (forkChainId !== null) { + return forkChainId + } + + // Not in fork mode - get actual chain ID from provider + const chainIdHex = await env.network.provider.request({ method: 'eth_chainId' }) + const providerChainId = Number(chainIdHex) + + // If we're on local chain 31337 without FORK_NETWORK set, the user is most + // likely running against an anvil fork. Try auto-detecting once so callers + // (per-component sync, status scripts) can resolve the right address book + // without requiring the global sync script to have run first. + if (providerChainId === 31337 && !getForkNetwork(env.name)) { + const detected = await autoDetectForkNetwork() + if (detected) { + const detectedForkChainId = getForkTargetChainId(env.name) + if (detectedForkChainId !== null) return detectedForkChainId + } + } + + return providerChainId +} + +// ============================================================================ +// Fork State Management +// ============================================================================ + +/** + * Get the directory for fork-local address book copies. + * Uses FORK_NETWORK to determine subdirectory. + * + * Note: This function doesn't have access to env.name, so it infers the hardhat + * network from process.env.HARDHAT_NETWORK (set by Hardhat at runtime). + * Falls back to 'localhost' if not set. + */ +function getForkAddressBooksDir(): string { + const forkNetwork = getForkNetwork() + if (!forkNetwork) { + throw new Error('getForkAddressBooksDir called but not in fork mode') + } + // Infer hardhat network from environment (set by hardhat at runtime) + const envName = process.env.HARDHAT_NETWORK || 'localhost' + return getForkStateDir(envName, forkNetwork) +} + +/** + * Ensure fork address book copies exist. + * Called once at the start of sync to set up fork-local copies. + * Copies canonical address books to fork-state directory on first use. + * + * @returns Object with paths to the fork-local address books + */ +export function ensureForkAddressBooks(): { + horizonPath: string + subgraphServicePath: string + issuancePath: string +} { + const forkNetwork = getForkNetwork() + if (!forkNetwork) { + throw new Error('ensureForkAddressBooks called but not in fork mode') + } + + const forkDir = getForkAddressBooksDir() + + // Create directory if it doesn't exist + if (!existsSync(forkDir)) { + mkdirSync(forkDir, { recursive: true }) + } + + const horizonSourcePath = require.resolve('@graphprotocol/horizon/addresses.json') + const ssSourcePath = require.resolve('@graphprotocol/subgraph-service/addresses.json') + const issuanceSourcePath = require.resolve('@graphprotocol/issuance/addresses.json') + + const horizonForkPath = path.join(forkDir, 'horizon-addresses.json') + const ssForkPath = path.join(forkDir, 'subgraph-service-addresses.json') + const issuanceForkPath = path.join(forkDir, 'issuance-addresses.json') + + // Copy if fork copies don't exist yet + if (!existsSync(horizonForkPath)) { + copyFileSync(horizonSourcePath, horizonForkPath) + } + if (!existsSync(ssForkPath)) { + copyFileSync(ssSourcePath, ssForkPath) + } + if (!existsSync(issuanceForkPath)) { + copyFileSync(issuanceSourcePath, issuanceForkPath) + } + + return { + horizonPath: horizonForkPath, + subgraphServicePath: ssForkPath, + issuancePath: issuanceForkPath, + } +} + +// ============================================================================ +// Address Book Path Utilities +// ============================================================================ + +/** + * Get the path to the Horizon address book. + * In fork mode, returns path to fork-local copy. + * In local network mode, returns path to addresses-local-network.json. + * In normal mode, returns path to package address book. + */ +export function getHorizonAddressBookPath(): string { + if (isForkMode()) { + const { horizonPath } = ensureForkAddressBooks() + return horizonPath + } + if (isLocalNetworkMode()) { + return require.resolve('@graphprotocol/horizon/addresses-local-network.json') + } + return require.resolve('@graphprotocol/horizon/addresses.json') +} + +/** + * Get the path to the SubgraphService address book. + * In fork mode, returns path to fork-local copy. + * In local network mode, returns path to addresses-local-network.json. + * In normal mode, returns path to package address book. + */ +export function getSubgraphServiceAddressBookPath(): string { + if (isForkMode()) { + const { subgraphServicePath } = ensureForkAddressBooks() + return subgraphServicePath + } + if (isLocalNetworkMode()) { + return require.resolve('@graphprotocol/subgraph-service/addresses-local-network.json') + } + return require.resolve('@graphprotocol/subgraph-service/addresses.json') +} + +/** + * Get the path to the Issuance address book. + * In fork mode, returns path to fork-local copy. + * In local network mode, returns path to addresses-local-network.json. + * In normal mode, returns path to package address book. + */ +export function getIssuanceAddressBookPath(): string { + if (isForkMode()) { + const { issuancePath } = ensureForkAddressBooks() + return issuancePath + } + if (isLocalNetworkMode()) { + return require.resolve('@graphprotocol/issuance/addresses-local-network.json') + } + return require.resolve('@graphprotocol/issuance/addresses.json') +} + +// ============================================================================ +// Address Book Factories +// ============================================================================ + +/** + * Get an AddressBookOps instance for Graph Horizon contracts. + * Automatically uses fork-local copy in fork mode. + * + * @param chainId - Target chain ID. In fork mode, uses fork target chain ID if not provided. + * In non-fork mode, must be provided by caller (from provider). + */ +export function getHorizonAddressBook(chainId?: number): AddressBookOps { + const targetChainId = chainId ?? getForkTargetChainId() ?? 31337 + const baseAddressBook = new GraphHorizonAddressBook(getHorizonAddressBookPath(), targetChainId) + return new AddressBookOps(baseAddressBook) +} + +/** + * Get an AddressBookOps instance for Subgraph Service contracts. + * Automatically uses fork-local copy in fork mode. + * + * @param chainId - Target chain ID. In fork mode, uses fork target chain ID if not provided. + * In non-fork mode, must be provided by caller (from provider). + */ +export function getSubgraphServiceAddressBook(chainId?: number): AddressBookOps { + const targetChainId = chainId ?? getForkTargetChainId() ?? 31337 + const baseAddressBook = new SubgraphServiceAddressBook(getSubgraphServiceAddressBookPath(), targetChainId) + return new AddressBookOps(baseAddressBook) +} + +/** + * Get an AddressBookOps instance for Graph Issuance contracts. + * Automatically uses fork-local copy in fork mode. + * + * @param chainId - Target chain ID. In fork mode, uses fork target chain ID if not provided. + * In non-fork mode, must be provided by caller (from provider). + */ +export function getIssuanceAddressBook(chainId?: number): AddressBookOps { + const targetChainId = chainId ?? getForkTargetChainId() ?? 31337 + const baseAddressBook = new GraphIssuanceAddressBook(getIssuanceAddressBookPath(), targetChainId) + return new AddressBookOps(baseAddressBook) +} + +/** + * Get the address book ops for a contract by its declared address book type. + * + * Single routing point — adding a new address book type will surface as a + * TypeScript exhaustiveness error here rather than silently misrouting. + */ +export function getAddressBookForType(type: AddressBookType, chainId?: number): AnyAddressBookOps { + switch (type) { + case 'horizon': + return getHorizonAddressBook(chainId) + case 'subgraph-service': + return getSubgraphServiceAddressBook(chainId) + case 'issuance': + return getIssuanceAddressBook(chainId) + default: { + const _exhaustive: never = type + throw new Error(`Unknown address book type: ${String(_exhaustive)}`) + } + } +} diff --git a/packages/deployment/lib/apply-configuration.ts b/packages/deployment/lib/apply-configuration.ts new file mode 100644 index 000000000..8bc4e14a1 --- /dev/null +++ b/packages/deployment/lib/apply-configuration.ts @@ -0,0 +1,164 @@ +/** + * Apply Configuration Utility + * + * Generic utility for checking and applying configuration conditions in deploy mode. + * Handles the standard pattern: check conditions → generate TXs for gaps → execute or save. + * Supports both param conditions (getter/setter) and role conditions (hasRole/grantRole). + */ + +import type { Environment } from '@rocketh/core/types' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +import { + type ConfigCondition, + type ConfigurationStatus, + type ParamCondition, + type RoleCondition, + checkConditions, +} from './contract-checks.js' +import { createGovernanceTxBuilder, executeTxBatchDirect, saveGovernanceTx } from './execute-governance.js' + +/** + * Options for applyConfiguration + */ +export interface ApplyConfigurationOptions { + /** Contract name (for messages and TX batch naming) */ + contractName: string + + /** Contract address */ + contractAddress: string + + /** Whether the caller can execute directly (has required role) */ + canExecuteDirectly: boolean + + /** Account to execute from (if canExecuteDirectly) */ + executor?: string +} + +/** + * Result of applyConfiguration + */ +export interface ApplyConfigurationResult { + /** Status of all conditions (T | boolean due to mixed param/role conditions) */ + status: ConfigurationStatus + + /** Whether any changes were made/proposed */ + changesNeeded: boolean + + /** Whether changes were executed directly (vs saved for governance) */ + executedDirectly: boolean +} + +/** + * Apply configuration conditions in deploy mode + * + * Standard flow: + * 1. Check all conditions against on-chain state + * 2. If all OK, return (no-op) + * 3. Build TX batch for conditions that need updating + * 4. If canExecuteDirectly: execute TXs and return + * 5. If not: save TX batch for governance and exit + * + * @example + * ```typescript + * const conditions = createREOConditions() + * const result = await applyConfiguration(env, client, conditions, { + * contractName: 'RewardsEligibilityOracle', + * contractAddress: reoAddress, + * canExecuteDirectly: deployerHasGovernorRole, + * executor: deployer, + * }) + * ``` + */ +export async function applyConfiguration( + env: Environment, + client: PublicClient, + conditions: ConfigCondition[], + options: ApplyConfigurationOptions, +): Promise> { + const { contractName, contractAddress, canExecuteDirectly, executor } = options + + // 1. Check all conditions + env.showMessage(`📋 Checking ${contractName} configuration...\n`) + + const status = await checkConditions(client, contractAddress, conditions) + + // Display results + for (const result of status.conditions) { + env.showMessage(` ${result.message}`) + } + + // 2. If all OK, no-op + if (status.allOk) { + env.showMessage(`\n✅ ${contractName} configuration already matches target\n`) + return { status, changesNeeded: false, executedDirectly: false } + } + + // 3. Build TX batch for failing conditions + env.showMessage('\n🔨 Building configuration TX batch...\n') + + const builder = await createGovernanceTxBuilder(env, `configure-${contractName}`) + + const failingConditions = conditions.filter((_, i) => !status.conditions[i].ok) + + for (const condition of failingConditions) { + if (condition.type === 'role') { + // Role condition: fetch role bytes32, then grantRole or revokeRole + const roleCondition = condition as RoleCondition + const action = roleCondition.action ?? 'grant' + const role = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: roleCondition.abi, + functionName: roleCondition.roleGetter, + })) as `0x${string}` + + const functionName = action === 'grant' ? 'grantRole' : 'revokeRole' + const data = encodeFunctionData({ + abi: roleCondition.abi, + functionName, + args: [role, roleCondition.targetAccount as `0x${string}`], + }) + builder.addTx({ to: contractAddress, value: '0', data }) + + const formatAccount = roleCondition.formatAccount ?? ((a) => a) + env.showMessage(` + ${functionName}(${roleCondition.roleGetter}, ${formatAccount(roleCondition.targetAccount)})`) + } else { + // Param condition: simple setter call + const paramCondition = condition as ParamCondition + const data = encodeFunctionData({ + abi: paramCondition.abi, + functionName: paramCondition.setter, + args: [paramCondition.target], + }) + builder.addTx({ to: contractAddress, value: '0', data }) + + const format = paramCondition.format ?? String + env.showMessage(` + ${paramCondition.setter}(${format(paramCondition.target)})`) + } + } + + // 4/5. Execute or save based on access + if (canExecuteDirectly && executor) { + env.showMessage('\n🔨 Executing configuration TX batch...\n') + await executeTxBatchDirect(env, builder, executor) + env.showMessage(`\n✅ ${contractName} configuration updated\n`) + return { status, changesNeeded: true, executedDirectly: true } + } else { + saveGovernanceTx(env, builder, `${contractName} configuration`) + return { status, changesNeeded: true, executedDirectly: false } + } +} + +/** + * Check configuration status only (no TX generation) + * + * Use this for status checks outside of deploy mode. + */ +export async function checkConfigurationStatus( + client: PublicClient, + contractAddress: string, + conditions: ConfigCondition[], +): Promise> { + return checkConditions(client, contractAddress, conditions) +} diff --git a/packages/deployment/lib/artifact-loaders.ts b/packages/deployment/lib/artifact-loaders.ts new file mode 100644 index 000000000..e48c6e587 --- /dev/null +++ b/packages/deployment/lib/artifact-loaders.ts @@ -0,0 +1,215 @@ +import { readFileSync } from 'node:fs' +import { createRequire } from 'node:module' + +import type { Artifact } from '@rocketh/core/types' + +import type { LibraryArtifactResolver, LinkReferences } from './bytecode-utils.js' + +// Create require for JSON imports in ESM +const require = createRequire(import.meta.url) + +/** + * Load artifact from @graphprotocol/contracts package + * + * @param contractPath - Path within contracts/ (e.g., 'rewards', 'l2/token') + * @param contractName - Contract name (e.g., 'RewardsManager', 'L2GraphToken') + */ +export function loadContractsArtifact(contractPath: string, contractName: string): Artifact { + const artifactPath = require.resolve( + `@graphprotocol/contracts/artifacts/contracts/${contractPath}/${contractName}.sol/${contractName}.json`, + ) + const artifact = JSON.parse(readFileSync(artifactPath, 'utf-8')) + return { + abi: artifact.abi, + bytecode: artifact.bytecode as `0x${string}`, + deployedBytecode: artifact.deployedBytecode as `0x${string}`, + metadata: artifact.metadata || '', + } +} + +/** + * Load artifact from @graphprotocol/subgraph-service package (Hardhat format) + * + * @param contractName - Contract name (e.g., 'SubgraphService') + */ +export function loadSubgraphServiceArtifact(contractName: string): Artifact { + // Support subdirectory names like 'libraries/IndexingAgreement' + const baseName = contractName.includes('/') ? contractName.split('/').pop()! : contractName + const artifactPath = require.resolve( + `@graphprotocol/subgraph-service/artifacts/contracts/${contractName}.sol/${baseName}.json`, + ) + const artifact = JSON.parse(readFileSync(artifactPath, 'utf-8')) + + return { + abi: artifact.abi, + bytecode: artifact.bytecode as `0x${string}`, + deployedBytecode: artifact.deployedBytecode as `0x${string}`, + metadata: artifact.metadata || '', + linkReferences: artifact.linkReferences, + deployedLinkReferences: artifact.deployedLinkReferences, + } +} + +/** + * Load artifact from @graphprotocol/issuance package + * + * @param artifactSubpath - Path within artifacts/ (e.g., 'contracts/allocate/IssuanceAllocator.sol/IssuanceAllocator') + */ +export function loadIssuanceArtifact(artifactSubpath: string): Artifact { + const artifactPath = require.resolve(`@graphprotocol/issuance/artifacts/${artifactSubpath}.json`) + const artifact = JSON.parse(readFileSync(artifactPath, 'utf-8')) + return { + abi: artifact.abi, + bytecode: artifact.bytecode as `0x${string}`, + deployedBytecode: artifact.deployedBytecode as `0x${string}`, + metadata: artifact.metadata || '', + linkReferences: artifact.linkReferences, + deployedLinkReferences: artifact.deployedLinkReferences, + } +} + +/** + * Load artifact from @graphprotocol/horizon package build directory + * + * @param artifactSubpath - Path within build/contracts/ (e.g., '@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol/ProxyAdmin') + */ +export function loadHorizonBuildArtifact(artifactSubpath: string): Artifact { + const artifactPath = require.resolve(`@graphprotocol/horizon/artifacts/${artifactSubpath}.json`) + const artifact = JSON.parse(readFileSync(artifactPath, 'utf-8')) + return { + abi: artifact.abi, + bytecode: artifact.bytecode as `0x${string}`, + deployedBytecode: artifact.deployedBytecode as `0x${string}`, + metadata: artifact.metadata || '', + linkReferences: artifact.linkReferences, + deployedLinkReferences: artifact.deployedLinkReferences, + } +} + +/** + * Load artifact from @openzeppelin/contracts package build directory + * + * @param contractName - Contract name (e.g., 'ProxyAdmin', 'AccessControl') + */ +export function loadOpenZeppelinArtifact(contractName: string): Artifact { + const artifactPath = require.resolve(`@openzeppelin/contracts/build/contracts/${contractName}.json`) + const artifact = JSON.parse(readFileSync(artifactPath, 'utf-8')) + return { + abi: artifact.abi, + bytecode: artifact.bytecode as `0x${string}`, + deployedBytecode: artifact.deployedBytecode as `0x${string}`, + metadata: artifact.metadata || '', + } +} + +/** + * Create a library artifact resolver for a given package. + * + * Library artifacts live at /artifacts//.json, + * mirroring the linkReferences source paths from Hardhat compilation. + */ +function createPackageLibraryResolver(packagePrefix: string): LibraryArtifactResolver { + return (sourcePath: string, libraryName: string) => { + try { + const libPath = require.resolve(`${packagePrefix}/${sourcePath}/${libraryName}.json`) + const artifact = JSON.parse(readFileSync(libPath, 'utf-8')) + return { + deployedBytecode: artifact.deployedBytecode as string, + deployedLinkReferences: artifact.deployedLinkReferences as LinkReferences | undefined, + } + } catch { + return undefined + } + } +} + +/** + * Get a library artifact resolver for the given artifact source type. + * Returns undefined if the source type doesn't support library resolution. + */ +export function getLibraryResolver(sourceType: string): LibraryArtifactResolver | undefined { + switch (sourceType) { + case 'subgraph-service': + return createPackageLibraryResolver('@graphprotocol/subgraph-service/artifacts') + case 'horizon': + return createPackageLibraryResolver('@graphprotocol/horizon/artifacts') + case 'issuance': + return createPackageLibraryResolver('@graphprotocol/issuance/artifacts') + case 'contracts': + return createPackageLibraryResolver('@graphprotocol/contracts/artifacts') + default: + return undefined + } +} + +/** + * Pre-link library addresses into an artifact's creation bytecode. + * + * Rocketh's deploy() stores the artifact's bytecode verbatim but compares + * against linked bytecode on subsequent runs. For artifacts with library + * references this causes a permanent mismatch (unlinked placeholders vs + * resolved addresses), triggering a redeploy every time. + * + * Call this before passing the artifact to rocketh's deploy(). The returned + * artifact has fully resolved bytecode and cleared linkReferences, so + * rocketh stores what it will compare against next run. + * + * @param artifact - Artifact with unlinked bytecode and linkReferences + * @param libraries - Map of library name → deployed address + */ +export function linkArtifactLibraries(artifact: Artifact, libraries: Record): Artifact { + let bytecode = artifact.bytecode as string + + if (artifact.linkReferences) { + for (const [, fileReferences] of Object.entries( + artifact.linkReferences as Record>>, + )) { + for (const [libName, fixups] of Object.entries(fileReferences)) { + const addr = libraries[libName] + if (!addr) continue + for (const fixup of fixups) { + bytecode = + bytecode.substring(0, 2 + fixup.start * 2) + + addr.substring(2) + + bytecode.substring(2 + (fixup.start + fixup.length) * 2) + } + } + } + } + + return { + ...artifact, + bytecode: bytecode as `0x${string}`, + linkReferences: undefined, + } +} + +/** + * Load OpenZeppelin TransparentUpgradeableProxy artifact (v5) + */ +export function loadTransparentProxyArtifact(): Artifact { + return loadOpenZeppelinArtifact('TransparentUpgradeableProxy') +} + +// Convenience functions for common issuance contracts + +/** + * Load IssuanceAllocator artifact + */ +export function loadIssuanceAllocatorArtifact(): Artifact { + return loadIssuanceArtifact('contracts/allocate/IssuanceAllocator.sol/IssuanceAllocator') +} + +/** + * Load DirectAllocation artifact + */ +export function loadDirectAllocationArtifact(): Artifact { + return loadIssuanceArtifact('contracts/allocate/DirectAllocation.sol/DirectAllocation') +} + +/** + * Load RewardsEligibilityOracle artifact + */ +export function loadRewardsEligibilityOracleArtifact(): Artifact { + return loadIssuanceArtifact('contracts/eligibility/RewardsEligibilityOracle.sol/RewardsEligibilityOracle') +} diff --git a/packages/deployment/lib/bytecode-utils.ts b/packages/deployment/lib/bytecode-utils.ts new file mode 100644 index 000000000..f08795b48 --- /dev/null +++ b/packages/deployment/lib/bytecode-utils.ts @@ -0,0 +1,149 @@ +import { keccak256, toUtf8Bytes } from 'ethers' + +/** + * Bytecode utilities for smart contract deployment. + * + * These utilities handle bytecode hashing for change detection: + * - Strip Solidity CBOR metadata (varies between compilations) + * - Resolve library placeholders using actual library bytecode + * - Compute stable bytecode hash for comparison + * + * This allows detecting when local artifact code has changed by comparing + * stored bytecodeHash with the current artifact's hash. + */ + +/** + * Hardhat artifact link references: sourcePath → libraryName → offsets[] + */ +export type LinkReferences = Record>> + +/** + * Resolves a library artifact given its source path and name. + * Returns the artifact's deployedBytecode and its own linkReferences (for recursion). + */ +export type LibraryArtifactResolver = ( + sourcePath: string, + libraryName: string, +) => { deployedBytecode: string; deployedLinkReferences?: LinkReferences } | undefined + +/** + * Strip Solidity metadata from bytecode. + * Metadata is CBOR-encoded at the end, with last 2 bytes indicating length. + */ +export function stripMetadata(bytecode: string): string { + if (!bytecode || bytecode.length < 4) return bytecode + // Remove 0x prefix for processing + const code = bytecode.startsWith('0x') ? bytecode.slice(2) : bytecode + if (code.length < 4) return bytecode + + // Last 2 bytes = metadata length (big-endian) + const metadataLength = parseInt(code.slice(-4), 16) + // Sanity check: metadata should be reasonable size (< 500 bytes = 1000 hex chars) + if (metadataLength > 500 || metadataLength * 2 + 4 > code.length) { + return bytecode // Can't strip, return as-is + } + // Strip metadata + 2-byte length suffix + const prefix = bytecode.startsWith('0x') ? '0x' : '' + return prefix + code.slice(0, -(metadataLength * 2 + 4)) +} + +/** + * Compute the Solidity library placeholder hash for a given source path and name. + * This is keccak256("sourcePath:libraryName") truncated to 34 hex chars (17 bytes). + */ +function libraryPlaceholderHash(sourcePath: string, libraryName: string): string { + return keccak256(toUtf8Bytes(`${sourcePath}:${libraryName}`)).slice(2, 36) +} + +/** + * Resolve library placeholders in bytecode using actual library bytecode hashes. + * + * For each library in deployedLinkReferences, computes its bytecode hash + * (recursively resolving its own library deps) and substitutes that hash + * (truncated to 20 bytes / 40 hex chars) into the placeholder slots. + * + * This means the final hash reflects both the contract's code and all + * transitive library code. If any library changes, the hash changes. + */ +function resolveLibraryPlaceholders( + bytecode: string, + linkReferences: LinkReferences | undefined, + resolver: LibraryArtifactResolver | undefined, +): string { + if (!linkReferences || !resolver) { + // No link references or no resolver — zero out any remaining placeholders + return bytecode.replace(/__\$[0-9a-fA-F]{34}\$__/g, '0'.repeat(40)) + } + + let result = bytecode + for (const [sourcePath, libraries] of Object.entries(linkReferences)) { + for (const libraryName of Object.keys(libraries)) { + const placeholderHash = libraryPlaceholderHash(sourcePath, libraryName) + const placeholder = `__\\$${placeholderHash}\\$__` + + const libArtifact = resolver(sourcePath, libraryName) + let replacement: string + if (libArtifact) { + // Recursively compute the library's bytecode hash (handles nested deps) + const libHash = computeBytecodeHashWithLibraries( + libArtifact.deployedBytecode, + libArtifact.deployedLinkReferences, + resolver, + ) + // Use first 40 hex chars (20 bytes) of the hash as the replacement + replacement = libHash.slice(2, 42) + } else { + // Library artifact not available — zero fill + replacement = '0'.repeat(40) + } + + result = result.replace(new RegExp(placeholder, 'g'), replacement) + } + } + + // Zero any remaining unresolved placeholders (shouldn't happen but defensive) + return result.replace(/__\$[0-9a-fA-F]{34}\$__/g, '0'.repeat(40)) +} + +/** + * Compute a stable hash of bytecode for change detection, with library resolution. + * + * Normalizations applied before hashing: + * - Strip CBOR metadata suffix (varies between compilations) + * - Resolve library placeholders with actual library bytecode hashes + * + * @param bytecode - The bytecode to hash + * @param linkReferences - Artifact's deployedLinkReferences (optional) + * @param resolver - Function to load library artifacts (optional) + * @returns keccak256 hash of the normalized bytecode + */ +function computeBytecodeHashWithLibraries( + bytecode: string, + linkReferences: LinkReferences | undefined, + resolver: LibraryArtifactResolver | undefined, +): string { + const stripped = stripMetadata(bytecode) + const resolved = resolveLibraryPlaceholders(stripped, linkReferences, resolver) + const prefixed = resolved.startsWith('0x') ? resolved : `0x${resolved}` + return keccak256(prefixed) +} + +/** + * Compute a stable hash of bytecode for change detection. + * + * For simple contracts (no library references), pass just the bytecode. + * For contracts with external libraries, pass linkReferences and a resolver + * to include transitive library code in the hash. + * + * @param bytecode - The bytecode to hash (typically artifact.deployedBytecode) + * @param linkReferences - Artifact's deployedLinkReferences (optional) + * @param resolver - Function to load library artifacts for recursive resolution (optional) + * @returns keccak256 hash of the bytecode with metadata stripped + */ +export function computeBytecodeHash( + bytecode: string, + linkReferences?: LinkReferences, + resolver?: LibraryArtifactResolver, +): string { + return computeBytecodeHashWithLibraries(bytecode, linkReferences, resolver) +} diff --git a/packages/deployment/lib/contract-checks.ts b/packages/deployment/lib/contract-checks.ts new file mode 100644 index 000000000..74e446779 --- /dev/null +++ b/packages/deployment/lib/contract-checks.ts @@ -0,0 +1,969 @@ +import type { Environment } from '@rocketh/core/types' +import type { Abi, PublicClient } from 'viem' + +import { + ACCESS_CONTROL_ENUMERABLE_ABI, + GRAPH_TOKEN_ABI, + IERC165_ABI, + IERC165_INTERFACE_ID, + IISSUANCE_TARGET_INTERFACE_ID, + ISSUANCE_TARGET_ABI, + PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + REWARDS_ELIGIBILITY_ORACLE_ABI, + REWARDS_MANAGER_ABI, + REWARDS_MANAGER_DEPRECATED_ABI, +} from './abis.js' +import { getTargetChainIdFromEnv } from './address-book-utils.js' +import { getGovernor, getPauseGuardian } from './controller-utils.js' +import { graph } from '../rocketh/deploy.js' + +/** + * Check if a contract supports a specific interface via ERC165 + * + * @param client - Viem public client + * @param contractAddress - Contract address to check + * @param interfaceId - Interface ID (4 bytes hex string like '0x01ffc9a7') + * @returns true if interface is supported, false otherwise + */ +export async function supportsInterface( + client: PublicClient, + contractAddress: string, + interfaceId: string, +): Promise { + try { + const supported = await client.readContract({ + address: contractAddress as `0x${string}`, + abi: IERC165_ABI, + functionName: 'supportsInterface', + args: [interfaceId as `0x${string}`], + }) + return supported as boolean + } catch { + return false + } +} + +/** + * Check if RewardsManager has been upgraded to support IIssuanceTarget + * + * The upgraded RewardsManager implements IERC165 and IIssuanceTarget interfaces. + * This check verifies the upgrade by testing for IIssuanceTarget support. + * + * @param client - Viem public client + * @param rmAddress - RewardsManager address + * @returns true if upgraded, false otherwise + */ +export async function isRewardsManagerUpgraded(client: PublicClient, rmAddress: string): Promise { + return supportsInterface(client, rmAddress, IISSUANCE_TARGET_INTERFACE_ID) +} + +/** + * Require RewardsManager to be upgraded, exiting if not + * + * @param client - Viem public client + * @param rmAddress - RewardsManager address + * @param env - Deployment environment for showing messages + * @exits 1 if RewardsManager has not been upgraded (expected prerequisite state) + */ +export async function requireRewardsManagerUpgraded( + client: PublicClient, + rmAddress: string, + env: Environment, +): Promise { + const upgraded = await isRewardsManagerUpgraded(client, rmAddress) + if (!upgraded) { + env.showMessage(`\n❌ RewardsManager has not been upgraded yet`) + env.showMessage(` The on-chain RewardsManager does not support IERC165/IIssuanceTarget`) + env.showMessage(` Run: npx hardhat deploy:execute-governance --network ${env.name}`) + env.showMessage(` (This will execute the pending RewardsManager upgrade TX)\n`) + process.exit(1) + } +} + +/** + * Check IssuanceAllocator activation state + * + * Returns status of: + * - Whether IA is set as issuanceAllocator on RewardsManager + * - Whether IA has minter role on GraphToken + */ +export interface ActivationStatus { + iaIntegrated: boolean + iaMinter: boolean + currentIssuanceAllocator: string +} + +export async function checkIssuanceAllocatorActivation( + client: PublicClient, + iaAddress: string, + rmAddress: string, + gtAddress: string, +): Promise { + // Check RM.issuanceAllocator() == IA + const currentIA = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: ISSUANCE_TARGET_ABI, + functionName: 'getIssuanceAllocator', + })) as string + + const iaIntegrated = currentIA.toLowerCase() === iaAddress.toLowerCase() + + // Check GraphToken.isMinter(IA) + const iaMinter = (await client.readContract({ + address: gtAddress as `0x${string}`, + abi: GRAPH_TOKEN_ABI, + functionName: 'isMinter', + args: [iaAddress as `0x${string}`], + })) as boolean + + return { + iaIntegrated, + iaMinter, + currentIssuanceAllocator: currentIA, + } +} + +/** + * Check if IssuanceAllocator is fully activated + * + * @returns true if both integrated with RM and has minter role + */ +export async function isIssuanceAllocatorActivated( + client: PublicClient, + iaAddress: string, + rmAddress: string, + gtAddress: string, +): Promise { + const status = await checkIssuanceAllocatorActivation(client, iaAddress, rmAddress, gtAddress) + return status.iaIntegrated && status.iaMinter +} + +/** + * Get issuancePerBlock from RewardsManager + */ +export async function getRewardsManagerRawIssuanceRate(client: PublicClient, rmAddress: string): Promise { + const rate = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: REWARDS_MANAGER_DEPRECATED_ABI, + functionName: 'issuancePerBlock', + })) as bigint + return rate +} + +// ============================================================================ +// REO Role Checks +// ============================================================================ + +/** + * Result of checking OPERATOR_ROLE assignment on an REO instance + */ +export interface OperatorRoleCheckResult { + /** Whether the check passed (correct assignment state) */ + ok: boolean + /** Number of addresses with OPERATOR_ROLE */ + count: number + /** The expected operator address (null if not configured) */ + expectedOperator: string | null + /** Actual role holders (if enumerable) */ + actualHolders: string[] + /** Human-readable status message */ + message: string +} + +/** + * Check OPERATOR_ROLE assignment on an REO instance + * + * This is the SINGLE authoritative check for OPERATOR_ROLE correctness. + * Used by both deployment scripts and status checks. + * + * Rules: + * - If expectedOperator is provided: exactly 1 holder, must be expectedOperator + * - If expectedOperator is null: exactly 0 holders + * + * @param client - Viem public client + * @param reoAddress - REO instance address + * @param expectedOperator - Expected operator address (from address book), or null if not configured + * @returns Check result with pass/fail status and details + */ +export async function checkOperatorRole( + client: PublicClient, + reoAddress: string, + expectedOperator: string | null, +): Promise { + // Get OPERATOR_ROLE constant + const operatorRole = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'OPERATOR_ROLE', + })) as `0x${string}` + + // Get role member count + const count = Number( + (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'getRoleMemberCount', + args: [operatorRole], + })) as bigint, + ) + + // Get actual holders + const actualHolders: string[] = [] + for (let i = 0; i < count; i++) { + const holder = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'getRoleMember', + args: [operatorRole, BigInt(i)], + })) as string + actualHolders.push(holder) + } + + // Validate based on expected state + if (expectedOperator === null) { + // No operator configured - must have zero holders + if (count === 0) { + return { + ok: true, + count, + expectedOperator, + actualHolders, + message: 'OPERATOR_ROLE: none assigned (NetworkOperator not configured)', + } + } else { + return { + ok: false, + count, + expectedOperator, + actualHolders, + message: `OPERATOR_ROLE: unexpected holders (${count}) when NetworkOperator not configured: ${actualHolders.join(', ')}`, + } + } + } else { + // Operator configured - must have exactly one holder matching expected + if (count === 0) { + return { + ok: false, + count, + expectedOperator, + actualHolders, + message: `OPERATOR_ROLE: not assigned (expected ${expectedOperator})`, + } + } else if (count === 1 && actualHolders[0].toLowerCase() === expectedOperator.toLowerCase()) { + return { + ok: true, + count, + expectedOperator, + actualHolders, + message: `OPERATOR_ROLE: ${expectedOperator}`, + } + } else if (count === 1) { + return { + ok: false, + count, + expectedOperator, + actualHolders, + message: `OPERATOR_ROLE: wrong holder (expected ${expectedOperator}, got ${actualHolders[0]})`, + } + } else { + return { + ok: false, + count, + expectedOperator, + actualHolders, + message: `OPERATOR_ROLE: too many holders (${count}): ${actualHolders.join(', ')} (expected only ${expectedOperator})`, + } + } + } +} + +// ============================================================================ +// Generic Configuration Condition Framework +// ============================================================================ + +/** + * Format seconds as human-readable duration + */ +export function formatDuration(seconds: bigint | number): string { + const secs = typeof seconds === 'bigint' ? Number(seconds) : seconds + const days = secs / 86400 + if (Number.isInteger(days)) { + return `${days} day${days === 1 ? '' : 's'}` + } + return `${days.toFixed(2)} days` +} + +/** + * A parameter condition - checks and sets a simple getter/setter value + * + * @template T - The type of the configuration value (e.g., bigint, string, boolean) + */ +export interface ParamCondition { + /** Condition type discriminator */ + type?: 'param' + + /** Condition name (used in messages and as identifier) */ + name: string + + /** Human-readable description */ + description: string + + /** ABI for contract reads/writes */ + abi: Abi + + /** Function name to read current value */ + getter: string + + /** Function name to set new value */ + setter: string + + /** Target value for this condition */ + target: T + + /** Compare current to target (defaults to strict equality) */ + compare?: (current: T, target: T) => boolean + + /** Format value for display (defaults to String()) */ + format?: (value: T) => string +} + +/** + * A role condition - checks and grants/revokes a role for an account + */ +export interface RoleCondition { + /** Condition type discriminator */ + type: 'role' + + /** Condition name (used in messages and as identifier) */ + name: string + + /** Human-readable description */ + description: string + + /** ABI for contract reads/writes */ + abi: Abi + + /** Function name to get role bytes32 (e.g., 'PAUSE_ROLE') */ + roleGetter: string + + /** Account that should have/not have the role */ + targetAccount: string + + /** Action: grant (account should have role) or revoke (account should NOT have role) */ + action?: 'grant' | 'revoke' + + /** Format account for display (defaults to address) */ + formatAccount?: (address: string) => string +} + +/** + * A single configuration condition - either a param or role condition + * + * @template T - The type for param conditions (e.g., bigint, string, boolean) + */ +export type ConfigCondition = ParamCondition | RoleCondition + +/** + * Result of checking a single condition + */ +export interface ConditionCheckResult { + /** Condition name */ + name: string + /** Whether current matches target */ + ok: boolean + /** Current on-chain value */ + current: T + /** Target value */ + target: T + /** Human-readable status message */ + message: string +} + +/** + * Result of checking multiple conditions + */ +export interface ConfigurationStatus { + /** Individual condition results */ + conditions: ConditionCheckResult[] + /** Whether all conditions passed */ + allOk: boolean +} + +/** + * Check a single condition against on-chain state + */ +export async function checkCondition( + client: PublicClient, + contractAddress: string, + condition: ConfigCondition, +): Promise> { + // Handle role conditions + if (condition.type === 'role') { + const role = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: condition.abi, + functionName: condition.roleGetter, + })) as `0x${string}` + + const hasRole = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: condition.abi, + functionName: 'hasRole', + args: [role, condition.targetAccount as `0x${string}`], + })) as boolean + + const action = condition.action ?? 'grant' + const formatAccount = condition.formatAccount ?? ((a) => a) + + // For grant: ok if hasRole=true. For revoke: ok if hasRole=false + const ok = action === 'grant' ? hasRole : !hasRole + const status = ok ? '✓' : action === 'grant' ? '✗ needs grant' : '✗ needs revoke' + + return { + name: condition.name, + ok, + current: hasRole as T | boolean, + target: (action === 'grant') as T | boolean, + message: `${condition.description}: ${formatAccount(condition.targetAccount)} ${status}`, + } + } + + // Handle param conditions (default) + const current = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: condition.abi, + functionName: condition.getter, + })) as T + + const compare = condition.compare ?? ((a, b) => a === b) + const format = condition.format ?? String + + const ok = compare(current, condition.target) + const status = ok ? '✓' : '✗ needs update' + + return { + name: condition.name, + ok, + current, + target: condition.target, + message: `${condition.description}: ${format(current)} [target: ${format(condition.target)}] ${status}`, + } +} + +/** + * Check multiple conditions against on-chain state + * + * Use this for status checks outside of deploy mode. + */ +export async function checkConditions( + client: PublicClient, + contractAddress: string, + conditions: ConfigCondition[], +): Promise> { + const results = await Promise.all(conditions.map((c) => checkCondition(client, contractAddress, c))) + + return { + conditions: results, + allOk: results.every((r) => r.ok), + } +} + +// ============================================================================ +// REO Conditions +// ============================================================================ + +/** Default REO configuration values */ +export const REO_DEFAULTS = { + eligibilityPeriod: 14n * 24n * 60n * 60n, // 14 days + oracleUpdateTimeout: 7n * 24n * 60n * 60n, // 7 days +} as const + +/** + * REO configuration conditions + * + * Reusable for both deploy-mode configuration and status checks. + */ +export function createREOParamConditions( + targets: { eligibilityPeriod?: bigint; oracleUpdateTimeout?: bigint } = {}, +): ParamCondition[] { + return [ + { + name: 'eligibilityPeriod', + description: 'Eligibility period', + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + getter: 'getEligibilityPeriod', + setter: 'setEligibilityPeriod', + target: targets.eligibilityPeriod ?? REO_DEFAULTS.eligibilityPeriod, + format: (v) => `${v} seconds (${formatDuration(v)})`, + }, + { + name: 'oracleUpdateTimeout', + description: 'Oracle update timeout', + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + getter: 'getOracleUpdateTimeout', + setter: 'setOracleUpdateTimeout', + target: targets.oracleUpdateTimeout ?? REO_DEFAULTS.oracleUpdateTimeout, + format: (v) => `${v} seconds (${formatDuration(v)})`, + }, + ] +} + +/** + * REO role condition targets + */ +export interface REORoleTargets { + /** Account to grant PAUSE_ROLE (pauseGuardian) */ + pauseGuardian: string + /** Account to grant OPERATOR_ROLE (networkOperator) */ + networkOperator: string + /** Account to grant GOVERNOR_ROLE (governor) */ + governor: string +} + +/** + * Create REO role conditions + * + * Returns conditions for granting: + * - PAUSE_ROLE to pauseGuardian + * - OPERATOR_ROLE to networkOperator + * - GOVERNOR_ROLE to governor + */ +export function createREORoleConditions(targets: REORoleTargets): RoleCondition[] { + return [ + { + type: 'role', + name: 'pauseRole', + description: 'PAUSE_ROLE', + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + roleGetter: 'PAUSE_ROLE', + targetAccount: targets.pauseGuardian, + }, + { + type: 'role', + name: 'operatorRole', + description: 'OPERATOR_ROLE', + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + roleGetter: 'OPERATOR_ROLE', + targetAccount: targets.networkOperator, + }, + { + type: 'role', + name: 'governorRole', + description: 'GOVERNOR_ROLE', + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + roleGetter: 'GOVERNOR_ROLE', + targetAccount: targets.governor, + }, + ] +} + +/** + * Create all REO conditions (params + roles) + * + * Low-level factory - prefer getREOConditions(env) which fetches targets automatically. + */ +export function createAllREOConditions( + paramTargets: { eligibilityPeriod?: bigint; oracleUpdateTimeout?: bigint } = {}, + roleTargets: REORoleTargets, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): ConfigCondition[] { + // Note: setEligibilityValidation requires OPERATOR_ROLE, not GOVERNOR_ROLE. + // It is enabled by the network operator after deployment, not in the configure step. + return [...createREOParamConditions(paramTargets), ...createREORoleConditions(roleTargets)] +} + +/** + * Create REO deployer revoke condition + * + * Checks that deployer does NOT have GOVERNOR_ROLE (should be revoked). + */ +export function createREODeployerRevokeCondition(deployer: string): RoleCondition { + return { + type: 'role', + name: 'deployerGovernorRoleRevoked', + description: 'Deployer GOVERNOR_ROLE', + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + roleGetter: 'GOVERNOR_ROLE', + targetAccount: deployer, + action: 'revoke', + } +} + +// ============================================================================ +// REO Condition Fetchers (single source of truth) +// ============================================================================ + +/** + * Get REO configuration conditions with targets fetched from environment + * + * This is the SINGLE SOURCE OF TRUTH for REO conditions. + * Fetches governor, pauseGuardian, networkOperator automatically. + * + * Requires NetworkOperator to be configured in the issuance address book. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function getREOConditions(env: Environment): Promise[]> { + const governor = await getGovernor(env) + const pauseGuardian = await getPauseGuardian(env) + const ab = graph.getIssuanceAddressBook(await getTargetChainIdFromEnv(env)) + + const networkOperator = ab.entryExists('NetworkOperator') ? ab.getEntry('NetworkOperator')?.address : null + if (!networkOperator) { + env.showMessage('\n❌ NetworkOperator not configured in issuance address book') + env.showMessage(' Add NetworkOperator to packages/issuance/addresses.json\n') + process.exit(1) + } + + return createAllREOConditions({}, { governor, pauseGuardian, networkOperator }) +} + +/** + * Get REO transfer governance conditions (revoke deployer role) + * + * Single source of truth for transfer-governance step. + */ +export function getREOTransferGovernanceConditions(deployer: string): ConfigCondition[] { + return [createREODeployerRevokeCondition(deployer)] +} + +// ============================================================================ +// REO Role Checks +// ============================================================================ + +/** + * Result of checking if an account has a specific role + */ +export interface RoleCheckResult { + /** Whether the account has the role */ + hasRole: boolean + /** The role being checked (bytes32) */ + role: `0x${string}` + /** The account being checked */ + account: string + /** Human-readable status message */ + message: string +} + +/** + * Check if an account has a specific role on an REO instance + */ +export async function checkREORole( + client: PublicClient, + reoAddress: string, + roleName: 'GOVERNOR_ROLE' | 'PAUSE_ROLE' | 'OPERATOR_ROLE' | 'ORACLE_ROLE', + account: string, +): Promise { + const role = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: roleName, + })) as `0x${string}` + + const hasRole = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'hasRole', + args: [role, account as `0x${string}`], + })) as boolean + + return { + hasRole, + role, + account, + message: `${roleName}: ${hasRole ? '✓' : '✗'} (${account})`, + } +} + +// ============================================================================ +// RewardsManager Integration Conditions +// ============================================================================ + +/** + * Compare addresses (case-insensitive) + */ +export function addressEquals(a: string, b: string): boolean { + return a.toLowerCase() === b.toLowerCase() +} + +/** + * Truncate address for display + */ +export function formatAddress(address: string): string { + return `${address.slice(0, 6)}...${address.slice(-4)}` +} + +/** + * Create RewardsManager integration condition for REO + * + * Checks that RewardsManager.getProviderEligibilityOracle() == reoAddress + */ +export function createRMIntegrationCondition(reoAddress: string): ParamCondition { + return { + name: 'providerEligibilityOracle', + description: 'REO instance', + abi: PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + getter: 'getProviderEligibilityOracle', + setter: 'setProviderEligibilityOracle', + target: reoAddress, + compare: addressEquals, + format: formatAddress, + } +} + +// ============================================================================ +// Generic Role Enumeration (for any BaseUpgradeable contract) +// ============================================================================ + +/** + * Information about a single role + */ +export interface RoleInfo { + /** Role name (e.g., 'GOVERNOR_ROLE') */ + name: string + /** Role bytes32 hash */ + role: `0x${string}` + /** Admin role bytes32 hash */ + adminRole: `0x${string}` + /** Number of members with this role */ + memberCount: number + /** Addresses that hold this role */ + members: string[] +} + +/** + * Result of enumerating all roles for a contract + */ +export interface RoleEnumerationResult { + /** Contract address */ + contractAddress: string + /** All roles that were enumerated */ + roles: RoleInfo[] + /** Roles that failed to read (may not exist on contract) */ + failedRoles: string[] +} + +/** + * Get the bytes32 value of a role constant from a contract + * + * @param client - Viem public client + * @param contractAddress - Contract address + * @param roleName - Name of the role constant (e.g., 'GOVERNOR_ROLE') + * @returns The bytes32 role value, or null if the role doesn't exist + */ +export async function getRoleHash( + client: PublicClient, + contractAddress: string, + roleName: string, +): Promise<`0x${string}` | null> { + try { + // Create a minimal ABI for reading the role constant + const roleAbi = [ + { + inputs: [], + name: roleName, + outputs: [{ type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + ] as const + + const role = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: roleAbi, + functionName: roleName, + })) as `0x${string}` + + return role + } catch { + return null + } +} + +/** + * Enumerate all members of a role + * + * @param client - Viem public client + * @param contractAddress - Contract address + * @param role - Role bytes32 hash + * @returns Array of member addresses + */ +export async function enumerateRoleMembers( + client: PublicClient, + contractAddress: string, + role: `0x${string}`, +): Promise { + const count = Number( + (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'getRoleMemberCount', + args: [role], + })) as bigint, + ) + + const members: string[] = [] + for (let i = 0; i < count; i++) { + const member = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'getRoleMember', + args: [role, BigInt(i)], + })) as string + members.push(member) + } + + return members +} + +/** + * Get full role information including admin and members + * + * @param client - Viem public client + * @param contractAddress - Contract address + * @param roleName - Name of the role constant (e.g., 'GOVERNOR_ROLE') + * @returns RoleInfo or null if role doesn't exist + */ +export async function getRoleInfo( + client: PublicClient, + contractAddress: string, + roleName: string, +): Promise { + const role = await getRoleHash(client, contractAddress, roleName) + if (!role) { + return null + } + + const adminRole = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'getRoleAdmin', + args: [role], + })) as `0x${string}` + + const members = await enumerateRoleMembers(client, contractAddress, role) + + return { + name: roleName, + role, + adminRole, + memberCount: members.length, + members, + } +} + +/** + * Enumerate all roles for a contract + * + * @param client - Viem public client + * @param contractAddress - Contract address + * @param roleNames - Array of role constant names to check + * @returns RoleEnumerationResult with all role info + */ +export async function enumerateContractRoles( + client: PublicClient, + contractAddress: string, + roleNames: readonly string[], +): Promise { + const roles: RoleInfo[] = [] + const failedRoles: string[] = [] + + for (const roleName of roleNames) { + const info = await getRoleInfo(client, contractAddress, roleName) + if (info) { + roles.push(info) + } else { + failedRoles.push(roleName) + } + } + + return { + contractAddress, + roles, + failedRoles, + } +} + +/** + * Check if an account has the admin role for a given role + * + * @param client - Viem public client + * @param contractAddress - Contract address + * @param role - Role bytes32 hash + * @param account - Account to check + * @returns true if account is an admin for the role + */ +export async function hasAdminRole( + client: PublicClient, + contractAddress: string, + role: `0x${string}`, + account: string, +): Promise { + const adminRole = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'getRoleAdmin', + args: [role], + })) as `0x${string}` + + const hasRole = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [adminRole, account as `0x${string}`], + })) as boolean + + return hasRole +} + +/** + * Check if an account already has a specific role + * + * @param client - Viem public client + * @param contractAddress - Contract address + * @param role - Role bytes32 hash + * @param account - Account to check + * @returns true if account has the role + */ +export async function accountHasRole( + client: PublicClient, + contractAddress: string, + role: `0x${string}`, + account: string, +): Promise { + const hasRole = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [role, account as `0x${string}`], + })) as boolean + + return hasRole +} + +/** + * Get admin role info for a given role + * + * @param client - Viem public client + * @param contractAddress - Contract address + * @param role - Role bytes32 hash + * @param knownRoles - Known roles for name resolution + * @returns Admin role hash and name (if known) + */ +export async function getAdminRoleInfo( + client: PublicClient, + contractAddress: string, + role: `0x${string}`, + knownRoles: RoleInfo[], +): Promise<{ adminRole: `0x${string}`; adminRoleName: string | null; adminMembers: string[] }> { + const adminRole = (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'getRoleAdmin', + args: [role], + })) as `0x${string}` + + const adminRoleName = knownRoles.find((r) => r.role === adminRole)?.name ?? null + const adminMembers = await enumerateRoleMembers(client, contractAddress, adminRole) + + return { adminRole, adminRoleName, adminMembers } +} diff --git a/packages/deployment/lib/contract-registry.ts b/packages/deployment/lib/contract-registry.ts new file mode 100644 index 000000000..06b2f640a --- /dev/null +++ b/packages/deployment/lib/contract-registry.ts @@ -0,0 +1,437 @@ +/** + * Contract Registry - Single source of truth for contract metadata + * + * This module consolidates all contract metadata that was previously scattered + * across sync scripts, deploy scripts, and utility functions. + * + * The registry is namespaced by address book to prevent key collisions when + * the same contract name appears in multiple address books. + */ + +import { ComponentTags } from './deployment-tags.js' + +/** + * Artifact source configuration - where to load contract ABI and bytecode from + */ +export type ArtifactSource = + | { type: 'contracts'; path: string; name: string } + | { type: 'subgraph-service'; name: string } + | { type: 'horizon'; path: string } + | { type: 'issuance'; path: string } + | { type: 'openzeppelin'; name: string } + +/** + * Proxy pattern types + * - 'graph': Graph Protocol's custom proxy (upgrade + acceptProxy via GraphProxyAdmin) + * - 'transparent': OpenZeppelin TransparentUpgradeableProxy (upgradeAndCall via ProxyAdmin) + * - undefined: Not a proxy contract + */ +export type ProxyType = 'graph' | 'transparent' + +/** + * Address book types - which address book a contract belongs to + */ +export type AddressBookType = 'horizon' | 'subgraph-service' | 'issuance' + +/** + * Interface ABI configuration for typed ABI generation. + * Maps an export name to an interface in @graphprotocol/interfaces. + */ +export interface InterfaceAbiConfig { + /** Export name for the generated ABI constant (e.g. 'REWARDS_MANAGER_ABI') */ + name: string + /** Interface name in @graphprotocol/interfaces artifacts (e.g. 'IRewardsManager') */ + interface: string +} + +/** + * Contract metadata specification + * Note: addressBook is no longer a field - it's implied by the registry namespace + */ +export interface ContractMetadata { + /** Address book entry name (if different from registry key) */ + addressBookName?: string + + /** Artifact source for loading ABI and bytecode */ + artifact?: ArtifactSource + + /** Proxy type if this is a proxied contract */ + proxyType?: ProxyType + + /** Name of the proxy admin deployment record */ + proxyAdminName?: string + + /** If true, contract must exist on-chain (for sync prerequisite check) */ + prerequisite?: boolean + + /** + * If true, contract is deployable by this system + * If false/undefined, contract is managed elsewhere (prerequisite or placeholder) + * Default: false (must explicitly opt-in) + */ + deployable?: boolean + + /** + * If true, entry is an address-only placeholder (code not required) + * Use for entries that may be EOA or contract - sync skips bytecode verification. + */ + addressOnly?: boolean + + /** + * Role constants exposed by the contract (for role enumeration) + * Array of function names that return bytes32 role constants (e.g., 'GOVERNOR_ROLE') + * Used by roles:list task to enumerate role holders. + */ + roles?: readonly string[] + + /** + * Component tag for deployment lifecycle management. + * Used by script factories to derive action tags (deploy, upgrade, etc.) + * and dependencies without per-script boilerplate. + * + * Must match the PascalCase contract name in deployment-tags.ts ComponentTags. + * Example: 'PaymentsEscrow' → tags: 'PaymentsEscrow:upgrade', deps: 'PaymentsEscrow:deploy' + * + * Multiple contracts may share a componentTag when they form a single + * deployment unit (e.g., REO A/B instances share 'RewardsEligibility'). + */ + componentTag?: string + + /** + * Lifecycle actions available for this component beyond the standard deploy+upgrade. + * Used by status modules to show available `--tags` actions. + * + * When omitted, defaults to ['deploy', 'upgrade'] for deployable proxy contracts, + * or ['deploy'] for non-proxy deployable contracts. + * Always includes 'all' implicitly. + */ + lifecycleActions?: readonly string[] + + /** + * Interface ABIs to generate for this contract. + * Used by the ABI codegen script to produce typed `as const` exports. + * Each entry maps to an interface artifact in @graphprotocol/interfaces. + * The codegen also extracts the interfaceId from the factory class. + */ + interfaces?: readonly InterfaceAbiConfig[] + + /** + * Generate a typed ABI from the contract's full artifact. + * Value is the export name (e.g. 'ISSUANCE_ALLOCATOR_ABI'). + * Requires `artifact` to be set on this entry. + */ + generateAbi?: string + + /** + * Name of the shared implementation entry when this proxy uses an + * implementation deployed separately (e.g. DirectAllocation_Implementation). + * + * Used by the upgrade pipeline to auto-detect when the shared implementation + * has been redeployed and set pendingImplementation accordingly. + */ + sharedImplementation?: string +} + +// ============================================================================ +// Horizon Contracts +// ============================================================================ + +const HORIZON_CONTRACTS = { + RewardsManager: { + artifact: { type: 'contracts', path: 'rewards', name: 'RewardsManager' }, + interfaces: [ + { name: 'REWARDS_MANAGER_ABI', interface: 'IRewardsManager' }, + { name: 'REWARDS_MANAGER_DEPRECATED_ABI', interface: 'IRewardsManagerDeprecated' }, + { name: 'PROVIDER_ELIGIBILITY_MANAGEMENT_ABI', interface: 'IProviderEligibilityManagement' }, + ], + proxyType: 'graph', + proxyAdminName: 'GraphProxyAdmin', + prerequisite: true, + deployable: true, + componentTag: ComponentTags.REWARDS_MANAGER, + lifecycleActions: ['deploy', 'upgrade'], + }, + GraphProxyAdmin: { + interfaces: [{ name: 'GRAPH_PROXY_ADMIN_ABI', interface: 'IGraphProxyAdmin' }], + prerequisite: true, + }, + L2GraphToken: { + artifact: { type: 'contracts', path: 'l2/token', name: 'L2GraphToken' }, + interfaces: [{ name: 'GRAPH_TOKEN_ABI', interface: 'IGraphToken' }], + prerequisite: true, + }, + Controller: { + interfaces: [{ name: 'CONTROLLER_ABI', interface: 'IControllerToolshed' }], + prerequisite: true, + }, + GraphTallyCollector: { + prerequisite: true, + }, + RecurringCollector: { + artifact: { type: 'horizon', path: 'contracts/payments/collectors/RecurringCollector.sol/RecurringCollector' }, + proxyType: 'transparent', + deployable: true, + componentTag: ComponentTags.RECURRING_COLLECTOR, + lifecycleActions: ['deploy', 'upgrade', 'configure', 'transfer'], + }, + L2Curation: { + artifact: { type: 'contracts', path: 'l2/curation', name: 'L2Curation' }, + proxyType: 'graph', + proxyAdminName: 'GraphProxyAdmin', + prerequisite: true, + deployable: true, + componentTag: ComponentTags.L2_CURATION, + }, + HorizonStaking: { + artifact: { type: 'horizon', path: 'contracts/staking/HorizonStaking.sol/HorizonStaking' }, + proxyType: 'graph', + proxyAdminName: 'GraphProxyAdmin', + prerequisite: true, + deployable: true, + componentTag: ComponentTags.HORIZON_STAKING, + }, + GraphPayments: { + prerequisite: true, + }, + PaymentsEscrow: { + artifact: { type: 'horizon', path: 'contracts/payments/PaymentsEscrow.sol/PaymentsEscrow' }, + proxyType: 'transparent', + prerequisite: true, + deployable: true, + componentTag: ComponentTags.PAYMENTS_ESCROW, + }, + // Contracts deployed by other systems (placeholders for address book type completeness) + EpochManager: {}, + L2GNS: {}, + L2GraphTokenGateway: {}, + SubgraphNFT: {}, +} as const satisfies Record + +// ============================================================================ +// SubgraphService Contracts +// ============================================================================ + +// NOTE: SubgraphService contracts are deployed via Ignition with contract-specific proxy admins. +// The proxy admin address is stored inline in each contract's address book entry (proxyAdmin field). +// During sync, deployment records are auto-generated as `${contractName}_ProxyAdmin`. +const SUBGRAPH_SERVICE_CONTRACTS = { + DisputeManager: { + artifact: { type: 'subgraph-service', name: 'DisputeManager' }, + proxyType: 'transparent', + // proxyAdminName omitted - auto-generates as DisputeManager_ProxyAdmin + prerequisite: true, + deployable: true, + componentTag: ComponentTags.DISPUTE_MANAGER, + }, + SubgraphService: { + artifact: { type: 'subgraph-service', name: 'SubgraphService' }, + proxyType: 'transparent', + // proxyAdminName omitted - auto-generates as SubgraphService_ProxyAdmin + prerequisite: true, + deployable: true, + componentTag: ComponentTags.SUBGRAPH_SERVICE, + lifecycleActions: ['deploy', 'upgrade', 'configure'], + }, + // Contracts deployed by other systems (placeholders for address book type completeness) + // These exist in the subgraph-service address book but are managed elsewhere + L2Curation: {}, + L2GNS: {}, + SubgraphNFT: {}, + LegacyDisputeManager: {}, + LegacyServiceRegistry: {}, +} as const satisfies Record + +// ============================================================================ +// Issuance Contracts +// ============================================================================ + +// NOTE: Issuance contracts use OZ v5 TransparentUpgradeableProxy which creates +// a per-proxy ProxyAdmin in the constructor. The deployer is the initial ProxyAdmin +// owner to allow post-deployment configuration; ownership is transferred to the +// protocol governor in the transfer-governance step. The ProxyAdmin address is stored +// inline in each contract's address book entry (proxyAdmin field), similar to +// subgraph-service contracts. + +// Base roles from BaseUpgradeable - all issuance contracts inherit these +const BASE_ROLES = ['GOVERNOR_ROLE', 'PAUSE_ROLE', 'OPERATOR_ROLE'] as const + +const ISSUANCE_CONTRACTS = { + // Address placeholder for network operator (may be EOA or contract) + // Used by deployment scripts to grant OPERATOR_ROLE + NetworkOperator: { addressOnly: true }, + + IssuanceAllocator: { + artifact: { type: 'issuance', path: 'contracts/allocate/IssuanceAllocator.sol/IssuanceAllocator' }, + generateAbi: 'ISSUANCE_ALLOCATOR_ABI', + proxyType: 'transparent', + // Per-proxy ProxyAdmin - address stored in address book entry's proxyAdmin field + deployable: true, + roles: BASE_ROLES, + componentTag: ComponentTags.ISSUANCE_ALLOCATOR, + lifecycleActions: ['deploy', 'upgrade', 'configure', 'transfer'], + }, + RecurringAgreementManager: { + artifact: { + type: 'issuance', + path: 'contracts/agreement/RecurringAgreementManager.sol/RecurringAgreementManager', + }, + proxyType: 'transparent', + deployable: true, + roles: [...BASE_ROLES, 'DATA_SERVICE_ROLE', 'COLLECTOR_ROLE', 'AGREEMENT_MANAGER_ROLE'] as const, + componentTag: ComponentTags.RECURRING_AGREEMENT_MANAGER, + lifecycleActions: ['deploy', 'upgrade', 'configure', 'transfer'], + }, + // A/B instances of RewardsEligibilityOracle - both share the same contract artifact + // but deploy as independent proxies. Only one is active (integrated with RewardsManager) at a time. + RewardsEligibilityOracleA: { + artifact: { type: 'issuance', path: 'contracts/eligibility/RewardsEligibilityOracle.sol/RewardsEligibilityOracle' }, + generateAbi: 'REWARDS_ELIGIBILITY_ORACLE_ABI', + proxyType: 'transparent', + deployable: true, + roles: [...BASE_ROLES, 'ORACLE_ROLE'] as const, + componentTag: ComponentTags.REWARDS_ELIGIBILITY_A, + // Integration with RewardsManager is a goal-level activation + // (--tags GIP-0088:eligibility-integrate), not a per-component lifecycle action. + lifecycleActions: ['deploy', 'upgrade', 'configure', 'transfer'], + }, + RewardsEligibilityOracleB: { + artifact: { type: 'issuance', path: 'contracts/eligibility/RewardsEligibilityOracle.sol/RewardsEligibilityOracle' }, + proxyType: 'transparent', + deployable: true, + roles: [...BASE_ROLES, 'ORACLE_ROLE'] as const, + componentTag: ComponentTags.REWARDS_ELIGIBILITY_B, + lifecycleActions: ['deploy', 'upgrade', 'configure', 'transfer'], + }, + // Testnet mock REO - indexers control own eligibility, upgradeable for deployment consistency + RewardsEligibilityOracleMock: { + artifact: { + type: 'issuance', + path: 'contracts/eligibility/mocks/MockRewardsEligibilityOracle.sol/MockRewardsEligibilityOracle', + }, + proxyType: 'transparent', + deployable: true, + roles: BASE_ROLES, + componentTag: ComponentTags.REWARDS_ELIGIBILITY_MOCK, + lifecycleActions: ['deploy', 'upgrade', 'transfer', 'integrate'], + }, + DirectAllocation_Implementation: { + artifact: { type: 'issuance', path: 'contracts/allocate/DirectAllocation.sol/DirectAllocation' }, + generateAbi: 'DIRECT_ALLOCATION_ABI', + deployable: true, + roles: BASE_ROLES, + componentTag: ComponentTags.DIRECT_ALLOCATION_IMPL, + }, + // Default target for IA — safety net for unallocated issuance + // Uses DirectAllocation implementation (per-proxy ProxyAdmin) + DefaultAllocation: { + proxyType: 'transparent', + sharedImplementation: 'DirectAllocation_Implementation', + deployable: true, + roles: BASE_ROLES, + componentTag: ComponentTags.DEFAULT_ALLOCATION, + lifecycleActions: ['deploy', 'upgrade', 'configure', 'transfer'], + }, + // Default reclaim address — receives reclaimed rewards for all reasons + // Uses DirectAllocation implementation (per-proxy ProxyAdmin) + ReclaimedRewards: { + proxyType: 'transparent', + sharedImplementation: 'DirectAllocation_Implementation', + deployable: true, + roles: BASE_ROLES, + componentTag: ComponentTags.REWARDS_RECLAIM, + lifecycleActions: ['deploy', 'upgrade', 'configure', 'transfer'], + }, +} as const satisfies Record + +// ============================================================================ +// Namespaced Registry +// ============================================================================ + +/** + * Contract registry namespaced by address book + * This prevents key collisions when the same contract name appears in multiple address books + */ +export const CONTRACT_REGISTRY = { + horizon: HORIZON_CONTRACTS, + 'subgraph-service': SUBGRAPH_SERVICE_CONTRACTS, + issuance: ISSUANCE_CONTRACTS, +} as const + +// Type helpers for the namespaced registry +export type HorizonContractName = keyof typeof HORIZON_CONTRACTS +export type SubgraphServiceContractName = keyof typeof SUBGRAPH_SERVICE_CONTRACTS +export type IssuanceContractName = keyof typeof ISSUANCE_CONTRACTS + +/** + * Registry entry with contract name and address book embedded + */ +export interface RegistryEntry extends ContractMetadata { + name: string + addressBook: AddressBookType +} + +/** + * Contract registry entries namespaced by address book + * Use these to pass to deployment functions with full context + * + * @example + * ```typescript + * await upgradeImplementation(env, Contracts.horizon.RewardsManager) + * await upgradeImplementation(env, Contracts['subgraph-service'].SubgraphService) + * ``` + */ +export const Contracts = { + horizon: Object.entries(HORIZON_CONTRACTS).reduce( + (acc, [name, metadata]) => { + acc[name as HorizonContractName] = { name, addressBook: 'horizon', ...metadata } + return acc + }, + {} as Record, + ), + 'subgraph-service': Object.entries(SUBGRAPH_SERVICE_CONTRACTS).reduce( + (acc, [name, metadata]) => { + acc[name as SubgraphServiceContractName] = { name, addressBook: 'subgraph-service', ...metadata } + return acc + }, + {} as Record, + ), + issuance: Object.entries(ISSUANCE_CONTRACTS).reduce( + (acc, [name, metadata]) => { + acc[name as IssuanceContractName] = { name, addressBook: 'issuance', ...metadata } + return acc + }, + {} as Record, + ), +} as const + +/** + * Get contract metadata by address book and name + */ +export function getContractMetadata(addressBook: AddressBookType, name: string): ContractMetadata | undefined { + const bookRegistry = CONTRACT_REGISTRY[addressBook] + return bookRegistry[name as keyof typeof bookRegistry] +} + +/** + * Get the address book entry name for a contract + * Falls back to the contract name if no override is specified + */ +export function getAddressBookEntryName(addressBook: AddressBookType, name: string): string { + const metadata = getContractMetadata(addressBook, name) + return metadata?.addressBookName ?? name +} + +/** + * Get all contracts for a specific address book + */ +export function getContractsByAddressBook(addressBook: AddressBookType): Array<[string, ContractMetadata]> { + const bookRegistry = CONTRACT_REGISTRY[addressBook] + return Object.entries(bookRegistry) +} + +/** + * List of proxied issuance contracts (for sync dynamic handling) + */ +export const PROXIED_ISSUANCE_CONTRACTS = Object.entries(ISSUANCE_CONTRACTS) + .filter(([_, meta]) => 'proxyType' in meta && meta.proxyType === 'transparent') + .map(([name]) => name) diff --git a/packages/deployment/lib/controller-utils.ts b/packages/deployment/lib/controller-utils.ts new file mode 100644 index 000000000..4dce12c4a --- /dev/null +++ b/packages/deployment/lib/controller-utils.ts @@ -0,0 +1,90 @@ +import type { Environment } from '@rocketh/core/types' +import type { PublicClient } from 'viem' + +import { CONTROLLER_ABI } from './abis.js' +import { Contracts } from './contract-registry.js' +import { requireContract } from './issuance-deploy-utils.js' +import { graph } from '../rocketh/deploy.js' + +/** + * Check if the provider can sign as the protocol governor + * + * With a mnemonic (local network), all derived accounts are available via eth_accounts. + * With explicit keys (production), only configured accounts are available. + * + * @param env - Deployment environment + * @returns Governor address and whether the provider can sign as governor + */ +export async function canSignAsGovernor(env: Environment): Promise<{ governor: string; canSign: boolean }> { + const governor = await getGovernor(env) + const accounts = (await env.network.provider.request({ method: 'eth_accounts' })) as string[] + const canSign = accounts.some((a) => a.toLowerCase() === governor.toLowerCase()) + + // Verify the rocketh named account 'governor' matches the on-chain governor. + // If they disagree, tx({ account: 'governor' }) would send from the wrong address. + if (canSign && env.namedAccounts['governor']) { + const named = env.namedAccounts['governor'] as string + if (named.toLowerCase() !== governor.toLowerCase()) { + throw new Error( + `Named account 'governor' (${named}) does not match Controller.getGovernor() (${governor}). ` + + `Check rocketh account config — mnemonic index may not match the on-chain governor.`, + ) + } + } + + return { governor, canSign } +} + +/** + * Get the protocol governor address from the Controller contract + * + * The Controller contract is the governance registry for the Graph Protocol. + * It stores the address of the protocol governor (typically a multi-sig). + * + * @param env - Deployment environment + * @returns Governor address from Controller.getGovernor() + */ +export async function getGovernor(env: Environment): Promise { + const client = graph.getPublicClient(env) as PublicClient + + // Get Controller from deployments (synced from Horizon address book) + const controller = requireContract(env, Contracts.horizon.Controller) + + // Query governor from Controller + const governor = (await client.readContract({ + address: controller.address as `0x${string}`, + abi: CONTROLLER_ABI, + functionName: 'getGovernor', + })) as string + + return governor +} + +/** + * Get pause guardian address from the Controller contract + * + * @param env - Deployment environment + * @returns Pause guardian address from Controller.pauseGuardian() + */ +export async function getPauseGuardian(env: Environment): Promise { + const client = graph.getPublicClient(env) as PublicClient + const controller = requireContract(env, Contracts.horizon.Controller) + + // Query pauseGuardian from Controller + // Use minimal ABI since pauseGuardian() is auto-generated getter, not in IController interface + const pauseGuardian = (await client.readContract({ + address: controller.address as `0x${string}`, + abi: [ + { + inputs: [], + name: 'pauseGuardian', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + ], + functionName: 'pauseGuardian', + })) as string + + return pauseGuardian +} diff --git a/packages/deployment/lib/deploy-implementation.ts b/packages/deployment/lib/deploy-implementation.ts new file mode 100644 index 000000000..dbaacc92b --- /dev/null +++ b/packages/deployment/lib/deploy-implementation.ts @@ -0,0 +1,437 @@ +import type { Artifact, Environment } from '@rocketh/core/types' +import { encodeAbiParameters, getAddress } from 'viem' + +import { getAddressBookForType, getTargetChainIdFromEnv } from './address-book-utils.js' +import { + getLibraryResolver, + linkArtifactLibraries, + loadContractsArtifact, + loadHorizonBuildArtifact, + loadIssuanceArtifact, + loadOpenZeppelinArtifact, + loadSubgraphServiceArtifact, +} from './artifact-loaders.js' +import { computeBytecodeHash } from './bytecode-utils.js' +import { getContractMetadata, type AddressBookType, type ArtifactSource, type ProxyType } from './contract-registry.js' +import { buildDeploymentMetadata } from './deployment-metadata.js' +import { deploy, graph } from '../rocketh/deploy.js' + +// Re-export artifact loaders for backwards compatibility +export { loadContractsArtifact, loadIssuanceArtifact, loadSubgraphServiceArtifact } + +// Re-export ArtifactSource for backwards compatibility +export type { ArtifactSource } + +// ERC1967 implementation storage slot (for OZ TransparentUpgradeableProxy) +const ERC1967_IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' as const + +/** + * Read the current implementation address for a proxy contract. + * + * @param client - Viem public client + * @param proxyAddress - Address of the proxy contract + * @param proxyType - 'graph' for Graph legacy proxy, 'transparent' for OZ TransparentProxy + * @param proxyAdminAddress - Address of the proxy admin (required for graph type) + */ +export async function getOnChainImplementation( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client: any, + proxyAddress: string, + proxyType: 'graph' | 'transparent', + proxyAdminAddress?: string, +): Promise { + if (proxyType === 'transparent') { + const implSlotValue = await client.getStorageAt({ + address: proxyAddress as `0x${string}`, + slot: ERC1967_IMPLEMENTATION_SLOT, + }) + return getAddress('0x' + (implSlotValue?.slice(26) ?? '')) + } else { + const data = await client.readContract({ + address: proxyAdminAddress as `0x${string}`, + abi: [ + { + name: 'getProxyImplementation', + type: 'function', + inputs: [{ name: '_proxy', type: 'address' }], + outputs: [{ name: '', type: 'address' }], + stateMutability: 'view', + }, + ], + functionName: 'getProxyImplementation', + args: [proxyAddress as `0x${string}`], + }) + return data as string + } +} + +/** + * Configuration for deploying an upgradeable implementation + */ +export interface ImplementationDeployConfig { + /** Contract name (e.g., 'RewardsManager', 'SubgraphService') */ + contractName: string + + /** + * Artifact source configuration + * + * For @graphprotocol/contracts: + * { type: 'contracts', path: 'rewards', name: 'RewardsManager' } + * + * For @graphprotocol/subgraph-service (Foundry format): + * { type: 'subgraph-service', name: 'SubgraphService' } + * + * For @graphprotocol/issuance: + * { type: 'issuance', path: 'contracts/allocate/DirectAllocation.sol/DirectAllocation' } + * + * Legacy shorthand (contracts only): + * artifactPath: 'rewards' + artifactName defaults to contractName + */ + artifact?: ArtifactSource + + /** @deprecated Use artifact.path instead */ + artifactPath?: string + + /** + * Proxy type + * - 'graph': Graph Protocol's custom proxy (upgrade + acceptProxy) + * - 'transparent': OpenZeppelin TransparentUpgradeableProxy (upgradeAndCall) + * + * Default: 'graph' + */ + proxyType?: ProxyType + + /** + * Name of the proxy admin deployment record. + * e.g., 'GraphProxyAdmin' for legacy GraphProxy contracts. + * + * Optional: If omitted, defaults to `${contractName}_ProxyAdmin`. + * Per-proxy admins (OZ v5 TransparentUpgradeableProxy contracts) follow this + * default and store the admin address inline in their address book entry. + */ + proxyAdminName?: string + + /** + * Address book to store pending implementation + * Default: 'horizon' + */ + addressBook?: AddressBookType + + /** Constructor arguments (default: []) */ + constructorArgs?: unknown[] +} + +/** + * Result of implementation deployment + */ +export interface ImplementationDeployResult { + /** Whether a new implementation was deployed */ + deployed: boolean + + /** Address of the implementation (new or existing) */ + address: string + + /** Whether the bytecode changed (deployment was needed) */ + bytecodeChanged: boolean + + /** Transaction hash if newly deployed */ + txHash?: string +} + +/** + * Load artifact based on source configuration. Throws if the artifact can't be loaded. + */ +export function loadArtifactFromSource(source: ArtifactSource): Artifact { + switch (source.type) { + case 'contracts': + return loadContractsArtifact(source.path, source.name) + case 'subgraph-service': + return loadSubgraphServiceArtifact(source.name) + case 'horizon': + return loadHorizonBuildArtifact(source.path) + case 'issuance': + return loadIssuanceArtifact(source.path) + case 'openzeppelin': + return loadOpenZeppelinArtifact(source.name) + } +} + +/** + * Like {@link loadArtifactFromSource}, but returns `undefined` instead of throwing. + * Intended for sync-style flows where a missing artifact shouldn't abort the pass. + */ +export function tryLoadArtifactFromSource(source: ArtifactSource | undefined): Artifact | undefined { + if (!source) return undefined + try { + return loadArtifactFromSource(source) + } catch { + return undefined + } +} + +/** + * Compute the bytecode hash for an artifact source, with library resolution. + * + * This is the canonical bytecodeHash recorded in address-book deployment metadata — + * deploy-time, sync backfill, and `checkShouldSync` all agree on this fingerprint. + * + * Use this instead of calling `computeBytecodeHash` directly on rocketh's linked + * `deployedBytecode`: linked bytecode has real library addresses substituted in, + * so its hash diverges from the placeholder-fingerprint hash this helper produces + * for any library-using contract. + * + * Throws if the artifact cannot be loaded. + */ +export function computeArtifactBytecodeHash(source: ArtifactSource): string { + const artifact = loadArtifactFromSource(source) + return computeBytecodeHash( + artifact.deployedBytecode ?? '0x', + artifact.deployedLinkReferences, + getLibraryResolver(source.type), + ) +} + +/** + * Like {@link computeArtifactBytecodeHash}, but returns `undefined` instead of + * throwing if the artifact can't be loaded. Intended for sync-style flows where + * a missing artifact shouldn't abort the whole pass. + */ +export function tryComputeArtifactBytecodeHash(source: ArtifactSource | undefined): string | undefined { + if (!source) return undefined + try { + return computeArtifactBytecodeHash(source) + } catch { + return undefined + } +} + +/** + * Build ImplementationDeployConfig from registry metadata + * + * This helper reduces boilerplate in deploy scripts by using the centralized + * contract registry for artifact paths, proxy patterns, and address books. + * + * @param addressBook - Which address book the contract belongs to + * @param contractName - The contract name (key in CONTRACT_REGISTRY[addressBook]) + * @param overrides - Optional overrides (e.g., constructorArgs) + * @returns Configuration ready for deployImplementation() + * + * @example + * ```typescript + * // Simple usage - all config from registry + * await deployImplementation(env, getImplementationConfig('horizon', 'RewardsManager')) + * + * // With constructor args + * await deployImplementation(env, getImplementationConfig('subgraph-service', 'SubgraphService', { + * constructorArgs: [controller, disputeManager, tallyCollector, curation], + * })) + * ``` + */ +export function getImplementationConfig( + addressBook: AddressBookType, + contractName: string, + overrides?: Partial>, +): ImplementationDeployConfig { + const metadata = getContractMetadata(addressBook, contractName) + if (!metadata) { + throw new Error(`Contract '${contractName}' not found in ${addressBook} registry`) + } + + return { + contractName, + artifact: metadata.artifact, + proxyType: metadata.proxyType, + proxyAdminName: metadata.proxyAdminName, // undefined if not in registry (will auto-generate) + addressBook, + ...overrides, + } +} + +/** + * Check if a contract has implementation deployment config in the registry + */ +export function hasImplementationConfig(addressBook: AddressBookType, contractName: string): boolean { + const metadata = getContractMetadata(addressBook, contractName) + return !!metadata?.artifact +} + +/** + * Deploy an upgradeable contract implementation with bytecode change detection + * + * This function handles the common pattern for deploying Graph Protocol + * upgradeable implementations: + * + * 1. Verify prerequisites (proxy and admin exist from sync) + * 2. Compare artifact bytecode with on-chain (accounting for metadata/immutables) + * 3. Deploy new implementation if bytecode changed + * 4. Store as pendingImplementation in address book for governance upgrade + * + * @example Graph Legacy (RewardsManager, Staking, Curation): + * ```typescript + * await deployImplementation(env, { + * contractName: 'RewardsManager', + * artifactPath: 'rewards', + * proxyAdminName: 'GraphProxyAdmin', + * }) + * ``` + * + * @example OZ Transparent (SubgraphService): + * ```typescript + * await deployImplementation(env, { + * contractName: 'SubgraphService', + * artifact: { type: 'subgraph-service', name: 'SubgraphService' }, + * proxyType: 'transparent', + * proxyAdminName: 'SubgraphService_ProxyAdmin', + * addressBook: 'subgraph-service', + * constructorArgs: [controller, disputeManager, tallyCollector, curation], + * }) + * ``` + */ +export async function deployImplementation( + env: Environment, + config: ImplementationDeployConfig, + libraries?: Record, +): Promise { + const { contractName, proxyAdminName, constructorArgs = [], proxyType = 'graph', addressBook = 'horizon' } = config + + // Resolve artifact source (support legacy artifactPath for backwards compatibility) + const artifactSource: ArtifactSource = config.artifact ?? { + type: 'contracts', + path: config.artifactPath!, + name: contractName, + } + + const deployFn = deploy(env) + + // Get deployer account + const deployer = env.namedAccounts.deployer + if (!deployer) { + throw new Error('No deployer account configured') + } + + // Create viem client for on-chain queries + const client = graph.getPublicClient(env) + + // 1) Verify imports completed (sync step must have run) + const proxy = env.getOrNull(contractName) + if (!proxy) { + throw new Error(`${contractName} not imported. Run sync step first.`) + } + + // Auto-generate proxy admin deployment record name if not provided + const proxyAdminDeploymentName = proxyAdminName ?? `${contractName}_ProxyAdmin` + const proxyAdmin = env.getOrNull(proxyAdminDeploymentName) + if (!proxyAdmin) { + throw new Error(`${proxyAdminDeploymentName} not imported. Run sync step first.`) + } + + // 2) Load artifact (pre-link libraries so rocketh stores linked bytecode) + const rawArtifact = loadArtifactFromSource(artifactSource) + const artifact = libraries + ? linkArtifactLibraries(rawArtifact, libraries as Record) + : rawArtifact + const implDeploymentName = `${contractName}_Implementation` + + // Get address book to check pending implementation + const targetChainId = await getTargetChainIdFromEnv(env) + const addressBookInstance = getAddressBookForType(addressBook, targetChainId) + + // Compute local artifact bytecode hash (for storing with deployment) + const localBytecodeHash = computeArtifactBytecodeHash(artifactSource) + + // 3) Pre-check: skip deployment if bytecodeHash and constructor args match + // Rocketh's comparison can false-positive when sync creates bare records (e.g., wrong + // argsData, unlinked library bytecodes). The content-aware bytecodeHash handles both + // cases — it strips CBOR metadata and resolves library references by content hash. + const contractEntry = addressBookInstance.entryExists(contractName) + ? addressBookInstance.getEntry(contractName) + : null + const pendingImpl = contractEntry?.pendingImplementation + const storedMetadata = pendingImpl?.deployment ?? addressBookInstance.getDeploymentMetadata(contractName) + + if (storedMetadata?.bytecodeHash && storedMetadata.bytecodeHash === localBytecodeHash) { + // Bytecode matches — also verify constructor args (immutable values) + let argsMatch = !storedMetadata.argsData // no stored args = can't compare, assume match + if (storedMetadata.argsData) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const constructorDef = (artifact.abi as any[])?.find((item: any) => item.type === 'constructor') + const localArgsData = + constructorDef?.inputs?.length && constructorArgs.length + ? encodeAbiParameters(constructorDef.inputs, constructorArgs as readonly unknown[]) + : '0x' + argsMatch = localArgsData === storedMetadata.argsData + } + + if (argsMatch) { + const existingAddress = pendingImpl?.address ?? contractEntry?.implementation + if (existingAddress) { + env.showMessage(`\n✓ ${contractName} implementation unchanged`) + return { + deployed: false, + address: existingAddress, + bytecodeChanged: false, + } + } + } + } + + // 4) Deploy implementation - let rocketh decide based on its own records + // Sync handles pending: if pending hash matches local, rocketh has bytecode to compare + // If pending hash differs, sync skipped bytecode so rocketh will deploy fresh + // Libraries are pre-linked into the artifact (step 2) so rocketh stores linked + // bytecode — its CBOR-stripping comparison then matches on subsequent runs. + const impl = await deployFn(implDeploymentName, { account: deployer, artifact, args: constructorArgs }) + + if (!impl.newlyDeployed) { + env.showMessage(`\n✓ ${contractName} implementation unchanged`) + return { + deployed: false, + address: impl.address, + bytecodeChanged: false, + } + } + + // 4) Get current on-chain implementation + const currentOnChainImpl = await getOnChainImplementation(client, proxy.address, proxyType, proxyAdmin.address) + + env.showMessage(`\n📋 New ${contractName} implementation deployed: ${impl.address}`) + env.showMessage(` Current on-chain implementation: ${currentOnChainImpl}`) + env.showMessage(` Storing as pending implementation...`) + + // 5) Store as pending implementation in address book with full deployment metadata + // (addressBookInstance already obtained above for bytecode hash check) + + // Get block info for timestamp + let blockNumber: number | undefined + let timestamp: string | undefined + if (impl.transaction?.hash) { + try { + const receipt = await client.getTransactionReceipt({ hash: impl.transaction.hash as `0x${string}` }) + if (receipt?.blockNumber) { + blockNumber = Number(receipt.blockNumber) + const block = await client.getBlock({ blockNumber: receipt.blockNumber }) + if (block?.timestamp) { + timestamp = new Date(Number(block.timestamp) * 1000).toISOString() + } + } + } catch { + // Block info lookup failed - not critical + } + } + + // Store with full deployment metadata for verification and reconstruction + const metadata = buildDeploymentMetadata(impl, localBytecodeHash, { blockNumber, timestamp }) + if (metadata) { + addressBookInstance.setPendingImplementationWithMetadata(contractName, impl.address, metadata) + } + + env.showMessage(`✓ Pending implementation stored with deployment metadata.`) + env.showMessage(` Run upgrade task to generate TX and execute.`) + + return { + deployed: true, + address: impl.address, + bytecodeChanged: true, + txHash: impl.transaction?.hash, + } +} diff --git a/packages/deployment/lib/deploy-standalone.ts b/packages/deployment/lib/deploy-standalone.ts new file mode 100644 index 000000000..4593cbaa0 --- /dev/null +++ b/packages/deployment/lib/deploy-standalone.ts @@ -0,0 +1,71 @@ +import type { Environment } from '@rocketh/core/types' + +import type { RegistryEntry } from './contract-registry.js' +import { loadArtifactFromSource } from './deploy-implementation.js' +import { requireDeployer } from './issuance-deploy-utils.js' +import { deploy, graph } from '../rocketh/deploy.js' + +/** + * Configuration for deploying a standalone (non-proxy) contract + */ +export interface StandaloneDeployConfig { + /** Contract registry entry (provides addressBook and artifact config) */ + contract: RegistryEntry + /** Constructor arguments */ + constructorArgs?: unknown[] +} + +/** + * Deploy a standalone (non-proxy) contract and update the address book + * + * This utility handles the common pattern for deploying contracts that + * are not behind a proxy (e.g., helper contracts). + * + * - Loads artifact from registry metadata + * - Deploys via rocketh (idempotent - skips if bytecode unchanged) + * - Updates the appropriate address book (horizon or issuance) + * + * @example + * ```typescript + * await deployStandaloneContract(env, { + * contract: Contracts.horizon.GraphTallyCollector, + * constructorArgs: [controllerAddress], + * }) + * ``` + */ +export async function deployStandaloneContract( + env: Environment, + config: StandaloneDeployConfig, +): Promise<{ address: string; newlyDeployed: boolean }> { + const { contract, constructorArgs = [] } = config + + if (!contract.artifact) { + throw new Error(`No artifact configured for ${contract.name} in registry`) + } + + const deployer = requireDeployer(env) + const artifact = loadArtifactFromSource(contract.artifact) + const deployFn = deploy(env) + + const result = await deployFn(contract.name, { + account: deployer, + artifact, + args: constructorArgs, + }) + + if (result.newlyDeployed) { + env.showMessage(`\n✓ ${contract.name} deployed at ${result.address}`) + } else { + env.showMessage(`\n✓ ${contract.name} unchanged at ${result.address}`) + } + + await graph.updateAddressBookForContract(env, contract, { + name: contract.name, + address: result.address, + }) + + return { + address: result.address, + newlyDeployed: !!result.newlyDeployed, + } +} diff --git a/packages/deployment/lib/deployment-config.ts b/packages/deployment/lib/deployment-config.ts new file mode 100644 index 000000000..b26cfb29e --- /dev/null +++ b/packages/deployment/lib/deployment-config.ts @@ -0,0 +1,131 @@ +import { readFileSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { Environment } from '@rocketh/core/types' +import JSON5 from 'json5' + +import { getTargetChainIdFromEnv } from './address-book-utils.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +/** Chain ID to config file name mapping */ +const CHAIN_CONFIG_MAP: Record = { + 1337: 'localNetwork', + 42161: 'arbitrumOne', + 421614: 'arbitrumSepolia', +} + +/** + * Raw on-disk shape of `config/.json5`. Every field is optional — + * networks override only what they need; the rest comes from `DEFAULT_SETTINGS`. + */ +interface DeploymentConfigFile { + IssuanceAllocator?: { + ramAllocatorMintingGrtPerBlock?: string + ramSelfMintingGrtPerBlock?: string + } + RewardsManager?: { + revertOnIneligible?: boolean + } + RecurringCollector?: { + revokeSignerThawingPeriod?: string + eip712Name?: string + eip712Version?: string + } +} + +/** + * Fully-resolved deployment settings for a given chain. + * + * Every field is concrete — defaults from `DEFAULT_SETTINGS` are applied for + * any field a network's config file omits. Consumers (deploy scripts and + * status checks) read this directly without per-call `??` fallbacks, so the + * "expected value" lives in exactly one place per field. + */ +export interface ResolvedSettings { + rewardsManager: { + /** Revert on reward claim attempts by ineligible indexers. */ + revertOnIneligible: boolean + } + issuanceAllocator: { + /** GRT/block minted by IA and routed to RAM. `'0'` means unconfigured (skip allocation). */ + ramAllocatorMintingGrtPerBlock: string + /** GRT/block self-minted by RAM. `'0'` means RAM does not self-mint. */ + ramSelfMintingGrtPerBlock: string + } + recurringCollector: { + /** Signer revocation thaw period in seconds (constructor arg). */ + revokeSignerThawingPeriod: string + /** EIP-712 domain name (init arg). */ + eip712Name: string + /** EIP-712 domain version (init arg). */ + eip712Version: string + } +} + +const DEFAULT_SETTINGS: ResolvedSettings = { + rewardsManager: { + revertOnIneligible: true, + }, + issuanceAllocator: { + ramAllocatorMintingGrtPerBlock: '0', + ramSelfMintingGrtPerBlock: '0', + }, + recurringCollector: { + revokeSignerThawingPeriod: '28800', // ~1 day at 3s blocks + eip712Name: 'RecurringCollector', + eip712Version: '1', + }, +} + +function loadConfigFile(chainId: number): DeploymentConfigFile { + const networkName = CHAIN_CONFIG_MAP[chainId] + if (!networkName) return {} + + const configPath = resolve(__dirname, '..', 'config', `${networkName}.json5`) + try { + const raw = readFileSync(configPath, 'utf-8') + return JSON5.parse(raw) + } catch { + return {} + } +} + +/** + * Get fully-resolved deployment settings for a chain. + * + * Reads `config/.json5` (if present) and applies `DEFAULT_SETTINGS` + * for any field the network omits. Pure / sync — safe to call from non-deploy + * contexts (e.g. the status task). Returns full defaults for unknown chains. + */ +export function getResolvedSettings(chainId: number): ResolvedSettings { + const file = loadConfigFile(chainId) + return { + rewardsManager: { + revertOnIneligible: file.RewardsManager?.revertOnIneligible ?? DEFAULT_SETTINGS.rewardsManager.revertOnIneligible, + }, + issuanceAllocator: { + ramAllocatorMintingGrtPerBlock: + file.IssuanceAllocator?.ramAllocatorMintingGrtPerBlock ?? + DEFAULT_SETTINGS.issuanceAllocator.ramAllocatorMintingGrtPerBlock, + ramSelfMintingGrtPerBlock: + file.IssuanceAllocator?.ramSelfMintingGrtPerBlock ?? + DEFAULT_SETTINGS.issuanceAllocator.ramSelfMintingGrtPerBlock, + }, + recurringCollector: { + revokeSignerThawingPeriod: + file.RecurringCollector?.revokeSignerThawingPeriod ?? + DEFAULT_SETTINGS.recurringCollector.revokeSignerThawingPeriod, + eip712Name: file.RecurringCollector?.eip712Name ?? DEFAULT_SETTINGS.recurringCollector.eip712Name, + eip712Version: file.RecurringCollector?.eip712Version ?? DEFAULT_SETTINGS.recurringCollector.eip712Version, + }, + } +} + +/** + * Convenience wrapper for deploy scripts that have an `env` but not a chainId. + */ +export async function getResolvedSettingsForEnv(env: Environment): Promise { + const chainId = await getTargetChainIdFromEnv(env) + return getResolvedSettings(chainId) +} diff --git a/packages/deployment/lib/deployment-metadata.ts b/packages/deployment/lib/deployment-metadata.ts new file mode 100644 index 000000000..936b445c3 --- /dev/null +++ b/packages/deployment/lib/deployment-metadata.ts @@ -0,0 +1,59 @@ +import type { DeploymentMetadata } from '@graphprotocol/toolshed/deployments' + +/** + * Subset of rocketh's `Deployment` / `DeployContractResult` shape needed + * to materialize a `DeploymentMetadata` entry. `receipt.blockNumber` may be + * a hex string (`DeployResult`), a bigint (viem receipt) or a number. + */ +type DeploymentResult = { + transaction?: { hash?: string } + argsData?: string + receipt?: { blockNumber?: `0x${string}` | bigint | number } +} + +/** + * Optional overrides for fields rocketh's result may not carry directly. + * `blockNumber` overrides any value extracted from `result.receipt.blockNumber`. + */ +type MetadataOverrides = { + blockNumber?: number + timestamp?: string +} + +/** + * Coerce rocketh's `receipt.blockNumber` (hex string, bigint, or number) to a plain + * number. Returns `undefined` for missing values. Use this everywhere instead of + * inline `parseInt`/`Number` so the conversion stays consistent. + */ +export function toBlockNumber(raw: `0x${string}` | bigint | number | undefined): number | undefined { + if (raw === undefined) return undefined + if (typeof raw === 'string') return Number(BigInt(raw)) + return Number(raw) +} + +/** + * Build a `DeploymentMetadata` entry from a rocketh deployment result. + * + * Returns `undefined` when the essential fields (txHash, argsData) are missing — + * callers should skip recording rather than write a half-populated entry with + * an empty sentinel txHash. + * + * @param result - Rocketh deployment / pending-impl result + * @param bytecodeHash - Pre-computed bytecode hash (hashing inputs vary by caller) + * @param overrides - Optional blockNumber / timestamp (e.g. fetched from a separate receipt query) + */ +export function buildDeploymentMetadata( + result: DeploymentResult, + bytecodeHash: string, + overrides?: MetadataOverrides, +): DeploymentMetadata | undefined { + if (!result.transaction?.hash || !result.argsData) return undefined + const blockNumber = overrides?.blockNumber ?? toBlockNumber(result.receipt?.blockNumber) + return { + txHash: result.transaction.hash, + argsData: result.argsData, + bytecodeHash, + ...(blockNumber !== undefined && { blockNumber }), + ...(overrides?.timestamp && { timestamp: overrides.timestamp }), + } +} diff --git a/packages/deployment/lib/deployment-tags.ts b/packages/deployment/lib/deployment-tags.ts new file mode 100644 index 000000000..9db4bbdad --- /dev/null +++ b/packages/deployment/lib/deployment-tags.ts @@ -0,0 +1,150 @@ +/** + * Deployment Tag Library + * + * Tags select components, skip functions gate actions: + * - Component tags: PascalCase contract name (e.g., 'IssuanceAllocator') + * - Action verbs: deploy, upgrade, configure, transfer, integrate, all + * - Phase scopes: GIP-NNNN:phase (e.g., 'GIP-0088:upgrade') + * - Activation goals: GIP-NNNN:phase-action (e.g., 'GIP-0088:eligibility-integrate') + * + * Usage: --tags IssuanceAllocator,deploy → matches component, deploy runs, others skip + */ + +/** + * Action suffixes for deployment scripts + */ +export const DeploymentActions = { + DEPLOY: 'deploy', + UPGRADE: 'upgrade', + CONFIGURE: 'configure', + TRANSFER: 'transfer', + INTEGRATE: 'integrate', + ALL: 'all', +} as const + +/** + * Core component tags (PascalCase contract names matching the registry) + */ +export const ComponentTags = { + // Core contracts with full lifecycle (deploy + upgrade + configure) + ISSUANCE_ALLOCATOR: 'IssuanceAllocator', + DEFAULT_ALLOCATION: 'DefaultAllocation', + REWARDS_RECLAIM: 'RewardsReclaim', + + // Implementations and support contracts + DIRECT_ALLOCATION_IMPL: 'DirectAllocation_Implementation', + REWARDS_ELIGIBILITY_A: 'RewardsEligibilityOracleA', + REWARDS_ELIGIBILITY_B: 'RewardsEligibilityOracleB', + REWARDS_ELIGIBILITY_MOCK: 'RewardsEligibilityOracleMock', + + // Horizon contracts + RECURRING_COLLECTOR: 'RecurringCollector', + REWARDS_MANAGER: 'RewardsManager', + HORIZON_STAKING: 'HorizonStaking', + PAYMENTS_ESCROW: 'PaymentsEscrow', + + // SubgraphService contracts + SUBGRAPH_SERVICE: 'SubgraphService', + DISPUTE_MANAGER: 'DisputeManager', + + // Legacy contracts (graph proxy, upgrade only) + L2_CURATION: 'L2Curation', + + // Issuance agreement contracts + RECURRING_AGREEMENT_MANAGER: 'RecurringAgreementManager', +} as const + +/** + * Goal tags - deployment goals that orchestrate component lifecycles + * + * Two-dimensional: phase scope × action verbs. + * - Phase scopes select which contracts (`GIP-0088:upgrade`, `GIP-0088:eligibility`, etc.) + * - Action verbs select which lifecycle step (`deploy`, `configure`, `transfer`, `upgrade`) + * - Activation goals are phase-scoped governance TXs (`GIP-0088:eligibility-integrate`) + * - Optional goals bypass the `all` wildcard + * + * Combined: `--tags GIP-0088:issuance,deploy` + */ +export const GoalTags = { + // Overall GIP scope (status + verification) + GIP_0088: 'GIP-0088', + + // Upgrade phase (deploy, configure, transfer, upgrade — combined with action verbs) + GIP_0088_UPGRADE: 'GIP-0088:upgrade', + + // Activation goals (governance TXs — after upgrade complete) + GIP_0088_ELIGIBILITY_INTEGRATE: 'GIP-0088:eligibility-integrate', + GIP_0088_ISSUANCE_CONNECT: 'GIP-0088:issuance-connect', + GIP_0088_ISSUANCE_ALLOCATE: 'GIP-0088:issuance-allocate', + + // Optional goals (not activated by `all`) + GIP_0088_ISSUANCE_CLOSE_GUARD: 'GIP-0088:issuance-close-guard', +} as const + +/** + * Special tags + */ +export const SpecialTags = { + SYNC: 'sync', +} as const + +/** + * Parse the value of --tags from argv. + * + * Supports both `--tags foo,bar` (space) and `--tags=foo,bar` (equals). + * Returns null when not present or when the space form has no following arg. + */ +function parseTagsArg(): string[] | null { + const argv = process.argv + for (let i = 0; i < argv.length; i++) { + const a = argv[i] + if (a === '--tags') { + if (i + 1 >= argv.length) return null + return argv[i + 1].split(',') + } + if (a.startsWith('--tags=')) { + return a.slice('--tags='.length).split(',') + } + } + return null +} + +/** + * Check whether --tags was specified on the command line. + * + * Returns true (skip) when no --tags are present. Used by status modules + * to skip when the user didn't request any specific component. + */ +export function noTagsRequested(): boolean { + return parseTagsArg() === null +} + +/** + * Check whether a deploy script should skip based on action verbs in --tags. + * + * Returns true (skip) when: + * - No --tags specified at all (safety: require explicit tags for mutations) + * - The verb is not present in the requested tags + * + * The 'all' verb is a wildcard: `--tags Component,all` activates every action + * (deploy, upgrade, configure, transfer, integrate) plus the end verification. + * + * Used by script factories and custom deploy scripts to gate mutations. + */ +export function shouldSkipAction(verb: string): boolean { + const tags = parseTagsArg() + if (tags === null) return true + return !tags.includes(verb) && !tags.includes(DeploymentActions.ALL) +} + +/** + * Check whether an optional goal should skip. + * + * Unlike `shouldSkipAction`, this does NOT respond to the `all` wildcard. + * Optional goals only run when their specific tag is explicitly requested. + */ +export function shouldSkipOptionalGoal(goalTag: string): boolean { + const tags = parseTagsArg() + if (tags === null) return true + return !tags.includes(goalTag) +} diff --git a/packages/deployment/lib/deployment-validation.ts b/packages/deployment/lib/deployment-validation.ts new file mode 100644 index 000000000..e811b3f8e --- /dev/null +++ b/packages/deployment/lib/deployment-validation.ts @@ -0,0 +1,312 @@ +/** + * Pre-flight validation for deployment records + * + * Validates that deployment records can be reconstructed and are consistent + * with on-chain state. Run before deployments to catch issues early. + */ + +import type { DeploymentMetadata } from '@graphprotocol/toolshed/deployments' + +import type { AnyAddressBookOps } from './address-book-ops.js' +import type { ArtifactSource } from './contract-registry.js' +import { computeBytecodeHash } from './bytecode-utils.js' +import { + getLibraryResolver, + loadContractsArtifact, + loadIssuanceArtifact, + loadOpenZeppelinArtifact, + loadSubgraphServiceArtifact, +} from './artifact-loaders.js' + +/** + * Result of validating a single contract + */ +export interface ValidationResult { + /** Contract name */ + contract: string + /** Validation status */ + status: 'valid' | 'warning' | 'error' + /** Human-readable message */ + message: string + /** Additional details for debugging */ + details?: Record +} + +/** + * Options for validation + */ +export interface ValidationOptions { + /** Whether to perform on-chain checks (requires provider) */ + checkOnChain?: boolean + /** Whether to verify argsData matches transaction input */ + verifyArgsData?: boolean +} + +/** + * Load artifact from source type + */ +function loadArtifact(source: ArtifactSource) { + switch (source.type) { + case 'contracts': + return loadContractsArtifact(source.path, source.name) + case 'subgraph-service': + return loadSubgraphServiceArtifact(source.name) + case 'issuance': + return loadIssuanceArtifact(source.path) + case 'openzeppelin': + return loadOpenZeppelinArtifact(source.name) + } +} + +/** + * Validate deployment metadata is complete + */ +function validateMetadataComplete(metadata: DeploymentMetadata | undefined): { + valid: boolean + missing: string[] +} { + if (!metadata) { + return { valid: false, missing: ['all fields'] } + } + + const missing: string[] = [] + if (!metadata.txHash) missing.push('txHash') + if (!metadata.argsData) missing.push('argsData') + if (!metadata.bytecodeHash) missing.push('bytecodeHash') + + return { valid: missing.length === 0, missing } +} + +/** + * Validate a single contract's deployment record + * + * Checks: + * 1. Entry exists in address book + * 2. Deployment metadata exists and is complete + * 3. Bytecode hash matches local artifact + * 4. (Optional) Address has code on-chain + * 5. (Optional) argsData matches transaction input + */ +export async function validateContract( + addressBook: AnyAddressBookOps, + contractName: string, + artifact: ArtifactSource, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client?: any, + options: ValidationOptions = {}, +): Promise { + // Check if entry exists + if (!addressBook.entryExists(contractName)) { + return { + contract: contractName, + status: 'valid', + message: 'not deployed (no entry)', + } + } + + const entry = addressBook.getEntry(contractName) + + // Check if address is valid + if (!entry.address || entry.address === '0x0000000000000000000000000000000000000000') { + return { + contract: contractName, + status: 'valid', + message: 'not deployed (zero address)', + } + } + + // Check deployment metadata + const metadata = addressBook.getDeploymentMetadata(contractName) + const metadataCheck = validateMetadataComplete(metadata) + + if (!metadataCheck.valid) { + return { + contract: contractName, + status: 'warning', + message: `missing deployment metadata: ${metadataCheck.missing.join(', ')}`, + details: { address: entry.address, missingFields: metadataCheck.missing }, + } + } + + // Load artifact and verify bytecode hash + let loadedArtifact + try { + loadedArtifact = loadArtifact(artifact) + } catch { + return { + contract: contractName, + status: 'warning', + message: 'could not load artifact for bytecode comparison', + details: { artifactSource: artifact }, + } + } + + if (loadedArtifact?.deployedBytecode && metadata?.bytecodeHash) { + const libResolver = getLibraryResolver(artifact.type) + const localHash = computeBytecodeHash( + loadedArtifact.deployedBytecode, + loadedArtifact.deployedLinkReferences, + libResolver, + ) + if (metadata.bytecodeHash !== localHash) { + return { + contract: contractName, + status: 'warning', + message: 'local bytecode differs from deployed version', + details: { + address: entry.address, + storedHash: metadata.bytecodeHash, + localHash, + }, + } + } + } + + // Optional: Check on-chain state + if (options.checkOnChain && client) { + try { + const code = await client.getCode({ address: entry.address as `0x${string}` }) + if (!code || code === '0x') { + return { + contract: contractName, + status: 'error', + message: 'no code at address on-chain', + details: { address: entry.address }, + } + } + } catch (error) { + return { + contract: contractName, + status: 'error', + message: `failed to check on-chain code: ${(error as Error).message}`, + details: { address: entry.address }, + } + } + + // Optional: Verify argsData matches transaction + if (options.verifyArgsData && metadata?.txHash && metadata?.argsData && loadedArtifact?.bytecode) { + try { + const tx = await client.getTransaction({ hash: metadata.txHash as `0x${string}` }) + if (tx?.input) { + // Extract args from tx input (after bytecode) + const bytecodeLength = loadedArtifact.bytecode.length + const extractedArgs = '0x' + tx.input.slice(bytecodeLength) + + if (extractedArgs.toLowerCase() !== metadata.argsData.toLowerCase()) { + return { + contract: contractName, + status: 'error', + message: 'argsData mismatch with deployment transaction', + details: { + txHash: metadata.txHash, + storedArgs: metadata.argsData, + extractedArgs, + }, + } + } + } + } catch { + // Transaction lookup failed - not a critical error + } + } + } + + return { + contract: contractName, + status: 'valid', + message: 'ok', + details: { + address: entry.address, + hasMetadata: true, + bytecodeHashMatches: true, + }, + } +} + +/** + * Validate multiple contracts + * + * @param addressBook - Address book ops instance + * @param contracts - List of contracts with their artifact sources + * @param client - Optional viem client for on-chain checks + * @param options - Validation options + */ +export async function validateContracts( + addressBook: AnyAddressBookOps, + contracts: Array<{ name: string; artifact: ArtifactSource }>, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client?: any, + options: ValidationOptions = {}, +): Promise { + const results: ValidationResult[] = [] + + for (const { name, artifact } of contracts) { + const result = await validateContract(addressBook, name, artifact, client, options) + results.push(result) + } + + return results +} + +/** + * Summary of validation results + */ +export interface ValidationSummary { + /** Total contracts checked */ + total: number + /** Contracts with valid status */ + valid: number + /** Contracts with warnings */ + warnings: number + /** Contracts with errors */ + errors: number + /** Whether all checks passed (no errors) */ + success: boolean + /** Individual results */ + results: ValidationResult[] +} + +/** + * Summarize validation results + */ +export function summarizeValidation(results: ValidationResult[]): ValidationSummary { + const summary: ValidationSummary = { + total: results.length, + valid: 0, + warnings: 0, + errors: 0, + success: true, + results, + } + + for (const result of results) { + switch (result.status) { + case 'valid': + summary.valid++ + break + case 'warning': + summary.warnings++ + break + case 'error': + summary.errors++ + summary.success = false + break + } + } + + return summary +} + +/** + * Format validation results for display + */ +export function formatValidationResults(results: ValidationResult[]): string[] { + const lines: string[] = [] + + for (const result of results) { + const icon = result.status === 'valid' ? '✓' : result.status === 'warning' ? '⚠' : '❌' + lines.push(`${icon} ${result.contract}: ${result.message}`) + } + + return lines +} diff --git a/packages/deployment/lib/execute-governance.ts b/packages/deployment/lib/execute-governance.ts new file mode 100644 index 000000000..e39cde9cc --- /dev/null +++ b/packages/deployment/lib/execute-governance.ts @@ -0,0 +1,494 @@ +import type { Environment } from '@rocketh/core/types' +import fs from 'fs' +import path from 'path' +import { createPublicClient, createWalletClient, custom, http, parseEther } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + +import { getForkNetwork, getForkStateDir, getTargetChainIdFromEnv, isForkMode } from './address-book-utils.js' +import { getGovernor } from './controller-utils.js' +import type { BuilderTx } from './tx-builder.js' +import { TxBuilder } from './tx-builder.js' + +/** + * Convert network name to env var prefix: arbitrumSepolia → ARBITRUM_SEPOLIA + */ +function networkToEnvPrefix(networkName: string): string { + return networkName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase() +} + +interface SafeTxBatch { + version: string + chainId: string + createdAt: number + meta?: unknown + transactions: BuilderTx[] +} + +/** + * Get governance TX directory path + * + * In fork mode: fork///txs/ + * In normal mode: txs// + * + * Stored outside deployments/ so rocketh manages its own directory cleanly. + * + * @param networkName - Network name (e.g., 'fork', 'localhost', 'arbitrumSepolia') + */ +export function getGovernanceTxDir(networkName: string): string { + const forkNetwork = getForkNetwork(networkName) + if (forkNetwork) { + return path.join(getForkStateDir(networkName, forkNetwork), 'txs') + } + return path.resolve(process.cwd(), 'txs', networkName) +} + +/** + * Count pending governance TX batch files + * + * @param networkName - Network name (e.g., 'fork', 'arbitrumSepolia') + */ +export function countPendingGovernanceTxs(networkName: string): number { + const txDir = getGovernanceTxDir(networkName) + if (!fs.existsSync(txDir)) { + return 0 + } + return fs.readdirSync(txDir).filter((f) => f.endsWith('.json') && !f.startsWith('.')).length +} + +/** + * Check if a specific governance TX file exists + * + * @param networkName - Network name (e.g., 'fork', 'arbitrumSepolia') + * @param name - TX file name (without .json extension) + */ +export function hasGovernanceTx(networkName: string, name: string): boolean { + const txFile = path.join(getGovernanceTxDir(networkName), `${name}.json`) + return fs.existsSync(txFile) +} + +/** + * Check for pending upgrade TX and exit if found + * + * Standard pattern for contract "ready" steps that depend on governance execution. + * Call this at the start of the final deploy step for any upgradeable contract. + * + * @param env - Deployment environment + * @param contractName - Contract name (used to derive TX filename: upgrade-{contractName}) + */ +export function requireUpgradeExecuted(env: Environment, contractName: string): void { + const txName = `upgrade-${contractName}` + if (hasGovernanceTx(env.name, txName)) { + const txFile = path.join(getGovernanceTxDir(env.name), `${txName}.json`) + env.showMessage(`\n⏳ ${contractName} pending governance (${txFile})`) + env.showMessage(` Run: npx hardhat deploy:execute-governance --network ${env.name}`) + process.exit(1) + } +} + +/** + * Create a TxBuilder configured for governance transactions + * + * Standard pattern for creating governance TX builders with correct: + * - Target chain ID (handles fork mode) + * - Output directory (handles fork mode) + * - Template path (uses default) + * + * @param env - Deployment environment + * @param name - TX batch name (without .json extension) + * @param meta - Optional metadata for the TX batch + * @returns Configured TxBuilder instance + */ +export async function createGovernanceTxBuilder( + env: Environment, + name: string, + meta?: { name?: string; description?: string }, +): Promise { + const targetChainId = await getTargetChainIdFromEnv(env) + const outputDir = getGovernanceTxDir(env.name) + + return new TxBuilder(targetChainId, { + outputDir, + name, + meta, + }) +} + +/** + * Save governance TX batch and exit with code 1 + * + * Standard completion pattern for scripts that generate governance TX batches. + * Saves the TX batch to file and displays a message. + * Returns the saved file path so the caller can continue. + * + * Subsequent scripts that depend on this TX being executed should check + * their own preconditions and exit if not met. + * + * @param env - Deployment environment + * @param builder - TX builder with batched transactions + * @param contractName - Optional contract name for contextual message + * @returns Path to the saved TX file + */ +export function saveGovernanceTx( + env: Environment, + builder: { saveToFile: () => string }, + contractName?: string, +): string { + const txFile = builder.saveToFile() + env.showMessage(` ✓ Governance TX saved: ${txFile}`) + + if (contractName) { + env.showMessage(` ${contractName} requires governance execution`) + } + env.showMessage(` Run: npx hardhat deploy:execute-governance --network ${env.name}`) + + return txFile +} + +/** + * @deprecated Use `saveGovernanceTx` instead. This function exits the process. + */ +export function saveGovernanceTxAndExit( + env: Environment, + builder: { saveToFile: () => string }, + contractName?: string, +): never { + saveGovernanceTx(env, builder, contractName) + process.exit(1) +} + +/** + * Execute a TX builder batch directly and save to executed/ folder + * + * Use this when the caller has authority to execute (e.g., deployer has GOVERNOR_ROLE). + * This maintains the consistent pattern of ALWAYS creating a TX batch, but executing + * it inline when possible. + * + * @param env - Deployment environment + * @param builder - TX builder with batched transactions + * @param account - Account to execute from (deployer address) + * @returns Number of transactions executed + */ +export async function executeTxBatchDirect(env: Environment, builder: TxBuilder, account: string): Promise { + const transactions = builder.getTransactions() + if (transactions.length === 0) { + return 0 + } + + // Create viem clients + const publicClient = createPublicClient({ + transport: custom(env.network.provider), + }) + const walletClient = createWalletClient({ + transport: custom(env.network.provider), + }) + + // Execute each transaction + for (let i = 0; i < transactions.length; i++) { + const tx = transactions[i] + env.showMessage(` ${i + 1}/${transactions.length} TX to ${tx.to.slice(0, 10)}...`) + + const hash = await walletClient.sendTransaction({ + chain: null, + account: account as `0x${string}`, + to: tx.to as `0x${string}`, + data: tx.data as `0x${string}`, + value: BigInt(tx.value), + }) + await publicClient.waitForTransactionReceipt({ hash }) + env.showMessage(` ✓ TX hash: ${hash}`) + } + + // Save to executed/ folder for audit trail + const txDir = getGovernanceTxDir(env.name) + const executedDir = path.join(txDir, 'executed') + if (!fs.existsSync(executedDir)) { + fs.mkdirSync(executedDir, { recursive: true }) + } + + // Save with original filename in executed/ + const originalFile = builder.outputFile + const filename = path.basename(originalFile) + const executedFile = path.join(executedDir, filename) + fs.writeFileSync(executedFile, JSON.stringify({ transactions }, null, 2) + '\n') + env.showMessage(` ✓ Saved to ${executedFile}`) + + return transactions.length +} + +export interface ExecuteGovernanceOptions { + /** Optional TX batch name filter */ + name?: string + /** Governor private key (from keystore or env var) */ + governorPrivateKey?: string + /** Lazy resolver for governor key - defers keystore access until actually needed */ + resolveGovernorKey?: () => Promise +} + +export async function executeGovernanceTxs(env: Environment, options?: ExecuteGovernanceOptions): Promise { + const { name, governorPrivateKey, resolveGovernorKey } = options ?? {} + // Determine TX directory - in fork mode, also check source network's TX directory + const forkNetwork = getForkNetwork(env.name) + let txDir = getGovernanceTxDir(env.name) + let sourceNetworkFallback = false + + if ( + !fs.existsSync(txDir) || + fs.readdirSync(txDir).filter((f) => f.endsWith('.json') && !f.startsWith('.')).length === 0 + ) { + // Fork-state directory empty - check source network's TX directory + if (forkNetwork) { + const sourceNetworkTxDir = path.resolve(process.cwd(), 'txs', forkNetwork) + if ( + fs.existsSync(sourceNetworkTxDir) && + fs.readdirSync(sourceNetworkTxDir).filter((f) => f.endsWith('.json') && !f.startsWith('.')).length > 0 + ) { + txDir = sourceNetworkTxDir + sourceNetworkFallback = true + env.showMessage(`\n📂 Using source network TXs: ${txDir}`) + } + } + } + + if (!fs.existsSync(txDir)) { + env.showMessage(`\n✓ No governance TXs directory: ${txDir}`) + if (forkNetwork) { + env.showMessage(` (Also checked: txs/${forkNetwork}/)`) + } + return 0 + } + + // Find pending TX batch files (optionally filtered by name) + let files: string[] + if (name) { + const specificFile = `${name}.json` + files = fs.existsSync(path.join(txDir, specificFile)) ? [specificFile] : [] + } else { + files = fs.readdirSync(txDir).filter((f) => f.endsWith('.json') && !f.startsWith('.')) + } + if (files.length === 0) { + env.showMessage(`\n✓ No pending governance TXs`) + if (forkNetwork && !sourceNetworkFallback) { + env.showMessage(` (Also checked: txs/${forkNetwork}/)`) + } + return 0 + } + + // Get governor address from Controller + const governor = (await getGovernor(env)) as `0x${string}` + + // Create viem client for checking governor type + const publicClient = createPublicClient({ + transport: custom(env.network.provider), + }) + + // Check if in fork mode (network-aware: ignores FORK_NETWORK on real networks) + const inForkMode = isForkMode(env.name) + + if (!inForkMode) { + // Not in fork mode - check if governor is EOA or Safe + const governorCode = await publicClient.getCode({ address: governor }) + const isContract = governorCode && governorCode !== '0x' + + // Governor private key passed from task (resolved from keystore or env var) + + if (isContract) { + // Governor is a Safe multisig - require Safe UI workflow + env.showMessage(`\n📋 Safe multisig governance execution required`) + env.showMessage(` Governor address: ${governor}`) + env.showMessage(`\nExecute via Safe Transaction Builder:`) + env.showMessage(`\n1. Go to https://app.safe.global/`) + env.showMessage(` - Connect wallet`) + env.showMessage(` - Select the governor Safe (${governor})`) + env.showMessage(` - Navigate to: Apps → Transaction Builder`) + env.showMessage(`\n2. Click "Upload a JSON" and select:`) + for (const file of files) { + env.showMessage(` - ${path.join(txDir, file)}`) + } + env.showMessage(`\n3. Review decoded transactions`) + env.showMessage(`4. Create batch → Collect signatures → Execute`) + env.showMessage(`\n5. After on-chain execution, sync address books:`) + env.showMessage(` npx hardhat deploy --tags sync --network ${env.name}`) + env.showMessage(`\nNote: If Safe is not available on ${env.name}, test in fork mode:`) + env.showMessage(` FORK_NETWORK=arbitrumOne npx hardhat deploy:execute-governance --network fork\n`) + return 0 + } + + // Governor is an EOA - resolve key now (deferred to avoid keystore prompt in fork mode) + const resolvedKey = governorPrivateKey ?? (await resolveGovernorKey?.()) + if (!resolvedKey) { + const keyName = `${networkToEnvPrefix(env.name)}_GOVERNOR_KEY` + env.showMessage(`\n❌ Cannot execute governance TXs on ${env.name}`) + env.showMessage(` Governor address: ${governor} (EOA)`) + env.showMessage(`\nTo execute with EOA private key:`) + env.showMessage(` npx hardhat keystore set ${keyName}`) + env.showMessage(` npx hardhat deploy:execute-governance --network ${env.name}`) + env.showMessage(`\nOr via environment variable:`) + env.showMessage(` export ${keyName}=0x...`) + env.showMessage(`\nTo test with Safe Transaction Builder (validation only):`) + env.showMessage(` 1. Go to https://app.safe.global/`) + env.showMessage(` 2. Apps → Transaction Builder → Upload JSON`) + env.showMessage(` 3. Select: ${path.join(txDir, files[0])}`) + env.showMessage(` 4. Review decoded transactions (don't execute)`) + env.showMessage(`\nOr test in fork mode:`) + env.showMessage(` FORK_NETWORK=${env.name} npx hardhat deploy:execute-governance --network fork\n`) + return 0 + } + + // Have private key - execute as EOA + env.showMessage(`\n🔓 Executing ${files.length} governance TX batch(es)...`) + env.showMessage(` Governor: ${governor} (EOA)`) + return await executeWithEOA(env, publicClient, files, txDir, resolvedKey) + } + + // Fork mode - use impersonation + env.showMessage(`\n🔓 Executing ${files.length} governance TX batch(es) via impersonation...`) + env.showMessage(` (Fork mode - impersonating governor for testing)`) + env.showMessage(` Governor: ${governor}`) + return await executeWithImpersonation(env, publicClient, files, txDir, governor) +} + +/** + * Execute governance TXs using EOA private key (testnet with EOA governor) + */ +async function executeWithEOA( + env: Environment, + publicClient: ReturnType, + files: string[], + txDir: string, + privateKey: string, +): Promise { + // Create wallet from private key + const account = privateKeyToAccount(privateKey as `0x${string}`) + + // Create wallet client with the account + const walletClient = createWalletClient({ + account, + transport: custom(env.network.provider), + }) + + let executedCount = 0 + const executedDir = path.join(txDir, 'executed') + + for (const file of files) { + const filePath = path.join(txDir, file) + env.showMessage(`\n 📋 ${file}`) + + try { + const batchContents = fs.readFileSync(filePath, 'utf8') + const batch: SafeTxBatch = JSON.parse(batchContents) + + // Execute each transaction + for (let i = 0; i < batch.transactions.length; i++) { + const tx = batch.transactions[i] + env.showMessage(` ${i + 1}/${batch.transactions.length} TX to ${tx.to.slice(0, 10)}...`) + + const hash = await walletClient.sendTransaction({ + chain: null, + to: tx.to as `0x${string}`, + data: tx.data as `0x${string}`, + value: BigInt(tx.value), + }) + await publicClient.waitForTransactionReceipt({ hash }) + env.showMessage(` ✓ TX hash: ${hash}`) + } + + // Move to executed directory + if (!fs.existsSync(executedDir)) { + fs.mkdirSync(executedDir, { recursive: true }) + } + fs.renameSync(filePath, path.join(executedDir, file)) + env.showMessage(` ✓ Executed and moved to executed/`) + executedCount++ + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + env.showMessage(` ✗ Failed: ${errorMessage.slice(0, 80)}...`) + throw error + } + } + + env.showMessage(`\n✅ Executed ${executedCount} governance TX batch(es)`) + return executedCount +} + +/** + * Execute governance TXs using impersonation (fork mode only) + */ +async function executeWithImpersonation( + env: Environment, + publicClient: ReturnType, + files: string[], + txDir: string, + governor: `0x${string}`, +): Promise { + const walletClient = createWalletClient({ + transport: custom(env.network.provider), + }) + + // Use provider.request for hardhat-specific RPC methods + const request = env.network.provider.request.bind(env.network.provider) as (args: { + method: string + params: unknown[] + }) => Promise + + // Impersonate governor + await request({ + method: 'hardhat_impersonateAccount', + params: [governor], + }) + + // Fund governor with ETH for gas + const tenEth = '0x' + parseEther('10').toString(16) + await request({ + method: 'hardhat_setBalance', + params: [governor, tenEth], + }) + + let executedCount = 0 + const executedDir = path.join(txDir, 'executed') + + for (const file of files) { + const filePath = path.join(txDir, file) + env.showMessage(`\n 📋 ${file}`) + + try { + const batchContents = fs.readFileSync(filePath, 'utf8') + const batch: SafeTxBatch = JSON.parse(batchContents) + + // Execute each transaction + for (let i = 0; i < batch.transactions.length; i++) { + const tx = batch.transactions[i] + env.showMessage(` ${i + 1}/${batch.transactions.length} TX to ${tx.to.slice(0, 10)}...`) + + const hash = await walletClient.sendTransaction({ + chain: null, + account: governor, + to: tx.to as `0x${string}`, + data: tx.data as `0x${string}`, + value: BigInt(tx.value), + }) + await publicClient.waitForTransactionReceipt({ hash }) + } + + // Move to executed directory + if (!fs.existsSync(executedDir)) { + fs.mkdirSync(executedDir, { recursive: true }) + } + fs.renameSync(filePath, path.join(executedDir, file)) + env.showMessage(` ✓ Executed and moved to executed/`) + executedCount++ + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + env.showMessage(` ✗ Failed: ${errorMessage.slice(0, 80)}...`) + throw error + } + } + + // Stop impersonating + await request({ + method: 'hardhat_stopImpersonatingAccount', + params: [governor], + }) + + env.showMessage(`\n✅ Executed ${executedCount} governance TX batch(es)`) + return executedCount +} diff --git a/packages/deployment/lib/format.ts b/packages/deployment/lib/format.ts new file mode 100644 index 000000000..fd1bf1359 --- /dev/null +++ b/packages/deployment/lib/format.ts @@ -0,0 +1,10 @@ +/** + * Formatting helpers for human-readable display of on-chain values. + */ + +import { formatEther } from 'viem' + +/** Format a wei amount as GRT (e.g. `6036500000000000000n` → `"6.0365 GRT"`). */ +export function formatGRT(wei: bigint): string { + return `${formatEther(wei)} GRT` +} diff --git a/packages/deployment/lib/issuance-deploy-utils.ts b/packages/deployment/lib/issuance-deploy-utils.ts new file mode 100644 index 000000000..704bde2e2 --- /dev/null +++ b/packages/deployment/lib/issuance-deploy-utils.ts @@ -0,0 +1,646 @@ +import type { DeploymentMetadata } from '@graphprotocol/toolshed/deployments' +import type { Environment } from '@rocketh/core/types' +import type { PublicClient } from 'viem' +import { encodeFunctionData } from 'viem' + +import { Contracts, type RegistryEntry } from './contract-registry.js' +import { getGovernor } from './controller-utils.js' +import { loadTransparentProxyArtifact } from './artifact-loaders.js' +import { INITIALIZE_GOVERNOR_ABI, OZ_PROXY_ADMIN_ABI } from './abis.js' +import { computeBytecodeHash } from './bytecode-utils.js' +import { getAddressBookForType, getTargetChainIdFromEnv } from './address-book-utils.js' +import { + computeArtifactBytecodeHash, + deployImplementation, + getImplementationConfig, + getOnChainImplementation, + loadArtifactFromSource, +} from './deploy-implementation.js' +import { buildDeploymentMetadata } from './deployment-metadata.js' +import { deploy, execute, graph } from '../rocketh/deploy.js' + +/** ERC1967 admin slot: keccak256("eip1967.proxy.admin") - 1 */ +const ERC1967_ADMIN_SLOT = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103' + +/** + * Require deployer account to be configured + * + * Standard pattern for checking deployer account exists in namedAccounts. + * Throws an error if deployer is not configured. + * + * @param env - Deployment environment + * @returns The deployer address + */ +export function requireDeployer(env: Environment): string { + const deployer = env.namedAccounts.deployer + if (!deployer) { + throw new Error('No deployer account configured') + } + return deployer +} + +/** + * Address derived from the dummy private key (0x…001) used for status-only runs. + * Filtered out so status scripts don't mistake it for the real deployer. + */ +const DUMMY_DEPLOYER_ADDRESS = '0x7e5f4552091a69125d5dfcb7b8c2659029395bdf' + +/** + * Get deployer address if available (non-throwing). + * + * Returns undefined when the deploy key is not loaded (e.g. status-only runs + * where the keystore password is not prompted). Status scripts infer the real + * deployer from the ProxyAdmin owner on-chain instead. + */ +export function getDeployer(env: Environment): string | undefined { + const deployer = env.namedAccounts.deployer + if (!deployer || deployer.toLowerCase() === DUMMY_DEPLOYER_ADDRESS) return undefined + return deployer +} + +/** + * Require a contract deployment to exist, throwing a helpful error if not found + */ +export function requireContract(env: Environment, contract: RegistryEntry) { + const deployment = env.getOrNull(contract.name) + if (!deployment) { + throw new Error(`${contract.name} not deployed. Run required deploy tags first.`) + } + return deployment +} + +/** + * Require L2GraphToken from deployments (synced from Horizon address book) + * Provides specific error message about running sync + */ +export function requireGraphToken(env: Environment) { + const deployment = env.getOrNull(Contracts.horizon.L2GraphToken.name) + if (!deployment) { + throw new Error( + `Missing deployments/${env.name}/${Contracts.horizon.L2GraphToken.name}.json. ` + + `Run sync to import ${Contracts.horizon.L2GraphToken.name} address from Horizon address book.`, + ) + } + return deployment +} + +/** + * Require multiple contract deployments to exist + * Lists all missing contracts in error message + */ +export function requireContracts(env: Environment, contracts: RegistryEntry[]) { + const missing: string[] = [] + const deployments = contracts.map((c) => { + const deployment = env.getOrNull(c.name) + if (!deployment) { + missing.push(c.name) + } + return deployment + }) + + if (missing.length > 0) { + throw new Error(`${missing.join(', ')} not deployed. Run required deploy tags first.`) + } + + return deployments as NonNullable<(typeof deployments)[number]>[] +} + +/** + * Get proxy infrastructure (implementation) for a proxied contract + */ +export function getProxyInfrastructure(env: Environment, contract: RegistryEntry) { + const implDep = env.getOrNull(`${contract.name}_Implementation`) + return { implementation: implDep } +} + +/** + * Read per-proxy ProxyAdmin address from ERC1967 admin slot + * OZ v5 TransparentUpgradeableProxy creates its own ProxyAdmin stored in this slot + */ +export async function getProxyAdminAddress(client: PublicClient, proxyAddress: string): Promise { + const adminSlotData = await client.getStorageAt({ + address: proxyAddress as `0x${string}`, + slot: ERC1967_ADMIN_SLOT as `0x${string}`, + }) + if (!adminSlotData) { + throw new Error(`Failed to read admin slot from proxy ${proxyAddress}`) + } + return `0x${adminSlotData.slice(-40)}` +} + +/** + * Show standard deployment status message + */ +export function showDeploymentStatus( + env: Environment, + contract: RegistryEntry, + result: { newlyDeployed?: boolean; address: string }, +) { + if (result.newlyDeployed) { + env.showMessage(`✓ ${contract.name} deployed at ${result.address}`) + } else { + env.showMessage(`✓ ${contract.name} unchanged at ${result.address}`) + } +} + +/** + * Show standard proxy deployment status messages + */ +export function showProxyDeploymentStatus( + env: Environment, + contract: RegistryEntry, + result: { newlyDeployed?: boolean; address: string }, + implAddress?: string, + governor?: string, +) { + if (result.newlyDeployed) { + env.showMessage(`✓ ${contract.name} proxy deployed at ${result.address}`) + if (implAddress) { + env.showMessage(`✓ ${contract.name} implementation at ${implAddress}`) + } + if (governor) { + env.showMessage(`✓ Governor role assigned to: ${governor}`) + } + } else { + env.showMessage(`✓ ${contract.name} deployed at ${result.address}`) + } +} + +/** + * Update address book with proxy deployment information. + * Routes to the correct address book based on contract.addressBook. + */ +export async function updateProxyAddressBook( + env: Environment, + graphUtils: typeof graph, + contract: RegistryEntry, + proxyAddress: string, + implAddress?: string, + proxyAdminAddress?: string, + implementationDeployment?: DeploymentMetadata, + proxyDeployment?: DeploymentMetadata, +) { + await graphUtils.updateAddressBookForContract(env, contract, { + name: contract.name, + address: proxyAddress, + proxy: 'transparent', + proxyAdmin: proxyAdminAddress, + implementation: implAddress, + proxyDeployment, + implementationDeployment, + }) +} + +/** + * Check if proxy has pending upgrade and display warning if needed + * + * Compares on-chain implementation with newly deployed implementation. + * If they differ, displays upgrade warning for governance action. + * + * @param env - Deployment environment + * @param client - Viem public client + * @param contract - Contract registry entry + * @param proxyAddress - Address of the proxy contract + * @param proxyType - 'transparent' for OZ TransparentProxy, 'graph' for Graph legacy proxy + * @param proxyAdminAddress - Address of proxy admin (required for 'graph' type) + */ +export async function checkPendingUpgrade( + env: Environment, + client: PublicClient, + contract: RegistryEntry, + proxyAddress: string, + proxyType: 'transparent' | 'graph' = 'transparent', + proxyAdminAddress?: string, +) { + // Get implementation deployment if it exists + const implDeployment = env.getOrNull(`${contract.name}_Implementation`) + if (!implDeployment) { + return + } + + // Get on-chain implementation + const onChainImpl = await getOnChainImplementation(client, proxyAddress, proxyType, proxyAdminAddress) + + // Check if upgrade is pending + if (onChainImpl.toLowerCase() !== implDeployment.address.toLowerCase()) { + env.showMessage(``) + env.showMessage(`⚠️ UPGRADE REQUIRED`) + env.showMessage(` Proxy: ${proxyAddress}`) + env.showMessage(` Current (on-chain): ${onChainImpl}`) + env.showMessage(` New implementation: ${implDeployment.address}`) + env.showMessage(``) + env.showMessage(` Governance must upgrade the proxy.`) + env.showMessage(``) + } else { + env.showMessage(`✓ Current implementation: ${onChainImpl}`) + } +} + +/** + * Configuration for deploying a proxy contract + */ +export interface ProxyDeployConfig { + /** Contract registry entry (provides addressBook and artifact config) */ + contract: RegistryEntry + /** Constructor arguments for implementation (not used when sharedImplementation provided) */ + constructorArgs?: unknown[] + /** Initialize function arguments (defaults to [governor] if not provided) */ + initializeArgs?: unknown[] + /** + * Shared implementation contract (optional) + * When provided, deploys proxy pointing to this existing implementation + * instead of deploying a new implementation from contract.artifact + */ + sharedImplementation?: RegistryEntry +} + +/** + * Deploy or upgrade a proxy contract using OZ v5 TransparentUpgradeableProxy + * + * Uses OpenZeppelin v5's per-proxy ProxyAdmin pattern: + * - Each proxy creates its own ProxyAdmin in the constructor + * - Deployer is the initial ProxyAdmin owner (for post-deployment configuration) + * - Ownership is transferred to governor in the transfer-governance step + * - No shared ProxyAdmin required + * + * Deployment scenarios: + * - Fresh deployment: Deploy implementation + OZ v5 proxy (creates per-proxy ProxyAdmin) + * - Existing proxy: Deploy new implementation, store as pending for governance upgrade + * + * For shared implementations (sharedImplementation provided): + * - Fresh deployment: Deploy OZ v5 proxy pointing to shared implementation + * - Existing proxy: Reports status only (shared impl managed separately) + * + * @param env - Deployment environment + * @param config - Deployment configuration + * @returns Deployment result with address and status + */ +export async function deployProxyContract( + env: Environment, + config: ProxyDeployConfig, +): Promise<{ address: string; newlyDeployed: boolean; upgraded: boolean }> { + const { contract, constructorArgs = [], initializeArgs, sharedImplementation } = config + + // Validate contract has required metadata + if (!sharedImplementation && !contract.artifact) { + throw new Error(`No artifact configured for ${contract.name} in registry (and no sharedImplementation provided)`) + } + + // Derive values from environment + const deployer = requireDeployer(env) + const governor = await getGovernor(env) + const actualInitializeArgs = initializeArgs ?? [governor] + + // Check if proxy already exists (synced from address book) + const existingProxy = env.getOrNull(`${contract.name}_Proxy`) + + if (existingProxy) { + if (sharedImplementation) { + // Shared implementation — detect if redeployed and set pendingImplementation + env.showMessage(`✓ ${contract.name} proxy already deployed at ${existingProxy.address}`) + env.showMessage(` Uses shared implementation: ${sharedImplementation.name}`) + + const implDep = env.getOrNull(sharedImplementation.name) + if (!implDep) { + // Missing impl record means the impl's deploy script didn't run, or sync + // skipped seeding because the artifact couldn't be verified against the + // address book. Either way, silently treating this as "no change" would + // mask a drift between artifact and on-chain bytecode (the shared impl + // bug fixed alongside this guard). Fail loud instead. + throw new Error( + `${contract.name}: shared implementation ${sharedImplementation.name} not in env. ` + + `Ensure ${sharedImplementation.name} is listed in dependencies and its deploy script ran successfully.`, + ) + } + + const client = graph.getPublicClient(env) + const onChainImpl = await getOnChainImplementation(client, existingProxy.address, 'transparent') + + if (onChainImpl.toLowerCase() !== implDep.address.toLowerCase()) { + // Shared implementation changed — store as pending for governance upgrade + const targetChainId = await getTargetChainIdFromEnv(env) + const addressBook = getAddressBookForType(contract.addressBook, targetChainId) + + // Get deployment metadata from the shared implementation's address book entry + const implMetadata = addressBook.getDeploymentMetadata(sharedImplementation.name) + if (!implMetadata) { + throw new Error( + `${contract.name}: deployment metadata missing for ${sharedImplementation.name}. ` + + `Run ${sharedImplementation.name}'s deploy script (or sync) before re-running.`, + ) + } + addressBook.setPendingImplementationWithMetadata(contract.name, implDep.address, implMetadata) + + env.showMessage(``) + env.showMessage(`⚠️ UPGRADE REQUIRED`) + env.showMessage(` Proxy: ${existingProxy.address}`) + env.showMessage(` Current (on-chain): ${onChainImpl}`) + env.showMessage(` New implementation: ${implDep.address}`) + env.showMessage(``) + env.showMessage(` Stored as pending — run upgrade task to generate governance TX.`) + + return { + address: existingProxy.address, + newlyDeployed: false, + upgraded: true, + } + } + + // No change — check existing pending status + await checkPendingUpgrade(env, client, contract, existingProxy.address, 'transparent') + + return { + address: existingProxy.address, + newlyDeployed: false, + upgraded: false, + } + } + + // Own implementation - use deployImplementation for upgrade pattern + env.showMessage(` Existing proxy found at ${existingProxy.address}, using upgrade pattern`) + + const implResult = await deployImplementation( + env, + getImplementationConfig(contract.addressBook, contract.name, { + constructorArgs, + }), + ) + + if (implResult.deployed) { + env.showMessage(`✓ New implementation deployed at ${implResult.address}`) + env.showMessage(` Upgrade TX required via governance`) + } else { + env.showMessage(`✓ Implementation unchanged at ${implResult.address}`) + } + + // Check pending upgrade status + const client = graph.getPublicClient(env) + await checkPendingUpgrade(env, client, contract, existingProxy.address, 'transparent') + + return { + address: existingProxy.address, + newlyDeployed: false, + upgraded: implResult.deployed, + } + } + + // Fresh deployment - deploy implementation first, then OZ v5 proxy + if (sharedImplementation) { + return deployProxyWithSharedImpl(env, contract, sharedImplementation, actualInitializeArgs, deployer) + } + + return deployProxyWithOwnImpl(env, contract, constructorArgs, actualInitializeArgs, deployer) +} + +/** + * Deploy proxy with its own implementation (OZ v5 pattern) + */ +async function deployProxyWithOwnImpl( + env: Environment, + contract: RegistryEntry, + constructorArgs: unknown[], + initializeArgs: unknown[], + deployer: string, +): Promise<{ address: string; newlyDeployed: boolean; upgraded: boolean }> { + const deployFn = deploy(env) + + // Deploy implementation + const implArtifact = loadArtifactFromSource(contract.artifact!) + const implResult = await deployFn( + `${contract.name}_Implementation`, + { + account: deployer, + artifact: implArtifact, + args: constructorArgs, + }, + { alwaysOverride: true }, + ) + + env.showMessage(` Implementation deployed at ${implResult.address}`) + + // Encode initialize call using the contract's own ABI + const initCalldata = encodeFunctionData({ + abi: implArtifact.abi, + functionName: 'initialize', + args: initializeArgs as [`0x${string}`], + }) + + // Deploy OZ v5 TransparentUpgradeableProxy + // Constructor: (address _logic, address initialOwner, bytes memory _data) + // Deployer is the initial ProxyAdmin owner to allow post-deployment configuration. + // Ownership is transferred to the protocol governor in the transfer-governance step. + // Use issuance-compiled proxy artifact (0.8.34) for consistent verification + const proxyArtifact = loadTransparentProxyArtifact() + const proxyResult = await deployFn( + `${contract.name}_Proxy`, + { + account: deployer, + artifact: proxyArtifact, + args: [implResult.address, deployer, initCalldata], + }, + { skipIfAlreadyDeployed: true }, + ) + + // Read per-proxy ProxyAdmin address from ERC1967 slot + const client = graph.getPublicClient(env) + const proxyAdminAddress = await getProxyAdminAddress(client, proxyResult.address) + + // Save main contract deployment (proxy address with implementation ABI) + await env.save(contract.name, { + ...proxyResult, + abi: implArtifact.abi, + }) + + // Build implementation deployment metadata. Hash from the artifact (with library resolution) + // so the stored value stays in lockstep with sync's artifact-side comparison. + const implementationDeployment = buildDeploymentMetadata(implResult, computeArtifactBytecodeHash(contract.artifact!)) + + // Build proxy deployment metadata from the proxy artifact bytecode + const proxyDeployment = buildDeploymentMetadata( + proxyResult, + computeBytecodeHash(proxyArtifact.deployedBytecode ?? '0x'), + ) + + // Update address book with per-proxy ProxyAdmin and deployment metadata + await updateProxyAddressBook( + env, + graph, + contract, + proxyResult.address, + implResult.address, + proxyAdminAddress, + implementationDeployment, + proxyDeployment, + ) + + if (proxyResult.newlyDeployed) { + env.showMessage(`✓ ${contract.name} proxy deployed at ${proxyResult.address}`) + env.showMessage(` Implementation: ${implResult.address}`) + env.showMessage(` ProxyAdmin (per-proxy, deployer-owned): ${proxyAdminAddress}`) + } else { + env.showMessage(`✓ ${contract.name} already deployed at ${proxyResult.address}`) + } + + return { + address: proxyResult.address, + newlyDeployed: !!proxyResult.newlyDeployed, + upgraded: false, + } +} + +/** + * Deploy proxy pointing to a shared implementation (OZ v5 pattern) + */ +async function deployProxyWithSharedImpl( + env: Environment, + contract: RegistryEntry, + sharedImplementation: RegistryEntry, + initializeArgs: unknown[], + deployer: string, +): Promise<{ address: string; newlyDeployed: boolean; upgraded: boolean }> { + const deployFn = deploy(env) + + // Get shared implementation deployment + const implDep = env.getOrNull(sharedImplementation.name) + if (!implDep) { + throw new Error(`Shared implementation ${sharedImplementation.name} not deployed. Deploy it first.`) + } + + env.showMessage(` Deploying ${contract.name} proxy with shared implementation: ${sharedImplementation.name}`) + + // Encode initialize call + const initCalldata = encodeFunctionData({ + abi: INITIALIZE_GOVERNOR_ABI, + functionName: 'initialize', + args: initializeArgs as [`0x${string}`], + }) + + // Deploy OZ v5 TransparentUpgradeableProxy + // Constructor: (address _logic, address initialOwner, bytes memory _data) + // Deployer is the initial ProxyAdmin owner to allow post-deployment configuration. + // Ownership is transferred to the protocol governor in the transfer-governance step. + // Use issuance-compiled proxy artifact (0.8.34) for consistent verification + const proxyArtifact = loadTransparentProxyArtifact() + const proxyResult = await deployFn( + `${contract.name}_Proxy`, + { + account: deployer, + artifact: proxyArtifact, + args: [implDep.address, deployer, initCalldata], + }, + { skipIfAlreadyDeployed: true }, + ) + + // Read per-proxy ProxyAdmin address from ERC1967 slot + const client = graph.getPublicClient(env) + const proxyAdminAddress = await getProxyAdminAddress(client, proxyResult.address) + + // Save main contract deployment (proxy address with implementation ABI) + await env.save(contract.name, { + ...proxyResult, + abi: implDep.abi, + }) + + // Build proxy deployment metadata from the proxy artifact bytecode + const proxyDeployment = buildDeploymentMetadata( + proxyResult, + computeBytecodeHash(proxyArtifact.deployedBytecode ?? '0x'), + ) + + // Update address book with per-proxy ProxyAdmin and proxy deployment metadata + await updateProxyAddressBook( + env, + graph, + contract, + proxyResult.address, + implDep.address, + proxyAdminAddress, + undefined, + proxyDeployment, + ) + + if (proxyResult.newlyDeployed) { + env.showMessage(`✓ ${contract.name} proxy deployed at ${proxyResult.address}`) + env.showMessage(` Implementation: ${implDep.address}`) + env.showMessage(` ProxyAdmin (per-proxy, deployer-owned): ${proxyAdminAddress}`) + } else { + env.showMessage(`✓ ${contract.name} already deployed at ${proxyResult.address}`) + } + + return { + address: proxyResult.address, + newlyDeployed: !!proxyResult.newlyDeployed, + upgraded: false, + } +} + +/** + * Transfer ProxyAdmin ownership for an issuance contract from deployer to governor. + * + * Reads the per-proxy ProxyAdmin address from the address book entry's proxyAdmin field, + * checks current ownership, and transfers if needed. Idempotent: skips if already owned + * by the target governor. + * + * @param env - Deployment environment + * @param contract - Registry entry for the contract whose ProxyAdmin to transfer + * @returns Whether a transfer was executed + * + * @example + * ```typescript + * await transferProxyAdminOwnership(env, Contracts.issuance.IssuanceAllocator) + * ``` + */ +export async function transferProxyAdminOwnership(env: Environment, contract: RegistryEntry): Promise { + const deployer = requireDeployer(env) + const governor = await getGovernor(env) + const client = graph.getPublicClient(env) as PublicClient + + // Get ProxyAdmin address from address book + const targetChainId = await getTargetChainIdFromEnv(env) + const ab = graph.getIssuanceAddressBook(targetChainId) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const entry = ab.getEntry(contract.name as any) + const proxyAdminAddress = entry?.proxyAdmin + + if (!proxyAdminAddress) { + throw new Error(`No proxyAdmin found in address book for ${contract.name}`) + } + + // Check current owner + const currentOwner = (await client.readContract({ + address: proxyAdminAddress as `0x${string}`, + abi: OZ_PROXY_ADMIN_ABI, + functionName: 'owner', + })) as string + + if (currentOwner.toLowerCase() === governor.toLowerCase()) { + env.showMessage(` ProxyAdmin ownership already transferred to governor: ${proxyAdminAddress}`) + return false + } + + if (currentOwner.toLowerCase() !== deployer.toLowerCase()) { + throw new Error( + `ProxyAdmin ${proxyAdminAddress} owned by ${currentOwner}, expected deployer ${deployer}. ` + + `Cannot transfer ownership.`, + ) + } + + // Transfer ownership to governor + env.showMessage(` Transferring ProxyAdmin ownership to governor...`) + env.showMessage(` ProxyAdmin: ${proxyAdminAddress}`) + env.showMessage(` From: ${deployer}`) + env.showMessage(` To: ${governor}`) + + const executeFn = execute(env) + await executeFn( + { address: proxyAdminAddress as `0x${string}`, abi: OZ_PROXY_ADMIN_ABI }, + { + account: deployer, + functionName: 'transferOwnership', + args: [governor as `0x${string}`], + }, + ) + + env.showMessage(` ✓ ProxyAdmin ownership transferred to governor`) + return true +} diff --git a/packages/deployment/lib/keystore-utils.ts b/packages/deployment/lib/keystore-utils.ts new file mode 100644 index 000000000..516175b34 --- /dev/null +++ b/packages/deployment/lib/keystore-utils.ts @@ -0,0 +1,49 @@ +import { configVariable } from 'hardhat/config' + +/** + * Convert network name to env var prefix: arbitrumSepolia → ARBITRUM_SEPOLIA + */ +export function networkToEnvPrefix(networkName: string): string { + return networkName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase() +} + +/** + * Resolve a configuration variable from environment. + * + * For deploy scripts that need config values at runtime (like API keys), + * keystore values must be exported to environment first: + * + * export ARBISCAN_API_KEY=$(npx hardhat keystore get ARBISCAN_API_KEY) + * + * Note: Deployer/governor keys in network config use configVariable() which + * Hardhat resolves automatically via the keystore plugin. This function is + * for runtime values that aren't part of network config. + * + * @param name - Configuration variable name (e.g., 'ARBISCAN_API_KEY') + * @returns The resolved value or undefined if not set + */ +export async function resolveConfigVar(name: string): Promise { + const envValue = process.env[name] + if (envValue) { + return envValue + } + return undefined +} + +/** + * Get deployer key name for a network. + * Always uses network-specific key (e.g., ARBITRUM_SEPOLIA_DEPLOYER_KEY). + */ +export function getDeployerKeyName(networkName: string): string { + const prefix = networkToEnvPrefix(networkName) + return `${prefix}_DEPLOYER_KEY` +} + +/** + * Get governor key name for a network. + * Always uses network-specific key (e.g., ARBITRUM_SEPOLIA_GOVERNOR_KEY). + */ +export function getGovernorKeyName(networkName: string): string { + const prefix = networkToEnvPrefix(networkName) + return `${prefix}_GOVERNOR_KEY` +} diff --git a/packages/deployment/lib/oz-proxy-verify.ts b/packages/deployment/lib/oz-proxy-verify.ts new file mode 100644 index 000000000..2e3b0f305 --- /dev/null +++ b/packages/deployment/lib/oz-proxy-verify.ts @@ -0,0 +1,247 @@ +import { readFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + +/** + * OpenZeppelin TransparentUpgradeableProxy verification utilities. + * + * OZ proxies are pre-compiled at a fixed Solidity version (0.8.27) that may not match + * the project config. This module provides direct Etherscan API verification using + * Standard JSON Input built from the installed OZ package sources. + * + * Uses Etherscan API V2 unified endpoint for all chains. + */ + +const require = createRequire(import.meta.url) + +/** Etherscan API V2 unified endpoint (for all chains) */ +const ETHERSCAN_API_V2_URL = 'https://api.etherscan.io/v2/api' + +/** Browser URLs for verified contract links */ +const ETHERSCAN_BROWSER_URLS: Record = { + 1: 'https://etherscan.io', + 42161: 'https://arbiscan.io', + 421614: 'https://sepolia.arbiscan.io', +} + +/** + * OZ TransparentUpgradeableProxy compiler settings (from OZ v5.4.0) + */ +const OZ_COMPILER_VERSION = 'v0.8.27+commit.40a35a09' +const OZ_COMPILER_SETTINGS = { + optimizer: { + enabled: true, + runs: 200, + }, + evmVersion: 'cancun', // Use cancun for broader compatibility (prague may not be supported) + outputSelection: { + '*': { + '*': ['abi', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'metadata'], + '': ['ast'], + }, + }, +} + +/** + * Source files required for TransparentUpgradeableProxy verification. + * Paths are relative to @openzeppelin/contracts package. + */ +const OZ_PROXY_SOURCE_FILES = [ + 'proxy/transparent/TransparentUpgradeableProxy.sol', + 'proxy/transparent/ProxyAdmin.sol', + 'proxy/ERC1967/ERC1967Proxy.sol', + 'proxy/ERC1967/ERC1967Utils.sol', + 'proxy/Proxy.sol', + 'proxy/beacon/IBeacon.sol', + 'interfaces/IERC1967.sol', + 'utils/Address.sol', + 'utils/Errors.sol', + 'utils/StorageSlot.sol', + 'access/Ownable.sol', + 'utils/Context.sol', +] + +/** + * Read an OZ contract source file from node_modules + */ +function readOZSource(relativePath: string): string { + const ozPackagePath = path.dirname(require.resolve('@openzeppelin/contracts/package.json')) + const fullPath = path.join(ozPackagePath, relativePath) + return readFileSync(fullPath, 'utf-8') +} + +/** + * Build Standard JSON Input for OZ TransparentUpgradeableProxy verification + */ +export function buildOZProxyStandardJsonInput(): string { + const sources: Record = {} + + for (const relativePath of OZ_PROXY_SOURCE_FILES) { + const sourcePath = `@openzeppelin/contracts/${relativePath}` + sources[sourcePath] = { + content: readOZSource(relativePath), + } + } + + const standardJson = { + language: 'Solidity', + sources, + settings: OZ_COMPILER_SETTINGS, + } + + return JSON.stringify(standardJson) +} + +/** + * Get Etherscan API V2 URL (unified endpoint for all chains) + */ +export function getApiUrl(): string { + return ETHERSCAN_API_V2_URL +} + +/** + * Get Etherscan browser URL for a chain + */ +export function getEtherscanBrowserUrl(chainId: number): string { + const url = ETHERSCAN_BROWSER_URLS[chainId] + if (!url) { + throw new Error(`No Etherscan browser URL configured for chainId ${chainId}`) + } + return url +} + +/** + * Check if a contract is already verified on Etherscan. + * + * Queries the getsourcecode API — a verified contract has a non-empty + * SourceCode field. Returns the explorer URL if verified, undefined otherwise. + */ +export async function checkEtherscanVerified( + address: string, + apiKey: string, + chainId: number, +): Promise { + const apiUrl = getApiUrl() + const browserUrl = getEtherscanBrowserUrl(chainId) + + const params = new URLSearchParams({ + module: 'contract', + action: 'getsourcecode', + address, + apikey: apiKey, + }) + + try { + const response = await fetch(`${apiUrl}?chainid=${chainId}&${params.toString()}`) + const data = (await response.json()) as { status: string; result: Array<{ SourceCode?: string }> } + if (data.status === '1' && data.result?.[0]?.SourceCode) { + return `${browserUrl}/address/${address}#code` + } + } catch { + // Network error — assume not verified, let the caller proceed + } + return undefined +} + +/** + * Verify OZ TransparentUpgradeableProxy via Etherscan API + * + * @param address - Proxy contract address + * @param constructorArgs - ABI-encoded constructor arguments (without 0x prefix is fine) + * @param apiKey - Etherscan API key + * @param chainId - Chain ID + * @returns Verification result with URL if successful + */ +export async function verifyOZProxy( + address: string, + constructorArgs: string, + apiKey: string, + chainId: number, +): Promise<{ success: boolean; url?: string; message?: string }> { + const apiUrl = getApiUrl() + const browserUrl = getEtherscanBrowserUrl(chainId) + + // Build standard JSON input from OZ sources + const sourceCode = buildOZProxyStandardJsonInput() + + // Strip 0x prefix from constructor args if present + const args = constructorArgs.startsWith('0x') ? constructorArgs.slice(2) : constructorArgs + + // Build params - V2 API requires chainid in URL query string, not POST body + const params = new URLSearchParams({ + apikey: apiKey, + module: 'contract', + action: 'verifysourcecode', + contractaddress: address, + sourceCode, + codeformat: 'solidity-standard-json-input', + contractname: + '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol:TransparentUpgradeableProxy', + compilerversion: OZ_COMPILER_VERSION, + constructorArguements: args, // Note: Etherscan API has this typo + }) + + console.log(` 📤 Submitting verification to Etherscan API V2 (chainId: ${chainId})`) + + // V2 API: chainid must be in URL query string + const submitUrl = `${apiUrl}?chainid=${chainId}` + const submitResponse = await fetch(submitUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params.toString(), + }) + + const submitResult = (await submitResponse.json()) as { status: string; result: string; message?: string } + + if (submitResult.status !== '1') { + // Check if already verified (case-insensitive, handles various API response formats) + if (submitResult.result?.toLowerCase().includes('already verified')) { + const url = `${browserUrl}/address/${address}#code` + return { success: true, url, message: 'Already verified' } + } + return { success: false, message: submitResult.result || submitResult.message || 'Unknown error' } + } + + const guid = submitResult.result + console.log(` ⏳ Verification submitted, GUID: ${guid}`) + + // Poll for verification result + const maxAttempts = 10 + const pollInterval = 3000 // 3 seconds + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + await new Promise((resolve) => setTimeout(resolve, pollInterval)) + + const checkParams = new URLSearchParams({ + apikey: apiKey, + module: 'contract', + action: 'checkverifystatus', + guid, + }) + + // V2 API: chainid must be in URL query string + const checkResponse = await fetch(`${apiUrl}?chainid=${chainId}&${checkParams.toString()}`) + const checkResult = (await checkResponse.json()) as { status: string; result: string } + + if (checkResult.result === 'Pending in queue') { + console.log(` ⏳ Verification pending (attempt ${attempt + 1}/${maxAttempts})...`) + continue + } + + if (checkResult.status === '1' || checkResult.result === 'Pass - Verified') { + const url = `${browserUrl}/address/${address}#code` + return { success: true, url } + } + + // "Already Verified" can appear during polling (not just at submission) + if (checkResult.result?.toLowerCase().includes('already verified')) { + const url = `${browserUrl}/address/${address}#code` + return { success: true, url, message: 'Already verified' } + } + + // Verification failed + return { success: false, message: checkResult.result } + } + + return { success: false, message: 'Verification timed out' } +} diff --git a/packages/deployment/lib/preconditions.ts b/packages/deployment/lib/preconditions.ts new file mode 100644 index 000000000..8f000597a --- /dev/null +++ b/packages/deployment/lib/preconditions.ts @@ -0,0 +1,380 @@ +/** + * Shared Precondition Checks + * + * Each function answers "is this action step done?" for a specific component. + * Used by BOTH action scripts (to skip if done) and status scripts (for next-step hints). + * + * This is the SINGLE SOURCE OF TRUTH for precondition logic. + * Action scripts and status scripts must call the same functions — no copies. + * + * Configure checks: params, integration references, and role GRANTS (PAUSE_ROLE, GOVERNOR_ROLE) + * Transfer checks: deployer GOVERNOR_ROLE REVOKE + ProxyAdmin ownership + */ + +import type { PublicClient } from 'viem' +import { keccak256, toHex } from 'viem' + +import { + ACCESS_CONTROL_ENUMERABLE_ABI, + ISSUANCE_ALLOCATOR_ABI, + ISSUANCE_TARGET_ABI, + OZ_PROXY_ADMIN_ABI, + REWARDS_MANAGER_ABI, + REWARDS_MANAGER_DEPRECATED_ABI, +} from './abis.js' + +// ============================================================================ +// Result type +// ============================================================================ + +/** + * Result of a precondition check + * + * @property done - true if the action step is complete (on-chain state matches target) + * @property reason - why not done (human-readable, for status display) + */ +export interface PreconditionResult { + done: boolean + reason?: string +} + +// ============================================================================ +// Helpers +// ============================================================================ + +// Precomputed role hashes (matches BaseUpgradeable constants) +const GOVERNOR_ROLE = keccak256(toHex('GOVERNOR_ROLE')) +const PAUSE_ROLE = keccak256(toHex('PAUSE_ROLE')) + +/** Check if account has a role on a contract */ +async function hasRole( + client: PublicClient, + contractAddress: string, + role: `0x${string}`, + account: string, +): Promise { + return (await client.readContract({ + address: contractAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [role, account as `0x${string}`], + })) as boolean +} + +/** + * Check role grants common to all deployer-initialized contracts + * + * Configure must grant: + * - GOVERNOR_ROLE to protocol governor + * - PAUSE_ROLE to pause guardian + */ +async function checkRoleGrants( + client: PublicClient, + contractAddress: string, + governor: string, + pauseGuardian: string, +): Promise<{ governorOk: boolean; pauseOk: boolean; reasons: string[] }> { + const governorOk = await hasRole(client, contractAddress, GOVERNOR_ROLE, governor) + const pauseOk = await hasRole(client, contractAddress, PAUSE_ROLE, pauseGuardian) + + const reasons: string[] = [] + if (!governorOk) reasons.push('governor missing GOVERNOR_ROLE') + if (!pauseOk) reasons.push('pauseGuardian missing PAUSE_ROLE') + + return { governorOk, pauseOk, reasons } +} + +// ============================================================================ +// Configure checks +// ============================================================================ + +/** + * Check if IssuanceAllocator is configured + * + * Matches the skip logic in allocate/allocator/04_configure.ts: + * - RM.issuancePerBlock must be > 0 (RM initialized) + * - IA.getIssuancePerBlock() must equal RM rate + * - governor has GOVERNOR_ROLE + * - pauseGuardian has PAUSE_ROLE + * + * Note: RM target allocation (setTargetAllocation) is an activation step + * in issuance-connect, not a configure step. + */ +export async function checkIAConfigured( + client: PublicClient, + iaAddress: string, + rmAddress: string, + governor: string, + pauseGuardian: string, +): Promise { + // Check RM issuance rate + const rmIssuanceRate = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: REWARDS_MANAGER_DEPRECATED_ABI, + functionName: 'issuancePerBlock', + })) as bigint + + if (rmIssuanceRate === 0n) { + return { done: false, reason: 'RM.issuancePerBlock is 0' } + } + + // Check IA rate matches RM + const iaIssuanceRate = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getIssuancePerBlock', + })) as bigint + + const rateOk = iaIssuanceRate === rmIssuanceRate && iaIssuanceRate > 0n + + // Check role grants + const roles = await checkRoleGrants(client, iaAddress, governor, pauseGuardian) + + if (rateOk && roles.governorOk && roles.pauseOk) { + return { done: true } + } + + const reasons: string[] = [] + if (!rateOk) reasons.push('rate mismatch') + reasons.push(...roles.reasons) + return { done: false, reason: reasons.join(', ') } +} + +/** + * Check if RecurringAgreementManager is configured + * + * Matches the skip logic in agreement/manager/04_configure.ts: + * - RC has COLLECTOR_ROLE + * - SS has DATA_SERVICE_ROLE + * - RAM.getIssuanceAllocator() == IA + * - governor has GOVERNOR_ROLE + * - pauseGuardian has PAUSE_ROLE + */ +export async function checkRAMConfigured( + client: PublicClient, + ramAddress: string, + rcAddress: string, + ssAddress: string, + iaAddress: string, + governor: string, + pauseGuardian: string, +): Promise { + const COLLECTOR_ROLE = keccak256(toHex('COLLECTOR_ROLE')) + const DATA_SERVICE_ROLE = keccak256(toHex('DATA_SERVICE_ROLE')) + + const rcHasCollectorRole = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [COLLECTOR_ROLE, rcAddress as `0x${string}`], + })) as boolean + + const ssHasDataServiceRole = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [DATA_SERVICE_ROLE, ssAddress as `0x${string}`], + })) as boolean + + let iaConfigured = false + try { + const currentIA = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: ISSUANCE_TARGET_ABI, + functionName: 'getIssuanceAllocator', + })) as string + iaConfigured = currentIA.toLowerCase() === iaAddress.toLowerCase() + } catch { + // Not set + } + + // Check role grants + const roles = await checkRoleGrants(client, ramAddress, governor, pauseGuardian) + + if (rcHasCollectorRole && ssHasDataServiceRole && iaConfigured && roles.governorOk && roles.pauseOk) { + return { done: true } + } + + const reasons: string[] = [] + if (!rcHasCollectorRole) reasons.push('RC missing COLLECTOR_ROLE') + if (!ssHasDataServiceRole) reasons.push('SS missing DATA_SERVICE_ROLE') + if (!iaConfigured) reasons.push('IssuanceAllocator not set') + reasons.push(...roles.reasons) + return { done: false, reason: reasons.join(', ') } +} + +/** + * Check Reclaim role grants only (governor has GOVERNOR_ROLE, pauseGuardian has PAUSE_ROLE) + * + * Use this when you need to know whether the deployer (with Reclaim GOVERNOR_ROLE) can + * fix the issue. The RM integration is governance-only and should be checked separately + * via checkReclaimRMIntegration. + */ +export async function checkReclaimRoles( + client: PublicClient, + reclaimAddress: string, + governor: string, + pauseGuardian: string, +): Promise { + const roles = await checkRoleGrants(client, reclaimAddress, governor, pauseGuardian) + if (roles.governorOk && roles.pauseOk) { + return { done: true } + } + return { done: false, reason: roles.reasons.join(', ') } +} + +/** + * Check RM integration with Reclaim: RM.getDefaultReclaimAddress() == reclaim address + * + * This is governance-only — only an account with GOVERNOR_ROLE on RM can fix it, + * which the deployer never has. Status logic should always treat a failure here + * as deferred (governance TX), not blocking on configure. + */ +export async function checkReclaimRMIntegration( + client: PublicClient, + rmAddress: string, + reclaimAddress: string, +): Promise { + try { + const currentDefault = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: REWARDS_MANAGER_ABI, + functionName: 'getDefaultReclaimAddress', + })) as string + + if (currentDefault.toLowerCase() === reclaimAddress.toLowerCase()) { + return { done: true } + } + return { done: false, reason: 'default reclaim address not set' } + } catch { + // Function not available — RM not upgraded + return { done: false, reason: 'RM not upgraded' } + } +} + +/** + * Check whether RM.getRevertOnIneligible() matches the desired value from config. + * + * Governance-only setter on RM — failure is deferred to the upgrade governance batch + * unless the deployer holds GOVERNOR_ROLE on RM (true on fresh networks where RM is + * deployed from scratch with the deployer as initial governor; false on networks + * where RM was deployed by separate horizon-Ignition infrastructure). + */ +export async function checkRMRevertOnIneligible( + client: PublicClient, + rmAddress: string, + desired: boolean, +): Promise { + try { + const onChain = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: REWARDS_MANAGER_ABI, + functionName: 'getRevertOnIneligible', + })) as boolean + if (onChain === desired) return { done: true } + return { done: false, reason: `revertOnIneligible=${onChain}, expected ${desired}` } + } catch { + return { done: false, reason: 'RM not upgraded' } + } +} + +/** + * Check if ReclaimedRewards is fully configured (roles + RM integration) + * + * Convenience wrapper that combines checkReclaimRoles and checkReclaimRMIntegration. + * Use the split functions when callers need to distinguish deployer-fixable role + * issues from governance-only RM integration issues. + */ +export async function checkReclaimConfigured( + client: PublicClient, + rmAddress: string, + reclaimAddress: string, + governor: string, + pauseGuardian: string, +): Promise { + const roles = await checkReclaimRoles(client, reclaimAddress, governor, pauseGuardian) + const rmIntegration = await checkReclaimRMIntegration(client, rmAddress, reclaimAddress) + + if (roles.done && rmIntegration.done) { + return { done: true } + } + + // If roles are done but RM not upgraded, report that specifically + if (roles.done && rmIntegration.reason === 'RM not upgraded') { + return { done: false, reason: 'RM not upgraded' } + } + + const reasons: string[] = [] + if (!roles.done && roles.reason) reasons.push(roles.reason) + if (!rmIntegration.done && rmIntegration.reason) reasons.push(rmIntegration.reason) + return { done: false, reason: reasons.join(', ') } +} + +/** + * Check if DefaultAllocation is configured + * + * - governor has GOVERNOR_ROLE on DefaultAllocation + * - pauseGuardian has PAUSE_ROLE on DefaultAllocation + * + * Note: IA.setDefaultTarget(DA) is an activation step in issuance-connect. + */ +export async function checkDefaultAllocationConfigured( + client: PublicClient, + daAddress: string, + governor: string, + pauseGuardian: string, +): Promise { + const roles = await checkRoleGrants(client, daAddress, governor, pauseGuardian) + + if (roles.governorOk && roles.pauseOk) { + return { done: true } + } + + return { done: false, reason: roles.reasons.join(', ') } +} + +// ============================================================================ +// Transfer checks +// ============================================================================ + +/** + * Check if deployer GOVERNOR_ROLE is revoked on a contract + * + * Transfer = revoke deployer access. Role grants happen in configure. + * Generic check used for IA, RAM, Reclaim. + */ +export async function checkDeployerRevoked( + client: PublicClient, + contractAddress: string, + deployer: string, +): Promise { + const deployerHasRole = await hasRole(client, contractAddress, GOVERNOR_ROLE, deployer) + + if (!deployerHasRole) { + return { done: true } + } + return { done: false, reason: 'deployer GOVERNOR_ROLE not revoked' } +} + +/** + * Check if ProxyAdmin ownership is transferred to governor + * + * Generic check used for any contract with an OZ v5 per-proxy ProxyAdmin. + * Used by transfer scripts for IA, RAM, Reclaim, REO. + */ +export async function checkProxyAdminTransferred( + client: PublicClient, + proxyAdminAddress: string, + governor: string, +): Promise { + const currentOwner = (await client.readContract({ + address: proxyAdminAddress as `0x${string}`, + abi: OZ_PROXY_ADMIN_ABI, + functionName: 'owner', + })) as string + + if (currentOwner.toLowerCase() === governor.toLowerCase()) { + return { done: true } + } + return { done: false, reason: `ProxyAdmin owned by ${currentOwner}, not governor` } +} diff --git a/packages/deployment/lib/script-factories.ts b/packages/deployment/lib/script-factories.ts new file mode 100644 index 000000000..6c1bb1de5 --- /dev/null +++ b/packages/deployment/lib/script-factories.ts @@ -0,0 +1,384 @@ +/** + * Deploy Script Factories - Create deployment modules with standard framework plumbing + * + * Two flavors: + * + * **Contract-based** (component lifecycle): + * Derive tags from registry componentTag. Action-verb skip gating. + * Post-action sync. Use for standard deploy/upgrade/configure/transfer steps. + * + * **Tag-based** (goals, multi-contract status, standalone actions): + * Accept a tag string directly. Skip when no --tags specified. + * Custom execute callback handles all logic. + * + * Skip gating uses func.skip (checked by rocketh's executor via patch) + * with early returns as a safety net. + */ + +import type { DeployScriptModule, Environment } from '@rocketh/core/types' + +import type { RegistryEntry } from './contract-registry.js' +import { deployImplementation, getImplementationConfig } from './deploy-implementation.js' +import { DeploymentActions, noTagsRequested, shouldSkipAction } from './deployment-tags.js' +import { requireUpgradeExecuted } from './execute-governance.js' +import { deployProxyContract } from './issuance-deploy-utils.js' +import { showDetailedComponentStatus } from './status-detail.js' +import { syncComponentFromRegistry, syncComponentsFromRegistry } from './sync-utils.js' +import type { ImplementationUpgradeOverrides } from './upgrade-implementation.js' +import { upgradeImplementation } from './upgrade-implementation.js' + +/** + * Require that the registry entry has a componentTag, throwing a clear error if not. + */ +function requireComponentTag(contract: RegistryEntry): string { + if (!contract.componentTag) { + throw new Error( + `Contract '${contract.name}' has no componentTag in the registry. ` + + `Add a componentTag to use script factories.`, + ) + } + return contract.componentTag +} + +/** + * Create a standard upgrade deploy script module. + * + * Generates a governance TX to upgrade the contract's proxy to its pending implementation. + * Tags and dependencies are derived from the contract's componentTag. + * + * @example Standard single-contract upgrade: + * ```typescript + * import { Contracts } from '@graphprotocol/deployment/lib/contract-registry.js' + * import { createUpgradeModule } from '@graphprotocol/deployment/lib/script-factories.js' + * + * export default createUpgradeModule(Contracts.horizon.PaymentsEscrow) + * ``` + * + * @example Upgrade with implementation name override: + * ```typescript + * export default createUpgradeModule(Contracts.issuance.SomeProxy, { + * overrides: { implementationName: 'DifferentImpl' }, + * }) + * ``` + */ +export function createUpgradeModule( + contract: RegistryEntry, + options?: { + overrides?: ImplementationUpgradeOverrides + extraDependencies?: string[] + /** Additional contracts to sync alongside `contract` before the upgrade runs. */ + prerequisites?: RegistryEntry[] + }, +): DeployScriptModule { + const tag = requireComponentTag(contract) + + const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.UPGRADE)) return + await syncComponentsFromRegistry(env, [contract, ...(options?.prerequisites ?? [])]) + await upgradeImplementation(env, contract, options?.overrides) + await syncComponentFromRegistry(env, contract) + } + + func.tags = [tag] + func.dependencies = options?.extraDependencies ?? [] + func.skip = async () => shouldSkipAction(DeploymentActions.UPGRADE) + + return func +} + +/** + * Create a standard end/complete deploy script module. + * + * Gates on `--tags ...,all`. Verifies the upgrade governance TX has been + * executed and shows a ready message. The actual lifecycle actions a component + * needs are encoded in its dependency chain via the component tag, not in this + * factory. + * + * @example + * ```typescript + * export default createEndModule(Contracts.horizon.PaymentsEscrow) + * ``` + */ +export function createEndModule(contract: RegistryEntry): DeployScriptModule { + const tag = requireComponentTag(contract) + + const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.ALL)) return + requireUpgradeExecuted(env, contract.name) + env.showMessage(`\n✓ ${contract.name} ready`) + } + + func.tags = [tag] + func.dependencies = [] + func.skip = async () => shouldSkipAction(DeploymentActions.ALL) + + return func +} + +/** + * Create a status deploy script module. + * + * Syncs the component with on-chain state and shows its current status. + * Tagged with the bare component name so `--tags IssuanceAllocator` is a + * safe, read-only operation. + * + * @example Single contract (default status display): + * ```typescript + * export default createStatusModule(Contracts.horizon.PaymentsEscrow) + * ``` + * + * @example Custom status with tag (multi-contract or cross-component): + * ```typescript + * export default createStatusModule(GoalTags.GIP_0088, async (env) => { + * // custom multi-phase status display + * }) + * ``` + */ +export function createStatusModule(contract: RegistryEntry): DeployScriptModule +export function createStatusModule(tag: string, execute: (env: Environment) => Promise): DeployScriptModule +export function createStatusModule( + contractOrTag: RegistryEntry | string, + execute?: (env: Environment) => Promise, +): DeployScriptModule { + const tag = typeof contractOrTag === 'string' ? contractOrTag : requireComponentTag(contractOrTag) + + const func: DeployScriptModule = async (env) => { + if (noTagsRequested()) return + if (execute) { + await execute(env) + } else { + await showDetailedComponentStatus(env, contractOrTag as RegistryEntry) + } + } + + func.tags = [tag] + func.dependencies = [] + func.skip = async () => noTagsRequested() + + return func +} + +// ============================================================================ +// Action Factories (custom logic with standard framework plumbing) +// ============================================================================ + +/** + * Create a deploy script module for a custom action. + * + * Two forms: + * + * **Contract-based** (component lifecycle steps): + * Uses action verb gating (`shouldSkipAction`) and post-action sync. + * Requires both component tag AND action verb in `--tags`. + * + * **Tag-based** (goal scripts, standalone actions): + * Uses tag gating (`noTagsRequested`). The tag in `--tags` is sufficient. + * No post-action sync — the execute callback handles everything. + * + * @example Contract-based configure: + * ```typescript + * export default createActionModule( + * Contracts.horizon.RecurringCollector, + * DeploymentActions.CONFIGURE, + * async (env) => { ... }, + * ) + * ``` + * + * @example Tag-based goal action: + * ```typescript + * export default createActionModule( + * GoalTags.GIP_0088_ISSUANCE_CONNECT, + * async (env) => { ... }, + * { dependencies: [ComponentTags.ISSUANCE_ALLOCATOR] }, + * ) + * ``` + */ +export function createActionModule( + contract: RegistryEntry, + action: (typeof DeploymentActions)[keyof typeof DeploymentActions], + execute: (env: Environment) => Promise, + options?: { extraDependencies?: string[]; prerequisites?: RegistryEntry[] }, +): DeployScriptModule +export function createActionModule( + tag: string, + execute: (env: Environment) => Promise, + options?: { dependencies?: string[] }, +): DeployScriptModule +export function createActionModule( + contractOrTag: RegistryEntry | string, + actionOrExecute: (typeof DeploymentActions)[keyof typeof DeploymentActions] | ((env: Environment) => Promise), + executeOrOptions?: ((env: Environment) => Promise) | { dependencies?: string[] }, + maybeOptions?: { extraDependencies?: string[]; prerequisites?: RegistryEntry[] }, +): DeployScriptModule { + if (typeof contractOrTag === 'string') { + // Tag-based: (tag, execute, options?) + const tag = contractOrTag + const execute = actionOrExecute as (env: Environment) => Promise + const options = executeOrOptions as { dependencies?: string[] } | undefined + + const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(tag)) return + await execute(env) + } + + func.tags = [tag] + func.dependencies = options?.dependencies ?? [] + func.skip = async () => shouldSkipAction(tag) + + return func + } + + // Contract-based: (contract, action, execute, options?) + const tag = requireComponentTag(contractOrTag) + const action = actionOrExecute as string + const execute = executeOrOptions as (env: Environment) => Promise + + const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(action)) return + await syncComponentsFromRegistry(env, [contractOrTag, ...(maybeOptions?.prerequisites ?? [])]) + await execute(env) + await syncComponentFromRegistry(env, contractOrTag) + } + + func.tags = [tag] + func.dependencies = maybeOptions?.extraDependencies ?? [] + func.skip = async () => shouldSkipAction(action) + + return func +} + +// ============================================================================ +// Deploy Factories +// ============================================================================ + +/** + * Options shared by deploy factories + */ +interface DeployModuleOptions { + /** Additional tags beyond the derived deploy action tag */ + extraTags?: string[] + /** Additional rocketh dependency tags */ + extraDependencies?: string[] + /** + * Additional registry entries to sync immediately before the action runs. + * Use for contracts read via `env.getOrNull(...)` inside `resolveArgs` / + * `resolveConstructorArgs` (e.g. Controller, shared implementations). + */ + prerequisites?: RegistryEntry[] +} + +/** + * Create a deploy module for prerequisite contracts (existing proxy, new implementation). + * + * Uses `deployImplementation` + `getImplementationConfig` to deploy a new implementation + * and store it as pendingImplementation for governance upgrade. + * + * @param contract - Registry entry (must have prerequisite: true, artifact, proxyType) + * @param resolveConstructorArgs - Optional callback to resolve constructor args from env. + * Called with the deployment environment. Return the args array. + * Omit for contracts with no constructor args (e.g., RewardsManager). + * + * @example No constructor args: + * ```typescript + * export default createImplementationDeployModule(Contracts.horizon.RewardsManager) + * ``` + * + * @example With synced dependency args: + * ```typescript + * export default createImplementationDeployModule( + * Contracts['subgraph-service'].DisputeManager, + * (env) => { + * const controller = env.getOrNull('Controller') + * if (!controller) throw new Error('Missing Controller') + * return [controller.address] + * }, + * ) + * ``` + */ +export function createImplementationDeployModule( + contract: RegistryEntry, + resolveConstructorArgs?: (env: Environment) => Promise | unknown[], + options?: DeployModuleOptions, +): DeployScriptModule { + const tag = requireComponentTag(contract) + + const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.DEPLOY)) return + await syncComponentsFromRegistry(env, [contract, ...(options?.prerequisites ?? [])]) + const constructorArgs = resolveConstructorArgs ? await resolveConstructorArgs(env) : undefined + await deployImplementation( + env, + getImplementationConfig(contract.addressBook, contract.name, constructorArgs ? { constructorArgs } : undefined), + ) + await syncComponentFromRegistry(env, contract) + } + + func.tags = [tag, ...(options?.extraTags ?? [])] + func.dependencies = options?.extraDependencies ?? [] + func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) + + return func +} + +/** + * Create a deploy module for new contracts (fresh proxy + implementation). + * + * Uses `deployProxyContract` to deploy an OZ v5 TransparentUpgradeableProxy with + * atomic initialization. On subsequent runs, deploys new implementation and stores + * as pendingImplementation. + * + * @param contract - Registry entry (must have deployable: true, artifact, proxyType) + * @param resolveArgs - Optional callback to resolve constructor and initialize args. + * Omit initializeArgs to use default [governor]. + * + * @example With graphToken constructor and deployer init: + * ```typescript + * export default createProxyDeployModule( + * Contracts.issuance.RewardsEligibilityOracleA, + * (env) => ({ + * constructorArgs: [requireGraphToken(env).address], + * initializeArgs: [requireDeployer(env)], + * }), + * ) + * ``` + * + * @example With default initialize args [governor]: + * ```typescript + * export default createProxyDeployModule( + * Contracts.issuance.RecurringAgreementManager, + * (env) => ({ + * constructorArgs: [requireGraphToken(env).address, paymentsEscrow.address], + * }), + * ) + * ``` + */ +export function createProxyDeployModule( + contract: RegistryEntry, + resolveArgs?: (env: Environment) => Promise | ProxyDeployArgs, + options?: DeployModuleOptions, +): DeployScriptModule { + const tag = requireComponentTag(contract) + + const func: DeployScriptModule = async (env) => { + if (shouldSkipAction(DeploymentActions.DEPLOY)) return + await syncComponentsFromRegistry(env, [contract, ...(options?.prerequisites ?? [])]) + const args = resolveArgs ? await resolveArgs(env) : {} + await deployProxyContract(env, { + contract, + constructorArgs: args.constructorArgs, + initializeArgs: args.initializeArgs, + }) + await syncComponentFromRegistry(env, contract) + } + + func.tags = [tag, ...(options?.extraTags ?? [])] + func.dependencies = options?.extraDependencies ?? [] + func.skip = async () => shouldSkipAction(DeploymentActions.DEPLOY) + + return func +} + +interface ProxyDeployArgs { + constructorArgs?: unknown[] + initializeArgs?: unknown[] +} diff --git a/packages/deployment/lib/status-detail.ts b/packages/deployment/lib/status-detail.ts new file mode 100644 index 000000000..a9a4cede8 --- /dev/null +++ b/packages/deployment/lib/status-detail.ts @@ -0,0 +1,1135 @@ +/** + * Status Detail - Detailed contract status with integration checks + * + * Extracted from deployment-status task so deploy scripts (10_status.ts) + * can show the same detail view. The task delegates to these functions. + */ + +import type { Environment } from '@rocketh/core/types' +import type { PublicClient } from 'viem' + +import { + ACCESS_CONTROL_ENUMERABLE_ABI, + CONTROLLER_ABI, + IISSUANCE_TARGET_INTERFACE_ID, + IREWARDS_MANAGER_INTERFACE_ID, + ISSUANCE_ALLOCATOR_ABI, + ISSUANCE_TARGET_ABI, + PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + REWARDS_ELIGIBILITY_ORACLE_ABI, + REWARDS_MANAGER_ABI, +} from './abis.js' +import type { AddressBookOps } from './address-book-ops.js' +import { getAddressBookForType, getTargetChainIdFromEnv } from './address-book-utils.js' +import { + checkIssuanceAllocatorActivation, + checkOperatorRole, + formatAddress, + supportsInterface, +} from './contract-checks.js' +import type { RegistryEntry } from './contract-registry.js' +import { getResolvedSettings } from './deployment-config.js' +import { countPendingGovernanceTxs } from './execute-governance.js' +import { formatGRT } from './format.js' +import { getContractStatusLine, type ContractStatusResult, type ProxyAdminOwnershipContext } from './sync-utils.js' +import { graph } from '../rocketh/deploy.js' + +// ============================================================================ +// Integration Check Types & Helpers +// ============================================================================ + +/** Integration check result */ +export interface IntegrationCheck { + ok: boolean | null // null = not applicable / not deployed + label: string +} + +function formatCheck(check: IntegrationCheck): string { + const icon = check.ok === null ? '○' : check.ok ? '✓' : '✗' + return ` ${icon} ${check.label}` +} + +function formatWarnings(warnings: string[] | undefined): string[] { + if (!warnings) return [] + return warnings.map((w) => ` ⚠ ${w}`) +} + +/** Format proxy admin detail lines */ +function formatProxyAdminDetail(result: ContractStatusResult): string[] { + if (!result.proxyAdminAddress) return [] + const lines: string[] = [] + const ownerIcon = result.proxyAdminOwner === 'governor' ? '✓' : result.proxyAdminOwner === 'unknown' ? '○' : '⚠' + const ownerRole = + result.proxyAdminOwner === 'governor' + ? 'governor' + : result.proxyAdminOwner === 'deployer' + ? 'deployer' + : result.proxyAdminOwner === 'other' + ? 'not governor' + : 'unknown' + const ownerAddr = result.proxyAdminOwnerAddress ? ` ${result.proxyAdminOwnerAddress}` : '' + lines.push(` ProxyAdmin: ${result.proxyAdminAddress}`) + lines.push(` ${ownerIcon} ProxyAdmin owner:${ownerAddr} (${ownerRole})`) + return lines +} + +// ============================================================================ +// Ownership Context Resolution +// ============================================================================ + +/** + * Resolve governor/deployer context for proxy admin ownership checks + */ +export async function resolveOwnershipContext( + client: PublicClient, + env: Environment, + chainId: number, +): Promise { + const horizonAddressBook = graph.getHorizonAddressBook(chainId) + try { + const controllerAddress = horizonAddressBook.entryExists('Controller') + ? horizonAddressBook.getEntry('Controller')?.address + : null + if (!controllerAddress) return undefined + + const governor = (await client.readContract({ + address: controllerAddress as `0x${string}`, + abi: CONTROLLER_ABI, + functionName: 'getGovernor', + })) as string + + if (!governor) return undefined + + // Deployer is best-effort: available when provider has accounts (fork/local) + let deployer: string | undefined + try { + const accounts = (await env.network.provider.request({ method: 'eth_accounts' })) as string[] | undefined + if (accounts && accounts.length > 0) { + deployer = accounts[0] + } + } catch { + // No accounts available (read-only provider) + } + + return { governor, deployer } + } catch { + return undefined + } +} + +// ============================================================================ +// Integration Check Functions +// ============================================================================ + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +export async function getRewardsManagerChecks( + client: PublicClient, + horizonBook: AddressBookOps, + chainId: number, + issuanceBook?: AddressBookOps, + ssBook?: AddressBookOps, +): Promise { + const checks: IntegrationCheck[] = [] + const rmAddress = horizonBook.entryExists('RewardsManager') ? horizonBook.getEntry('RewardsManager')?.address : null + + if (!rmAddress) return checks + + // Interface support + const supportsRewardsManager = await supportsInterface(client, rmAddress, IREWARDS_MANAGER_INTERFACE_ID) + checks.push({ ok: supportsRewardsManager, label: `implements IRewardsManager (${IREWARDS_MANAGER_INTERFACE_ID})` }) + + const supportsIssuanceTarget = await supportsInterface(client, rmAddress, IISSUANCE_TARGET_INTERFACE_ID) + checks.push({ ok: supportsIssuanceTarget, label: `implements IIssuanceTarget (${IISSUANCE_TARGET_INTERFACE_ID})` }) + + if (!supportsRewardsManager) return checks + + // Helper: read a contract value, returning null on failure + async function rmRead(functionName: string, abi: readonly unknown[] = REWARDS_MANAGER_ABI): Promise { + try { + return (await client.readContract({ + address: rmAddress as `0x${string}`, + abi, + functionName, + })) as T + } catch { + return null + } + } + + // Issuance rates + const rawRate = await rmRead('getRawIssuancePerBlock') + const allocatedRate = await rmRead('getAllocatedIssuancePerBlock') + if (rawRate !== null) { + checks.push({ ok: rawRate > 0n, label: `issuancePerBlock: ${formatGRT(rawRate)} (raw)` }) + } + if (allocatedRate !== null) { + checks.push({ + ok: allocatedRate > 0n, + label: `issuancePerBlock: ${formatGRT(allocatedRate)} (after IA allocation)`, + }) + } + + // SubgraphService + const ss = await rmRead('subgraphService') + if (ss !== null) { + const expected = ssBook?.entryExists('SubgraphService') + ? (ssBook.getEntry('SubgraphService')?.address ?? null) + : null + const matches = expected ? ss.toLowerCase() === expected.toLowerCase() : null + checks.push({ + ok: ss !== ZERO_ADDRESS ? matches : false, + label: `subgraphService: ${ss}${matches === false && expected ? ` (expected ${expected})` : ''}`, + }) + } + + // IssuanceAllocator + const ia = await rmRead('getIssuanceAllocator', ISSUANCE_TARGET_ABI) + if (ia !== null) { + const iaBook = issuanceBook?.entryExists('IssuanceAllocator') + ? issuanceBook.getEntry('IssuanceAllocator')?.address + : null + const isSet = ia !== ZERO_ADDRESS + const matches = iaBook ? ia.toLowerCase() === iaBook.toLowerCase() : null + checks.push({ + ok: isSet ? matches : null, + label: isSet + ? `issuanceAllocator: ${ia}${matches === false ? ` (expected ${iaBook!})` : ''}` + : 'issuanceAllocator: not set', + }) + } + + // Provider eligibility oracle + const reo = await rmRead('getProviderEligibilityOracle', PROVIDER_ELIGIBILITY_MANAGEMENT_ABI) + if (reo !== null) { + const reoA = issuanceBook?.entryExists('RewardsEligibilityOracleA') + ? issuanceBook.getEntry('RewardsEligibilityOracleA')?.address + : null + const isSet = reo !== ZERO_ADDRESS + const matchesA = reoA ? reo.toLowerCase() === reoA.toLowerCase() : null + checks.push({ + ok: isSet ? matchesA : null, + label: isSet + ? `providerEligibilityOracle: ${reo}${matchesA === false ? ' (not REO-A)' : matchesA ? ' (REO-A)' : ''}` + : 'providerEligibilityOracle: not set', + }) + } else { + checks.push({ ok: null, label: 'providerEligibilityOracle: not set' }) + } + + // Revert on ineligible — compare against resolved settings + const revertOnIneligible = await rmRead('getRevertOnIneligible') + if (revertOnIneligible !== null) { + const desired = getResolvedSettings(chainId).rewardsManager.revertOnIneligible + const matches = revertOnIneligible === desired + checks.push({ + ok: matches, + label: `revertOnIneligible: ${revertOnIneligible}${matches ? '' : ` (expected ${desired})`}`, + }) + } + + // Default reclaim address + const defaultReclaim = await rmRead('getDefaultReclaimAddress') + if (defaultReclaim !== null) { + const expectedAddr = issuanceBook?.entryExists('ReclaimedRewards') + ? issuanceBook.getEntry('ReclaimedRewards')?.address + : null + const isSet = defaultReclaim !== ZERO_ADDRESS + const matches = isSet && expectedAddr ? defaultReclaim.toLowerCase() === expectedAddr.toLowerCase() : null + checks.push({ + ok: isSet ? (matches ?? true) : null, + label: isSet + ? `defaultReclaimAddress: ${defaultReclaim}${matches === false ? ` (expected ${expectedAddr!})` : ''}` + : 'defaultReclaimAddress: not set', + }) + } + + return checks +} + +export async function getIssuanceAllocatorChecks( + client: PublicClient, + horizonBook: AddressBookOps, + issuanceBook: AddressBookOps, +): Promise { + const checks: IntegrationCheck[] = [] + + const iaAddress = issuanceBook.entryExists('IssuanceAllocator') + ? issuanceBook.getEntry('IssuanceAllocator')?.address + : null + const rmAddress = horizonBook.entryExists('RewardsManager') ? horizonBook.getEntry('RewardsManager')?.address : null + const gtAddress = horizonBook.entryExists('L2GraphToken') ? horizonBook.getEntry('L2GraphToken')?.address : null + + if (!iaAddress || !rmAddress || !gtAddress) return checks + + const rmSupportsTarget = await supportsInterface(client, rmAddress, IISSUANCE_TARGET_INTERFACE_ID) + checks.push({ ok: rmSupportsTarget, label: `RM implements IIssuanceTarget (${IISSUANCE_TARGET_INTERFACE_ID})` }) + + if (rmSupportsTarget) { + const activation = await checkIssuanceAllocatorActivation(client, iaAddress, rmAddress, gtAddress) + checks.push({ ok: activation.iaIntegrated, label: 'RM.issuanceAllocator == this' }) + checks.push({ ok: activation.iaMinter, label: 'GraphToken.MINTER_ROLE granted' }) + } else { + checks.push({ ok: null, label: 'RM.issuanceAllocator == this (RM not upgraded)' }) + checks.push({ ok: null, label: 'GraphToken.MINTER_ROLE granted (RM not upgraded)' }) + } + + try { + const targetCount = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getTargetCount', + })) as bigint + const hasDefaultTarget = targetCount > 0n + checks.push({ ok: hasDefaultTarget, label: 'defaultTarget configured' }) + } catch { + // Function not available + } + + // Confirm 100% allocation: getTotalAllocation().totalAllocationRate == issuancePerBlock. + // Once a real defaultTarget is set (issuance-connect), the contract reports + // exactly issuancePerBlock; if it doesn't, the default is still address(0) + // and some issuance is unallocated (not minted). Skipped (○) when + // issuancePerBlock is 0 — the IA hasn't been configured with a rate yet, + // so the question is not yet meaningful. + try { + const issuancePerBlock = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getIssuancePerBlock', + })) as bigint + const totalAllocation = (await client.readContract({ + address: iaAddress as `0x${string}`, + abi: ISSUANCE_ALLOCATOR_ABI, + functionName: 'getTotalAllocation', + })) as { totalAllocationRate: bigint; allocatorMintingRate: bigint; selfMintingRate: bigint } + if (issuancePerBlock === 0n) { + checks.push({ ok: null, label: '100% allocated (issuancePerBlock not set)' }) + } else { + const fullyAllocated = totalAllocation.totalAllocationRate === issuancePerBlock + checks.push({ + ok: fullyAllocated, + label: `100% allocated (${formatGRT(totalAllocation.totalAllocationRate)} of ${formatGRT(issuancePerBlock)})`, + }) + } + } catch { + // Function not available + } + + return checks +} + +export async function getRewardsEligibilityOracleChecks( + client: PublicClient, + horizonBook: AddressBookOps, + issuanceBook: AddressBookOps, + entryName: string, +): Promise { + const checks: IntegrationCheck[] = [] + + const reoAddress = issuanceBook.entryExists(entryName) ? issuanceBook.getEntry(entryName)?.address : null + const rmAddress = horizonBook.entryExists('RewardsManager') ? horizonBook.getEntry('RewardsManager')?.address : null + const controllerAddress = horizonBook.entryExists('Controller') ? horizonBook.getEntry('Controller')?.address : null + + if (!reoAddress || !rmAddress) return checks + + let governor: string | null = null + let pauseGuardian: string | null = null + if (controllerAddress) { + try { + governor = (await client.readContract({ + address: controllerAddress as `0x${string}`, + abi: [ + { + inputs: [], + name: 'getGovernor', + outputs: [{ type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + ], + functionName: 'getGovernor', + })) as string + } catch { + // Controller doesn't have getGovernor + } + try { + pauseGuardian = (await client.readContract({ + address: controllerAddress as `0x${string}`, + abi: [ + { + inputs: [], + name: 'pauseGuardian', + outputs: [{ type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + ], + functionName: 'pauseGuardian', + })) as string + } catch { + // Controller doesn't have pauseGuardian + } + } + + try { + const governorRole = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'GOVERNOR_ROLE', + })) as `0x${string}` + + if (governor) { + const governorHasRole = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'hasRole', + args: [governorRole, governor as `0x${string}`], + })) as boolean + checks.push({ ok: governorHasRole, label: 'governor has GOVERNOR_ROLE' }) + } + } catch { + // Role check not available + } + + try { + const pauseRole = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'PAUSE_ROLE', + })) as `0x${string}` + + if (pauseGuardian) { + const pauseGuardianHasRole = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'hasRole', + args: [pauseRole, pauseGuardian as `0x${string}`], + })) as boolean + checks.push({ ok: pauseGuardianHasRole, label: 'pause guardian has PAUSE_ROLE' }) + } + } catch { + // Role check not available + } + + const networkOperator = issuanceBook.entryExists('NetworkOperator') + ? (issuanceBook.getEntry('NetworkOperator')?.address ?? null) + : null + + try { + const operatorCheck = await checkOperatorRole(client, reoAddress, networkOperator) + const statusOk = networkOperator === null ? false : operatorCheck.ok + checks.push({ ok: statusOk, label: operatorCheck.message }) + } catch { + checks.push({ ok: null, label: 'OPERATOR_ROLE (check failed)' }) + } + + try { + const currentREO = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + functionName: 'getProviderEligibilityOracle', + })) as string + const configured = currentREO.toLowerCase() === reoAddress.toLowerCase() + checks.push({ ok: configured, label: 'RM.providerEligibilityOracle == this' }) + } catch { + // Function not available on old RM + } + + try { + const enabled = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'getEligibilityValidation', + })) as boolean + checks.push({ ok: enabled, label: 'eligibility validation enabled' }) + } catch { + // Function not available + } + + try { + const lastUpdate = (await client.readContract({ + address: reoAddress as `0x${string}`, + abi: REWARDS_ELIGIBILITY_ORACLE_ABI, + functionName: 'getLastOracleUpdateTime', + })) as bigint + const hasUpdates = lastUpdate > 0n + checks.push({ ok: hasUpdates, label: 'oracle has processed updates' }) + } catch { + // Function not available + } + + return checks +} + +export async function getReclaimAddressChecks( + client: PublicClient, + horizonBook: AddressBookOps, + issuanceBook: AddressBookOps, +): Promise { + const checks: IntegrationCheck[] = [] + + const rmAddress = horizonBook.entryExists('RewardsManager') ? horizonBook.getEntry('RewardsManager')?.address : null + const reclaimAddress = issuanceBook.entryExists('ReclaimedRewards') + ? issuanceBook.getEntry('ReclaimedRewards')?.address + : null + + if (!rmAddress || !reclaimAddress) return checks + + try { + const defaultReclaim = (await client.readContract({ + address: rmAddress as `0x${string}`, + abi: REWARDS_MANAGER_ABI, + functionName: 'getDefaultReclaimAddress', + })) as string + const configured = defaultReclaim.toLowerCase() === reclaimAddress.toLowerCase() + checks.push({ ok: configured, label: 'configured as RM.defaultReclaimAddress' }) + } catch { + checks.push({ ok: false, label: 'configured as RM.defaultReclaimAddress' }) + } + + return checks +} + +// Minimal ABI for RecurringAgreementManager-specific view functions +const RECURRING_AGREEMENT_MANAGER_ABI = [ + { + inputs: [], + name: 'COLLECTOR_ROLE', + outputs: [{ type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DATA_SERVICE_ROLE', + outputs: [{ type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getCollectorCount', + outputs: [{ type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'paused', + outputs: [{ type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, +] as const + +export async function getRecurringAgreementManagerChecks( + client: PublicClient, + horizonBook: AddressBookOps, + issuanceBook: AddressBookOps, + ssBook: AddressBookOps, +): Promise { + const checks: IntegrationCheck[] = [] + + const ramAddress = issuanceBook.entryExists('RecurringAgreementManager') + ? issuanceBook.getEntry('RecurringAgreementManager')?.address + : null + if (!ramAddress) return checks + + // COLLECTOR_ROLE → RecurringCollector + const rcAddress = horizonBook.entryExists('RecurringCollector') + ? horizonBook.getEntry('RecurringCollector')?.address + : null + if (rcAddress) { + try { + const collectorRole = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: RECURRING_AGREEMENT_MANAGER_ABI, + functionName: 'COLLECTOR_ROLE', + })) as `0x${string}` + const hasRole = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [collectorRole, rcAddress as `0x${string}`], + })) as boolean + checks.push({ ok: hasRole, label: 'RecurringCollector has COLLECTOR_ROLE' }) + } catch { + // Role check not available + } + } + + // DATA_SERVICE_ROLE → SubgraphService + const ssAddress = ssBook?.entryExists('SubgraphService') ? ssBook.getEntry('SubgraphService')?.address : null + if (ssAddress) { + try { + const dataServiceRole = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: RECURRING_AGREEMENT_MANAGER_ABI, + functionName: 'DATA_SERVICE_ROLE', + })) as `0x${string}` + const hasRole = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: ACCESS_CONTROL_ENUMERABLE_ABI, + functionName: 'hasRole', + args: [dataServiceRole, ssAddress as `0x${string}`], + })) as boolean + checks.push({ ok: hasRole, label: 'SubgraphService has DATA_SERVICE_ROLE' }) + } catch { + // Role check not available + } + } + + // IssuanceAllocator + const iaAddress = issuanceBook.entryExists('IssuanceAllocator') + ? issuanceBook.getEntry('IssuanceAllocator')?.address + : null + try { + const currentIA = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: ISSUANCE_TARGET_ABI, + functionName: 'getIssuanceAllocator', + })) as string + const isSet = currentIA !== ZERO_ADDRESS + const matches = iaAddress ? currentIA.toLowerCase() === iaAddress.toLowerCase() : null + checks.push({ + ok: isSet ? matches : false, + label: isSet + ? `issuanceAllocator: ${formatAddress(currentIA)}${matches === false ? ` (expected ${formatAddress(iaAddress!)})` : ''}` + : 'issuanceAllocator: not set', + }) + } catch { + // Function not available + } + + // Provider eligibility oracle + try { + const reo = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: PROVIDER_ELIGIBILITY_MANAGEMENT_ABI, + functionName: 'getProviderEligibilityOracle', + })) as string + const reoA = issuanceBook.entryExists('RewardsEligibilityOracleA') + ? issuanceBook.getEntry('RewardsEligibilityOracleA')?.address + : null + const isSet = reo !== ZERO_ADDRESS + const matchesA = reoA ? reo.toLowerCase() === reoA.toLowerCase() : null + checks.push({ + ok: isSet ? matchesA : null, + label: isSet + ? `providerEligibilityOracle: ${reo}${matchesA === false ? ' (not REO-A)' : matchesA ? ' (REO-A)' : ''}` + : 'providerEligibilityOracle: not set', + }) + } catch { + // Function not available + } + + // Paused state + try { + const paused = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: RECURRING_AGREEMENT_MANAGER_ABI, + functionName: 'paused', + })) as boolean + checks.push({ ok: !paused, label: paused ? 'PAUSED' : 'not paused' }) + } catch { + // Function not available + } + + // Collector count + try { + const count = (await client.readContract({ + address: ramAddress as `0x${string}`, + abi: RECURRING_AGREEMENT_MANAGER_ABI, + functionName: 'getCollectorCount', + })) as bigint + checks.push({ ok: null, label: `collectors: ${count}` }) + } catch { + // Function not available + } + + return checks +} + +// ============================================================================ +// Horizon / SubgraphService Contract Checks +// ============================================================================ + +// Minimal ABIs for contracts not in the abis.ts module +const PAUSABLE_ABI = [ + { inputs: [], name: 'paused', outputs: [{ type: 'bool' }], stateMutability: 'view', type: 'function' }, +] as const + +const PAUSE_GUARDIAN_ABI = [ + { + inputs: [{ name: '_pauseGuardian', type: 'address' }], + name: 'pauseGuardians', + outputs: [{ type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, +] as const + +const DISPUTE_MANAGER_ABI = [ + { inputs: [], name: 'arbitrator', outputs: [{ type: 'address' }], stateMutability: 'view', type: 'function' }, + { inputs: [], name: 'getDisputePeriod', outputs: [{ type: 'uint64' }], stateMutability: 'view', type: 'function' }, + { inputs: [], name: 'disputeDeposit', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' }, + { + inputs: [], + name: 'getFishermanRewardCut', + outputs: [{ type: 'uint32' }], + stateMutability: 'view', + type: 'function', + }, + { inputs: [], name: 'maxSlashingCut', outputs: [{ type: 'uint32' }], stateMutability: 'view', type: 'function' }, + { inputs: [], name: 'subgraphService', outputs: [{ type: 'address' }], stateMutability: 'view', type: 'function' }, +] as const + +const SUBGRAPH_SERVICE_ABI = [ + { + inputs: [], + name: 'getProvisionTokensRange', + outputs: [{ type: 'uint256' }, { type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getDelegationRatio', + outputs: [{ type: 'uint32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'stakeToFeesRatio', + outputs: [{ type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'curationFeesCut', + outputs: [{ type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getDisputeManager', + outputs: [{ type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getGraphTallyCollector', + outputs: [{ type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { inputs: [], name: 'getCuration', outputs: [{ type: 'address' }], stateMutability: 'view', type: 'function' }, +] as const + +/** PPM denominator (1,000,000) for percentage display */ +const PPM = 1_000_000 + +export async function getRecurringCollectorChecks( + client: PublicClient, + address: string, + horizonBook: AddressBookOps, +): Promise { + const checks: IntegrationCheck[] = [] + + // Pause guardian + try { + const controllerAddress = horizonBook.entryExists('Controller') ? horizonBook.getEntry('Controller')?.address : null + if (controllerAddress) { + // pauseGuardian is a public storage variable auto-getter, not in IControllerToolshed + const pauseGuardian = (await client.readContract({ + address: controllerAddress as `0x${string}`, + abi: [ + { + inputs: [], + name: 'pauseGuardian', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + ] as const, + functionName: 'pauseGuardian', + })) as string + const isGuardian = (await client.readContract({ + address: address as `0x${string}`, + abi: PAUSE_GUARDIAN_ABI, + functionName: 'pauseGuardians', + args: [pauseGuardian as `0x${string}`], + })) as boolean + checks.push({ ok: isGuardian, label: `pauseGuardian: ${pauseGuardian} ${isGuardian ? '' : '(not set)'}` }) + } + } catch { + // Not available + } + + // Paused state + try { + const paused = (await client.readContract({ + address: address as `0x${string}`, + abi: PAUSABLE_ABI, + functionName: 'paused', + })) as boolean + checks.push({ ok: !paused, label: paused ? 'PAUSED' : 'not paused' }) + } catch { + // paused() not available + } + + // Thawing period + try { + const thawing = (await client.readContract({ + address: address as `0x${string}`, + abi: [ + { + inputs: [], + name: 'REVOKE_AUTHORIZATION_THAWING_PERIOD', + outputs: [{ type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + ], + functionName: 'REVOKE_AUTHORIZATION_THAWING_PERIOD', + })) as bigint + checks.push({ ok: null, label: `REVOKE_AUTHORIZATION_THAWING_PERIOD: ${thawing}` }) + } catch { + // Not available + } + + return checks +} + +export async function getDisputeManagerChecks( + client: PublicClient, + address: string, + horizonBook: AddressBookOps, + ssBook: AddressBookOps, +): Promise { + const checks: IntegrationCheck[] = [] + + async function dmRead(functionName: (typeof DISPUTE_MANAGER_ABI)[number]['name']): Promise { + try { + return (await client.readContract({ + address: address as `0x${string}`, + abi: DISPUTE_MANAGER_ABI, + functionName, + })) as T + } catch { + return null + } + } + + // Arbitrator + const arbitrator = await dmRead('arbitrator') + if (arbitrator !== null) { + checks.push({ ok: arbitrator !== ZERO_ADDRESS, label: `arbitrator: ${arbitrator}` }) + } + + // SubgraphService reference + const ss = await dmRead('subgraphService') + if (ss !== null) { + const expected = ssBook?.entryExists('SubgraphService') + ? (ssBook.getEntry('SubgraphService')?.address ?? null) + : null + const matches = expected ? ss.toLowerCase() === expected.toLowerCase() : null + checks.push({ + ok: ss !== ZERO_ADDRESS ? matches : false, + label: `subgraphService: ${ss}${matches === false && expected ? ` (expected ${expected})` : ''}`, + }) + } + + // Dispute period + const disputePeriod = await dmRead('getDisputePeriod') + if (disputePeriod !== null) { + checks.push({ ok: disputePeriod > 0n, label: `disputePeriod: ${disputePeriod}s` }) + } + + // Dispute deposit + const disputeDeposit = await dmRead('disputeDeposit') + if (disputeDeposit !== null) { + checks.push({ ok: disputeDeposit > 0n, label: `disputeDeposit: ${formatGRT(disputeDeposit)}` }) + } + + // Fisherman reward cut (PPM) + const fishermanCut = await dmRead('getFishermanRewardCut') + if (fishermanCut !== null) { + checks.push({ + ok: null, + label: `fishermanRewardCut: ${fishermanCut} (${((fishermanCut / PPM) * 100).toFixed(2)}%)`, + }) + } + + // Max slashing cut (PPM) + const maxSlashing = await dmRead('maxSlashingCut') + if (maxSlashing !== null) { + checks.push({ ok: null, label: `maxSlashingCut: ${maxSlashing} (${((maxSlashing / PPM) * 100).toFixed(2)}%)` }) + } + + return checks +} + +export async function getSubgraphServiceChecks( + client: PublicClient, + address: string, + horizonBook: AddressBookOps, + ssBook: AddressBookOps, +): Promise { + const checks: IntegrationCheck[] = [] + + async function ssRead(functionName: (typeof SUBGRAPH_SERVICE_ABI)[number]['name']): Promise { + try { + return (await client.readContract({ + address: address as `0x${string}`, + abi: SUBGRAPH_SERVICE_ABI, + functionName, + })) as T + } catch { + return null + } + } + + // DisputeManager reference + const dm = await ssRead('getDisputeManager') + if (dm !== null) { + const expected = ssBook?.entryExists('DisputeManager') ? (ssBook.getEntry('DisputeManager')?.address ?? null) : null + const matches = expected ? dm.toLowerCase() === expected.toLowerCase() : null + checks.push({ + ok: dm !== ZERO_ADDRESS ? matches : false, + label: `disputeManager: ${dm}${matches === false && expected ? ` (expected ${expected})` : ''}`, + }) + } + + // GraphTallyCollector reference + const gtc = await ssRead('getGraphTallyCollector') + if (gtc !== null) { + const expected = horizonBook.entryExists('GraphTallyCollector') + ? (horizonBook.getEntry('GraphTallyCollector')?.address ?? null) + : null + const matches = expected ? gtc.toLowerCase() === expected.toLowerCase() : null + checks.push({ + ok: gtc !== ZERO_ADDRESS ? matches : false, + label: `graphTallyCollector: ${gtc}${matches === false && expected ? ` (expected ${expected})` : ''}`, + }) + } + + // Curation reference + const curation = await ssRead('getCuration') + if (curation !== null) { + const expected = horizonBook.entryExists('L2Curation') + ? (horizonBook.getEntry('L2Curation')?.address ?? null) + : null + const matches = expected ? curation.toLowerCase() === expected.toLowerCase() : null + checks.push({ + ok: curation !== ZERO_ADDRESS ? matches : false, + label: `curation: ${curation}${matches === false && expected ? ` (expected ${expected})` : ''}`, + }) + } + + // Provision tokens range + const provisionRange = await ssRead('getProvisionTokensRange') + if (provisionRange !== null) { + checks.push({ + ok: null, + label: `provisionTokensRange: [${formatGRT(provisionRange[0])}, ${formatGRT(provisionRange[1])}]`, + }) + } + + // Delegation ratio + const delegationRatio = await ssRead('getDelegationRatio') + if (delegationRatio !== null) { + checks.push({ ok: null, label: `delegationRatio: ${delegationRatio}` }) + } + + // Stake to fees ratio + const stakeToFees = await ssRead('stakeToFeesRatio') + if (stakeToFees !== null) { + checks.push({ ok: null, label: `stakeToFeesRatio: ${stakeToFees}` }) + } + + // Curation fees cut (PPM) + const curationCut = await ssRead('curationFeesCut') + if (curationCut !== null) { + checks.push({ + ok: null, + label: `curationFeesCut: ${curationCut} (${((Number(curationCut) / PPM) * 100).toFixed(2)}%)`, + }) + } + + return checks +} + +// ============================================================================ +// High-Level Status Display +// ============================================================================ + +/** + * Show detailed status for a single component from the registry. + * + * Displays: status line + proxy admin detail + contract-specific integration checks. + * This is the detail view shown when running `--tags IssuanceAllocator`. + */ +export async function showDetailedComponentStatus( + env: Environment, + contract: RegistryEntry, + options?: { showHints?: boolean }, +): Promise { + const chainId = await getTargetChainIdFromEnv(env) + const client = graph.getPublicClient(env) as PublicClient + + // Resolve address books + const horizonBook = graph.getHorizonAddressBook(chainId) + const addressBook = + contract.addressBook === 'horizon' ? horizonBook : getAddressBookForType(contract.addressBook, chainId) + + // Resolve ownership context + const ownershipCtx = await resolveOwnershipContext(client, env, chainId) + + // Get status line with detail + const result = await getContractStatusLine( + client, + contract.addressBook, + addressBook, + contract.name, + undefined, + ownershipCtx, + ) + env.showMessage(` ${result.line}`) + for (const line of formatWarnings(result.warnings)) { + env.showMessage(line) + } + // Show ProxyAdmin detail for OZ v5 transparent proxies (not old Graph proxies, + // which are controller-governed and don't expose owner()) + if (contract.proxyType !== 'graph') { + for (const line of formatProxyAdminDetail(result)) { + env.showMessage(line) + } + } + + // Verification status from address book + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (result.exists && (addressBook as any).entryExists(contract.name)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const entry = (addressBook as any).getEntry(contract.name) + if (entry.proxy) { + const proxyVerified = entry.proxyDeployment?.verified + const implVerified = entry.implementationDeployment?.verified + env.showMessage(` ${proxyVerified ? '✓' : '✗'} proxy verified${proxyVerified ? `: ${proxyVerified}` : ''}`) + env.showMessage(` ${implVerified ? '✓' : '✗'} impl verified${implVerified ? `: ${implVerified}` : ''}`) + } else { + const verified = entry.deployment?.verified + env.showMessage(` ${verified ? '✓' : '✗'} verified${verified ? `: ${verified}` : ''}`) + } + } + + const showHints = options?.showHints !== false + + // Contract-specific integration checks + if (!result.exists) { + if (showHints && contract.componentTag && contract.deployable) { + showLifecycleHints(env, contract, result) + } + return result + } + + const issuanceBook = contract.addressBook === 'issuance' ? addressBook : graph.getIssuanceAddressBook(chainId) + + let checks: IntegrationCheck[] = [] + if (contract.name === 'RewardsManager') { + checks = await getRewardsManagerChecks( + client, + horizonBook, + chainId, + issuanceBook, + graph.getSubgraphServiceAddressBook(chainId), + ) + } else if (contract.name === 'IssuanceAllocator') { + checks = await getIssuanceAllocatorChecks(client, horizonBook, issuanceBook) + } else if ( + contract.name === 'RewardsEligibilityOracleA' || + contract.name === 'RewardsEligibilityOracleB' || + contract.name === 'RewardsEligibilityOracleMock' + ) { + checks = await getRewardsEligibilityOracleChecks(client, horizonBook, issuanceBook, contract.name) + } else if (contract.name === 'RecurringAgreementManager') { + checks = await getRecurringAgreementManagerChecks( + client, + horizonBook, + issuanceBook, + graph.getSubgraphServiceAddressBook(chainId), + ) + } else if (contract.name === 'ReclaimedRewards') { + checks = await getReclaimAddressChecks(client, horizonBook, issuanceBook) + } else if (contract.name === 'RecurringCollector') { + const addr = horizonBook.entryExists('RecurringCollector') + ? horizonBook.getEntry('RecurringCollector')?.address + : null + if (addr) checks = await getRecurringCollectorChecks(client, addr, horizonBook) + } else if (contract.name === 'DisputeManager') { + const ssBook = graph.getSubgraphServiceAddressBook(chainId) + const addr = ssBook.entryExists('DisputeManager') ? ssBook.getEntry('DisputeManager')?.address : null + if (addr) checks = await getDisputeManagerChecks(client, addr, horizonBook, ssBook) + } else if (contract.name === 'SubgraphService') { + const ssBook = graph.getSubgraphServiceAddressBook(chainId) + const addr = ssBook.entryExists('SubgraphService') ? ssBook.getEntry('SubgraphService')?.address : null + if (addr) checks = await getSubgraphServiceChecks(client, addr, horizonBook, ssBook) + } + + for (const check of checks) { + env.showMessage(formatCheck(check)) + } + + // Lifecycle action hints + if (showHints && contract.componentTag && contract.deployable) { + showLifecycleHints(env, contract, result) + } + + return result +} + +/** + * Show available lifecycle actions and state-based hint for a component. + */ +function showLifecycleHints(env: Environment, contract: RegistryEntry, result: ContractStatusResult): void { + const tag = contract.componentTag! + + // State-based hint + if (!result.exists) { + env.showMessage(`\n → Not deployed. Run with: --tags ${tag},deploy`) + } else if (result.codeChanged && !result.hasPendingImplementation) { + env.showMessage(`\n → Code changed. Run with: --tags ${tag},deploy`) + } else if (result.hasPendingImplementation) { + env.showMessage(`\n → Pending implementation. Run with: --tags ${tag},upgrade`) + } else { + env.showMessage(`\n → Up to date`) + } + + // Available actions — use explicit list if provided, otherwise derive from metadata + let actions: readonly string[] + if (contract.lifecycleActions) { + actions = contract.lifecycleActions + } else { + const derived: string[] = ['deploy'] + if (contract.proxyType) derived.push('upgrade') + actions = derived + } + env.showMessage(` Actions: --tags ${tag},<${[...actions, 'all'].join('|')}>`) +} + +/** + * Show pending governance TX count with execute command if any exist. + * Call once at the end of a status display, not per-component. + */ +export function showPendingGovernanceTxs(env: Environment): void { + const count = countPendingGovernanceTxs(env.name) + if (count > 0) { + env.showMessage(`\n ⚠ ${count} pending governance TX(s)`) + env.showMessage(` Run: npx hardhat deploy:execute-governance --network ${env.name}`) + } +} diff --git a/packages/deployment/lib/sync-utils.ts b/packages/deployment/lib/sync-utils.ts new file mode 100644 index 000000000..dd8734d12 --- /dev/null +++ b/packages/deployment/lib/sync-utils.ts @@ -0,0 +1,1441 @@ +import { existsSync } from 'node:fs' + +import type { Environment } from '@rocketh/core/types' +import type { DeploymentMetadata } from '@graphprotocol/toolshed/deployments' + +import { + autoDetectForkNetwork, + getAddressBookForType, + getForkNetwork, + getForkStateDir, + getForkTargetChainId, + getIssuanceAddressBookPath, + getTargetChainIdFromEnv, + isForkMode, +} from './address-book-utils.js' +import { computeBytecodeHash } from './bytecode-utils.js' +import { + type AddressBookType, + type ArtifactSource, + type ContractMetadata, + type RegistryEntry, + getAddressBookEntryName, + getContractMetadata, + getContractsByAddressBook, +} from './contract-registry.js' +import { SpecialTags } from './deployment-tags.js' +import { + computeArtifactBytecodeHash, + getOnChainImplementation, + tryComputeArtifactBytecodeHash, + tryLoadArtifactFromSource, +} from './deploy-implementation.js' +import { toBlockNumber } from './deployment-metadata.js' +import { graph } from '../rocketh/deploy.js' +import type { AnyAddressBookOps } from './address-book-ops.js' + +/** + * Format an address based on SHOW_ADDRESSES environment variable + * - 0: return empty string (no addresses shown) + * - 1: return truncated address (0x1234...5678) + * - 2 (default): return full address + */ +function formatAddress(address: string): string { + const showAddresses = process.env.SHOW_ADDRESSES ?? '2' + + if (showAddresses === '0') { + return '' + } else if (showAddresses === '1') { + return address.slice(0, 10) + '...' + } else { + // Default to full address (showAddresses === '2' or any other value) + return address + } +} + +// ============================================================================ +// Sync Change Detection & Record Reconstruction +// ============================================================================ + +/** + * Result of checking whether a contract needs to be synced + */ +export interface SyncCheckResult { + /** Whether sync should proceed */ + shouldSync: boolean + /** Reason for the decision */ + reason: string + /** Warning to display (e.g., bytecode changed) */ + warning?: string +} + +/** + * Check whether a contract needs to be synced + * + * Uses deployment metadata to determine if: + * - Contract is new (no existing record) → sync + * - Address changed → sync + * - Local bytecode changed since deployment → warn, don't overwrite + * - No changes → skip sync + * + * @param addressBook - Address book ops instance + * @param contractName - Name of the contract + * @param newAddress - Address to sync to + * @param artifact - Artifact for bytecode comparison + */ +export function checkShouldSync( + addressBook: AnyAddressBookOps, + contractName: string, + newAddress: string, + artifact?: ArtifactSource, +): SyncCheckResult { + // No existing entry - must sync + if (!addressBook.entryExists(contractName)) { + return { shouldSync: true, reason: 'new contract' } + } + + const entry = addressBook.getEntry(contractName) + + // Address changed - must sync + if (entry.address.toLowerCase() !== newAddress.toLowerCase()) { + return { shouldSync: true, reason: 'address changed' } + } + + // Check bytecode hash if deployment metadata exists + const metadata = addressBook.getDeploymentMetadata(contractName) + if (metadata?.bytecodeHash && artifact) { + const localHash = tryComputeArtifactBytecodeHash(artifact) + if (localHash && metadata.bytecodeHash !== localHash) { + return { + shouldSync: false, + reason: 'local bytecode changed since deployment', + warning: `${contractName}: local bytecode differs from deployed (hash mismatch)`, + } + } + } + + // No changes detected - skip sync but still valid + return { shouldSync: false, reason: 'unchanged' } +} + +/** + * Reconstruct a complete rocketh deployment record from address book metadata + * + * This enables verification and other operations that need full deployment records, + * without storing the large records in the repo. + * + * @param addressBook - Address book ops instance + * @param contractName - Name of the contract + * @param artifact - Artifact source for ABI and bytecode + * @returns Reconstructed deployment record, or undefined if metadata is incomplete + */ +export function reconstructDeploymentRecord( + addressBook: AnyAddressBookOps, + contractName: string, + artifact: ArtifactSource, +): + | { + address: `0x${string}` + abi: readonly unknown[] + bytecode: `0x${string}` + deployedBytecode?: `0x${string}` + argsData: `0x${string}` + metadata: string + } + | undefined { + if (!addressBook.entryExists(contractName)) { + return undefined + } + + const entry = addressBook.getEntry(contractName) + const deploymentMetadata = addressBook.getDeploymentMetadata(contractName) + + // Need at minimum argsData to reconstruct + if (!deploymentMetadata?.argsData) { + return undefined + } + + // Verify bytecode hash matches if available + const loadedArtifact = tryLoadArtifactFromSource(artifact) + if (!loadedArtifact) { + return undefined + } + + if (deploymentMetadata.bytecodeHash && loadedArtifact.deployedBytecode) { + const localHash = computeArtifactBytecodeHash(artifact) + if (deploymentMetadata.bytecodeHash !== localHash) { + // Bytecode has changed - cannot reconstruct reliably + return undefined + } + } + + return { + address: entry.address as `0x${string}`, + abi: (loadedArtifact.abi ?? []) as readonly unknown[], + bytecode: (loadedArtifact.bytecode ?? '0x') as `0x${string}`, + deployedBytecode: loadedArtifact.deployedBytecode as `0x${string}` | undefined, + argsData: deploymentMetadata.argsData as `0x${string}`, + metadata: '', + } +} + +/** + * Check if local artifact bytecode differs from what was last deployed. + * + * Compares the local artifact's bytecodeHash against the stored hash in the + * address book. The stored hash is recorded from the local artifact at deploy + * time, so this is a local-to-local comparison (no on-chain bytecode fetch). + * + * @returns codeChanged flag and the computed localHash (needed for hashMatches checks) + */ +function checkCodeChanged( + artifactSource: ArtifactSource | undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addressBook: any, + contractName: string, +): { codeChanged: boolean; localHash?: string } { + if (!artifactSource) return { codeChanged: false } + + const localArtifact = tryLoadArtifactFromSource(artifactSource) + const localHash = tryComputeArtifactBytecodeHash(artifactSource) + + const deploymentMetadata = addressBook.getDeploymentMetadata(contractName) + if (deploymentMetadata?.bytecodeHash && localHash) { + return { codeChanged: localHash !== deploymentMetadata.bytecodeHash, localHash } + } + if (localArtifact?.deployedBytecode) { + // No stored bytecodeHash but artifact exists - untracked/legacy state + return { codeChanged: true, localHash } + } + return { codeChanged: false, localHash } +} + +/** + * Decide whether sync should seed rocketh's record from the local artifact. + * + * Seeding writes the local artifact's bytecode into rocketh's deployment + * record. That's correct when the artifact reflects what's deployed on-chain, + * and harmful when the artifact has drifted: rocketh's native bytecode + * comparison would then match its (just-seeded) record against the artifact + * and skip the redeploy that the drift demands — the address book never + * advances, and proxies that depend on the impl miss their pendingImplementation. + * + * Gate (only contracts we ourselves deploy carry the dedup-masking risk): + * - Synthetic names not in the registry → seed (proxy sync recurses with + * `${name}_Implementation` names that aren't real entries; the proxy path + * already has its own hashMatches gate before recursing). + * - Prerequisites → seed (deployed externally; never run through deployFn). + * - No artifact → seed (no local bytecode to compare against). + * + * Within the gated set: skip the seed only on a *verified mismatch* — i.e. + * we have a stored hash and the local artifact's hash differs. If there's no + * stored hash at all (no entry, or entry without a hash), fall through to + * the legacy seed: there's nothing to mask. + */ +export function shouldSeedRocketh( + spec: ContractSpec, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addressBook: any, +): { seed: boolean; reason: string } { + const registered = getContractMetadata(spec.addressBookType, spec.name) + if (!registered) return { seed: true, reason: 'unregistered name (legacy seed)' } + if (spec.prerequisite) return { seed: true, reason: 'prerequisite (legacy seed)' } + if (!spec.artifact) return { seed: true, reason: 'no artifact (legacy seed)' } + + if (!addressBook?.entryExists?.(spec.name)) { + return { seed: true, reason: 'no entry, nothing to mask (legacy seed)' } + } + + const storedHash = addressBook.getDeploymentMetadata?.(spec.name)?.bytecodeHash + const { codeChanged, localHash } = checkCodeChanged(spec.artifact, addressBook, spec.name) + + if (!storedHash || !localHash) return { seed: true, reason: 'no hash to compare (legacy seed)' } + if (codeChanged) return { seed: false, reason: 'artifact unverified vs. address book' } + return { seed: true, reason: 'artifact verified' } +} + +/** + * Proxy admin ownership state + */ +export type ProxyAdminOwner = 'governor' | 'deployer' | 'other' | 'unknown' + +/** + * Input for proxy status line generation + */ +interface ProxyStatusInput { + /** Contract name */ + name: string + /** Proxy address */ + proxyAddress: string + /** Current implementation address */ + implAddress: string + /** Pending implementation address (if any) */ + pendingAddress?: string + /** Sync-specific status icon override: ↑ (upgraded), ↻ (synced) */ + syncIcon?: string + /** Sync-specific notes to prepend (e.g., "upgraded from 0x...", "impl synced") */ + syncNotes?: string[] + /** Whether local bytecode differs from deployed (shows △ icon) */ + codeChanged?: boolean + /** ProxyAdmin ownership state — 'deployer' shows 🔑 warning icon */ + proxyAdminOwner?: ProxyAdminOwner +} + +/** + * Result of proxy status line generation + */ +interface ProxyStatusResult { + /** Formatted status line */ + line: string +} + +/** + * Generate proxy contract status line + * + * Format: [codeIcon] [statusIcon] ContractName @ proxyAddr → implAddr (notes) + * - codeIcon: ✓ (ok), △ (code changed) + * - statusIcon: ◷ (pending), ↑ (upgraded), ↻ (synced), ' ' (none) + * + * @param input - Proxy status input data + */ +function formatProxyStatusLine(input: ProxyStatusInput): ProxyStatusResult { + const codeIcon = input.codeChanged ? '△' : '✓' + let statusIcon = input.syncIcon ?? ' ' + const notes: string[] = [...(input.syncNotes ?? [])] + + // Check for pending implementation (only set icon if no sync override) + if (input.pendingAddress) { + if (!input.syncIcon) { + statusIcon = '◷' + } + notes.push(`pending upgrade to ${formatAddress(input.pendingAddress)}`) + } + + // Add code changed note if applicable and not already implied by sync notes + if (input.codeChanged && !input.pendingAddress && !input.syncNotes?.length) { + notes.push('code changed') + } + + // ProxyAdmin ownership warning: 🔑 when known to be non-governor (deployer or other) + const adminIcon = + input.proxyAdminOwner && input.proxyAdminOwner !== 'governor' && input.proxyAdminOwner !== 'unknown' ? ' 🔑' : '' + + // Format the line + const suffix = notes.length > 0 ? ` (${notes.join(', ')})` : '' + const line = `${codeIcon} ${statusIcon} ${input.name} @ ${formatAddress(input.proxyAddress)} → ${formatAddress(input.implAddress)}${suffix}${adminIcon}` + + return { line } +} + +/** + * Specification for a contract to sync + */ +export interface ContractSpec { + name: string + /** Which address book this contract belongs to */ + addressBookType: AddressBookType + address: string + /** If true, contract must exist on-chain (prerequisite). If false, may not exist yet. */ + prerequisite: boolean + /** External artifact to load ABI from */ + artifactName?: string + /** Artifact source for loading ABI (if provided, ABI is saved to deployment record) */ + artifact?: ArtifactSource + /** If true, address-only placeholder (code not required) */ + addressOnly?: boolean + /** ABI-encoded constructor args from address book deployment metadata. + * Used to seed rocketh records with real argsData instead of '0x'. */ + deploymentArgsData?: string + /** Proxy sync fields (if present, will sync implementation with on-chain) */ + proxy?: { + proxyAdminAddress: string + proxyType: 'graph' | 'transparent' + bookImpl: string | undefined + bookPending: string | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addressBook: any + /** Artifact source for bytecode hash comparison */ + artifact?: ArtifactSource + } +} + +/** + * A group of contracts from the same address book + */ +export interface AddressBookGroup { + label: string + contracts: ContractSpec[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addressBook?: any +} + +/** + * Build a ContractSpec from registry metadata and address book entry + * + * @param addressBookType - Which address book this contract belongs to + * @param contractName - The deployment record name (key in CONTRACT_REGISTRY) + * @param metadata - Contract metadata from registry + * @param addressBook - The address book instance to read from + * @param targetChainId - Chain ID for error messages + */ +export function buildContractSpec( + addressBookType: AddressBookType, + contractName: string, + metadata: ContractMetadata, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addressBook: any, + targetChainId: number, +): ContractSpec { + const addressBookEntryName = getAddressBookEntryName(addressBookType, contractName) + + // Get entry from address book + const entry = addressBook.entryExists(addressBookEntryName) ? addressBook.getEntry(addressBookEntryName) : null + + if (!entry && metadata.prerequisite) { + throw new Error(`${addressBookEntryName} not found in address book for chainId ${targetChainId}`) + } + + // Get deployment argsData from address book for accurate rocketh record seeding + let deploymentArgsData: string | undefined + if (entry) { + const deploymentMeta = entry.proxy ? entry.implementationDeployment : entry.deployment + if (deploymentMeta?.argsData && deploymentMeta.argsData !== '0x') { + deploymentArgsData = deploymentMeta.argsData + } + } + + const spec: ContractSpec = { + name: contractName, + addressBookType, + address: entry?.address ?? '', + prerequisite: metadata.prerequisite ?? false, + artifact: metadata.artifact, + addressOnly: metadata.addressOnly, + deploymentArgsData, + } + + // Add proxy configuration if this is a proxied contract + if (metadata.proxyType && entry) { + // Get proxy admin address - either from entry or from a separate address book entry + let proxyAdminAddress: string + if (entry.proxyAdmin) { + // Proxy admin stored inline in contract entry (e.g., SubgraphService) + proxyAdminAddress = entry.proxyAdmin + } else if (metadata.proxyAdminName) { + // Proxy admin is a separate address book entry (e.g., GraphProxyAdmin) + const adminEntryName = getAddressBookEntryName(addressBookType, metadata.proxyAdminName) + const adminEntry = addressBook.entryExists(adminEntryName) ? addressBook.getEntry(adminEntryName) : null + if (!adminEntry) { + throw new Error(`${adminEntryName} not found in address book for chainId ${targetChainId}`) + } + proxyAdminAddress = adminEntry.address + } else { + throw new Error(`No proxy admin address found for ${contractName} (missing proxyAdminName and entry.proxyAdmin)`) + } + + spec.proxy = { + proxyAdminAddress, + proxyType: metadata.proxyType, + bookImpl: entry.implementation, + bookPending: entry.pendingImplementation?.address, + addressBook, + artifact: metadata.artifact, + } + } + + return spec +} + +/** + * Result of syncing contracts + */ +export interface SyncResult { + success: boolean + totalSynced: number + failures: string[] +} + +/** + * Sync a single contract - returns status and whether it succeeded + */ +export async function syncContract( + env: Environment, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client: any, + spec: ContractSpec, +): Promise<{ success: boolean; status: string }> { + // Handle contracts with empty/zero addresses (not deployed yet) + if (!spec.address || spec.address === '0x0000000000000000000000000000000000000000') { + if (spec.prerequisite) { + return { success: false, status: `❌ ${spec.name}: missing address (prerequisite)` } + } + return { success: true, status: `○ ${spec.name} (not deployed)` } + } + + // Address-only entries don't require code - just display the address + if (spec.addressOnly) { + return { success: true, status: `✓ ${spec.name} @ ${formatAddress(spec.address)}` } + } + + // Sync-specific icons and notes (determined by sync operations) + let syncIcon: string | undefined + const syncNotes: string[] = [] + + // If this is a proxy, sync implementation with on-chain state first + if (spec.proxy) { + try { + const onChainImpl = await getOnChainImplementation( + client, + spec.address, + spec.proxy.proxyType, + spec.proxy.proxyAdminAddress, + ) + + const bookImplMatches = spec.proxy.bookImpl?.toLowerCase() === onChainImpl.toLowerCase() + + if (!bookImplMatches) { + // On-chain impl differs from address book - reconcile + const oldImpl = spec.proxy.bookImpl + const pendingMatches = spec.proxy.bookPending?.toLowerCase() === onChainImpl.toLowerCase() + + if (pendingMatches) { + // Pending was upgraded on-chain → promote with metadata + spec.proxy.addressBook.promotePendingImplementationWithMetadata(spec.name) + syncIcon = '↑' + syncNotes.push(oldImpl ? `upgraded from ${formatAddress(oldImpl)}` : 'upgraded') + } else { + // External change (not through pending) → update address, wipe stale metadata + spec.proxy.addressBook.setImplementation(spec.name, onChainImpl) + spec.proxy.addressBook.setImplementationDeploymentMetadata(spec.name, { + txHash: '', + argsData: '0x', + bytecodeHash: '', + }) + syncIcon = '↻' + syncNotes.push(oldImpl ? `on-chain changed from ${formatAddress(oldImpl)}` : 'on-chain changed') + } + } else if (spec.proxy.bookPending) { + if (spec.proxy.bookPending.toLowerCase() === onChainImpl.toLowerCase()) { + // Pending matches on-chain impl but book impl already matched - promote pending + spec.proxy.addressBook.promotePendingImplementationWithMetadata(spec.name) + syncNotes.push('pending promoted') + } + // Note: if pending doesn't match on-chain, it's still pending - formatProxyStatusLine handles ◷ icon + } + + // Get updated entry for formatProxyStatusLine + const updatedEntry = spec.proxy.addressBook.getEntry(spec.name) + + const pendingImpl = updatedEntry.pendingImplementation + const implAddress = pendingImpl?.address ?? updatedEntry.implementation + const implDeployment = pendingImpl + ? pendingImpl.deployment + : spec.proxy.addressBook.getDeploymentMetadata(spec.name) + + const { codeChanged, localHash } = checkCodeChanged(spec.proxy.artifact, spec.proxy.addressBook, spec.name) + + const result = formatProxyStatusLine({ + name: spec.name, + proxyAddress: spec.address, + implAddress: updatedEntry.implementation, + pendingAddress: updatedEntry.pendingImplementation?.address, + syncIcon, + syncNotes, + codeChanged, + }) + + // Check for code on-chain (still needed for non-proxy parts below) + const code = await client.getCode({ address: spec.address as `0x${string}` }) + if (!code || code === '0x') { + if (spec.prerequisite) { + return { success: false, status: `❌ ${spec.name} @ ${formatAddress(spec.address)}: no code on-chain` } + } + return { success: false, status: `❌ ${spec.name} @ ${formatAddress(spec.address)}: stale (no code)` } + } + + // Save deployment records for proxy + // CRITICAL: Only set rocketh bytecode when NO existing record. + // If rocketh already has a record, preserve its bytecode - it came from + // a real deployment and rocketh's native change detection depends on it. + // The backfill logic (rocketh → address book) handles the other direction. + const existing = env.getOrNull(spec.name) + const addressChanged = existing && existing.address.toLowerCase() !== spec.address.toLowerCase() + + if (!existing) { + // No existing record - create from artifact + // IMPORTANT: For proxy contracts, we only load the ABI, not bytecode + // The artifact is for the implementation, not the proxy itself + let abi: readonly unknown[] = [] + if (spec.artifact) { + const artifact = tryLoadArtifactFromSource(spec.artifact) + if (artifact?.abi) { + abi = artifact.abi + } + } + await env.save(spec.name, { + address: spec.address as `0x${string}`, + abi: abi as typeof abi & readonly unknown[], + bytecode: '0x' as `0x${string}`, // Don't store impl bytecode for proxy record + deployedBytecode: undefined, + argsData: '0x' as `0x${string}`, + metadata: '', + } as unknown as Parameters[1]) + } else if (addressChanged) { + // Address changed - update address and clear bytecode (proxy address changed) + let abi: readonly unknown[] = existing.abi as readonly unknown[] + // Update ABI from artifact if available (ABI doesn't affect change detection) + if (spec.artifact) { + const artifact = tryLoadArtifactFromSource(spec.artifact) + if (artifact?.abi) { + abi = artifact.abi + } + } + await env.save(spec.name, { + address: spec.address as `0x${string}`, + abi: abi as typeof abi & readonly unknown[], + bytecode: '0x' as `0x${string}`, // Clear bytecode - proxy changed + deployedBytecode: undefined, + argsData: '0x' as `0x${string}`, + metadata: '', + } as unknown as Parameters[1]) + } + // else: existing record with same address - do nothing, preserve rocketh's state + + // Save proxy deployment record (rocketh expects {name}_Proxy) + const proxyDeploymentName = `${spec.name}_Proxy` + const proxyDeployment = env.getOrNull(proxyDeploymentName) + if (!proxyDeployment || proxyDeployment.address.toLowerCase() !== spec.address.toLowerCase()) { + await env.save(proxyDeploymentName, { + address: spec.address as `0x${string}`, + abi: [], + bytecode: '0x' as `0x${string}`, + argsData: '0x' as `0x${string}`, + metadata: '', + } as unknown as Parameters[1]) + } + + // Backfill proxy deployment metadata from rocketh if rocketh is newer + const existingProxyDeployment = env.getOrNull(proxyDeploymentName) + if (existingProxyDeployment?.argsData && existingProxyDeployment.argsData !== '0x') { + const entry = spec.proxy.addressBook.getEntry(spec.name) + const proxyRockethBlockNumber = toBlockNumber(existingProxyDeployment.receipt?.blockNumber) + const proxyAddressBookBlockNumber = entry.proxyDeployment?.blockNumber + + // Backfill if: + // - Address book has no proxy metadata at all + // - Rocketh has blockNumber but address book doesn't (rocketh is newer) + // - Rocketh has newer blockNumber + const proxyRockethIsNewer = + !entry.proxyDeployment?.argsData || + (proxyRockethBlockNumber !== undefined && proxyAddressBookBlockNumber === undefined) || + (proxyRockethBlockNumber !== undefined && + proxyAddressBookBlockNumber !== undefined && + proxyRockethBlockNumber > proxyAddressBookBlockNumber) + + if (proxyRockethIsNewer) { + const proxyMetadata: DeploymentMetadata = { + txHash: existingProxyDeployment.transaction?.hash ?? '', + argsData: existingProxyDeployment.argsData, + bytecodeHash: existingProxyDeployment.deployedBytecode + ? computeBytecodeHash(existingProxyDeployment.deployedBytecode) + : '', + ...(proxyRockethBlockNumber !== undefined && { blockNumber: proxyRockethBlockNumber }), + } + spec.proxy.addressBook.setProxyDeploymentMetadata(spec.name, proxyMetadata) + syncNotes.push('backfilled proxy metadata') + } + } + + // Save proxy admin deployment record + const metadata = getContractMetadata(spec.addressBookType, spec.name) + const proxyAdminDeploymentName = metadata?.proxyAdminName ?? `${spec.name}_ProxyAdmin` + const proxyAdminDeployment = env.getOrNull(proxyAdminDeploymentName) + if ( + !proxyAdminDeployment || + proxyAdminDeployment.address.toLowerCase() !== spec.proxy.proxyAdminAddress.toLowerCase() + ) { + // Load proxy admin ABI from its metadata if available + let proxyAdminAbi: readonly unknown[] = [] + const proxyAdminMetadata = getContractMetadata(spec.addressBookType, proxyAdminDeploymentName) + if (proxyAdminMetadata?.artifact) { + const proxyAdminArtifact = tryLoadArtifactFromSource(proxyAdminMetadata.artifact) + if (proxyAdminArtifact?.abi) { + proxyAdminAbi = proxyAdminArtifact.abi + } + } + await env.save(proxyAdminDeploymentName, { + address: spec.proxy.proxyAdminAddress as `0x${string}`, + abi: proxyAdminAbi as typeof proxyAdminAbi & readonly unknown[], + bytecode: '0x' as `0x${string}`, + argsData: '0x' as `0x${string}`, + metadata: '', + } as unknown as Parameters[1]) + } + + // Save implementation deployment record (if local hash matches stored) + if (implAddress) { + const storedHash = implDeployment?.bytecodeHash + let hashMatches = false + + if (storedHash && localHash) { + hashMatches = storedHash === localHash + } + + // When hash doesn't match, leave the existing rocketh record untouched. + // The old record (with real bytecode from the previous deploy) lets rocketh + // correctly detect the bytecode change and trigger a fresh deployment. + // NOTE: Do NOT clear the record to bytecode '0x' — rocketh's CBOR-stripping + // comparison treats '0x' as NaN length, causing slice(0, NaN) → '' for both + // old and new bytecodes, making them falsely compare as equal. + + if (hashMatches) { + const implResult = await syncContract(env, client, { + name: `${spec.name}_Implementation`, + addressBookType: spec.addressBookType, + address: implAddress, + prerequisite: true, + artifact: spec.proxy.artifact, + }) + if (!implResult.success) { + return implResult + } + + // Patch implementation record with deployment metadata for accurate + // rocketh comparison. syncContract creates bare records without argsData, + // but rocketh's deploy() compares argsData to decide if redeployment is + // needed. Without the real argsData, rocketh falsely detects a change + // and redeploys implementations that haven't changed. + const implRecordName = `${spec.name}_Implementation` + const implRecord = env.getOrNull(implRecordName) + if (implRecord && implDeployment?.argsData && (!implRecord.argsData || implRecord.argsData === '0x')) { + await env.save(implRecordName, { + address: implRecord.address as `0x${string}`, + abi: implRecord.abi as typeof implRecord.abi & readonly unknown[], + bytecode: (implRecord.bytecode ?? '0x') as `0x${string}`, + deployedBytecode: implRecord.deployedBytecode as `0x${string}` | undefined, + argsData: implDeployment.argsData as `0x${string}`, + metadata: (implRecord as Record).metadata ?? '', + } as unknown as Parameters[1]) + } + + // Backfill address book metadata from rocketh if rocketh is newer + const rockethImpl = env.getOrNull(`${spec.name}_Implementation`) + if (rockethImpl?.argsData && rockethImpl.argsData !== '0x') { + const rockethBlockNumber = toBlockNumber(rockethImpl.receipt?.blockNumber) + const bookBlockNumber = implDeployment?.blockNumber + + // Backfill if: + // - Address book has no metadata at all + // - Rocketh has blockNumber but address book doesn't (rocketh is newer) + // - Rocketh has newer blockNumber + const rockethIsNewer = + !implDeployment?.argsData || + (rockethBlockNumber !== undefined && bookBlockNumber === undefined) || + (rockethBlockNumber !== undefined && + bookBlockNumber !== undefined && + rockethBlockNumber > bookBlockNumber) + + if (rockethIsNewer) { + // Hash from the artifact (with library resolution) so the stored value stays + // in lockstep with checkShouldSync's artifact-side comparison. Hashing + // rocketh's linked `deployedBytecode` would diverge for library-using impls. + const metadata: DeploymentMetadata = { + txHash: rockethImpl.transaction?.hash ?? '', + argsData: rockethImpl.argsData, + bytecodeHash: tryComputeArtifactBytecodeHash(spec.proxy.artifact) ?? '', + ...(rockethBlockNumber !== undefined && { blockNumber: rockethBlockNumber }), + } + // Write to correct location based on pending vs current + if (pendingImpl) { + spec.proxy.addressBook.setPendingDeploymentMetadata(spec.name, metadata) + } else { + spec.proxy.addressBook.setImplementationDeploymentMetadata(spec.name, metadata) + } + syncNotes.push('backfilled metadata') + } + } + } + } + + return { success: true, status: result.line } + } catch (error) { + return { + success: false, + status: `⚠️ ${spec.name}: could not read on-chain state: ${(error as Error).message}`, + } + } + } + + // Non-proxy contract handling + // Note: Proxy contracts return early above, so we only reach here for non-proxies + let nonProxySyncIcon = ' ' + const statusNotes: string[] = [] + + // Verify code exists on-chain (just checking existence, not storing bytecode) + try { + const code = await client.getCode({ address: spec.address as `0x${string}` }) + if (!code || code === '0x') { + if (spec.prerequisite) { + return { success: false, status: `❌ ${spec.name} @ ${formatAddress(spec.address)}: no code on-chain` } + } + // Non-prerequisite with address but no code - stale state + return { success: false, status: `❌ ${spec.name} @ ${formatAddress(spec.address)}: stale (no code)` } + } + } catch (error) { + return { + success: false, + status: `⚠️ ${spec.name} @ ${formatAddress(spec.address)}: ${(error as Error).message}`, + } + } + + // Check existing deployment record + // CRITICAL: Only set rocketh bytecode when NO existing record. + // If rocketh already has a record, preserve its bytecode - it came from + // a real deployment and rocketh's native change detection depends on it. + const existing = env.getOrNull(spec.name) + const addressChanged = existing && existing.address.toLowerCase() !== spec.address.toLowerCase() + + if (existing && addressChanged) { + nonProxySyncIcon = '↻' + statusNotes.push('re-imported') + } + + // Decide whether to seed rocketh's record from the local artifact (see + // `shouldSeedRocketh` for the rationale and gate). + const chainIdForVerify = await getTargetChainIdFromEnv(env) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const addressBookForVerify: any = getAddressBookForType(spec.addressBookType, chainIdForVerify) + const seedDecision = shouldSeedRocketh(spec, addressBookForVerify) + + if (!existing) { + if (seedDecision.seed) { + // Either no artifact to compare (legacy/external entry) or hash verified — + // safe to seed rocketh from the artifact. + let abi: readonly unknown[] = [] + let bytecode: `0x${string}` = '0x' + let deployedBytecode: `0x${string}` | undefined + if (spec.artifact) { + const artifact = tryLoadArtifactFromSource(spec.artifact) + if (artifact?.abi) { + abi = artifact.abi + } + if (artifact?.bytecode) { + bytecode = artifact.bytecode as `0x${string}` + } + if (artifact?.deployedBytecode) { + deployedBytecode = artifact.deployedBytecode as `0x${string}` + } + } + await env.save(spec.name, { + address: spec.address as `0x${string}`, + abi: abi as typeof abi & readonly unknown[], + bytecode, + deployedBytecode, + argsData: (spec.deploymentArgsData ?? '0x') as `0x${string}`, + metadata: '', + } as unknown as Parameters[1]) + } else { + // Cannot verify artifact matches what's on-chain — leave the rocketh + // record absent so the next deployFn detects no prior bytecode and + // deploys fresh. Seeding from a stale or new artifact would mask the + // drift: rocketh would compare new artifact to itself and skip redeploy. + statusNotes.push(`seed skipped (${seedDecision.reason})`) + } + } else if (addressChanged) { + // Address changed - update address but preserve existing bytecode + let abi: readonly unknown[] = existing.abi as readonly unknown[] + if (spec.artifact) { + const artifact = tryLoadArtifactFromSource(spec.artifact) + if (artifact?.abi) { + abi = artifact.abi + } + } + await env.save(spec.name, { + address: spec.address as `0x${string}`, + abi: abi as typeof abi & readonly unknown[], + bytecode: existing.bytecode as `0x${string}`, + deployedBytecode: existing.deployedBytecode as `0x${string}`, + argsData: existing.argsData as `0x${string}`, + metadata: existing.metadata ?? '', + } as unknown as Parameters[1]) + } + // else: existing record with same address - do nothing, preserve rocketh's state + + // Backfill deployment metadata from rocketh → address book (mirrors proxy backfill) + // Only for real registry entries — skip synthetic names (e.g. HorizonStaking_Implementation) + // created by proxy sync as rocketh-only records + const registryMetadata = getContractMetadata(spec.addressBookType, spec.name) + const rockethRecord = env.getOrNull(spec.name) + if (registryMetadata && rockethRecord?.argsData && rockethRecord.argsData !== '0x') { + const chainId = await getTargetChainIdFromEnv(env) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const addressBook: any = getAddressBookForType(spec.addressBookType, chainId) + const entry = addressBook.getEntry(spec.name) + const rockethBlockNumber = toBlockNumber(rockethRecord.receipt?.blockNumber) + const addressBookBlockNumber = entry.deployment?.blockNumber + + const rockethIsNewer = + !entry.deployment?.argsData || + (rockethBlockNumber !== undefined && addressBookBlockNumber === undefined) || + (rockethBlockNumber !== undefined && + addressBookBlockNumber !== undefined && + rockethBlockNumber > addressBookBlockNumber) + + if (rockethIsNewer) { + // Hash from the artifact (with library resolution) so the stored value stays in lockstep + // with checkShouldSync's artifact-side comparison. Hashing rocketh's linked + // `deployedBytecode` would diverge for library-using contracts. + const deploymentMetadata: DeploymentMetadata = { + txHash: rockethRecord.transaction?.hash ?? '', + argsData: rockethRecord.argsData, + bytecodeHash: tryComputeArtifactBytecodeHash(spec.artifact) ?? '', + ...(rockethBlockNumber !== undefined && { blockNumber: rockethBlockNumber }), + } + addressBook.setDeploymentMetadata(spec.name, deploymentMetadata) + statusNotes.push('backfilled metadata') + } + } + + // Format status line for non-proxy contracts (two-column format with blank status icon position) + const statusSuffix = statusNotes.length > 0 ? ` (${statusNotes.join(', ')})` : '' + return { success: true, status: `✓ ${nonProxySyncIcon} ${spec.name} @ ${formatAddress(spec.address)}${statusSuffix}` } +} + +/** + * Options for sync display filtering + */ +export interface SyncOptions { + /** + * Tags requested in the deploy command (e.g., ['IssuanceAllocator:deploy', 'sync']). + * When set, only contracts matching these tags or with detected changes are displayed. + * Sync still runs for all contracts regardless of filter. + */ + tagFilter?: string[] +} + +/** + * Extract component names from deployment tags. + * + * Strips action suffixes (e.g., 'IssuanceAllocator:deploy' → 'IssuanceAllocator') + * and filters out the special 'sync' tag. + */ +function extractComponentNames(tags: string[]): Set { + const components = new Set() + for (const tag of tags) { + if (tag === SpecialTags.SYNC) continue + components.add(tag.split(':')[0]) + } + return components +} + +/** + * Check whether a sync status line indicates changes were detected. + * + * Icons: ↑ upgraded, ↻ synced/re-imported, ◷ pending, △ code changed + * Parenthetical notes also indicate notable state (but not "(not deployed)"). + */ +function statusHasChanges(status: string): boolean { + if (/[↑↻◷△]/.test(status)) return true + if (status.includes('(') && !status.includes('(not deployed)')) return true + return false +} + +/** + * Determine whether a contract's sync result should be displayed. + */ +function shouldDisplay( + spec: ContractSpec, + result: { success: boolean; status: string }, + filterComponents: Set | null, +): boolean { + if (!filterComponents) return true + if (!result.success) return true + if (statusHasChanges(result.status)) return true + const metadata = getContractMetadata(spec.addressBookType, spec.name) + return !!metadata?.componentTag && filterComponents.has(metadata.componentTag) +} + +/** + * Sync contract groups with on-chain state + * + * For each contract: + * - Sync proxy implementations with on-chain state + * - Import contract addresses into rocketh deployment records + * - Validate prerequisites exist on-chain + * - Show code changed indicator (△) when local bytecode differs from deployed + * + * When options.tagFilter is set, only contracts matching the requested tags + * or with detected changes are displayed. Sync still runs for all contracts. + */ +export async function syncContractGroups( + env: Environment, + groups: AddressBookGroup[], + options?: SyncOptions, +): Promise { + const client = graph.getPublicClient(env) + const failures: string[] = [] + let totalSynced = 0 + + // Build component filter from tags (null = no filtering) + const filterComponents = + options?.tagFilter && options.tagFilter.length > 0 ? extractComponentNames(options.tagFilter) : null + const isFiltering = filterComponents !== null && filterComponents.size > 0 + let totalSuppressed = 0 + + for (const group of groups) { + // Buffer results so we can filter display without affecting sync + const results: Array<{ spec: ContractSpec; result: { success: boolean; status: string } }> = [] + + for (const spec of group.contracts) { + const result = await syncContract(env, client, spec) + results.push({ spec, result }) + + if (!result.success) { + failures.push(spec.name) + } else { + totalSynced++ + if (spec.proxy) { + totalSynced++ + } + } + } + + // Filter which results to display + const visible = isFiltering + ? results.filter(({ spec, result }) => shouldDisplay(spec, result, filterComponents)) + : results + const suppressed = results.length - visible.length + totalSuppressed += suppressed + + if (visible.length > 0) { + env.showMessage(`\n📦 ${group.label}`) + for (const { result } of visible) { + env.showMessage(` ${result.status}`) + } + if (suppressed > 0) { + env.showMessage(` ... ${suppressed} unchanged`) + } + } + } + + if (isFiltering && totalSuppressed > 0) { + env.showMessage(`\n ... ${totalSuppressed} unchanged contracts hidden (--tags sync for full output)`) + } + + return { success: failures.length === 0, totalSynced, failures } +} + +/** + * Sync a single component from the contract registry with on-chain state. + * + * Resolves the address book, builds a ContractSpec, and runs the same sync + * logic as the full sync script — reading on-chain state to confirm and + * propagate reality into address books and rocketh records. + * + * Components call this immediately before and after mutating actions so the + * action operates on a confirmed-fresh view, without requiring a separate + * global sync to have run first. + */ +export async function syncComponentFromRegistry(env: Environment, contract: RegistryEntry): Promise { + const chainId = await getTargetChainIdFromEnv(env) + const addressBook = getAddressBookForType(contract.addressBook, chainId) + const metadata = getContractMetadata(contract.addressBook, contract.name) + if (!metadata) { + throw new Error(`Contract '${contract.name}' not found in ${contract.addressBook} registry`) + } + + const spec = buildContractSpec(contract.addressBook, contract.name, metadata, addressBook, chainId) + const client = graph.getPublicClient(env) + const result = await syncContract(env, client, spec) + + env.showMessage(` ${result.status}`) + if (!result.success) { + throw new Error(`Sync failed for ${contract.name}: ${result.status}`) + } +} + +/** + * Sync multiple components from the contract registry with on-chain state. + * + * Convenience wrapper around `syncComponentFromRegistry` for scripts that need + * a small set of contracts in sync before they read them — typically the + * contract being acted on plus its direct on-chain prerequisites (Controller, + * shared implementations, etc.). + */ +export async function syncComponentsFromRegistry(env: Environment, contracts: RegistryEntry[]): Promise { + for (const contract of contracts) { + await syncComponentFromRegistry(env, contract) + } +} + +/** + * Run the full address book sync across every deployable contract in every + * address book (Horizon, SubgraphService, Issuance). + * + * This is the implementation behind both the `00_sync.ts` deploy script (run + * via `--tags sync`) and the `deploy:sync` Hardhat task. Orchestration scripts + * that need many contracts in sync before they run (e.g. the GIP-0088 upgrade + * batch builder) call this directly instead of relying on a tag dependency. + * + * On failure, exits the process with code 1 after printing remediation hints. + */ +export async function runFullSync(env: Environment): Promise { + // Get chainId from provider (will be 31337 in fork mode) + const chainIdHex = await env.network.provider.request({ method: 'eth_chainId' }) + const providerChainId = Number(chainIdHex) + + // Auto-detect fork network from anvil if not explicitly set + if (providerChainId === 31337 && !getForkNetwork(env.name)) { + const detected = await autoDetectForkNetwork() + if (detected) { + env.showMessage(`\n🔍 Auto-detected fork network: ${detected}`) + } + } + + // Determine target chain ID for address book lookups + const forkNetwork = getForkNetwork(env.name) + const isForking = isForkMode(env.name) + const forkChainId = getForkTargetChainId(env.name) + const targetChainId = forkChainId ?? providerChainId + + // Check for common misconfiguration: localhost without FORK_NETWORK and not a detectable fork + if (providerChainId === 31337 && !forkNetwork) { + throw new Error( + `Running on localhost (chainId 31337) without FORK_NETWORK set.\n\n` + + `If you're testing against a forked network, set the environment variable:\n` + + ` export FORK_NETWORK=arbitrumSepolia\n` + + ` npx hardhat deploy:sync --network localhost\n\n` + + `Or use ephemeral fork mode:\n` + + ` HARDHAT_FORK=arbitrumSepolia npx hardhat deploy:sync`, + ) + } + + if (forkNetwork) { + const forkStateDir = getForkStateDir(env.name, forkNetwork) + env.showMessage(`\n🔄 Sync: ${forkNetwork} fork (chainId: ${targetChainId})`) + env.showMessage(` Using fork-local address books (${forkStateDir}/)`) + } else { + env.showMessage(`\n🔄 Sync: ${env.name} (chainId: ${providerChainId})`) + } + + // Get address books (automatically uses fork-local copies in fork mode) + const horizonAddressBook = graph.getHorizonAddressBook(targetChainId) + const ssAddressBook = graph.getSubgraphServiceAddressBook(targetChainId) + + const groups: AddressBookGroup[] = [] + + // --- Horizon contracts --- + const horizonContracts: ContractSpec[] = getDeployableContracts('horizon').map((name) => { + const metadata = getContractMetadata('horizon', name) + if (!metadata) throw new Error(`Contract ${name} not found in horizon registry`) + return buildContractSpec('horizon', name, metadata, horizonAddressBook, targetChainId) + }) + groups.push({ label: 'Horizon', contracts: horizonContracts, addressBook: horizonAddressBook }) + + // --- SubgraphService contracts --- + const ssContracts: ContractSpec[] = getDeployableContracts('subgraph-service').map((name) => { + const metadata = getContractMetadata('subgraph-service', name) + if (!metadata) throw new Error(`Contract ${name} not found in subgraph-service registry`) + return buildContractSpec('subgraph-service', name, metadata, ssAddressBook, targetChainId) + }) + groups.push({ label: 'SubgraphService', contracts: ssContracts, addressBook: ssAddressBook }) + + // --- Issuance contracts --- + const issuanceBookPath = getIssuanceAddressBookPath() + const issuanceAddressBook = existsSync(issuanceBookPath) ? graph.getIssuanceAddressBook(targetChainId) : null + + if (issuanceAddressBook) { + const issuanceContracts: ContractSpec[] = getDeployableContracts('issuance').map((name) => { + const metadata = getContractMetadata('issuance', name) + if (!metadata) throw new Error(`Contract ${name} not found in issuance registry`) + return buildContractSpec('issuance', name, metadata, issuanceAddressBook, targetChainId) + }) + if (issuanceContracts.length > 0) { + groups.push({ label: 'Issuance', contracts: issuanceContracts, addressBook: issuanceAddressBook }) + } + } + + // Parse --tags from process.argv to filter sync display when invoked via + // `hardhat deploy --tags ...` (does nothing for the standalone deploy:sync task) + const tagsIndex = process.argv.indexOf('--tags') + const requestedTags = + tagsIndex !== -1 && tagsIndex < process.argv.length - 1 ? process.argv[tagsIndex + 1].split(',') : [] + + const syncOptions: SyncOptions = requestedTags.length > 0 ? { tagFilter: requestedTags } : {} + + const result = await syncContractGroups(env, groups, syncOptions) + + if (!result.success) { + env.showMessage(`\n❌ Sync failed: address book does not match chain state.\n`) + env.showMessage(`The following contracts are in address book but have no code on-chain:`) + env.showMessage(` ${result.failures.join(', ')}\n`) + if (isForking) { + env.showMessage(`This is likely because the fork was restarted.\n`) + env.showMessage(`To fix, reset fork state and re-run:`) + env.showMessage(` npx hardhat deploy:reset-fork --network localhost`) + } else { + env.showMessage(`Possible causes:`) + env.showMessage(` 1. Address book has incorrect addresses for this network`) + env.showMessage(` 2. Running against wrong network`) + } + process.exit(1) + } + + env.showMessage(`\n✅ Sync complete: ${result.totalSynced} contracts synced\n`) +} + +/** Filter deployable contracts from a registry namespace. */ +function getDeployableContracts(addressBook: AddressBookType): string[] { + return getContractsByAddressBook(addressBook) + .filter(([_, metadata]) => metadata.deployable !== false) + .map(([name]) => name) +} + +/** + * Contract status result (read-only, no sync operations) + */ +export interface ContractStatusResult { + /** Status line to display */ + line: string + /** Whether contract exists on-chain */ + exists: boolean + /** Optional warnings (e.g., address book stale) */ + warnings?: string[] + /** Proxy admin ownership state (only for proxied contracts) */ + proxyAdminOwner?: ProxyAdminOwner + /** Proxy admin address (only for proxied contracts) */ + proxyAdminAddress?: string + /** Proxy admin owner address (only for proxied contracts with on-chain query) */ + proxyAdminOwnerAddress?: string + /** Whether local compiled bytecode differs from deployed bytecode */ + codeChanged?: boolean + /** Whether a pending implementation upgrade exists */ + hasPendingImplementation?: boolean +} + +/** + * Options for querying proxy admin ownership during status checks + */ +export interface ProxyAdminOwnershipContext { + /** Governor address (from Controller) — required */ + governor: string + /** Deployer address (from named accounts) — optional, used for labelling */ + deployer?: string +} + +/** + * Query ProxyAdmin ownership and classify as governor/deployer/unknown + * + * The 🔑 warning icon is shown for anything NOT governor-owned. + * Deployer detection is best-effort (only when deployer address is known). + */ +async function queryProxyAdminOwnership( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client: any, + proxyAdminAddress: string, + ctx: ProxyAdminOwnershipContext, +): Promise<{ owner: ProxyAdminOwner; ownerAddress: string }> { + try { + const ownerAddress = (await client.readContract({ + address: proxyAdminAddress as `0x${string}`, + abi: [ + { + inputs: [], + name: 'owner', + outputs: [{ type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + ], + functionName: 'owner', + })) as string + + if (ownerAddress.toLowerCase() === ctx.governor.toLowerCase()) { + return { owner: 'governor', ownerAddress } + } else if (ctx.deployer && ownerAddress.toLowerCase() === ctx.deployer.toLowerCase()) { + return { owner: 'deployer', ownerAddress } + } + return { owner: 'other', ownerAddress } + } catch { + return { owner: 'unknown', ownerAddress: '' } + } +} + +/** + * Get contract status line (read-only, no sync operations) + * + * Returns a formatted status line similar to sync output: + * - ✓ = ok, △ = code changed, ◷ = pending upgrade, ○ = not deployed, ❌ = error + * - 🔑 = ProxyAdmin still owned by deployer (not yet transferred to governor) + * + * @param client - Viem public client + * @param addressBookType - Which address book this contract belongs to + * @param addressBook - Address book instance + * @param contractName - Name of the contract in the registry + * @param metadata - Contract metadata from registry (optional, will look up if not provided) + * @param ownershipCtx - Governor/deployer context for proxy admin ownership checks + */ +export async function getContractStatusLine( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client: any, + addressBookType: AddressBookType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addressBook: any, + contractName: string, + metadata?: ContractMetadata, + ownershipCtx?: ProxyAdminOwnershipContext, +): Promise { + const meta = metadata ?? getContractMetadata(addressBookType, contractName) + const entryName = getAddressBookEntryName(addressBookType, contractName) + + try { + const entry = addressBook.entryExists(entryName) ? addressBook.getEntry(entryName) : null + if (!entry?.address) { + return { line: `○ ${contractName} (not deployed)`, exists: false } + } + + // Address-only entries don't require code + if (meta?.addressOnly) { + return { line: `✓ ${contractName} @ ${formatAddress(entry.address)}`, exists: true } + } + + // If no client available, show address book status without on-chain verification + if (!client) { + if (meta?.proxyType && entry.implementation) { + return { + line: `? ${contractName} @ ${formatAddress(entry.address)} → ${formatAddress(entry.implementation)} (no on-chain check)`, + exists: true, + } + } + return { line: `? ${contractName} @ ${formatAddress(entry.address)} (no on-chain check)`, exists: true } + } + + // Check if code exists on-chain + const code = await client.getCode({ address: entry.address as `0x${string}` }) + if (!code || code === '0x') { + return { line: `❌ ${contractName} @ ${formatAddress(entry.address)}: no code`, exists: false } + } + + // For proxies, read actual on-chain implementation (not address book's possibly-stale value) + if (meta?.proxyType) { + // Get proxy admin address + let proxyAdminAddress: string | undefined + if (entry.proxyAdmin) { + proxyAdminAddress = entry.proxyAdmin + } else if (meta.proxyAdminName) { + const adminEntryName = getAddressBookEntryName(addressBookType, meta.proxyAdminName) + proxyAdminAddress = addressBook.entryExists(adminEntryName) + ? addressBook.getEntry(adminEntryName)?.address + : undefined + } + + // Read actual implementation from chain + let actualImpl: string | undefined + try { + actualImpl = await getOnChainImplementation(client, entry.address, meta.proxyType, proxyAdminAddress) + } catch { + // Fall back to address book if on-chain read fails + actualImpl = entry.implementation + } + + if (actualImpl) { + // Check code changes: own artifact first, then shared implementation's artifact + let { codeChanged } = checkCodeChanged(meta.artifact, addressBook, entryName) + if (!codeChanged && meta.sharedImplementation) { + const sharedMeta = getContractMetadata(addressBookType, meta.sharedImplementation) + if (sharedMeta?.artifact) { + const sharedCheck = checkCodeChanged(sharedMeta.artifact, addressBook, meta.sharedImplementation) + codeChanged = sharedCheck.codeChanged + } + } + + // Query proxy admin ownership for OZ v5 transparent proxies only + // (old Graph proxies are controller-governed, owner() doesn't exist) + let proxyAdminOwner: ProxyAdminOwner | undefined + let proxyAdminOwnerAddress: string | undefined + if (ownershipCtx && proxyAdminAddress && meta.proxyType !== 'graph') { + const ownership = await queryProxyAdminOwnership(client, proxyAdminAddress, ownershipCtx) + proxyAdminOwner = ownership.owner + proxyAdminOwnerAddress = ownership.ownerAddress + } + + const result = formatProxyStatusLine({ + name: contractName, + proxyAddress: entry.address, + implAddress: actualImpl, + pendingAddress: entry.pendingImplementation?.address, + codeChanged, + proxyAdminOwner, + }) + + // Check if address book is stale (on-chain impl differs from recorded impl) + const warnings: string[] = [] + const bookImpl = entry.implementation + if (bookImpl && actualImpl.toLowerCase() !== bookImpl.toLowerCase()) { + warnings.push(`address book stale: recorded impl ${formatAddress(bookImpl)}`) + } + + return { + line: result.line, + exists: true, + warnings: warnings.length > 0 ? warnings : undefined, + proxyAdminOwner, + proxyAdminAddress, + proxyAdminOwnerAddress, + codeChanged, + hasPendingImplementation: !!entry.pendingImplementation?.address, + } + } + } + + // Non-proxy contract — check for code changes against stored bytecodeHash + const { codeChanged } = meta?.artifact + ? checkCodeChanged(meta.artifact, addressBook, entryName) + : { codeChanged: false } + const icon = codeChanged ? '△' : '✓' + return { line: `${icon} ${contractName} @ ${formatAddress(entry.address)}`, exists: true, codeChanged } + } catch (e) { + const errMsg = e instanceof Error ? e.message.split('\n')[0].slice(0, 120) : String(e).slice(0, 120) + return { line: `⚠ ${contractName}: error reading (${errMsg})`, exists: false } + } +} + +/** + * Check if any deployable proxy across all address books has a pending + * implementation or local code that differs from the deployed version. + * + * Used by status scripts for next-step guidance without duplicating + * address book scanning logic. + */ +export function checkAllProxyStates(targetChainId: number): { anyCodeChanged: boolean; anyPending: boolean } { + const addressBookTypes: AddressBookType[] = ['horizon', 'subgraph-service', 'issuance'] + let anyCodeChanged = false + let anyPending = false + + for (const abType of addressBookTypes) { + const ab: AnyAddressBookOps = getAddressBookForType(abType, targetChainId) + + for (const [name, meta] of getContractsByAddressBook(abType)) { + if (!meta.deployable || !meta.proxyType) continue + if (!ab.entryExists(name)) continue + const entry = ab.getEntry(name) + if (!entry?.address) continue + + if (entry.pendingImplementation?.address) anyPending = true + if (meta.artifact) { + const { codeChanged } = checkCodeChanged(meta.artifact, ab, name) + if (codeChanged) anyCodeChanged = true + } else if (meta.sharedImplementation) { + const sharedMeta = getContractMetadata(abType, meta.sharedImplementation) + if (sharedMeta?.artifact) { + const { codeChanged } = checkCodeChanged(sharedMeta.artifact, ab, meta.sharedImplementation) + if (codeChanged) anyCodeChanged = true + } + } + + if (anyCodeChanged && anyPending) return { anyCodeChanged, anyPending } + } + } + + return { anyCodeChanged, anyPending } +} diff --git a/packages/deployment/lib/task-utils.ts b/packages/deployment/lib/task-utils.ts new file mode 100644 index 000000000..62826905d --- /dev/null +++ b/packages/deployment/lib/task-utils.ts @@ -0,0 +1,134 @@ +/** + * Shared Task Utilities + * + * Common functions used across Hardhat tasks. Consolidates helpers that were + * previously duplicated across grant-role, revoke-role, reo-tasks, eth-tasks, + * grt-tasks, and check-deployer. + */ + +import { configVariable } from 'hardhat/config' + +import { getAddressBookForType } from './address-book-utils.js' +import { type AddressBookType, CONTRACT_REGISTRY } from './contract-registry.js' + +/** + * Convert network name to env var prefix: arbitrumSepolia → ARBITRUM_SEPOLIA + */ +export function networkToEnvPrefix(networkName: string): string { + return networkName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase() +} + +/** + * Resolve a configuration variable using Hardhat's hook chain (keystore + env fallback) + * + * Tries the Hardhat keystore plugin first, then falls back to environment variables. + * Returns undefined if the variable is not found in either location. + * + * @param hre - Hardhat Runtime Environment + * @param name - Configuration variable name (e.g., 'ARBITRUM_SEPOLIA_DEPLOYER_KEY') + * @returns The resolved value or undefined if not set + */ +export async function resolveConfigVar(hre: unknown, name: string): Promise { + try { + const variable = configVariable(name) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hooks = (hre as any).hooks + + const value = await hooks.runHandlerChain( + 'configurationVariables', + 'fetchValue', + [variable], + async (_context: unknown, v: { name: string }) => { + const envValue = process.env[v.name] + if (typeof envValue !== 'string') { + throw new Error(`Variable ${v.name} not found`) + } + return envValue + }, + ) + return value + } catch { + return undefined + } +} + +/** + * Get the deployer key name for a network, handling fork mode. + * + * In fork mode (network name is 'fork'), uses the HARDHAT_FORK env var to + * determine the source network. Falls back to 'arbitrumSepolia'. + * + * @param networkName - Network name (e.g., 'fork', 'arbitrumSepolia') + * @returns Key name (e.g., 'ARBITRUM_SEPOLIA_DEPLOYER_KEY') + */ +export function getDeployerKeyName(networkName: string): string { + const effectiveNetwork = networkName === 'fork' ? (process.env.HARDHAT_FORK ?? 'arbitrumSepolia') : networkName + return `${networkToEnvPrefix(effectiveNetwork)}_DEPLOYER_KEY` +} + +/** + * Resolve contract from registry by name + * + * Searches across all address books for a matching contract with roles defined. + * Returns the address book type and role list if found. + */ +export function resolveContractFromRegistry( + contractName: string, +): { addressBook: AddressBookType; roles: readonly string[] } | null { + for (const [book, contracts] of Object.entries(CONTRACT_REGISTRY)) { + const contract = contracts[contractName as keyof typeof contracts] as { roles?: readonly string[] } | undefined + if (contract?.roles) { + return { addressBook: book as AddressBookType, roles: contract.roles } + } + } + return null +} + +/** + * Get contract address from address book + */ +export function getContractAddress(addressBook: AddressBookType, contractName: string, chainId: number): string | null { + const book = getAddressBookForType(addressBook, chainId) + + // Address book type is a union — cast to access entryExists/getEntry with a runtime name + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anyBook = book as any + if (!anyBook.entryExists(contractName)) { + return null + } + + return anyBook.getEntry(contractName)?.address ?? null +} + +/** + * Format duration in seconds to human-readable string (e.g., "2d 3h 15m") + */ +export function formatDuration(seconds: bigint): string { + const days = seconds / 86400n + const hours = (seconds % 86400n) / 3600n + const mins = (seconds % 3600n) / 60n + + if (days > 0n) { + return `${days}d ${hours}h ${mins}m` + } else if (hours > 0n) { + return `${hours}h ${mins}m` + } else { + return `${mins}m` + } +} + +/** + * Format timestamp to human-readable string (ISO format without milliseconds) + */ +export function formatTimestamp(timestamp: bigint): string { + if (timestamp === 0n) { + return 'never' + } + + const date = new Date(Number(timestamp) * 1000) + return date + .toISOString() + .replace(/\.000Z$/, '') + .replace(/Z$/, '') + .replace('T', ' ') +} diff --git a/packages/deployment/lib/tx-builder-template.json b/packages/deployment/lib/tx-builder-template.json new file mode 100644 index 000000000..480d414bb --- /dev/null +++ b/packages/deployment/lib/tx-builder-template.json @@ -0,0 +1,14 @@ +{ + "version": "1.0", + "chainId": "42161", + "createdAt": 0, + "meta": { + "name": "Governance Transaction Batch", + "description": "", + "txBuilderVersion": "1.11.1", + "createdFromSafeAddress": "", + "createdFromOwnerAddress": "", + "checksum": "" + }, + "transactions": [] +} diff --git a/packages/deployment/lib/tx-builder.ts b/packages/deployment/lib/tx-builder.ts new file mode 100644 index 000000000..c5160970c --- /dev/null +++ b/packages/deployment/lib/tx-builder.ts @@ -0,0 +1,181 @@ +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +// ESM equivalent of __dirname +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +/** + * Core transaction fields (Safe TX Builder compatible) + */ +export interface BuilderTx { + to: string + data: string + value: string | number + // The Safe Tx Builder UI expects these keys even when null + contractMethod?: null + contractInputsValues?: null +} + +/** + * Human-readable decoded function call + */ +export interface DecodedCall { + /** Function signature, e.g., "upgradeAndCall(address,address,bytes)" */ + function: string + /** Decoded arguments with labels */ + args: Record +} + +/** + * State change information for upgrade transactions + */ +export interface StateChange { + /** Current value (before TX) */ + current: string + /** New value (after TX) */ + new: string +} + +/** + * Rich transaction metadata for governance transparency + */ +export interface TxMetadata { + /** Human-readable label for 'to' address, e.g., "IssuanceAllocator_ProxyAdmin" */ + toLabel?: string + /** Decoded function call */ + decoded?: DecodedCall + /** State changes this TX will cause */ + stateChanges?: Record + /** Related contract name */ + contractName?: string + /** Notes for governance reviewers */ + notes?: string +} + +/** + * Enhanced transaction with metadata (internal representation) + */ +export interface EnhancedBuilderTx extends BuilderTx { + /** Rich metadata for governance review (not part of Safe TX format) */ + _metadata?: TxMetadata +} + +/** + * Safe TX Builder JSON format (compatible with Safe{Wallet} Transaction Builder) + */ +interface SafeTxBuilderContents { + version: string + chainId: string + createdAt: number + meta?: { + name?: string + description?: string + txBuilderVersion?: string + createdFromSafeAddress?: string + createdFromOwnerAddress?: string + checksum?: string + [key: string]: unknown + } + transactions: BuilderTx[] +} + +/** + * Enhanced TX builder contents with governance metadata + */ +interface EnhancedTxBuilderContents extends SafeTxBuilderContents { + /** Rich metadata for each transaction (parallel array to transactions) */ + _transactionMetadata?: TxMetadata[] +} + +export interface TxBuilderOptions { + template?: string + outputDir?: string + /** Optional name for the output file (without extension). If not provided, uses timestamp. */ + name?: string + /** Optional metadata to describe the transaction batch */ + meta?: { + name?: string + description?: string + } +} + +export class TxBuilder { + private contents: EnhancedTxBuilderContents + private metadata: TxMetadata[] = [] + public readonly outputFile: string + + constructor(chainId: string | number | bigint, options: TxBuilderOptions = {}) { + const templatePath = options.template ?? path.resolve(__dirname, 'tx-builder-template.json') + const createdAt = Date.now() + + this.contents = JSON.parse(fs.readFileSync(templatePath, 'utf8')) as EnhancedTxBuilderContents + this.contents.createdAt = createdAt + this.contents.chainId = chainId.toString() + if (!Array.isArray(this.contents.transactions)) { + this.contents.transactions = [] + } + + // Override metadata if provided + if (options.meta) { + this.contents.meta = { + ...this.contents.meta, + ...options.meta, + } + } + + const outputDir = options.outputDir ?? process.cwd() + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + const filename = options.name ? `${options.name}.json` : `tx-builder-${createdAt}.json` + this.outputFile = path.join(outputDir, filename) + } + + /** + * Add a transaction to the batch + * @param tx - Transaction data + * @param metadata - Optional rich metadata for governance review + */ + addTx(tx: BuilderTx, metadata?: TxMetadata) { + this.contents.transactions.push({ ...tx, contractMethod: null, contractInputsValues: null }) + this.metadata.push(metadata ?? {}) + } + + /** + * Get the transactions in the batch + */ + getTransactions(): readonly BuilderTx[] { + return this.contents.transactions + } + + /** + * Get the metadata for transactions + */ + getMetadata(): readonly TxMetadata[] { + return this.metadata + } + + /** + * Check if the batch has any transactions + */ + isEmpty(): boolean { + return this.contents.transactions.length === 0 + } + + /** + * Save to file with metadata for governance review + * Outputs both Safe-compatible format and enhanced metadata + */ + saveToFile() { + // Include metadata in output for governance review + const output: EnhancedTxBuilderContents = { + ...this.contents, + _transactionMetadata: this.metadata.length > 0 ? this.metadata : undefined, + } + fs.writeFileSync(this.outputFile, JSON.stringify(output, null, 2) + '\n') + return this.outputFile + } +} diff --git a/packages/deployment/lib/tx-executor.ts b/packages/deployment/lib/tx-executor.ts new file mode 100644 index 000000000..41b479f9f --- /dev/null +++ b/packages/deployment/lib/tx-executor.ts @@ -0,0 +1,134 @@ +import fs from 'fs' + +import type { BuilderTx } from '../lib/tx-builder.js' + +interface SafeTxBatch { + version: string + chainId: string + createdAt: number + meta?: unknown + transactions: BuilderTx[] +} + +// Extended HRE with ethers and network plugins +interface ExtendedHRE { + ethers: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getSigner: (address: string) => Promise + } + network: { + provider: { + request: (args: { method: string; params: unknown[] }) => Promise + send: (method: string, params: unknown[]) => Promise + } + } +} + +/** + * Execute Safe Transaction Builder JSON batches via impersonated governance + * + * This utility allows tests to execute the same TX batches that will be sent + * to governance in production, ensuring end-to-end validation. + * + * Usage in tests: + * ```typescript + * // 1. Generate TX batch (same as production) + * const result = await buildRewardsEligibilityUpgradeTxs(hre, params) + * + * // 2. Execute via impersonation (test only) + * const executor = new GovernanceTxExecutor(hre) + * await executor.executeBatch(result.outputFile, governorAddress) + * + * // 3. Verify integration (same as production) + * await run('deploy:verify-integration') + * ``` + */ +export class GovernanceTxExecutor { + private extHre: ExtendedHRE + + constructor(hre: unknown) { + this.extHre = hre as ExtendedHRE + } + + /** + * Execute Safe TX Builder JSON batch via impersonated governance account + * + * This simulates governance execution in a test environment by: + * 1. Parsing the Safe TX Builder JSON file + * 2. Impersonating the governor address + * 3. Funding the governor with ETH for gas + * 4. Executing each transaction in sequence + * 5. Stopping impersonation + * + * @param txBatchFile - Path to Safe TX Builder JSON file + * @param governorAddress - Address to impersonate as governor + * @throws Error if any transaction fails + */ + async executeBatch(txBatchFile: string, governorAddress: string): Promise { + const { ethers } = this.extHre + + // 1. Parse Safe TX Builder JSON + const batchContents = fs.readFileSync(txBatchFile, 'utf8') + const batch: SafeTxBatch = JSON.parse(batchContents) + + console.log(`\n📋 Executing TX batch from: ${txBatchFile}`) + console.log(` Chain ID: ${batch.chainId}`) + console.log(` Transactions: ${batch.transactions.length}`) + console.log(` Governor: ${governorAddress}\n`) + + // 2. Impersonate governor + await this.extHre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [governorAddress], + }) + + // 3. Fund governor with ETH for gas + await this.extHre.network.provider.send('hardhat_setBalance', [ + governorAddress, + '0x56BC75E2D63100000', // 100 ETH + ]) + + // 4. Execute each transaction in batch + const governor = await ethers.getSigner(governorAddress) + + for (let i = 0; i < batch.transactions.length; i++) { + const tx = batch.transactions[i] + console.log(` ${i + 1}/${batch.transactions.length} Executing TX to ${tx.to}...`) + + try { + const receipt = await governor.sendTransaction({ + to: tx.to, + data: tx.data, + value: tx.value, + }) + + await receipt.wait() + console.log(` ✓ Success (gas: ${receipt.gasLimit})`) + } catch (error: unknown) { + console.error(` ✗ Failed: ${error instanceof Error ? error.message : String(error)}`) + throw new Error(`Transaction ${i + 1} failed: ${error instanceof Error ? error.message : String(error)}`) + } + } + + // 5. Stop impersonation + await this.extHre.network.provider.request({ + method: 'hardhat_stopImpersonatingAccount', + params: [governorAddress], + }) + + console.log(`\n✅ All ${batch.transactions.length} transactions executed successfully\n`) + } + + /** + * Parse Safe TX Builder JSON without executing + * + * Useful for validation and inspection of TX batches + * + * @param txBatchFile - Path to Safe TX Builder JSON file + * @returns Parsed Safe TX batch + */ + parseBatch(txBatchFile: string): SafeTxBatch { + const batchContents = fs.readFileSync(txBatchFile, 'utf8') + return JSON.parse(batchContents) as SafeTxBatch + } +} diff --git a/packages/deployment/lib/upgrade-implementation.ts b/packages/deployment/lib/upgrade-implementation.ts new file mode 100644 index 000000000..81b5fc814 --- /dev/null +++ b/packages/deployment/lib/upgrade-implementation.ts @@ -0,0 +1,319 @@ +import type { Environment } from '@rocketh/core/types' +import { encodeFunctionData } from 'viem' + +import { getAddressBookForType, getTargetChainIdFromEnv } from './address-book-utils.js' +import { GRAPH_PROXY_ADMIN_ABI, OZ_PROXY_ADMIN_ABI } from './abis.js' +import { type AddressBookType, type ProxyType, type RegistryEntry } from './contract-registry.js' +import { getOnChainImplementation } from './deploy-implementation.js' +import { createGovernanceTxBuilder, saveGovernanceTx } from './execute-governance.js' +import { graph } from '../rocketh/deploy.js' +import type { TxBuilder, TxMetadata } from './tx-builder.js' + +/** + * Configuration for upgrading an implementation (manual override mode) + * @deprecated Use registry-driven approach instead: upgradeImplementation(env, 'ContractName', overrides?) + */ +export interface ImplementationUpgradeConfig { + /** Contract name (e.g., 'RewardsManager', 'SubgraphService') */ + contractName: string + + /** + * Name of the proxy admin entry in address book. + * Example: 'GraphProxyAdmin' for legacy GraphProxy contracts. + * + * Optional for OZ v5 TransparentUpgradeableProxy contracts (subgraph-service + * and issuance) — the per-proxy admin address is read from the contract + * entry's proxyAdmin field. + */ + proxyAdminName?: string + + /** + * Implementation contract name if different from contractName. + * Used when a proxy is upgraded to a different contract type. + * + * Example: ReclaimedRewards proxy upgraded to DirectAllocation implementation + * contractName: 'ReclaimedRewards' + * implementationName: 'DirectAllocation' + * + * Default: same as contractName + */ + implementationName?: string + + /** + * Proxy type + * - 'graph': Graph Protocol's custom proxy (upgrade + acceptProxy) + * - 'transparent': OpenZeppelin TransparentUpgradeableProxy (upgradeAndCall) + * + * Default: 'graph' + */ + proxyType?: ProxyType + + /** + * Address book to use + * Default: 'horizon' + */ + addressBook?: AddressBookType +} + +/** + * Optional overrides for registry-driven upgrade + */ +export interface ImplementationUpgradeOverrides { + /** + * Implementation contract name if different from contractName. + * Used when a proxy is upgraded to a different contract type. + * + * Example: ReclaimedRewards proxy upgraded to DirectAllocation implementation + */ + implementationName?: string + + /** + * Override proxy admin name from registry + */ + proxyAdminName?: string +} + +/** + * Result of implementation upgrade + */ +export interface ImplementationUpgradeResult { + /** Whether upgrade was needed */ + upgraded: boolean + + /** Path to the generated TX batch file */ + txFile?: string + + /** Whether TX was executed (fork mode only) */ + executed: boolean +} + +/** + * Create upgrade config from registry entry + */ +function createUpgradeConfigFromRegistry( + entry: RegistryEntry, + overrides?: ImplementationUpgradeOverrides, +): ImplementationUpgradeConfig { + return { + contractName: entry.name, + proxyAdminName: overrides?.proxyAdminName ?? entry.proxyAdminName, + implementationName: overrides?.implementationName, + proxyType: entry.proxyType, + addressBook: entry.addressBook, + } +} + +/** + * Upgrade an implementation via governance TX (registry-driven) + * + * @example Registry-driven with Contracts object (recommended): + * ```typescript + * import { Contracts } from '../../lib/contract-registry.js' + * await upgradeImplementation(env, Contracts.horizon.RewardsManager) + * await upgradeImplementation(env, Contracts["subgraph-service"].SubgraphService) + * await upgradeImplementation(env, Contracts.issuance.ReclaimedRewards, { + * implementationName: 'DirectAllocation', // Upgrade to different implementation + * }) + * ``` + * + * @example Config-based (legacy): + * ```typescript + * await upgradeImplementation(env, { + * contractName: 'SubgraphService', + * proxyType: 'transparent', + * addressBook: 'subgraph-service', + * }) + * ``` + */ +/** + * Build upgrade TXs for a contract and add them to an existing builder. + * + * Checks the address book for a pendingImplementation. If found, encodes upgrade + * TX(s) and adds them to the provided builder. Returns without exiting. + * + * Use this when building a batch of upgrades (e.g., GIP-level stage scripts). + * For single-contract upgrades that save and exit, use `upgradeImplementation`. + * + * @returns Whether an upgrade was needed (pendingImplementation existed) + */ +export async function buildUpgradeTxs( + env: Environment, + entryOrConfig: RegistryEntry | ImplementationUpgradeConfig, + builder: TxBuilder, + overrides?: ImplementationUpgradeOverrides, +): Promise<{ upgraded: boolean }> { + const config: ImplementationUpgradeConfig = + 'name' in entryOrConfig ? createUpgradeConfigFromRegistry(entryOrConfig, overrides) : entryOrConfig + const { contractName, proxyAdminName, proxyType = 'graph', addressBook = 'horizon' } = config + + const targetChainId = await getTargetChainIdFromEnv(env) + const addressBookInstance = getAddressBookForType(addressBook, targetChainId) + + // Check for pending implementation + const contractEntry = addressBookInstance.getEntry(contractName) + if (!contractEntry?.pendingImplementation?.address) { + // No pending implementation stored — check if a shared implementation has changed on-chain + const implName = config.implementationName + if (implName && contractEntry?.address) { + const implDepName = `${implName}_Implementation` + const implDep = env.getOrNull(implDepName) + if (implDep) { + const client = graph.getPublicClient(env) + const onChainImpl = await getOnChainImplementation(client, contractEntry.address, proxyType) + if (onChainImpl.toLowerCase() !== implDep.address.toLowerCase()) { + // Shared implementation changed — auto-set pendingImplementation + const implMetadata = addressBookInstance.getDeploymentMetadata(implDepName) + if (!implMetadata) { + throw new Error( + `${contractName}: deployment metadata missing for ${implDepName}. ` + + `Run the implementation deploy script (or sync) before invoking upgrade.`, + ) + } + addressBookInstance.setPendingImplementationWithMetadata(contractName, implDep.address, implMetadata) + env.showMessage(` ⚠️ ${contractName}: shared implementation changed, setting pending upgrade`) + // Fall through to process the upgrade + } else { + env.showMessage(` ✓ ${contractName}: no pending implementation`) + return { upgraded: false } + } + } else { + env.showMessage(` ✓ ${contractName}: no pending implementation`) + return { upgraded: false } + } + } else { + env.showMessage(` ✓ ${contractName}: no pending implementation`) + return { upgraded: false } + } + } + + // Re-read entry after potential auto-set + const updatedEntry = addressBookInstance.getEntry(contractName) + if (!updatedEntry?.pendingImplementation?.address) { + return { upgraded: false } + } + + // Get proxy admin address + let proxyAdminAddress: string | undefined + if (updatedEntry.proxyAdmin) { + proxyAdminAddress = updatedEntry.proxyAdmin + } else if (proxyAdminName) { + proxyAdminAddress = addressBookInstance.getEntry(proxyAdminName)?.address + } + + if (!proxyAdminAddress) { + throw new Error( + `No proxy admin found for ${contractName}. ` + + `Expected proxyAdmin field in address book entry or proxyAdminName in registry.`, + ) + } + + const proxyAddress = updatedEntry.address + const pendingImpl = updatedEntry.pendingImplementation!.address + const currentImpl = updatedEntry.implementation ?? 'unknown' + + env.showMessage(` + ${contractName}: ${pendingImpl.slice(0, 10)}... (${proxyType} proxy)`) + + if (proxyType === 'transparent') { + const upgradeData = encodeFunctionData({ + abi: OZ_PROXY_ADMIN_ABI, + functionName: 'upgradeAndCall', + args: [proxyAddress as `0x${string}`, pendingImpl as `0x${string}`, '0x'], + }) + + const metadata: TxMetadata = { + toLabel: `${contractName}_ProxyAdmin`, + contractName, + decoded: { + function: 'upgradeAndCall(address,address,bytes)', + args: { proxy: proxyAddress, implementation: pendingImpl, data: '0x [empty]' }, + }, + stateChanges: { + [`${contractName} implementation`]: { current: currentImpl, new: pendingImpl }, + }, + notes: 'OZ TransparentUpgradeableProxy upgrade via per-proxy ProxyAdmin', + } + builder.addTx({ to: proxyAdminAddress, value: '0', data: upgradeData }, metadata) + } else { + const upgradeData = encodeFunctionData({ + abi: GRAPH_PROXY_ADMIN_ABI, + functionName: 'upgrade', + args: [proxyAddress as `0x${string}`, pendingImpl as `0x${string}`], + }) + const acceptData = encodeFunctionData({ + abi: GRAPH_PROXY_ADMIN_ABI, + functionName: 'acceptProxy', + args: [pendingImpl as `0x${string}`, proxyAddress as `0x${string}`], + }) + + builder.addTx( + { to: proxyAdminAddress, value: '0', data: upgradeData }, + { + toLabel: 'GraphProxyAdmin', + contractName, + decoded: { + function: 'upgrade(address,address)', + args: { proxy: proxyAddress, implementation: pendingImpl }, + }, + notes: 'Graph legacy proxy upgrade (step 1/2: set pending implementation)', + }, + ) + builder.addTx( + { to: proxyAdminAddress, value: '0', data: acceptData }, + { + toLabel: 'GraphProxyAdmin', + contractName, + decoded: { + function: 'acceptProxy(address,address)', + args: { implementation: pendingImpl, proxy: proxyAddress }, + }, + stateChanges: { + [`${contractName} implementation`]: { current: currentImpl, new: pendingImpl }, + }, + notes: 'Graph legacy proxy upgrade (step 2/2: accept and activate)', + }, + ) + } + + return { upgraded: true } +} + +/** + * Upgrade an implementation via governance TX (registry-driven) + * + * Generates a governance TX batch file for a single contract upgrade, then exits. + * For batch upgrades (multiple contracts in one TX batch), use `buildUpgradeTxs` instead. + * + * @example Registry-driven with Contracts object (recommended): + * ```typescript + * import { Contracts } from '../../lib/contract-registry.js' + * await upgradeImplementation(env, Contracts.horizon.RewardsManager) + * await upgradeImplementation(env, Contracts["subgraph-service"].SubgraphService) + * await upgradeImplementation(env, Contracts.issuance.ReclaimedRewards, { + * implementationName: 'DirectAllocation', // Upgrade to different implementation + * }) + * ``` + */ +export async function upgradeImplementation( + env: Environment, + entryOrConfig: RegistryEntry | ImplementationUpgradeConfig, + overrides?: ImplementationUpgradeOverrides, +): Promise { + const config: ImplementationUpgradeConfig = + 'name' in entryOrConfig ? createUpgradeConfigFromRegistry(entryOrConfig, overrides) : entryOrConfig + + const builder = await createGovernanceTxBuilder(env, `upgrade-${config.contractName}`, { + name: `${config.contractName} Upgrade`, + description: `Upgrade ${config.contractName} proxy to new implementation`, + }) + + env.showMessage(`\n🔧 Upgrading ${config.contractName}...`) + const { upgraded } = await buildUpgradeTxs(env, entryOrConfig, builder, overrides) + + if (!upgraded) { + env.showMessage(`\n✓ No pending ${config.contractName} implementation to upgrade`) + return { upgraded: false, executed: false } + } + + saveGovernanceTx(env, builder, `${config.contractName} upgrade`) + return { upgraded: true, executed: false } +} diff --git a/packages/deployment/package.json b/packages/deployment/package.json new file mode 100644 index 000000000..9cd1d0e5f --- /dev/null +++ b/packages/deployment/package.json @@ -0,0 +1,82 @@ +{ + "name": "@graphprotocol/deployment", + "version": "0.1.0", + "description": "Unified deployment for Graph Protocol contracts", + "private": true, + "scripts": { + "build": "pnpm build:deps && pnpm build:self", + "build:self": "pnpm generate:abis", + "build:deps": "pnpm --filter @graphprotocol/deployment^... build", + "build:clean": "pnpm --filter @graphprotocol/contracts clean && pnpm build:deps", + "generate:abis": "tsx scripts/generate-abis.ts", + "deploy": "pnpm build:clean && hardhat deploy", + "deploy:sync": "hardhat deploy --tags sync", + "deploy:status": "hardhat deploy:deployment-status", + "test": "pnpm build:deps && pnpm test:self", + "test:self": "NODE_OPTIONS='--import tsx' mocha 'test/**/*.test.ts'", + "clean": "rm -rf cache", + "lint": "pnpm lint:ts; pnpm lint:md; pnpm lint:json", + "lint:ts": "eslint --fix --cache '**/*.{js,ts,cjs,mjs,jsx,tsx}'; prettier -w --cache --log-level warn '**/*.{js,ts,cjs,mjs,jsx,tsx}'", + "lint:md": "markdownlint --fix --ignore-path ../../.gitignore '**/*.md'; prettier -w --cache --log-level warn '**/*.md'", + "lint:json": "prettier -w --cache --log-level warn '**/*.json'" + }, + "dependencies": { + "@graphprotocol/contracts": "workspace:*", + "@graphprotocol/horizon": "workspace:*", + "@graphprotocol/interfaces": "workspace:*", + "@graphprotocol/issuance": "workspace:*", + "@graphprotocol/subgraph-service": "workspace:*", + "@graphprotocol/toolshed": "workspace:*", + "@rocketh/core": "^0.17.8", + "ethers": "^6.15.0", + "hardhat": "^3.1.5", + "viem": "catalog:" + }, + "devDependencies": { + "@nomicfoundation/hardhat-keystore": "catalog:", + "@nomicfoundation/hardhat-ethers": "^4.0.0", + "@nomicfoundation/hardhat-network-helpers": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^3.0.0", + "@openzeppelin/contracts": "5.4.0", + "@rocketh/deploy": "^0.17.8", + "@rocketh/diamond": "^0.17.11", + "@rocketh/doc": "^0.17.16", + "@rocketh/export": "^0.17.16", + "@rocketh/node": "^0.17.16", + "@rocketh/proxy": "^0.17.12", + "@rocketh/read-execute": "^0.17.8", + "@rocketh/verifier": "^0.17.16", + "@types/chai": "^4.3.0", + "@types/mocha": "^10.0.0", + "@types/node": "^20.0.0", + "chai": "^4.3.0", + "hardhat-deploy": "2.0.0-next.61", + "json5": "^2.2.3", + "mocha": "^10.7.0", + "rocketh": "^0.17.13", + "tsx": "^4.19.0", + "ts-node": "^10.9.0", + "typescript": "^5.5.0", + "eslint": "catalog:", + "lint-staged": "catalog:" + }, + "lint-staged": { + "**/*.ts": [ + "pnpm lint:ts" + ], + "**/*.js": [ + "pnpm lint:ts" + ], + "**/*.json": [ + "pnpm lint:json" + ] + }, + "engines": { + "node": ">=18.0.0" + }, + "type": "module", + "exports": { + "./lib/*": "./lib/*.js", + "./rocketh/*": "./rocketh/*.js" + } +} diff --git a/packages/deployment/prettier.config.cjs b/packages/deployment/prettier.config.cjs new file mode 100644 index 000000000..4e8dcf4f3 --- /dev/null +++ b/packages/deployment/prettier.config.cjs @@ -0,0 +1,5 @@ +const baseConfig = require('../../prettier.config.cjs') + +module.exports = { + ...baseConfig, +} diff --git a/packages/deployment/rocketh/config.ts b/packages/deployment/rocketh/config.ts new file mode 100644 index 000000000..e0ef1b47b --- /dev/null +++ b/packages/deployment/rocketh/config.ts @@ -0,0 +1,92 @@ +import type { ChainInfo, UserConfig } from '@rocketh/core/types' + +/** + * Rocketh configuration for The Graph deployment package + * + * This defines: + * - Named accounts (deployer, etc.) + * - Network-specific data + * - Chain configurations + * - Deploy scripts location + */ + +// Named accounts configuration +// Keys are account names, values define how to resolve the address per network/chain +export const accounts = { + // Default deployer - uses first account from the provider + deployer: { + default: 0, + }, + // Governor — second mnemonic account on local/test networks. + // On mainnet, governance is a multisig (not available via mnemonic). + // The on-chain source of truth is Controller.getGovernor() — see lib/controller-utils.ts. + // This named account exists so rocketh registers a signer, allowing deploy + // scripts to send TXs as governor via tx(). + governor: { + default: 1, + }, +} as const satisfies UserConfig['accounts'] + +// Network-specific data (can be extended as needed) +export const data = {} as const satisfies UserConfig['data'] + +// Chain info for networks we deploy to +const hardhatLocalChain: ChainInfo = { + id: 31337, + name: 'Hardhat Local', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: ['http://127.0.0.1:8545'] } }, + testnet: true, +} + +const graphLocalNetworkChain: ChainInfo = { + id: 1337, + name: 'Graph Local Network', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: ['http://chain:8545'] } }, + testnet: true, +} + +const arbitrumSepoliaChain: ChainInfo = { + id: 421614, + name: 'Arbitrum Sepolia', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: ['https://sepolia-rollup.arbitrum.io/rpc'] } }, + testnet: true, +} + +const arbitrumOneChain: ChainInfo = { + id: 42161, + name: 'Arbitrum One', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: ['https://arb1.arbitrum.io/rpc'] } }, + testnet: false, +} + +// Full rocketh configuration +// Note: Fork mode always uses chainId 31337 (rocketh/hardhat-deploy v2 expects this) +// The FORK_NETWORK env var is used by sync script to determine which address books to load +export const config: UserConfig = { + accounts, + data, + deployments: 'deployments', + scripts: ['deploy'], + chains: { + 1337: { info: graphLocalNetworkChain }, + 31337: { info: hardhatLocalChain }, + 421614: { info: arbitrumSepoliaChain }, + 42161: { info: arbitrumOneChain }, + }, + // Environment configurations + // Note: hardhat/localhost/fork all use 31337 for rocketh compatibility + environments: { + hardhat: { chain: 31337 }, + localhost: { chain: 31337 }, + fork: { chain: 31337 }, + localNetwork: { chain: 1337 }, + arbitrumSepolia: { chain: 421614 }, + arbitrumOne: { chain: 42161 }, + }, +} + +export default config diff --git a/packages/deployment/rocketh/deploy.ts b/packages/deployment/rocketh/deploy.ts new file mode 100644 index 000000000..e2d4eed0f --- /dev/null +++ b/packages/deployment/rocketh/deploy.ts @@ -0,0 +1,168 @@ +import type { DeploymentMetadata } from '@graphprotocol/toolshed/deployments' +import type { Environment } from '@rocketh/core/types' +import { deploy } from '@rocketh/deploy' +import { deployViaProxy } from '@rocketh/proxy' +import { execute, read, tx } from '@rocketh/read-execute' +import { createPublicClient, custom } from 'viem' + +import type { AnyAddressBookOps } from '../lib/address-book-ops.js' +import { + autoDetectForkNetwork, + getAddressBookForType, + getForkTargetChainId, + getHorizonAddressBook, + getIssuanceAddressBook, + getSubgraphServiceAddressBook, + getTargetChainIdFromEnv, + isForkMode, +} from '../lib/address-book-utils.js' +import type { RegistryEntry } from '../lib/contract-registry.js' +import { accounts, data } from './config.js' + +/** + * Options for updating an address book after deployment + */ +export interface DeploymentUpdate { + /** Contract name in the address book */ + name: string + /** Deployed address (proxy address if proxied) */ + address: string + /** For proxied contracts: proxy admin address */ + proxyAdmin?: string + /** For proxied contracts: implementation address */ + implementation?: string + /** Proxy type if this is a proxied contract */ + proxy?: 'transparent' | 'graph' + /** Proxy deployment metadata (for verification of the proxy contract itself) */ + proxyDeployment?: DeploymentMetadata + /** Implementation deployment metadata (for verification of proxied contracts) */ + implementationDeployment?: DeploymentMetadata + /** Deployment metadata (for verification of non-proxied contracts) */ + deployment?: DeploymentMetadata +} + +/** + * Graph Protocol deployment helpers + * + * These helpers provide common functionality for deploy scripts: + * - Address book access (fork-aware) + * - Viem public client creation + * - Chain ID utilities + * + * @example + * ```typescript + * import type { DeployScriptModule } from '@rocketh/core/types' + * import { deploy } from '@rocketh/deploy' + * import { graph } from '../../rocketh/deploy.js' + * + * const func: DeployScriptModule = async (env) => { + * const deployFn = deploy(env) + * const client = graph.getPublicClient(env) + * const addressBook = graph.getHorizonAddressBook() + * // ... + * } + * ``` + */ +export const graph = { + /** + * Auto-detect fork network by querying anvil. + * Call at the top of any task that needs fork awareness. + * No-op if FORK_NETWORK is already set or node isn't an anvil fork. + */ + autoDetect: () => autoDetectForkNetwork(), + + /** + * Get a viem public client for on-chain queries + */ + getPublicClient: (env: Environment) => + createPublicClient({ + transport: custom(env.network.provider), + }), + + /** + * Get fork target chain ID (null if not in fork mode). + * Maps FORK_NETWORK env var to actual chain ID. + */ + getForkTargetChainId: () => getForkTargetChainId(), + + /** + * Check if running in fork mode + */ + isForkMode: () => isForkMode(), + + /** + * Get the Horizon address book (fork-aware) + */ + getHorizonAddressBook: (chainId?: number) => getHorizonAddressBook(chainId), + + /** + * Get the SubgraphService address book (fork-aware) + */ + getSubgraphServiceAddressBook: (chainId?: number) => getSubgraphServiceAddressBook(chainId), + + /** + * Get the Issuance address book (fork-aware) + */ + getIssuanceAddressBook: (chainId?: number) => getIssuanceAddressBook(chainId), + + /** + * Update horizon address book after deploying a contract. + * Supports both standalone and proxied contracts. + */ + updateHorizonAddressBook: async (env: Environment, update: DeploymentUpdate) => { + const chainId = await getTargetChainIdFromEnv(env) + await applyDeploymentUpdate(getHorizonAddressBook(chainId), update) + }, + + /** + * Update subgraph-service address book after deploying a contract. + * Supports both standalone and proxied contracts. + */ + updateSubgraphServiceAddressBook: async (env: Environment, update: DeploymentUpdate) => { + const chainId = await getTargetChainIdFromEnv(env) + await applyDeploymentUpdate(getSubgraphServiceAddressBook(chainId), update) + }, + + /** + * Update issuance address book after deploying a contract. + * Call this after rocketh's deployViaProxy or deploy to sync the address book. + */ + updateIssuanceAddressBook: async (env: Environment, update: DeploymentUpdate) => { + const chainId = await getTargetChainIdFromEnv(env) + await applyDeploymentUpdate(getIssuanceAddressBook(chainId), update) + }, + + /** + * Update the address book for a contract, choosing the correct book from + * `contract.addressBook`. Single dispatch point — adding a new address book + * type will surface as a TypeScript error in `getAddressBookForType`. + */ + updateAddressBookForContract: async (env: Environment, contract: RegistryEntry, update: DeploymentUpdate) => { + const chainId = await getTargetChainIdFromEnv(env) + await applyDeploymentUpdate(getAddressBookForType(contract.addressBook, chainId), update) + }, +} + +function applyDeploymentUpdate(addressBook: AnyAddressBookOps, update: DeploymentUpdate): void { + if (update.proxy) { + addressBook.setProxy(update.name, update.address, update.implementation!, update.proxyAdmin!, update.proxy) + if (update.proxyDeployment) { + addressBook.setProxyDeploymentMetadata(update.name, update.proxyDeployment) + } + if (update.implementationDeployment) { + addressBook.setImplementationDeploymentMetadata(update.name, update.implementationDeployment) + } + } else { + addressBook.setContract(update.name, update.address) + if (update.deployment) { + addressBook.setDeploymentMetadata(update.name, update.deployment) + } + } +} + +// Re-export rocketh functions for convenience +export { deploy, deployViaProxy, execute, read, tx } + +// Re-export types and config +export type { Environment } +export { accounts, data } diff --git a/packages/deployment/scripts/check-bytecode.ts b/packages/deployment/scripts/check-bytecode.ts new file mode 100644 index 000000000..9d9178b2a --- /dev/null +++ b/packages/deployment/scripts/check-bytecode.ts @@ -0,0 +1,54 @@ +import { createPublicClient, http } from 'viem' + +import { loadSubgraphServiceArtifact } from '../lib/artifact-loaders.js' +import { computeBytecodeHash } from '../lib/bytecode-utils.js' +import { graph } from '../rocketh/deploy.js' + +async function main() { + const chainId = 421614 // arbitrumSepolia + + // Get address book + const addressBook = graph.getSubgraphServiceAddressBook(chainId) + const entry = addressBook.getEntry('SubgraphService') + const deploymentMetadata = addressBook.getDeploymentMetadata('SubgraphService') + + console.log('\n📋 SubgraphService Bytecode Analysis\n') + console.log('Proxy address:', entry.address) + console.log('Current implementation:', entry.implementation) + console.log('Pending implementation:', entry.pendingImplementation?.address ?? 'none') + + // Get local artifact + const artifact = loadSubgraphServiceArtifact('SubgraphService') + const localHash = computeBytecodeHash(artifact.deployedBytecode ?? '0x') + console.log('\nLocal artifact bytecode hash:', localHash) + + // Get address book stored hash + console.log('Address book stored hash:', deploymentMetadata?.bytecodeHash ?? '(none)') + + // Get on-chain bytecode + const client = createPublicClient({ + transport: http('https://sepolia-rollup.arbitrum.io/rpc'), + }) + + const onChainBytecode = await client.getCode({ + address: entry.implementation as `0x${string}`, + }) + + if (onChainBytecode && onChainBytecode !== '0x') { + const onChainHash = computeBytecodeHash(onChainBytecode) + console.log('On-chain implementation hash:', onChainHash) + + console.log('\n🔍 Comparison:') + console.log( + 'Local vs Address Book:', + localHash === (deploymentMetadata?.bytecodeHash ?? '') ? '✓ MATCH' : '✗ DIFFERENT', + ) + console.log('Local vs On-chain:', localHash === onChainHash ? '✓ MATCH' : '✗ DIFFERENT') + console.log( + 'Address Book vs On-chain:', + (deploymentMetadata?.bytecodeHash ?? '') === onChainHash ? '✓ MATCH' : '✗ DIFFERENT (or missing)', + ) + } +} + +main().catch(console.error) diff --git a/packages/deployment/scripts/check-rocketh-bytecode.ts b/packages/deployment/scripts/check-rocketh-bytecode.ts new file mode 100644 index 000000000..aff8f394a --- /dev/null +++ b/packages/deployment/scripts/check-rocketh-bytecode.ts @@ -0,0 +1,34 @@ +import { readFileSync } from 'fs' + +import { loadSubgraphServiceArtifact } from '../lib/artifact-loaders.js' +import { computeBytecodeHash } from '../lib/bytecode-utils.js' + +async function main() { + console.log('\n📋 Rocketh vs Local Artifact Comparison\n') + + // Get local artifact + const artifact = loadSubgraphServiceArtifact('SubgraphService') + const localHash = computeBytecodeHash(artifact.deployedBytecode ?? '0x') + console.log('Local artifact hash:', localHash) + + // Check rocketh stored bytecode + try { + const rockethPath = '.rocketh/deployments/arbitrumSepolia/SubgraphService_Implementation.json' + const rockethData = JSON.parse(readFileSync(rockethPath, 'utf-8')) + + if (rockethData.deployedBytecode) { + const rockethHash = computeBytecodeHash(rockethData.deployedBytecode) + console.log('Rocketh stored hash:', rockethHash) + console.log( + '\nComparison:', + localHash === rockethHash ? '✓ MATCH (deploy will skip)' : '✗ DIFFERENT (deploy will redeploy)', + ) + } else { + console.log('Rocketh stored hash: (no deployedBytecode)') + } + } catch { + console.log('Rocketh record:', 'not found') + } +} + +main().catch(console.error) diff --git a/packages/deployment/scripts/debug-deploy-state.ts b/packages/deployment/scripts/debug-deploy-state.ts new file mode 100644 index 000000000..6267734f2 --- /dev/null +++ b/packages/deployment/scripts/debug-deploy-state.ts @@ -0,0 +1,27 @@ +import { loadSubgraphServiceArtifact } from '../lib/artifact-loaders.js' +import { computeBytecodeHash } from '../lib/bytecode-utils.js' + +async function main() { + console.log('\n📋 Investigating Deploy "Unchanged" Message\n') + + // The deploy script checks env.getOrNull('SubgraphService_Implementation') + // But rocketh state is in-memory during deploy runs + // We can't easily check that without running deploy + + // What we CAN check is: + // 1. If sync step would have synced the implementation + // 2. The actual bytecode hashes + + const artifact = loadSubgraphServiceArtifact('SubgraphService') + const localHash = computeBytecodeHash(artifact.deployedBytecode ?? '0x') + + console.log('Local artifact bytecode hash:', localHash) + console.log('\n⚠️ The issue:') + console.log('1. Sync shows "code changed" because address book has different/missing hash') + console.log('2. Deploy says "unchanged" - this suggests rocketh has the implementation') + console.log('3. But local bytecode IS different from on-chain') + console.log('\nThis means deploy will NOT deploy the new implementation!') + console.log('The local changes will be ignored.\n') +} + +main().catch(console.error) diff --git a/packages/deployment/scripts/generate-abis.ts b/packages/deployment/scripts/generate-abis.ts new file mode 100644 index 000000000..f4ac49a14 --- /dev/null +++ b/packages/deployment/scripts/generate-abis.ts @@ -0,0 +1,264 @@ +/** + * ABI Codegen Script + * + * Generates typed `as const` ABI exports from the contract registry. + * Reads interface declarations and artifact sources from the registry, + * resolves them to JSON artifacts, and writes a generated TypeScript file. + * + * Usage: tsx scripts/generate-abis.ts + */ + +import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { toFunctionSelector } from 'viem' + +import { CONTRACT_REGISTRY, type ContractMetadata, type InterfaceAbiConfig } from '../lib/contract-registry.js' + +const require = createRequire(import.meta.url) +const __dirname = dirname(fileURLToPath(import.meta.url)) +const OUTPUT_DIR = join(__dirname, '..', 'lib', 'generated') +const OUTPUT_FILE = join(OUTPUT_DIR, 'abis.ts') + +// --------------------------------------------------------------------------- +// Utility ABIs — not tied to any registry entry +// --------------------------------------------------------------------------- + +const UTILITY_ABIS: Array<{ name: string; artifactPath: string }> = [ + { + name: 'IERC165_ABI', + artifactPath: '@graphprotocol/interfaces/artifacts/@openzeppelin/contracts/introspection/IERC165.sol/IERC165.json', + }, + { + name: 'ISSUANCE_TARGET_ABI', + artifactPath: + '@graphprotocol/interfaces/artifacts/contracts/issuance/allocate/IIssuanceTarget.sol/IIssuanceTarget.json', + }, + { + name: 'OZ_PROXY_ADMIN_ABI', + artifactPath: + '@graphprotocol/horizon/artifacts/@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol/ProxyAdmin.json', + }, +] + +// Alias re-exports (source export name → alias export name) +const ABI_ALIASES: Array<{ source: string; alias: string }> = [ + { source: 'ISSUANCE_ALLOCATOR_ABI', alias: 'SET_TARGET_ALLOCATION_ABI' }, + { source: 'DIRECT_ALLOCATION_ABI', alias: 'INITIALIZE_GOVERNOR_ABI' }, +] + +// Interface IDs to extract (export name → interface name used in ABI_SOURCES or registry) +// Derived from registry interfaces + utility ABIs +const INTERFACE_IDS: Array<{ name: string; abiExportName: string }> = [ + { name: 'IERC165_INTERFACE_ID', abiExportName: 'IERC165_ABI' }, + { name: 'IISSUANCE_TARGET_INTERFACE_ID', abiExportName: 'ISSUANCE_TARGET_ABI' }, + { name: 'IREWARDS_MANAGER_INTERFACE_ID', abiExportName: 'REWARDS_MANAGER_ABI' }, +] + +// --------------------------------------------------------------------------- +// Interface artifact discovery +// --------------------------------------------------------------------------- + +/** + * Build an index of interface name → artifact path by scanning the + * @graphprotocol/interfaces artifacts directory. + */ +function buildInterfaceIndex(): Map { + const index = new Map() + + // Resolve the interfaces package artifacts root + // Use a known artifact to locate the package, then walk up + const knownArtifact = + require.resolve('@graphprotocol/interfaces/artifacts/contracts/contracts/rewards/IRewardsManager.sol/IRewardsManager.json') + // Walk up to find the 'artifacts' directory + let artifactsRoot = dirname(knownArtifact) + while (!artifactsRoot.endsWith('/artifacts') && artifactsRoot !== '/') { + artifactsRoot = dirname(artifactsRoot) + } + + // Recursively scan for JSON files + function scan(dir: string): void { + for (const entry of readdirSync(dir)) { + const full = join(dir, entry) + if (entry === 'build-info') continue + if (statSync(full).isDirectory()) { + scan(full) + } else if (entry.endsWith('.json') && !entry.endsWith('.dbg.json')) { + // Extract interface name from filename (e.g. IRewardsManager.json → IRewardsManager) + const name = entry.replace('.json', '') + // Store as package-relative path for require.resolve + const relativePath = full.slice(full.indexOf('/artifacts/') + 1) + index.set(name, `@graphprotocol/interfaces/${relativePath}`) + } + } + } + + scan(artifactsRoot) + return index +} + +// --------------------------------------------------------------------------- +// Artifact loading +// --------------------------------------------------------------------------- + +type AbiEntry = Record + +function loadAbiFromArtifact(artifactPath: string): AbiEntry[] { + const resolved = require.resolve(artifactPath) + const artifact = JSON.parse(readFileSync(resolved, 'utf-8')) + return artifact.abi +} + +/** + * Resolve artifact path for a generateAbi entry based on its ArtifactSource. + */ +function resolveContractArtifactPath(artifact: { type: string; path?: string; name?: string }): string { + switch (artifact.type) { + case 'contracts': + return `@graphprotocol/contracts/artifacts/contracts/${artifact.path}/${artifact.name}.sol/${artifact.name}.json` + case 'subgraph-service': { + const baseName = (artifact.name ?? '').includes('/') ? (artifact.name ?? '').split('/').pop()! : artifact.name + return `@graphprotocol/subgraph-service/artifacts/contracts/${artifact.name}.sol/${baseName}.json` + } + case 'horizon': + return `@graphprotocol/horizon/artifacts/${artifact.path}.json` + case 'issuance': + return `@graphprotocol/issuance/artifacts/${artifact.path}.json` + case 'openzeppelin': + return `@openzeppelin/contracts/build/contracts/${artifact.name}.json` + default: + throw new Error(`Unknown artifact type: ${artifact.type}`) + } +} + +// --------------------------------------------------------------------------- +// Interface ID calculation +// --------------------------------------------------------------------------- + +/** + * Calculate ERC-165 interface ID from an ABI. + * The interface ID is XOR of all function selectors. + */ +function calculateInterfaceId(abi: AbiEntry[]): string { + const functions = abi.filter((entry) => entry.type === 'function') + if (functions.length === 0) return '0x00000000' + + let id = BigInt(0) + for (const fn of functions) { + const inputs = (fn.inputs as Array<{ type: string }>) ?? [] + const sig = `${fn.name}(${inputs.map((i) => i.type).join(',')})` + const selector = toFunctionSelector(sig) + id ^= BigInt(selector) + } + + return '0x' + id.toString(16).padStart(8, '0') +} + +// --------------------------------------------------------------------------- +// Code generation +// --------------------------------------------------------------------------- + +function formatAbiEntry(entry: AbiEntry, indent: string): string { + return `${indent}${JSON.stringify(entry)}` +} + +function generateAbiExport(name: string, abi: AbiEntry[]): string { + const entries = abi.map((entry) => formatAbiEntry(entry, ' ')).join(',\n') + return `export const ${name} = [\n${entries},\n] as const\n` +} + +function main(): void { + const verbose = process.argv.includes('--verbose') + + const interfaceIndex = buildInterfaceIndex() + const abiMap = new Map() + const lines: string[] = [ + '/**', + ' * Auto-generated typed ABI exports', + ' *', + ' * DO NOT EDIT — regenerate with: pnpm generate:abis', + ' */', + '', + ] + + // 1. Walk registry for interface ABIs + for (const [bookName, book] of Object.entries(CONTRACT_REGISTRY)) { + for (const [contractName, rawMeta] of Object.entries(book)) { + const meta = rawMeta as ContractMetadata + // Interface ABIs + if (meta.interfaces) { + for (const iface of meta.interfaces as readonly InterfaceAbiConfig[]) { + const artifactPath = interfaceIndex.get(iface.interface) + if (!artifactPath) { + throw new Error( + `Interface "${iface.interface}" not found in @graphprotocol/interfaces artifacts ` + + `(referenced by ${bookName}.${contractName})`, + ) + } + const abi = loadAbiFromArtifact(artifactPath) + abiMap.set(iface.name, abi) + if (verbose) console.log(` ${iface.name} ← ${iface.interface} (${abi.length} entries)`) + } + } + + // Full contract ABI + if (meta.generateAbi && meta.artifact) { + const exportName = meta.generateAbi as string + const artifactPath = resolveContractArtifactPath( + meta.artifact as { type: string; path?: string; name?: string }, + ) + const abi = loadAbiFromArtifact(artifactPath) + abiMap.set(exportName, abi) + if (verbose) console.log(` ${exportName} ← ${contractName} (${abi.length} entries)`) + } + } + } + + // 2. Utility ABIs + for (const util of UTILITY_ABIS) { + const abi = loadAbiFromArtifact(util.artifactPath) + abiMap.set(util.name, abi) + if (verbose) console.log(` ${util.name} ← utility (${abi.length} entries)`) + } + + // 3. Generate ABI exports + for (const [name, abi] of abiMap) { + lines.push(generateAbiExport(name, abi)) + } + + // 4. Alias re-exports + for (const { source, alias } of ABI_ALIASES) { + if (!abiMap.has(source)) { + throw new Error(`Alias source "${source}" not found in generated ABIs`) + } + lines.push(`export { ${source} as ${alias} }\n`) + if (verbose) console.log(` ${alias} → ${source}`) + } + + // 5. Interface IDs + lines.push('// Interface IDs (computed from ABI function selectors)') + for (const { name, abiExportName } of INTERFACE_IDS) { + const abi = abiMap.get(abiExportName) + if (!abi) { + throw new Error(`ABI "${abiExportName}" not found for interface ID "${name}"`) + } + const id = calculateInterfaceId(abi) + lines.push(`export const ${name} = '${id}' as const`) + if (verbose) console.log(` ${name} = ${id}`) + } + lines.push('') + + // Write output + if (!existsSync(OUTPUT_DIR)) { + mkdirSync(OUTPUT_DIR, { recursive: true }) + } + writeFileSync(OUTPUT_FILE, lines.join('\n')) + + console.log( + `Generated ${abiMap.size} ABIs, ${ABI_ALIASES.length} aliases, ${INTERFACE_IDS.length} interface IDs → lib/generated/abis.ts`, + ) +} + +main() diff --git a/packages/deployment/scripts/tag-deployment.sh b/packages/deployment/scripts/tag-deployment.sh new file mode 100755 index 000000000..3e05e28a9 --- /dev/null +++ b/packages/deployment/scripts/tag-deployment.sh @@ -0,0 +1,337 @@ +#!/usr/bin/env bash +# +# tag-deployment.sh - Create annotated git tag for a contract deployment +# +# Produces tags in the format `deploy//YYYY-MM-DD/` as defined by +# DEPLOYMENT.md (the bare-date form `deploy//YYYY-MM-DD` is permitted as a +# fallback when --name is omitted, but a descriptive name is recommended). The +# tag body records the deployer, network, commit, and a list of changed +# contracts per address book (detected by diffing address-book JSON against a +# base ref). +# +# Usage: +# ./scripts/tag-deployment.sh --deployer --network [--name ] [options] +# +# Options: +# --deployer What performed the deployment (free-form, e.g., "packages/deployment --tags RewardsManager") +# --network Network: arbitrumOne (→ mainnet) or arbitrumSepolia (→ testnet) +# --name Recommended release/upgrade short name appended to the tag as a further path segment +# (e.g. "reward-manager-and-subgraph-service" → deploy//YYYY-MM-DD/). +# If omitted, the tag is the bare-date form deploy//YYYY-MM-DD — permitted as a +# fallback but exceptional; prefer a name that describes the deploy. +# --base Git ref to diff address books against. Defaults to the latest `deploy//*` +# tag for the target environment. If none exists (initial deploy), pass --base +# explicitly (e.g. --base HEAD~1 or the empty-tree sentinel). +# --dry-run Print the preview and exit without creating the tag. +# --yes, -y Skip the interactive confirmation prompt (required for non-interactive use). +# --no-sign Create an unsigned annotated tag. Default is signed (-s). +# --help Show this help +# +# By default the script prints a preview (tag name, commit, annotation body) and +# then asks for confirmation before creating the tag. Use --yes to skip the +# prompt, or --dry-run to stop after the preview. +# +set -euo pipefail + +# --- Dependencies --- +for cmd in git jq; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: $cmd is required but not found" + exit 1 + fi +done + +REPO_ROOT="$(git rev-parse --show-toplevel)" + +# --- Defaults --- +DEPLOYER="" +NETWORK="" +UPGRADE_NAME="" +BASE_REF="" # Empty means "auto: use latest deploy//* tag". Overridden by --base. +DRY_RUN=false +ASSUME_YES=false +SIGN_FLAG="-s" # Signed by default. --no-sign switches to -a (annotated, unsigned). + +# --- Address books managed by packages/deployment --- +ADDRESS_BOOKS=( + "packages/horizon/addresses.json:horizon" + "packages/subgraph-service/addresses.json:subgraph-service" + "packages/issuance/addresses.json:issuance" +) + +# --- Network to chain ID / label mapping --- +network_to_chain_id() { + case "$1" in + arbitrumOne) echo "42161" ;; + arbitrumSepolia) echo "421614" ;; + *) echo "unknown" ;; + esac +} + +network_to_label() { + case "$1" in + arbitrumOne) echo "mainnet" ;; + arbitrumSepolia) echo "testnet" ;; + *) echo "unknown" ;; + esac +} + +network_to_display() { + case "$1" in + arbitrumOne) echo "arbitrum-one" ;; + arbitrumSepolia) echo "arbitrum-sepolia" ;; + *) echo "$1" ;; + esac +} + +# --- Parse arguments --- +usage() { + sed -n '3,32p' "$0" | sed 's/^# \?//' + exit "${1:-0}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --deployer) DEPLOYER="$2"; shift 2 ;; + --network) NETWORK="$2"; shift 2 ;; + --name) UPGRADE_NAME="$2"; shift 2 ;; + --base) BASE_REF="$2"; shift 2 ;; + --dry-run) DRY_RUN=true; shift ;; + --yes|-y) ASSUME_YES=true; shift ;; + --no-sign) SIGN_FLAG="-a"; shift ;; + --help) usage 0 ;; + *) echo "Unknown option: $1"; usage 1 ;; + esac +done + +if [[ -z "$DEPLOYER" ]]; then + echo "Error: --deployer is required" + usage 1 +fi + +if [[ -z "$NETWORK" ]]; then + echo "Error: --network is required" + usage 1 +fi + +# --name is recommended but not required. When provided, validate format: lowercase, digits, hyphens only. +if [[ -n "$UPGRADE_NAME" ]]; then + if [[ ! "$UPGRADE_NAME" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then + echo "Error: --name must be lowercase alphanumeric with hyphens (e.g., 'reward-manager-and-subgraph-service')" + exit 1 + fi +else + echo "Warning: --name not provided; creating bare-date tag (fallback form)." + echo " Prefer a descriptive --name (e.g. 'reward-manager', 'fix-activation') for self-describing tags." +fi + +CHAIN_ID="$(network_to_chain_id "$NETWORK")" +LABEL="$(network_to_label "$NETWORK")" +DISPLAY="$(network_to_display "$NETWORK")" + +if [[ "$CHAIN_ID" == "unknown" ]]; then + echo "Error: unknown network '$NETWORK' (expected arbitrumOne or arbitrumSepolia)" + exit 1 +fi + +# --- Resolve --base default --- +# If --base was not provided, use the latest deploy/