Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
df9a846
docs: update audit extracts for PR1301 v02 report
RembrandtK Apr 17, 2026
cb6c45c
fix(collector): add gas overhead buffer to callback prechecks (TRST-L-9)
RembrandtK Apr 17, 2026
3ce5813
fix(collector): cap returndata copy in payer callbacks (TRST-M-4)
RembrandtK Apr 17, 2026
8e50abd
docs: add response to TRST-L-10 EIP-7702 callback dispatch (won't fix)
RembrandtK Apr 17, 2026
6a0ac79
feat(RAM): drop pair tracking below residual escrow threshold (TRST-M…
RembrandtK Apr 17, 2026
f96a731
docs: add responses to TRST-L-6, TRST-R-7 (both won't fix)
RembrandtK Apr 19, 2026
35447e7
docs(audit): acknowledge TRST-R-3 cancelAgreement defensive check
RembrandtK Apr 20, 2026
2dd2372
fix(collector): remove dead oldHash guard (TRST-R-6)
RembrandtK Apr 20, 2026
c1ef1cb
fix(collector): non-zero offer types, reserve OFFER_TYPE_NONE=0 senti…
RembrandtK Apr 19, 2026
3621793
refactor(interfaces): drop unused state and offer-option flags, tight…
RembrandtK Apr 19, 2026
f32e550
docs(audit): acknowledge trust-boundary correction in TRST-H-4
RembrandtK Apr 19, 2026
d2fd364
docs(audit): acknowledge reclaim-reason change in TRST-R-13
RembrandtK Apr 19, 2026
b61d441
docs(ram): document collector replay-protection assumption (TRST-R-4)
RembrandtK Apr 19, 2026
0271015
docs(ram): document non-retroactive role-change semantics (TRST-R-10)
RembrandtK Apr 19, 2026
1ee49f2
docs(ram): align pause-escalation prose with whenNotPaused scope (TRS…
RembrandtK Apr 19, 2026
9396dbd
docs(collector): note self-authorization auth-check obligation (TRST-…
RembrandtK Apr 19, 2026
1e5a6b3
fix(subgraph-service): validate update terms against RCAU rate, not s…
RembrandtK Apr 21, 2026
8be1aa0
refactor(collector): preparatory helpers, signatures, and version con…
RembrandtK Apr 22, 2026
cfaf39b
refactor(collector): drop unreachable agreementId-zero check
RembrandtK Apr 21, 2026
35748ff
refactor(collector): extract _requireValidTerms from duplicated valid…
RembrandtK Apr 21, 2026
0ad0be4
refactor(collector): split accept logic out of _validateAndStoreAgree…
RembrandtK Apr 21, 2026
bfe7754
refactor(collector): split update apply out of _validateAndStoreUpdate
RembrandtK Apr 21, 2026
594d19b
feat(subgraph-service): idempotent accept/update with allocation rebi…
RembrandtK Apr 27, 2026
885555e
refactor(collector): hoist solhint-disable, idiomatic deadline compar…
RembrandtK Apr 27, 2026
572853b
fix(collector): validate offer terms against deadline, not block.time…
RembrandtK Apr 22, 2026
b6adbf1
refactor(collector): extract _getAgreementDetails/_versionHashAt helpers
RembrandtK Apr 27, 2026
8b48437
fix(collector): persistent agreement.payer for independent cancellati…
RembrandtK Apr 27, 2026
769b252
feat(collector): idempotent accept/update/cancel-on-nothing
RembrandtK Apr 27, 2026
f96b4ea
feat(collector): add OfferCancelled event for SCOPE_PENDING cancellat…
RembrandtK Apr 25, 2026
c1dfc34
feat(collector): per-version semantics in getAgreementDetails (TRST-L…
RembrandtK Apr 27, 2026
33d2ced
feat(collector): compose cancel/settled flags in getAgreementDetails …
RembrandtK Apr 19, 2026
fe13b11
feat(collector): add SCOPE_SIGNED to cancel() for EOA offer revocatio…
RembrandtK Apr 19, 2026
b13d910
feat(issuance): expose getIssuanceAllocator on IIssuanceTarget
RembrandtK Apr 10, 2026
e4cd9e0
fix(collector): validate full terms at offer time
RembrandtK Apr 26, 2026
6772545
fix(collector): respect deadlines in scoped claim cap
RembrandtK Apr 26, 2026
067168e
refactor(collector): collapse redundant state guard in _getMaxNextClaim
RembrandtK Apr 27, 2026
757da41
fix(collector): use dedicated error for invalid offer type in offer()
RembrandtK Apr 27, 2026
87ee8b6
chore(collector): make module-level constants internal to free EIP-17…
RembrandtK May 1, 2026
f44fc5a
feat(collector): add CONDITION_AGREEMENT_OWNER for ERC-165-validated …
RembrandtK May 1, 2026
5b07b58
docs(audits): drop withdrawn TRST findings, retitle and park remainin…
RembrandtK May 4, 2026
68cd77e
docs(audits): rename parked v03 lows from TRST-L-{old}-{new}.md to fi…
RembrandtK May 4, 2026
f09c2e3
docs(audits): incorporate Trust Security PR1325 v03 fix-review
RembrandtK May 4, 2026
9b30707
docs(collector): clarify cancel() signer-vs-payer caller for SCOPE_SI…
RembrandtK May 4, 2026
c21d7f8
test(collector): assert callbacks receive MAX_PAYER_CALLBACK_GAS with…
RembrandtK May 4, 2026
75cae7c
docs(collector): document EIP-7702 trust assumption for CONDITION_ELI…
RembrandtK May 4, 2026
c8a5c21
refactor(ram): use VERSION_CURRENT instead of magic 0 in getAgreement…
RembrandtK May 4, 2026
a3c73f8
chore(ci): fix flaky CI tests and silence block-timestamp lint
RembrandtK May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ describe('RewardsManager interfaces', () => {
})

it('IIssuanceTarget should have stable interface ID', () => {
expect(IIssuanceTarget__factory.interfaceId).to.equal('0xaee4dc43')
expect(IIssuanceTarget__factory.interfaceId).to.equal('0x19f6601a')
})

it('IRewardsManager should have stable interface ID', () => {
expect(IRewardsManager__factory.interfaceId).to.equal('0x337b092e')
expect(IRewardsManager__factory.interfaceId).to.equal('0x8469b577')
})
})

Expand Down
17 changes: 9 additions & 8 deletions packages/contracts/contracts/rewards/RewardsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -173,24 +173,25 @@ contract RewardsManager is
* Note that the IssuanceAllocator can be set to the zero address to disable use of an allocator, and
* use the local `issuancePerBlock` variable instead to control issuance.
*/
function setIssuanceAllocator(address newIssuanceAllocator) external override onlyGovernor {
if (address(issuanceAllocator) != newIssuanceAllocator) {
function setIssuanceAllocator(IIssuanceAllocationDistribution newIssuanceAllocator) external override onlyGovernor {
if (issuanceAllocator != newIssuanceAllocator) {
// Update rewards calculation before changing the issuance allocator
updateAccRewardsPerSignal();

// Check that the contract supports the IIssuanceAllocationDistribution interface
// Allow zero address to disable the allocator
if (newIssuanceAllocator != address(0)) {
if (address(newIssuanceAllocator) != address(0)) {
// solhint-disable-next-line gas-small-strings
require(
IERC165(newIssuanceAllocator).supportsInterface(type(IIssuanceAllocationDistribution).interfaceId),
IERC165(address(newIssuanceAllocator)).supportsInterface(
type(IIssuanceAllocationDistribution).interfaceId
),
"Contract does not support IIssuanceAllocationDistribution interface"
);
}

address oldIssuanceAllocator = address(issuanceAllocator);
issuanceAllocator = IIssuanceAllocationDistribution(newIssuanceAllocator);
emit IssuanceAllocatorSet(oldIssuanceAllocator, newIssuanceAllocator);
emit IssuanceAllocatorSet(issuanceAllocator, newIssuanceAllocator);
issuanceAllocator = newIssuanceAllocator;
}
}

Expand Down Expand Up @@ -325,7 +326,7 @@ contract RewardsManager is
}

/**
* @inheritdoc IRewardsManager
* @inheritdoc IIssuanceTarget
*/
function getIssuanceAllocator() external view override returns (IIssuanceAllocationDistribution) {
return issuanceAllocator;
Expand Down
10 changes: 5 additions & 5 deletions packages/deployment/lib/abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ function loadAbi(artifactPath: string): Abi {
return artifact.abi as Abi
}

// Interface IDs - these match the generated values from TypeChain factories
// Verified by tests: packages/issuance/testing/tests/allocate/InterfaceIdStability.test.ts
// and packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts
// Interface IDs - these mirror the values the compiler derives from the
// corresponding ABI. Cross-checked by test/interface-id-stability.test.ts;
// update both together whenever an interface changes.
export const IERC165_INTERFACE_ID = '0x01ffc9a7' as const
export const IISSUANCE_TARGET_INTERFACE_ID = '0xaee4dc43' as const
export const IREWARDS_MANAGER_INTERFACE_ID = '0xa0a2f219' as const
export const IISSUANCE_TARGET_INTERFACE_ID = '0x19f6601a' as const
export const IREWARDS_MANAGER_INTERFACE_ID = '0x8469b577' as const

export const REWARDS_MANAGER_ABI = loadAbi(
'@graphprotocol/interfaces/artifacts/contracts/contracts/rewards/IRewardsManager.sol/IRewardsManager.json',
Expand Down
34 changes: 34 additions & 0 deletions packages/deployment/test/interface-id-stability.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect } from 'chai'
import type { Abi } from 'viem'
import { toFunctionSelector } from 'viem'

import {
IERC165_ABI,
IERC165_INTERFACE_ID,
IISSUANCE_TARGET_INTERFACE_ID,
IREWARDS_MANAGER_INTERFACE_ID,
ISSUANCE_TARGET_ABI,
REWARDS_MANAGER_ABI,
} from '../lib/abis.js'

function computeInterfaceId(abi: Abi): `0x${string}` {
const xor = abi
.filter((item): item is Extract<(typeof abi)[number], { type: 'function' }> => item.type === 'function')
.map((f) => Number.parseInt(toFunctionSelector(f).slice(2), 16) >>> 0)
.reduce((a, s) => (a ^ s) >>> 0, 0)
return `0x${xor.toString(16).padStart(8, '0')}`
}

describe('Interface ID Stability', function () {
it('IERC165_INTERFACE_ID matches the IERC165 ABI', function () {
expect(IERC165_INTERFACE_ID).to.equal(computeInterfaceId(IERC165_ABI))
})

it('IISSUANCE_TARGET_INTERFACE_ID matches the IIssuanceTarget ABI', function () {
expect(IISSUANCE_TARGET_INTERFACE_ID).to.equal(computeInterfaceId(ISSUANCE_TARGET_ABI))
})

it('IREWARDS_MANAGER_INTERFACE_ID matches the IRewardsManager ABI', function () {
expect(IREWARDS_MANAGER_INTERFACE_ID).to.equal(computeInterfaceId(REWARDS_MANAGER_ABI))
})
})
57 changes: 57 additions & 0 deletions packages/horizon/contracts/mocks/CallbackGasProbe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.27;

import { IProviderEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IProviderEligibility.sol";

/**
* @title CallbackGasProbe
* @author Edge & Node
* @notice Test-only contract that replicates the precheck + STATICCALL pattern used by
* `RecurringCollector._preCollectCallbacks` for the eligibility path. Exists so that
* Hardhat-side tests can verify, on a real EIP-2929-applying EVM (foundry's REVM in this
* project does not differentiate cold/warm in `gasleft()`-derived measurements), that
* `CALLBACK_GAS_OVERHEAD` covers the cold-account access cost on the staticcall.
*
* @dev MUST be kept in sync with the equivalent block in `RecurringCollector.sol`. If the
* production constants (`MAX_PAYER_CALLBACK_GAS`, `CALLBACK_GAS_OVERHEAD`) or the precheck /
* staticcall sequence change, mirror the change here. This probe is not deployed to any
* production network.
*/
contract CallbackGasProbe {
uint256 internal constant MAX_PAYER_CALLBACK_GAS = 1_500_000;
uint256 internal constant CALLBACK_GAS_OVERHEAD = 3_000;

error CallbackGasProbeInsufficientCallbackGas();
error CallbackGasProbeNotEligible();

/**
* @notice Re-runs the eligibility precheck + STATICCALL exactly as
* `RecurringCollector._preCollectCallbacks` does, against `payer`. Reverts with
* `CallbackGasProbeInsufficientCallbackGas` if the precheck blocks, or
* `CallbackGasProbeNotEligible` if the staticcall returned an explicit `false` (i.e.
* the forwarded gas was below whatever the payer mock requires). Used by the
* boundary test to discriminate "precheck is the gate" (overhead healthy) from
* "precheck passed but forwarded < threshold" (overhead insufficient for cold δ).
* @param payer The contract to staticcall for eligibility.
* @param provider The provider address passed through to `isEligible`.
*/
function probeEligibility(address payer, address provider) external view {
if (gasleft() < (MAX_PAYER_CALLBACK_GAS * 64) / 63 + CALLBACK_GAS_OVERHEAD) {
revert CallbackGasProbeInsufficientCallbackGas();
}
bytes memory cd = abi.encodeCall(IProviderEligibility.isEligible, (provider));
bool success;
uint256 returnLen;
uint256 result;
// solhint-disable-next-line no-inline-assembly
assembly {
success := staticcall(MAX_PAYER_CALLBACK_GAS, payer, add(cd, 0x20), mload(cd), 0x00, 0x20)
returnLen := returndatasize()
result := mload(0x00)
}
if (success && !(returnLen < 32) && result == 0) {
revert CallbackGasProbeNotEligible();
}
}
}
33 changes: 33 additions & 0 deletions packages/horizon/contracts/mocks/GasReportingEligibilityMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.27;

import { IProviderEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IProviderEligibility.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

/**
* @title GasReportingEligibilityMock
* @author Edge & Node
* @notice Test-only mock that returns `gasleft() >= MIN_REQUIRED_GASLEFT` from `isEligible`.
* Encoding the budget check into the return value is the only signal a STATICCALL
* callee can give (no state writes, no logs), so the boundary discriminator at the
* caller side is "precheck reverted" vs "got false return → eligibility revert".
*/
contract GasReportingEligibilityMock is IProviderEligibility, IERC165 {
/// @notice Minimum forwarded `gasleft()` required for `isEligible` to return true.
uint256 public immutable MIN_REQUIRED_GASLEFT;

constructor(uint256 minRequiredGasleft_) {
MIN_REQUIRED_GASLEFT = minRequiredGasleft_;
}

/// @inheritdoc IProviderEligibility
function isEligible(address) external view override returns (bool) {
return !(gasleft() < MIN_REQUIRED_GASLEFT);
}

/// @inheritdoc IERC165
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IProviderEligibility).interfaceId || interfaceId == type(IERC165).interfaceId;
}
}
Loading
Loading