Skip to content

Deployment preparation for testnet-dips#1326

Draft
RembrandtK wants to merge 166 commits intomainfrom
deployment/testnet-dips/2024-04-28/audit-fix-2
Draft

Deployment preparation for testnet-dips#1326
RembrandtK wants to merge 166 commits intomainfrom
deployment/testnet-dips/2024-04-28/audit-fix-2

Conversation

@RembrandtK
Copy link
Copy Markdown
Contributor

@RembrandtK RembrandtK commented Apr 28, 2026

Working branch. Expected to be dropped in favour of rebased versions when contract audit completes and/or other changes are merged to main.

matiasedgeandnode and others added 30 commits June 11, 2025 14:08
Fixes TRST-M-1 audit finding:

Wrong TYPEHASH string is used for agreement updates, limiting functionality.

* Fixed EIP712_RCAU_TYPEHASH to use correct uint64 types for deadline and endsAt fields (was incorrectly using uint256)
* This prevents signature verification failures for RecurringCollectionAgreementUpdate
Fixes TRST-M-2 audit finding:

Collection for an elapsed or canceled agreement could be wrong due to
temporal calculation inconsistencies between IndexingAgreement and
RecurringCollector layers.

* Replace isCollectable() with getCollectionInfo() that returns both collectability and duration
* Make RecurringCollector the single source of truth for temporal logic
* Update IndexingAgreement to call getCollectionInfo() once and pass duration to _tokensToCollect()
Fixes signature replay attack vulnerability where old signed RecurringCollectionAgreementUpdate
messages could be replayed to revert agreements to previous terms.

 ## Changes

- Add `nonce` field to RecurringCollectionAgreementUpdate struct (uint32)
- Add `updateNonce` field to AgreementData struct to track current nonce
- Add nonce validation in RecurringCollector.update() to ensure sequential updates
- Update EIP712_RCAU_TYPEHASH to include nonce field
- Add comprehensive tests for nonce validation and replay attack prevention
- Add RecurringCollectorInvalidUpdateNonce error for invalid nonce attempts

 ## Implementation Details

- Nonces start at 0 when agreement is accepted
- Each update must use current nonce + 1
- Nonce is incremented after successful update
- Uses uint32 for gas optimization (supports 4B+ updates per agreement)
- Single source of truth: nonce stored in AgreementData struct
Implements slippage protection mechanism to prevent silent token loss
during rate-limited collections in RecurringCollector agreements.

The implementation uses type(uint256).max convention to disable slippage
checks, providing users full control over acceptable token loss during
rate limiting.

Resolves audit finding TRST-L-5: "RecurringCollector silently reduces
collected tokens without user consent"
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Replace GRAPHPROTOCOL_NPM_TOKEN with GitHub OIDC (id-token: write) and
pnpm --provenance so the workflow mints a short-lived credential and
attaches a SLSA build attestation. Each target package needs its own
Trusted Publisher entry on npmjs.com (owner: graphprotocol, repo:
contracts, workflow: publish.yml); verify per-package via a dry_run
dispatch before a real publish.

Pattern follows graphprotocol/graph-node#6460. contents: write is
retained so the existing Tag release step can push the annotated tag.
pnpm publish delegates registry auth to the underlying npm CLI, which
needs to be >= 11.5.1 to exchange the GitHub OIDC token for an npm
publish credential. The shared setup action brings Node 22 which ships
an older npm; install the latest npm globally in the publish job
before pnpm publish runs.

Pattern follows eslint/config-inspector#174.
The repo's root package.json declares pnpm@10.28.0 as the
packageManager, but the shared setup action's corepack prepare step
pinned the older 10.17.0. Resolve the drift by matching the declared
version. Minor-version bump within pnpm 10.x; affects lint, build-test,
verifydeployed, and publish workflows that share this setup.
Reproducibility: pinning avoids surprise behavior changes or Node engine
bumps from a future npm "latest". Bump the pin intentionally; only
constraint is npm >= 11.5.1 for OIDC trusted publishing.
- actions/setup-node: v4 → v6, node-version 22 → 24
- actions/checkout: v3/v4 → v6
- actions/upload-artifact: v3 → v7 (v3 was sunset by GitHub)
- actions/download-artifact: v3 → v8 (v3 was sunset by GitHub)
- actions/github-script: v7 → v9
- codecov/codecov-action: v3 → v6
- Add description: field to setup composite action (schema)

Fixes the npm install -g npm@11.13.0 cross-major self-upgrade crash
seen on Node 22 — Node 24 ships npm 11.x, so this is now an 11→11 swap.
Also clears the Node 20 runtime deprecation warning on JS actions.

cache: 'pnpm' on setup-node@v6 verified: v6's "limit automatic caching
to npm" change only affects auto-detection when cache is unset; explicit
cache: 'pnpm' is unaffected.
Node 24 ships npm 11.x bundled (24.15.0 → npm 11.12.1), which already
satisfies the OIDC trusted-publishing requirement of npm >= 11.5.1.
The explicit `npm install -g npm@11.13.0` step was a Node 22 workaround
and is now redundant.

Pinning the Node version effectively pins npm; if stricter pinning is
ever needed it can be reintroduced.
Bump from 0.6.6 → 0.7.0-test.0 to validate the OIDC trusted-publishing
flow end-to-end. Prerelease tag (-test.0) keeps it off the latest
dist-tag and makes it easy to discard if the test reveals issues.

Real 0.7.0 release will follow once trusted publishing is verified;
0.6.6 → 0.7.0 reflects the breaking changes accumulated since 0.6.6
(deleted IDataServiceFees/Rescuable/IHorizonStakingExtension, renamed
IRewardsEligibility → IProviderEligibility, reworked IRewardsManager).
RembrandtK and others added 21 commits April 29, 2026 09:12
npm's trusted-publishing flow cross-checks package.json's repository.url
against the source repo recorded in the Sigstore provenance attestation.
Without this field, publish fails at the registry side after provenance
has already been signed:

  Failed to validate repository information: package.json: "repository.url"
  is "", expected to match "https://github.com/graphprotocol/contracts"
  from provenance

Mirrors the shape used in @graphprotocol/contracts; adds directory for
proper monorepo source linking.
Single source of truth for the Node version. Local fnm reads .nvmrc and
auto-switches; CI setup-node reads the same file via node-version-file.
Corepack picks pnpm from the packageManager field, so the explicit
`corepack prepare pnpm@X.Y.Z --activate` step is no longer needed.

- Add .nvmrc (24)
- Add .npmrc (engine-strict=true) so pnpm refuses wrong Node/pnpm
- package.json: add engines (node ^24, pnpm ^10.28)
- .github/actions/setup/action.yml: switch to node-version-file: '.nvmrc',
  drop the redundant Corepack prepare step
npm's trusted-publishing flow cross-checks package.json's repository.url
against the source repo recorded in the Sigstore provenance attestation.
Without this field, publish fails at the registry side after provenance
has already been signed:

  Failed to validate repository information: package.json: "repository.url"
  is "", expected to match "https://github.com/graphprotocol/contracts"
  from provenance

Mirrors the shape applied to @graphprotocol/interfaces in a61ed9a;
adds directory for proper monorepo source linking.
…tion

Mirrors the fix applied to interfaces (a61ed9a) and toolshed
(9ba6366a3). Without this field, OIDC trusted-publishing will fail
at the registry's provenance verification step:

  Failed to validate repository information: package.json: "repository.url"
  is "", expected to match "https://github.com/graphprotocol/contracts"
  from provenance

Preemptive — address-book has not been republished since the OIDC
switch, but the workflow lists it as a choice.
@graphprotocol/sdk was extracted to its own repo in 32ef00c ("chore:
move sdk to own repo, update versions"); packages/sdk/ no longer exists
in this monorepo. Dispatching package=sdk would fail at the Read package
info step trying to require a non-existent package.json.
…orepo source linking

Drop the .git suffix from repository.url and add directory: packages/contracts
to match the shape used in interfaces (a61ed9a), toolshed (9ba6366a3),
and address-book (c3e219ba1). The .git suffix is normalized away by npm's
provenance verification, so the previous shape would likely have worked,
but this keeps the four publishable packages consistent and gives
npmjs.com a correct subfolder link.
- Add IRecurringCollector to GraphHorizonContracts interface
- Re-export IRecurringCollector from interfaces main entrypoint
- Add encodeCollectIndexingFeesData() helper for indexing fee collection
- Add decoders: decodeSignedRCA, decodeAcceptIndexingAgreementMetadata,
  decodeIndexingAgreementTermsV1
- Add round-trip tests for all decoders and encoder
The audit-branch RecurringCollectionAgreement struct added a uint16
conditions field at position 9 (between maxSecondsPerCollection and
nonce). The toolshed decoder tuple was missing it, so decodeSignedRCA
read 10 fields against 11 ABI-encoded fields: nonce read what was
actually conditions, metadata read the wrong offset, decode threw.

The indexer-agent relies on this decoder to read pending RCA proposals
the indexer-service persists. Without the fix, every proposal logs
"Failed to decode pending RCA proposal" and is skipped, so the agent
never calls acceptIndexingAgreement on-chain and DIPs agreements
expire in dipper's DB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Re-publish needed because 1.2.1-dips.0 from audit-fix-2 was missing the
recurring-collector module (decodeSignedRCA, decodeAcceptIndexingAgreementMetadata,
decodeIndexingAgreementTermsV1, SignedRCA type). Cherry-picked from
mb9/dips-local-testing-fixes:
  - 25c0b88 feat: add RecurringCollector type and DIPs helpers to toolshed
  - 3e739a3 fix(toolshed): add conditions field to RCA decoder tuple

These decoders are imported by indexer-common's pending-rca-consumer and
types modules; without them indexer-agent's yarn prepare fails.
…les' ACCOUNT1

Phase 1 deploy modules hardcode m.getAccount(1) (ACCOUNT1) when
transferring ProxyAdmin ownership for new proxies, but
migrate.localNetwork.json5 declared ACCOUNT0 as governor. New
ProxyAdmins minted by the GIP-0088 upgrade phase therefore landed
with ACCOUNT0 as owner, while local-network's issuance.run.sh signs
upgrade txs with GOVERNOR_KEY=ACCOUNT1_SECRET, causing
OwnableUnauthorizedAccount reverts mid-batch.

Align migrate config with the deploy modules and the run-script.
…oy modules' ACCOUNT1

deploy:protocol Phase 1.3 (Deploy2Module → DisputeManager.ts /
SubgraphService.ts) transfers the freshly-minted ProxyAdmins to
m.getParameter('governor'), which on localNetwork resolved to
ACCOUNT0. Local-network's issuance.run.sh signs subsequent
upgradeAndCall txs with GOVERNOR_KEY=ACCOUNT1_SECRET, so the
DisputeManager / SubgraphService PA upgrades reverted with
OwnableUnauthorizedAccount.

Bumping the SS-side protocol-config governor to ACCOUNT1 brings
local-network into alignment with both the run-script and the
m.getAccount(1) convention horizon's deploy modules already use.
Production migrate paths (migrate.<network>.json5) are unaffected.
…izon sibling

Mirror the earlier horizon migrate-config bump on the SS side so both
migrate.localNetwork.json5 files use ACCOUNT1 as governor. Not
load-bearing for any current flow (the migrate path isn't run on
local-network), but keeps the two sibling files in sync and
consistent with the m.getAccount(1) convention used by horizon's
deploy modules.
…lityOracle addresses

The header comment claims addresses are derived from the hardhat
default mnemonic, but the values were copied from protocol.default.json5
which uses the older 'myth like bonus' mnemonic. Replace with the
correct hardhat-default-mnemonic addresses at the stated indices.

Latent — both values are passed to setPauseGuardian / setSubgraph-
AvailabilityOracle on local-network, but no current local-network
test exercises pause or availability-oracle functionality, so the
mismatch hadn't surfaced. Caught while building the cross-package
config reconciliation test.
Static unit test that catches drift between per-network Ignition
config files in packages/horizon/ignition/configs/ and
packages/subgraph-service/ignition/configs/.

Four checks:

1. Cross-package sibling agreement. For each (prefix, network) pair
   where both packages have a file (e.g. both migrate.arbitrumOne.json5),
   every overlapping non-empty $global field must match.

2. localNetwork all-files $global agreement. For localNetwork
   specifically (one stack, one governor) every $global field
   meaningfully declared in more than one of the four
   {horizon,subgraph-service}/{migrate,protocol}.localNetwork.json5
   files must match across all of them. Stricter than #1 — catches
   same-package cross-prefix drift.

3. localNetwork same-package cross-prefix sub-object agreement. Each
   package's per-contract config blocks (e.g. "DisputeManager": { ... },
   "RecurringCollector": { ... }) must agree leaf-by-leaf between
   migrate and protocol. Catches drift in fields like eip712Name /
   eip712Version (signature verification breakers) and disputePeriod /
   disputeDeposit parameters. Restricted to localNetwork because for
   other networks (notably default) migrate and protocol are
   intentionally different templates.

4. localNetwork mnemonic-index correctness. Lines like
     "governor": "0x70997970…", // index 1
   must have an address that derives from the hardhat default mnemonic
   at the stated BIP44 index. Catches copy-paste mistakes where the
   value updates but the comment doesn't, or vice versa.

Verified to fail on the SS protocol governor drift the test was
designed against: reverting governor in
subgraph-service/ignition/configs/protocol.localNetwork.json5 to
ACCOUNT0 produces a clean three-way mismatch report from check #2.

Adds json5 as a direct devDep (was transitively available via
toolshed but not declarable that way under pnpm strict mode).
…0 headroom

Drops the auto-generated public getters for MIN_SECONDS_COLLECTION_WINDOW,
CONDITION_ELIGIBILITY_CHECK, EIP712_RCA_TYPEHASH, and EIP712_RCAU_TYPEHASH
(~50 bytes each) so the contract stays under the EIP-170 24576-byte limit
when later additions land. Tests that read these via the public ABI switch
to the literal value with a naming comment.

Mirrors the dropped constants and EIP-712 typestrings under
@graphprotocol/toolshed/core/recurring-collector so off-chain agents
constructing offers have a typed source of truth.
…callback opt-in

Replaces the live `payer.code.length != 0` callback dispatch in
_preCollectCallbacks/_postCollectCallback with a stored condition flag.
An offer that sets CONDITION_AGREEMENT_OWNER is only acceptable when the
payer declares ERC-165 support for IAgreementOwner; the flag is then read
at collection time, so callback dispatch is frozen to acceptance and
unaffected by post-acceptance code changes such as EIP-7702 delegation
swaps. This closes the gas-estimator griefing vector where an EOA payer
could attach delegation between estimation and execution and cause
collect() to revert.

Consolidates the two interface-support errors into a single
RecurringCollectorPayerDoesNotSupportInterface(payer, interfaceId).
Renames _requirePayerToSupportEligibilityCheck to
_requirePayerInterfaceSupport and folds both ERC-165 checks into it.

Test coverage:
- offer(NEW) reverts when CONDITION_AGREEMENT_OWNER is set on a payer
  that does not declare IAgreementOwner via ERC-165.
- offer(UPDATE) re-validates ERC-165 support when an RCAU adds the flag
  to an already-accepted agreement, and reverts if the payer doesn't
  declare it.
- collect skips both beforeCollection and afterCollection when the flag
  is unset, even with a contract payer — proves dispatch gates on the
  stored flag, not live payer.code.length.

Addresses TRST-L-10.
…nt/testnet-dips/2024-04-28/audit-fix-2

# Conflicts:
#	packages/toolshed/src/core/recurring-collector.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants