From f84840b123750f8a9ec07f2a7971454080f2c1d8 Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 14:09:40 +0200 Subject: [PATCH 01/16] Ensure Ponder builds contract artifacts on start --- packages/ponder/scripts/start.mjs | 130 ++++++++++++++++++++------ packages/ponder/scripts/start.test.ts | 104 +++++++++++++++++++++ 2 files changed, 204 insertions(+), 30 deletions(-) create mode 100644 packages/ponder/scripts/start.test.ts diff --git a/packages/ponder/scripts/start.mjs b/packages/ponder/scripts/start.mjs index 81eabe043..d86f8aeba 100644 --- a/packages/ponder/scripts/start.mjs +++ b/packages/ponder/scripts/start.mjs @@ -1,38 +1,108 @@ -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { pathToFileURL, fileURLToPath } from "node:url"; import { buildPonderStartArgs } from "./databaseSchema.mjs"; -const { args, env, schemaInfo } = buildPonderStartArgs(process.argv.slice(2), process.env); - -if (schemaInfo?.ignoredLegacyDatabaseSchema) { - console.warn( - `[ponder:start] Ignoring DATABASE_SCHEMA=ponder to avoid colliding with legacy Ponder app metadata; using ${schemaInfo.schema}.`, - ); - console.warn("[ponder:start] Set RATELOOP_PONDER_DATABASE_SCHEMA to choose a different Ponder schema."); -} else if (schemaInfo?.source === "RAILWAY_DEPLOYMENT_ID") { - console.warn( - `[ponder:start] Using Railway deployment-scoped Ponder schema ${schemaInfo.schema}.`, - ); -} else if (schemaInfo?.source === "default") { - console.warn( - `[ponder:start] DATABASE_SCHEMA is not set; using RateLoop's production default schema ${schemaInfo.schema}.`, - ); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, "../../.."); +const contractsRoot = resolve(repoRoot, "packages/contracts"); +export const requiredContractsArtifacts = [ + resolve(contractsRoot, "dist/esm/abis/index.js"), + resolve(contractsRoot, "dist/esm/deployedContracts.js"), + resolve(contractsRoot, "dist/esm/deployments.js"), + resolve(contractsRoot, "dist/esm/protocol.js"), +]; + +export function contractsArtifactsExist({ + exists = existsSync, + requiredArtifacts = requiredContractsArtifacts, +} = {}) { + return requiredArtifacts.every((path) => exists(path)); +} + +export function ensureContractsArtifacts({ + exists = existsSync, + spawnSyncImpl = spawnSync, + cwd = repoRoot, + requiredArtifacts = requiredContractsArtifacts, +} = {}) { + if (contractsArtifactsExist({ exists, requiredArtifacts })) { + return false; + } + + console.warn("[ponder:start] Missing @rateloop/contracts build artifacts; building the contracts workspace."); + const result = spawnSyncImpl("yarn", ["workspace", "@rateloop/contracts", "build"], { + cwd, + stdio: "inherit", + }); + + if (result.error) { + throw new Error(`Failed to build @rateloop/contracts: ${result.error.message}`); + } + if (result.status !== 0) { + throw new Error(`Failed to build @rateloop/contracts: yarn exited with status ${result.status ?? "unknown"}.`); + } + if (!contractsArtifactsExist({ exists, requiredArtifacts })) { + throw new Error("Built @rateloop/contracts, but required Ponder contract artifacts are still missing."); + } + + return true; } -const child = spawn("ponder", args, { - env, - stdio: "inherit", -}); +export function startPonder({ + argv = process.argv.slice(2), + env = process.env, + spawnImpl = spawn, + ensureContractsArtifactsImpl = ensureContractsArtifacts, +} = {}) { + ensureContractsArtifactsImpl(); -child.on("error", (error) => { - console.error(`[ponder:start] Failed to start Ponder: ${error.message}`); - process.exitCode = 1; -}); + const { args, env: childEnv, schemaInfo } = buildPonderStartArgs(argv, env); -child.on("exit", (code, signal) => { - if (signal) { - process.kill(process.pid, signal); - return; + if (schemaInfo?.ignoredLegacyDatabaseSchema) { + console.warn( + `[ponder:start] Ignoring DATABASE_SCHEMA=ponder to avoid colliding with legacy Ponder app metadata; using ${schemaInfo.schema}.`, + ); + console.warn("[ponder:start] Set RATELOOP_PONDER_DATABASE_SCHEMA to choose a different Ponder schema."); + } else if (schemaInfo?.source === "RAILWAY_DEPLOYMENT_ID") { + console.warn( + `[ponder:start] Using Railway deployment-scoped Ponder schema ${schemaInfo.schema}.`, + ); + } else if (schemaInfo?.source === "default") { + console.warn( + `[ponder:start] DATABASE_SCHEMA is not set; using RateLoop's production default schema ${schemaInfo.schema}.`, + ); } - process.exitCode = code ?? 1; -}); + const child = spawnImpl("ponder", args, { + env: childEnv, + stdio: "inherit", + }); + + child.on("error", (error) => { + console.error(`[ponder:start] Failed to start Ponder: ${error.message}`); + process.exitCode = 1; + }); + + child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + + process.exitCode = code ?? 1; + }); + + return child; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + try { + startPonder(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`[ponder:start] ${message}`); + process.exitCode = 1; + } +} diff --git a/packages/ponder/scripts/start.test.ts b/packages/ponder/scripts/start.test.ts new file mode 100644 index 000000000..188cda88f --- /dev/null +++ b/packages/ponder/scripts/start.test.ts @@ -0,0 +1,104 @@ +import { EventEmitter } from "node:events"; +import { + contractsArtifactsExist, + ensureContractsArtifacts, + startPonder, +} from "./start.mjs"; + +describe("Ponder production launcher", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("detects when required contracts artifacts already exist", () => { + const requiredArtifacts = ["/repo/packages/contracts/dist/esm/abis/index.js"]; + const exists = vi.fn(() => true); + + expect(contractsArtifactsExist({ exists, requiredArtifacts })).toBe(true); + expect(exists).toHaveBeenCalledWith(requiredArtifacts[0]); + }); + + test("does not rebuild contracts when required artifacts exist", () => { + const spawnSyncImpl = vi.fn(); + + expect( + ensureContractsArtifacts({ + exists: () => true, + spawnSyncImpl, + requiredArtifacts: ["/repo/packages/contracts/dist/esm/abis/index.js"], + }), + ).toBe(false); + expect(spawnSyncImpl).not.toHaveBeenCalled(); + }); + + test("builds contracts when required artifacts are missing", () => { + let built = false; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const spawnSyncImpl = vi.fn(() => { + built = true; + return { status: 0 }; + }); + + expect( + ensureContractsArtifacts({ + exists: () => built, + spawnSyncImpl, + cwd: "/repo", + requiredArtifacts: ["/repo/packages/contracts/dist/esm/abis/index.js"], + }), + ).toBe(true); + + expect(warnSpy).toHaveBeenCalledWith( + "[ponder:start] Missing @rateloop/contracts build artifacts; building the contracts workspace.", + ); + expect(spawnSyncImpl).toHaveBeenCalledWith( + "yarn", + ["workspace", "@rateloop/contracts", "build"], + { + cwd: "/repo", + stdio: "inherit", + }, + ); + }); + + test("throws when the contracts build fails", () => { + vi.spyOn(console, "warn").mockImplementation(() => {}); + + expect(() => + ensureContractsArtifacts({ + exists: () => false, + spawnSyncImpl: () => ({ status: 1 }), + requiredArtifacts: ["/repo/packages/contracts/dist/esm/abis/index.js"], + }), + ).toThrow("Failed to build @rateloop/contracts: yarn exited with status 1."); + }); + + test("starts Ponder with the resolved schema after checking contracts artifacts", () => { + class FakeChild extends EventEmitter {} + + const child = new FakeChild(); + const ensureContractsArtifactsImpl = vi.fn(); + const spawnImpl = vi.fn(() => child); + + expect( + startPonder({ + argv: ["--port", "42069"], + env: { DATABASE_SCHEMA: "rateloop_ponder_preview" }, + spawnImpl, + ensureContractsArtifactsImpl, + }), + ).toBe(child); + + expect(ensureContractsArtifactsImpl).toHaveBeenCalled(); + expect(spawnImpl).toHaveBeenCalledWith( + "ponder", + ["start", "--schema", "rateloop_ponder_preview", "--port", "42069"], + { + env: { + DATABASE_SCHEMA: "rateloop_ponder_preview", + }, + stdio: "inherit", + }, + ); + }); +}); From 1c6080bfcc03b9dbba79f5d11a6ec991bc2f3aff Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 14:09:57 +0200 Subject: [PATCH 02/16] Stub Ponder config RPC probe in tests --- packages/ponder/ponder.config.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/ponder/ponder.config.test.ts b/packages/ponder/ponder.config.test.ts index 69dfcc2da..d1c3d5a20 100644 --- a/packages/ponder/ponder.config.test.ts +++ b/packages/ponder/ponder.config.test.ts @@ -81,6 +81,12 @@ const VALID_ENV = { PONDER_CONTENT_REGISTRY_START_BLOCK: String(expectedContentRegistryStartBlock), }; +function getExpectedProbeChainId(env: Record) { + if (env.PONDER_NETWORK === "worldchain") return 480; + if (env.PONDER_NETWORK === "hardhat") return 31337; + return 4801; +} + async function loadPonderConfig(overrides: Record = {}, removals: string[] = []) { vi.resetModules(); process.env = { @@ -93,11 +99,23 @@ async function loadPonderConfig(overrides: Record = delete process.env[key]; } + vi.stubGlobal( + "fetch", + vi.fn(async () => { + const chainId = getExpectedProbeChainId(process.env); + return new Response(JSON.stringify({ result: `0x${chainId.toString(16)}` }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }), + ); + return import("./ponder.config.ts"); } afterEach(() => { process.env = { ...ORIGINAL_ENV }; + vi.unstubAllGlobals(); vi.restoreAllMocks(); vi.resetModules(); }); From a127c43936cbd4e119d5581394c5a66fcb92b937 Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 14:13:06 +0200 Subject: [PATCH 03/16] Build workspace packages before Next.js --- packages/nextjs/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 2a172f394..46f519c94 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -4,7 +4,8 @@ "private": true, "scripts": { "analyze": "ANALYZE=true yarn build", - "build": "rm -rf .next && next build", + "build": "yarn build:workspace-deps && rm -rf .next && next build", + "build:workspace-deps": "yarn workspace @rateloop/contracts build && yarn workspace @rateloop/sdk build && yarn workspace @rateloop/agents build", "check-types": "RATELOOP_E2E_PRODUCTION_BUILD=true NEXT_PUBLIC_RATELOOP_E2E_PRODUCTION_BUILD=true next typegen && tsc --noEmit --incremental", "dev": "node scripts/dev-preflight.mjs", "format": "prettier --write . '!(node_modules|.next|contracts)/**/*'", From 80bb8bd351b797b8c6134a6cf5a8cdd1de221189 Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 16:25:09 +0200 Subject: [PATCH 04/16] Trim RoundVotingEngine role mutation surface --- .../src/abis/RoundVotingEngineAbi.ts | 185 +++------ packages/contracts/src/deployedContracts.ts | 370 ++++++------------ .../foundry/contracts/RoundVotingEngine.sol | 25 -- 3 files changed, 168 insertions(+), 412 deletions(-) diff --git a/packages/contracts/src/abis/RoundVotingEngineAbi.ts b/packages/contracts/src/abis/RoundVotingEngineAbi.ts index c904c931d..7887b4eb3 100644 --- a/packages/contracts/src/abis/RoundVotingEngineAbi.ts +++ b/packages/contracts/src/abis/RoundVotingEngineAbi.ts @@ -512,25 +512,6 @@ export const RoundVotingEngineAbi = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "getRoleAdmin", - "inputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "pure" - }, { "type": "function", "name": "getRoundCommitKey", @@ -560,24 +541,6 @@ export const RoundVotingEngineAbi = [ ], "stateMutability": "view" }, - { - "type": "function", - "name": "grantRole", - "inputs": [ - { - "name": "role", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "account", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "hasCommits", @@ -910,24 +873,6 @@ export const RoundVotingEngineAbi = [ "outputs": [], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "renounceRole", - "inputs": [ - { - "name": "role", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "callerConfirmation", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "replayBundleObserverNotify", @@ -1024,24 +969,6 @@ export const RoundVotingEngineAbi = [ "outputs": [], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "revokeRole", - "inputs": [ - { - "name": "role", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "account", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, { "type": "function", "name": "rewardDistributorConfigShape", @@ -1628,62 +1555,6 @@ export const RoundVotingEngineAbi = [ ], "anonymous": false }, - { - "type": "event", - "name": "SettlementCallerIncentivePaid", - "inputs": [ - { - "name": "contentId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "roundId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "caller", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "SettlementCallerIncentiveSkipped", - "inputs": [ - { - "name": "contentId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "roundId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, { "type": "event", "name": "RbtsVoteRevealed", @@ -1958,6 +1829,62 @@ export const RoundVotingEngineAbi = [ ], "anonymous": false }, + { + "type": "event", + "name": "SettlementCallerIncentivePaid", + "inputs": [ + { + "name": "contentId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "roundId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "caller", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SettlementCallerIncentiveSkipped", + "inputs": [ + { + "name": "contentId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "roundId", + "type": "uint256", + "indexed": true, + "internalType": "uint256" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, { "type": "event", "name": "TreasuryFeeDistributed", diff --git a/packages/contracts/src/deployedContracts.ts b/packages/contracts/src/deployedContracts.ts index d37faf4f4..16ef68c00 100644 --- a/packages/contracts/src/deployedContracts.ts +++ b/packages/contracts/src/deployedContracts.ts @@ -7517,25 +7517,6 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, - { - type: "function", - name: "getRoleAdmin", - inputs: [ - { - name: "", - type: "bytes32", - internalType: "bytes32", - }, - ], - outputs: [ - { - name: "", - type: "bytes32", - internalType: "bytes32", - }, - ], - stateMutability: "pure", - }, { type: "function", name: "getRoundCommitKey", @@ -7565,24 +7546,6 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, - { - type: "function", - name: "grantRole", - inputs: [ - { - name: "role", - type: "bytes32", - internalType: "bytes32", - }, - { - name: "account", - type: "address", - internalType: "address", - }, - ], - outputs: [], - stateMutability: "nonpayable", - }, { type: "function", name: "hasCommits", @@ -7915,24 +7878,6 @@ const deployedContracts: GenericContractsDeclaration = { outputs: [], stateMutability: "nonpayable", }, - { - type: "function", - name: "renounceRole", - inputs: [ - { - name: "role", - type: "bytes32", - internalType: "bytes32", - }, - { - name: "callerConfirmation", - type: "address", - internalType: "address", - }, - ], - outputs: [], - stateMutability: "nonpayable", - }, { type: "function", name: "replayBundleObserverNotify", @@ -8029,24 +7974,6 @@ const deployedContracts: GenericContractsDeclaration = { outputs: [], stateMutability: "nonpayable", }, - { - type: "function", - name: "revokeRole", - inputs: [ - { - name: "role", - type: "bytes32", - internalType: "bytes32", - }, - { - name: "account", - type: "address", - internalType: "address", - }, - ], - outputs: [], - stateMutability: "nonpayable", - }, { type: "function", name: "rewardDistributorConfigShape", @@ -8633,62 +8560,6 @@ const deployedContracts: GenericContractsDeclaration = { ], anonymous: false, }, - { - type: "event", - name: "SettlementCallerIncentivePaid", - inputs: [ - { - name: "contentId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "roundId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "caller", - type: "address", - indexed: true, - internalType: "address", - }, - { - name: "amount", - type: "uint256", - indexed: false, - internalType: "uint256", - }, - ], - anonymous: false, - }, - { - type: "event", - name: "SettlementCallerIncentiveSkipped", - inputs: [ - { - name: "contentId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "roundId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "amount", - type: "uint256", - indexed: false, - internalType: "uint256", - }, - ], - anonymous: false, - }, { type: "event", name: "RbtsVoteRevealed", @@ -8963,6 +8834,62 @@ const deployedContracts: GenericContractsDeclaration = { ], anonymous: false, }, + { + type: "event", + name: "SettlementCallerIncentivePaid", + inputs: [ + { + name: "contentId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "roundId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "caller", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "SettlementCallerIncentiveSkipped", + inputs: [ + { + name: "contentId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "roundId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, { type: "event", name: "TreasuryFeeDistributed", @@ -34427,25 +34354,6 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, - { - type: "function", - name: "getRoleAdmin", - inputs: [ - { - name: "", - type: "bytes32", - internalType: "bytes32", - }, - ], - outputs: [ - { - name: "", - type: "bytes32", - internalType: "bytes32", - }, - ], - stateMutability: "pure", - }, { type: "function", name: "getRoundCommitKey", @@ -34475,24 +34383,6 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, - { - type: "function", - name: "grantRole", - inputs: [ - { - name: "role", - type: "bytes32", - internalType: "bytes32", - }, - { - name: "account", - type: "address", - internalType: "address", - }, - ], - outputs: [], - stateMutability: "nonpayable", - }, { type: "function", name: "hasCommits", @@ -34825,24 +34715,6 @@ const deployedContracts: GenericContractsDeclaration = { outputs: [], stateMutability: "nonpayable", }, - { - type: "function", - name: "renounceRole", - inputs: [ - { - name: "role", - type: "bytes32", - internalType: "bytes32", - }, - { - name: "callerConfirmation", - type: "address", - internalType: "address", - }, - ], - outputs: [], - stateMutability: "nonpayable", - }, { type: "function", name: "replayBundleObserverNotify", @@ -34939,24 +34811,6 @@ const deployedContracts: GenericContractsDeclaration = { outputs: [], stateMutability: "nonpayable", }, - { - type: "function", - name: "revokeRole", - inputs: [ - { - name: "role", - type: "bytes32", - internalType: "bytes32", - }, - { - name: "account", - type: "address", - internalType: "address", - }, - ], - outputs: [], - stateMutability: "nonpayable", - }, { type: "function", name: "rewardDistributorConfigShape", @@ -35543,62 +35397,6 @@ const deployedContracts: GenericContractsDeclaration = { ], anonymous: false, }, - { - type: "event", - name: "SettlementCallerIncentivePaid", - inputs: [ - { - name: "contentId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "roundId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "caller", - type: "address", - indexed: true, - internalType: "address", - }, - { - name: "amount", - type: "uint256", - indexed: false, - internalType: "uint256", - }, - ], - anonymous: false, - }, - { - type: "event", - name: "SettlementCallerIncentiveSkipped", - inputs: [ - { - name: "contentId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "roundId", - type: "uint256", - indexed: true, - internalType: "uint256", - }, - { - name: "amount", - type: "uint256", - indexed: false, - internalType: "uint256", - }, - ], - anonymous: false, - }, { type: "event", name: "RbtsVoteRevealed", @@ -35873,6 +35671,62 @@ const deployedContracts: GenericContractsDeclaration = { ], anonymous: false, }, + { + type: "event", + name: "SettlementCallerIncentivePaid", + inputs: [ + { + name: "contentId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "roundId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "caller", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "SettlementCallerIncentiveSkipped", + inputs: [ + { + name: "contentId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "roundId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "amount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, { type: "event", name: "TreasuryFeeDistributed", diff --git a/packages/foundry/contracts/RoundVotingEngine.sol b/packages/foundry/contracts/RoundVotingEngine.sol index b54907a79..0a32ead8a 100644 --- a/packages/foundry/contracts/RoundVotingEngine.sol +++ b/packages/foundry/contracts/RoundVotingEngine.sol @@ -325,23 +325,6 @@ contract RoundVotingEngine is return _accessControlStorage().roles[role].hasRole[account]; } - function getRoleAdmin(bytes32) public pure returns (bytes32) { - return DEFAULT_ADMIN_ROLE; - } - - function grantRole(bytes32 role, address account) public onlyRole(DEFAULT_ADMIN_ROLE) { - _grantRole(role, account); - } - - function revokeRole(bytes32 role, address account) public onlyRole(DEFAULT_ADMIN_ROLE) { - _revokeRole(role, account); - } - - function renounceRole(bytes32 role, address callerConfirmation) public { - if (callerConfirmation != msg.sender) revert AccessControlBadConfirmation(); - _revokeRole(role, callerConfirmation); - } - modifier onlyRole(bytes32 role) { _checkRole(role, msg.sender); _; @@ -359,14 +342,6 @@ contract RoundVotingEngine is return true; } - function _revokeRole(bytes32 role, address account) internal returns (bool) { - AccessControlStorage storage $ = _accessControlStorage(); - if (!$.roles[role].hasRole[account]) return false; - $.roles[role].hasRole[account] = false; - emit RoleRevoked(role, account, msg.sender); - return true; - } - function _accessControlStorage() private pure returns (AccessControlStorage storage $) { assembly ("memory-safe") { $.slot := ACCESS_CONTROL_STORAGE_LOCATION From 59b1ca65a3967f7cc31ebf9d4b9a6d81b4a036e8 Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 16:26:00 +0200 Subject: [PATCH 05/16] Sync FrontendRegistry deployment ABIs --- packages/contracts/src/deployedContracts.ts | 310 +++++++++++++++++++- 1 file changed, 304 insertions(+), 6 deletions(-) diff --git a/packages/contracts/src/deployedContracts.ts b/packages/contracts/src/deployedContracts.ts index 16ef68c00..b4b5d9cc8 100644 --- a/packages/contracts/src/deployedContracts.ts +++ b/packages/contracts/src/deployedContracts.ts @@ -10847,6 +10847,19 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, + { + type: "function", + name: "CHALLENGER_BOUNTY_BPS", + inputs: [], + outputs: [ + { + name: "", + type: "uint16", + internalType: "uint16", + }, + ], + stateMutability: "view", + }, { type: "function", name: "DEFAULT_ADMIN_ROLE", @@ -10873,6 +10886,19 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, + { + type: "function", + name: "FEE_WITHDRAWAL_DELAY", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, { type: "function", name: "GOVERNANCE_ROLE", @@ -11015,21 +11041,21 @@ const deployedContracts: GenericContractsDeclaration = { }, { type: "function", - name: "claimFees", + name: "clearSnapshotProposer", inputs: [], outputs: [], stateMutability: "nonpayable", }, { type: "function", - name: "clearSnapshotProposer", + name: "completeDeregister", inputs: [], outputs: [], stateMutability: "nonpayable", }, { type: "function", - name: "completeDeregister", + name: "completeFeeWithdrawal", inputs: [], outputs: [], stateMutability: "nonpayable", @@ -11441,6 +11467,44 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, + { + type: "function", + name: "pendingFeeWithdrawalAmount", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "pendingFeeWithdrawalReleaseAt", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, { type: "function", name: "register", @@ -11505,6 +11569,13 @@ const deployedContracts: GenericContractsDeclaration = { outputs: [], stateMutability: "nonpayable", }, + { + type: "function", + name: "requestFeeWithdrawal", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, { type: "function", name: "revokeRole", @@ -11585,6 +11656,34 @@ const deployedContracts: GenericContractsDeclaration = { outputs: [], stateMutability: "nonpayable", }, + { + type: "function", + name: "slashFrontendWithBounty", + inputs: [ + { + name: "frontend", + type: "address", + internalType: "address", + }, + { + name: "amount", + type: "uint256", + internalType: "uint256", + }, + { + name: "reason", + type: "string", + internalType: "string", + }, + { + name: "bountyRecipient", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, { type: "function", name: "snapshotProposerForFrontend", @@ -11662,6 +11761,31 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, + { + type: "event", + name: "ChallengerBountyPaid", + inputs: [ + { + name: "frontend", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "recipient", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "lrepAmount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, { type: "event", name: "ConfiscationRecipientUpdated", @@ -11700,6 +11824,31 @@ const deployedContracts: GenericContractsDeclaration = { ], anonymous: false, }, + { + type: "event", + name: "FeeWithdrawalRequested", + inputs: [ + { + name: "frontend", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "lrepAmount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "releaseAt", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, { type: "event", name: "FeesClaimed", @@ -37684,6 +37833,19 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, + { + type: "function", + name: "CHALLENGER_BOUNTY_BPS", + inputs: [], + outputs: [ + { + name: "", + type: "uint16", + internalType: "uint16", + }, + ], + stateMutability: "view", + }, { type: "function", name: "DEFAULT_ADMIN_ROLE", @@ -37710,6 +37872,19 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, + { + type: "function", + name: "FEE_WITHDRAWAL_DELAY", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, { type: "function", name: "GOVERNANCE_ROLE", @@ -37852,21 +38027,21 @@ const deployedContracts: GenericContractsDeclaration = { }, { type: "function", - name: "claimFees", + name: "clearSnapshotProposer", inputs: [], outputs: [], stateMutability: "nonpayable", }, { type: "function", - name: "clearSnapshotProposer", + name: "completeDeregister", inputs: [], outputs: [], stateMutability: "nonpayable", }, { type: "function", - name: "completeDeregister", + name: "completeFeeWithdrawal", inputs: [], outputs: [], stateMutability: "nonpayable", @@ -38278,6 +38453,44 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, + { + type: "function", + name: "pendingFeeWithdrawalAmount", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "pendingFeeWithdrawalReleaseAt", + inputs: [ + { + name: "", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, { type: "function", name: "register", @@ -38342,6 +38555,13 @@ const deployedContracts: GenericContractsDeclaration = { outputs: [], stateMutability: "nonpayable", }, + { + type: "function", + name: "requestFeeWithdrawal", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, { type: "function", name: "revokeRole", @@ -38422,6 +38642,34 @@ const deployedContracts: GenericContractsDeclaration = { outputs: [], stateMutability: "nonpayable", }, + { + type: "function", + name: "slashFrontendWithBounty", + inputs: [ + { + name: "frontend", + type: "address", + internalType: "address", + }, + { + name: "amount", + type: "uint256", + internalType: "uint256", + }, + { + name: "reason", + type: "string", + internalType: "string", + }, + { + name: "bountyRecipient", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, { type: "function", name: "snapshotProposerForFrontend", @@ -38499,6 +38747,31 @@ const deployedContracts: GenericContractsDeclaration = { ], stateMutability: "view", }, + { + type: "event", + name: "ChallengerBountyPaid", + inputs: [ + { + name: "frontend", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "recipient", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "lrepAmount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, { type: "event", name: "ConfiscationRecipientUpdated", @@ -38537,6 +38810,31 @@ const deployedContracts: GenericContractsDeclaration = { ], anonymous: false, }, + { + type: "event", + name: "FeeWithdrawalRequested", + inputs: [ + { + name: "frontend", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "lrepAmount", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + { + name: "releaseAt", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, { type: "event", name: "FeesClaimed", From d2eaaf21bdb51c16ae256cde3fd22d461e0676c6 Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 16:31:38 +0200 Subject: [PATCH 06/16] Update storage layout snapshots --- .../FrontendRegistry.json | 16 ++++++++++++++-- .../RoundVotingEngine.json | 14 ++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/foundry/scripts/expected-storage-layouts/FrontendRegistry.json b/packages/foundry/scripts/expected-storage-layouts/FrontendRegistry.json index 7bbdd48dd..4df0a5ebf 100644 --- a/packages/foundry/scripts/expected-storage-layouts/FrontendRegistry.json +++ b/packages/foundry/scripts/expected-storage-layouts/FrontendRegistry.json @@ -87,6 +87,18 @@ { "slot": 13, "offset": 0, + "label": "pendingFeeWithdrawalAmount", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "slot": 14, + "offset": 0, + "label": "pendingFeeWithdrawalReleaseAt", + "type": "t_mapping(t_address,t_uint256)" + }, + { + "slot": 15, + "offset": 0, "label": "__gap", "type": "t_array(t_uint256)" } @@ -108,8 +120,8 @@ { "id": "t_array(t_uint256)", "encoding": "inplace", - "label": "uint256[41]", - "numberOfBytes": "1312", + "label": "uint256[39]", + "numberOfBytes": "1248", "base": "t_uint256" }, { diff --git a/packages/foundry/scripts/expected-storage-layouts/RoundVotingEngine.json b/packages/foundry/scripts/expected-storage-layouts/RoundVotingEngine.json index 908528afe..c6f443f20 100644 --- a/packages/foundry/scripts/expected-storage-layouts/RoundVotingEngine.json +++ b/packages/foundry/scripts/expected-storage-layouts/RoundVotingEngine.json @@ -387,35 +387,29 @@ { "slot": 64, "offset": 0, - "label": "_roundRbtsSeedRefreshCount", - "type": "t_mapping(t_uint256,t_mapping(t_uint256,t_uint8))" - }, - { - "slot": 65, - "offset": 0, "label": "_pendingTreasuryForfeitLrep", "type": "t_uint256" }, { - "slot": 66, + "slot": 65, "offset": 0, "label": "commitCredentialMask", "type": "t_mapping(t_uint256,t_mapping(t_uint256,t_mapping(t_bytes32,t_uint8)))" }, { - "slot": 67, + "slot": 66, "offset": 0, "label": "commitFreshCredentialMask", "type": "t_mapping(t_uint256,t_mapping(t_uint256,t_mapping(t_bytes32,t_uint8)))" }, { - "slot": 68, + "slot": 67, "offset": 0, "label": "roundContentDormantCountSnapshot", "type": "t_mapping(t_uint256,t_mapping(t_uint256,t_uint8))" }, { - "slot": 69, + "slot": 68, "offset": 0, "label": "__gap", "type": "t_array(t_uint256)" From bbdec69952f3934df667742ce4ec2d7e84a77003 Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 16:32:15 +0200 Subject: [PATCH 07/16] Apply Foundry formatting --- .../foundry/contracts/ClusterPayoutOracle.sol | 2 +- .../foundry/contracts/FrontendRegistry.sol | 11 +++--- .../contracts/libraries/RewardMath.sol | 6 +--- packages/foundry/script/Deploy.s.sol | 13 +++---- .../foundry/test/ClusterPayoutOracle.t.sol | 36 +++---------------- .../test/DeployRateLoopAllocations.t.sol | 13 ++----- .../test/QuestionRewardParticipantFloor.t.sol | 13 +------ .../test/QuestionRewardPoolEscrow.t.sol | 26 +++----------- packages/foundry/test/RewardMath.t.sol | 6 +--- .../test/RoundVotingEngineBranches.t.sol | 17 ++++----- 10 files changed, 33 insertions(+), 110 deletions(-) diff --git a/packages/foundry/contracts/ClusterPayoutOracle.sol b/packages/foundry/contracts/ClusterPayoutOracle.sol index ad30787e3..cf5cca6d7 100644 --- a/packages/foundry/contracts/ClusterPayoutOracle.sol +++ b/packages/foundry/contracts/ClusterPayoutOracle.sol @@ -1166,7 +1166,7 @@ contract ClusterPayoutOracle is IClusterPayoutOracle, AccessControl, ReentrancyG catch { revert InvalidAddress(); } - try IFrontendRegistry(newFrontendRegistry).snapshotProposerForFrontend(address(0)) returns (address) {} + try IFrontendRegistry(newFrontendRegistry).snapshotProposerForFrontend(address(0)) returns (address) { } catch { revert InvalidAddress(); } diff --git a/packages/foundry/contracts/FrontendRegistry.sol b/packages/foundry/contracts/FrontendRegistry.sol index b46a5c51f..f644579a7 100644 --- a/packages/foundry/contracts/FrontendRegistry.sol +++ b/packages/foundry/contracts/FrontendRegistry.sol @@ -447,12 +447,11 @@ contract FrontendRegistry is IFrontendRegistry, Initializable, AccessControlUpgr /// @param amount Amount of LREP to slash /// @param reason Reason for the slash /// @param bountyRecipient The challenger receiving the fixed bounty share - function slashFrontendWithBounty( - address frontend, - uint256 amount, - string calldata reason, - address bountyRecipient - ) external nonReentrant onlyRole(GOVERNANCE_ROLE) { + function slashFrontendWithBounty(address frontend, uint256 amount, string calldata reason, address bountyRecipient) + external + nonReentrant + onlyRole(GOVERNANCE_ROLE) + { require(bountyRecipient != address(0), "Invalid bounty recipient"); require(bountyRecipient != frontend, "Bounty recipient is frontend"); require(bountyRecipient != snapshotProposerForFrontend[frontend], "Bounty recipient is proposer"); diff --git a/packages/foundry/contracts/libraries/RewardMath.sol b/packages/foundry/contracts/libraries/RewardMath.sol index 3ccab94b5..4dd645dad 100644 --- a/packages/foundry/contracts/libraries/RewardMath.sol +++ b/packages/foundry/contracts/libraries/RewardMath.sol @@ -86,11 +86,7 @@ library RewardMath { uint16 scoreBps, uint16 meanScoreBps, uint256 revealedCount - ) - internal - pure - returns (uint256) - { + ) internal pure returns (uint256) { if (revealedCount < SCORE_SPREAD_FORFEIT_MIN_REVEALS) return 0; if (stakeAmount == 0 || scoreBps >= meanScoreBps) return 0; uint256 deltaBps = uint256(meanScoreBps) - scoreBps; diff --git a/packages/foundry/script/Deploy.s.sol b/packages/foundry/script/Deploy.s.sol index e8180c4e8..b3c1d4789 100644 --- a/packages/foundry/script/Deploy.s.sol +++ b/packages/foundry/script/Deploy.s.sol @@ -47,8 +47,7 @@ contract DeployRateLoop is ScaffoldETHDeploy { address internal constant WORLD_CHAIN_MAINNET_USDC = 0x79A02482A880bCE3F13e09Da970dC34db4CD24d1; address internal constant WORLD_CHAIN_SEPOLIA_USDC = 0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88; address internal constant WORLD_CHAIN_WORLD_ID_V4_VERIFIER = 0x00000000009E00F9FE82CfeeBB4556686da094d7; - address internal constant WORLD_CHAIN_WORLD_ID_V4_STAGING_VERIFIER = - 0x703a6316c975DEabF30b637c155edD53e24657DB; + address internal constant WORLD_CHAIN_WORLD_ID_V4_STAGING_VERIFIER = 0x703a6316c975DEabF30b637c155edD53e24657DB; string internal constant WORLD_ID_V4_VERIFIER_ADDRESS_ENV = "WORLD_ID_V4_VERIFIER_ADDRESS"; string internal constant RATELOOP_MAINNET_CANARY_ENV = "RATELOOP_MAINNET_CANARY"; uint64 internal constant WORLD_ID_CREDENTIAL_TTL_SECONDS = 365 days; @@ -530,9 +529,8 @@ contract DeployRateLoop is ScaffoldETHDeploy { function _resolveWorldIdVerifierAddress(bool isLocalDev) internal view returns (address) { bool hasOverride = vm.envExists(WORLD_ID_V4_VERIFIER_ADDRESS_ENV); address verifierOverride = hasOverride ? vm.envOr(WORLD_ID_V4_VERIFIER_ADDRESS_ENV, address(0)) : address(0); - return _resolveWorldIdVerifierAddressForChain( - isLocalDev, hasOverride, verifierOverride, _isMainnetCanaryEnabled() - ); + return + _resolveWorldIdVerifierAddressForChain(isLocalDev, hasOverride, verifierOverride, _isMainnetCanaryEnabled()); } function _resolveWorldIdVerifierAddressForChain(bool isLocalDev, bool hasOverride, address verifierOverride) @@ -540,9 +538,8 @@ contract DeployRateLoop is ScaffoldETHDeploy { view returns (address) { - return _resolveWorldIdVerifierAddressForChain( - isLocalDev, hasOverride, verifierOverride, _isMainnetCanaryEnabled() - ); + return + _resolveWorldIdVerifierAddressForChain(isLocalDev, hasOverride, verifierOverride, _isMainnetCanaryEnabled()); } function _resolveWorldIdVerifierAddressForChain( diff --git a/packages/foundry/test/ClusterPayoutOracle.t.sol b/packages/foundry/test/ClusterPayoutOracle.t.sol index 711dd6b70..4393acd4d 100644 --- a/packages/foundry/test/ClusterPayoutOracle.t.sol +++ b/packages/foundry/test/ClusterPayoutOracle.t.sol @@ -1136,14 +1136,7 @@ contract ClusterPayoutOracleTest is Test { bytes32 clusterRoot = keccak256("cluster-root"); bytes32 artifactHash = keccak256("epoch-artifact"); oracle.proposeCorrelationEpoch( - 1, - 1, - 20, - clusterRoot, - keccak256("params"), - artifactHash, - "ipfs://bad-epoch-uri", - _defaultEpochSources() + 1, 1, 20, clusterRoot, keccak256("params"), artifactHash, "ipfs://bad-epoch-uri", _defaultEpochSources() ); vm.warp(1 hours + 2); oracle.finalizeCorrelationEpoch(1); @@ -1156,14 +1149,7 @@ contract ClusterPayoutOracleTest is Test { vm.expectRevert(ClusterPayoutOracle.InvalidSnapshot.selector); oracle.proposeCorrelationEpoch( - 1, - 1, - 20, - clusterRoot, - keccak256("params"), - artifactHash, - "ipfs://bad-epoch-uri", - _defaultEpochSources() + 1, 1, 20, clusterRoot, keccak256("params"), artifactHash, "ipfs://bad-epoch-uri", _defaultEpochSources() ); oracle.proposeCorrelationEpoch( @@ -1254,14 +1240,7 @@ contract ClusterPayoutOracleTest is Test { vm.prank(squatter); oracle.proposeCorrelationEpoch( - 1, - 1, - 20, - clusterRoot, - keccak256("params"), - artifactHash, - "ipfs://bad-epoch-uri", - _defaultEpochSources() + 1, 1, 20, clusterRoot, keccak256("params"), artifactHash, "ipfs://bad-epoch-uri", _defaultEpochSources() ); bytes32 rejectedDigest = oracle.correlationEpochProposalDigest(1); @@ -1272,14 +1251,7 @@ contract ClusterPayoutOracleTest is Test { vm.expectRevert(ClusterPayoutOracle.InvalidSnapshot.selector); oracle.proposeCorrelationEpoch( - 1, - 1, - 20, - clusterRoot, - keccak256("params"), - artifactHash, - "ipfs://bad-epoch-uri", - _defaultEpochSources() + 1, 1, 20, clusterRoot, keccak256("params"), artifactHash, "ipfs://bad-epoch-uri", _defaultEpochSources() ); oracle.proposeCorrelationEpoch( diff --git a/packages/foundry/test/DeployRateLoopAllocations.t.sol b/packages/foundry/test/DeployRateLoopAllocations.t.sol index 7a908f827..c452a5f67 100644 --- a/packages/foundry/test/DeployRateLoopAllocations.t.sol +++ b/packages/foundry/test/DeployRateLoopAllocations.t.sol @@ -340,10 +340,7 @@ contract DeployRateLoopAllocationsTest is Test { vm.etch(bundledVerifier, address(verifier).code); vm.chainId(480); - assertEq( - deployScript.resolveWorldIdVerifierAddressForChain(false, false, address(0), false), - bundledVerifier - ); + assertEq(deployScript.resolveWorldIdVerifierAddressForChain(false, false, address(0), false), bundledVerifier); } function test_WorldChainMainnetRejectsVerifierOverrideWithoutCanary() public { @@ -375,10 +372,7 @@ contract DeployRateLoopAllocationsTest is Test { vm.etch(stagingVerifier, address(verifier).code); vm.chainId(480); - assertEq( - deployScript.resolveWorldIdVerifierAddressForChain(false, false, address(0), true), - stagingVerifier - ); + assertEq(deployScript.resolveWorldIdVerifierAddressForChain(false, false, address(0), true), stagingVerifier); } function test_WorldChainMainnetCanaryAcceptsStagingVerifierOverride() public { @@ -389,8 +383,7 @@ contract DeployRateLoopAllocationsTest is Test { vm.chainId(480); assertEq( - deployScript.resolveWorldIdVerifierAddressForChain(false, true, stagingVerifier, true), - stagingVerifier + deployScript.resolveWorldIdVerifierAddressForChain(false, true, stagingVerifier, true), stagingVerifier ); } diff --git a/packages/foundry/test/QuestionRewardParticipantFloor.t.sol b/packages/foundry/test/QuestionRewardParticipantFloor.t.sol index 40a2f0a0a..f85be23c7 100644 --- a/packages/foundry/test/QuestionRewardParticipantFloor.t.sol +++ b/packages/foundry/test/QuestionRewardParticipantFloor.t.sol @@ -11,18 +11,7 @@ contract QuestionRewardParticipantFloorHarness { } function validateSubmissionReward(uint256 amount, uint256 requiredVoters) external pure { - ContentRegistryRewardLib.validateSubmissionReward( - 1, - amount, - requiredVoters, - 1, - 0, - 0, - 0, - 0, - 1, - 1_000 - ); + ContentRegistryRewardLib.validateSubmissionReward(1, amount, requiredVoters, 1, 0, 0, 0, 0, 1, 1_000); } } diff --git a/packages/foundry/test/QuestionRewardPoolEscrow.t.sol b/packages/foundry/test/QuestionRewardPoolEscrow.t.sol index 561cb0b63..746eb0711 100644 --- a/packages/foundry/test/QuestionRewardPoolEscrow.t.sol +++ b/packages/foundry/test/QuestionRewardPoolEscrow.t.sol @@ -586,24 +586,12 @@ contract QuestionRewardPoolEscrowTest is VotingTestBase { usdc.approve(address(rewardPoolEscrow), veryHighValueAmount); vm.expectRevert("High-value floor"); rewardPoolEscrow.createRewardPool( - contentId, - veryHighValueAmount, - MIN_HIGH_VALUE_PARTICIPANTS, - 1, - block.timestamp + 30 days, - 30 days, - 0 + contentId, veryHighValueAmount, MIN_HIGH_VALUE_PARTICIPANTS, 1, block.timestamp + 30 days, 30 days, 0 ); usdc.approve(address(rewardPoolEscrow), veryHighValueAmount); uint256 rewardPoolId = rewardPoolEscrow.createRewardPool( - contentId, - veryHighValueAmount, - MIN_VERY_HIGH_VALUE_PARTICIPANTS, - 1, - block.timestamp + 30 days, - 30 days, - 0 + contentId, veryHighValueAmount, MIN_VERY_HIGH_VALUE_PARTICIPANTS, 1, block.timestamp + 30 days, 30 days, 0 ); vm.stopPrank(); @@ -946,10 +934,8 @@ contract QuestionRewardPoolEscrowTest is VotingTestBase { epochDuration: uint32(EPOCH_DURATION), maxDuration: uint32(7 days), minVoters: 8, maxVoters: 8 }); uint256[] memory contentIds = new uint256[](2); - contentIds[0] = - _submitQuestionWithContextAndRoundConfig("https://example.com/vh-bundle-a", "vh-a", roundConfig); - contentIds[1] = - _submitQuestionWithContextAndRoundConfig("https://example.com/vh-bundle-b", "vh-b", roundConfig); + contentIds[0] = _submitQuestionWithContextAndRoundConfig("https://example.com/vh-bundle-a", "vh-a", roundConfig); + contentIds[1] = _submitQuestionWithContextAndRoundConfig("https://example.com/vh-bundle-b", "vh-b", roundConfig); usdc.mint(funder, 2 * VERY_HIGH_VALUE_REWARD_POOL_THRESHOLD); @@ -4290,9 +4276,7 @@ contract QuestionRewardPoolEscrowTest is VotingTestBase { expectedRewards[1] = 33_333_333; expectedRewards[2] = 44_444_445; - _runSurpriseWeightedClaimScenario( - baseWeights, independenceBps, expectedRewards, _directions(true, true, false) - ); + _runSurpriseWeightedClaimScenario(baseWeights, independenceBps, expectedRewards, _directions(true, true, false)); } function testClusterClaimRejectsBaseWeightAboveSurpriseCap() public { diff --git a/packages/foundry/test/RewardMath.t.sol b/packages/foundry/test/RewardMath.t.sol index e31a25919..dded839a7 100644 --- a/packages/foundry/test/RewardMath.t.sol +++ b/packages/foundry/test/RewardMath.t.sol @@ -36,11 +36,7 @@ contract RewardMathHarness { uint16 scoreBps, uint16 meanScoreBps, uint256 revealedCount - ) - external - pure - returns (uint256) - { + ) external pure returns (uint256) { return RewardMath.calculateNegativeScoreSpreadForfeit(stakeAmount, scoreBps, meanScoreBps, revealedCount); } diff --git a/packages/foundry/test/RoundVotingEngineBranches.t.sol b/packages/foundry/test/RoundVotingEngineBranches.t.sol index 1e566ac90..89e1984bc 100644 --- a/packages/foundry/test/RoundVotingEngineBranches.t.sol +++ b/packages/foundry/test/RoundVotingEngineBranches.t.sol @@ -1719,8 +1719,7 @@ contract RoundVotingEngineBranchesTest is VotingTestBase { } function test_BasicLifecycle_VoterPoolAndWinningStake() public { - (uint256 contentId, uint256 roundId,) = - _setupEconomicPredictionRound(_fiveUpThreeDownDirections(), address(0)); + (uint256 contentId, uint256 roundId,) = _setupEconomicPredictionRound(_fiveUpThreeDownDirections(), address(0)); _settleRoundAfterRbtsSeed(contentId, roundId); @@ -2602,8 +2601,7 @@ contract RoundVotingEngineBranchesTest is VotingTestBase { function test_FrontendFee_EligibleFrontend_FeeAccumulated() public { _registerFrontend(frontend1); - (uint256 contentId, uint256 roundId,) = - _setupEconomicPredictionRound(_fiveUpThreeDownDirections(), frontend1); + (uint256 contentId, uint256 roundId,) = _setupEconomicPredictionRound(_fiveUpThreeDownDirections(), frontend1); _settleRoundAfterRbtsSeed(contentId, roundId); uint256 feesBefore = frontendRegistry.getAccumulatedFees(frontend1); @@ -2616,8 +2614,7 @@ contract RoundVotingEngineBranchesTest is VotingTestBase { function test_FrontendFee_ClaimSucceeds() public { _registerFrontend(frontend1); - (uint256 contentId, uint256 roundId,) = - _setupEconomicPredictionRound(_fiveUpThreeDownDirections(), frontend1); + (uint256 contentId, uint256 roundId,) = _setupEconomicPredictionRound(_fiveUpThreeDownDirections(), frontend1); _settleRoundAfterRbtsSeed(contentId, roundId); @@ -2631,8 +2628,7 @@ contract RoundVotingEngineBranchesTest is VotingTestBase { function test_FrontendFee_CannotClaimTwice() public { _registerFrontend(frontend1); - (uint256 contentId, uint256 roundId,) = - _setupEconomicPredictionRound(_fiveUpThreeDownDirections(), frontend1); + (uint256 contentId, uint256 roundId,) = _setupEconomicPredictionRound(_fiveUpThreeDownDirections(), frontend1); _settleRoundAfterRbtsSeed(contentId, roundId); @@ -2709,8 +2705,9 @@ contract RoundVotingEngineBranchesTest is VotingTestBase { for (uint256 i = 0; i < voters.length; i++) { predictions[i] = _predictionBps(i, directions[i]); if (i == 0) { - (commitKeys[i], salts[i]) = - _commitPredictionWithFrontend(voters[i], contentId, directions[i], predictions[i], STAKE, frontend1); + (commitKeys[i], salts[i]) = _commitPredictionWithFrontend( + voters[i], contentId, directions[i], predictions[i], STAKE, frontend1 + ); } else { (commitKeys[i], salts[i]) = _commitPrediction(voters[i], contentId, directions[i], predictions[i], STAKE); From e10afb6c4716a839370eee405210d77768c05cfd Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 16:48:11 +0200 Subject: [PATCH 08/16] Build workspace artifacts before root checks --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4191bd2b8..d63521e29 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "chain": "yarn foundry:chain", "compile": "yarn foundry:compile", "deploy": "yarn workspace @rateloop/foundry deploy", + "build:workspace-deps": "yarn workspace @rateloop/contracts build && yarn workspace @rateloop/sdk build && yarn workspace @rateloop/agents build", "dead-code": "yarn dead-code:scan", "dead-code:scan": "knip --no-progress --no-exit-code", "dev:db": "node scripts/dev-db.mjs up", @@ -77,7 +78,7 @@ "ponder:check-types": "yarn workspace @rateloop/ponder check-types", "agents:ask": "yarn workspace @rateloop/agents ask", "agents:check-types": "yarn workspace @rateloop/agents check-types", - "agents:lint": "yarn workspace @rateloop/agents lint:questions", + "agents:lint": "yarn build:workspace-deps && yarn workspace @rateloop/agents lint:questions", "agents:quote": "yarn workspace @rateloop/agents quote", "agents:result": "yarn workspace @rateloop/agents result", "agents:sandbox": "yarn workspace @rateloop/agents sandbox", @@ -94,7 +95,7 @@ "e2e:ci:app": "yarn workspace @rateloop/nextjs e2e:ci:app", "e2e:ci:full": "yarn workspace @rateloop/nextjs e2e:ci:full", "e2e:ui": "yarn workspace @rateloop/nextjs e2e:ui", - "test:ts": "yarn contracts:check-types && yarn node-utils:check-types && yarn sdk:check-types && yarn ponder:check-types && yarn agents:check-types && yarn keeper:check-types && yarn test:node && yarn contracts:test && yarn node-utils:test && yarn sdk:test && yarn next:test && yarn workspace @rateloop/keeper test && yarn workspace @rateloop/agents test && yarn workspace @rateloop/ponder test" + "test:ts": "yarn build:workspace-deps && yarn contracts:check-types && yarn node-utils:check-types && yarn sdk:check-types && yarn ponder:check-types && yarn agents:check-types && yarn keeper:check-types && yarn test:node && yarn contracts:test && yarn node-utils:test && yarn sdk:test && yarn next:test && yarn workspace @rateloop/keeper test && yarn workspace @rateloop/agents test && yarn workspace @rateloop/ponder test" }, "devDependencies": { "husky": "~9.1.6", From 87146feaa097240217ea02fa6613074f9651f58f Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 16:48:18 +0200 Subject: [PATCH 09/16] Update sponsored frontend fee operations --- packages/nextjs/lib/thirdweb/freeTransactions.test.ts | 3 ++- packages/nextjs/lib/thirdweb/freeTransactions.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/lib/thirdweb/freeTransactions.test.ts b/packages/nextjs/lib/thirdweb/freeTransactions.test.ts index 023506d76..eee1b80b2 100644 --- a/packages/nextjs/lib/thirdweb/freeTransactions.test.ts +++ b/packages/nextjs/lib/thirdweb/freeTransactions.test.ts @@ -650,7 +650,8 @@ test("supported sponsored operation families are allowlisted", async () => { [encodeCall(contentRegistryContract, "cancelReservedSubmission", [`0x${"2".repeat(64)}`])], [submitQuestionWithRewardCall()], [encodeCall(frontendRegistryContract, "register")], - [encodeCall(frontendRegistryContract, "claimFees")], + [encodeCall(frontendRegistryContract, "requestFeeWithdrawal")], + [encodeCall(frontendRegistryContract, "completeFeeWithdrawal")], [encodeCall(frontendRegistryContract, "setSnapshotProposer", [WALLET])], [encodeCall(frontendRegistryContract, "clearSnapshotProposer")], [ diff --git a/packages/nextjs/lib/thirdweb/freeTransactions.ts b/packages/nextjs/lib/thirdweb/freeTransactions.ts index 52dd514e2..12e05375d 100644 --- a/packages/nextjs/lib/thirdweb/freeTransactions.ts +++ b/packages/nextjs/lib/thirdweb/freeTransactions.ts @@ -956,7 +956,8 @@ async function validateSponsoredCalls( functionName === "register" || functionName === "requestDeregister" || functionName === "completeDeregister" || - functionName === "claimFees" || + functionName === "requestFeeWithdrawal" || + functionName === "completeFeeWithdrawal" || functionName === "setSnapshotProposer" || functionName === "clearSnapshotProposer" ) { From 003d958e1983d0437f0f2cdf5da7773c8efe3ffe Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 17:46:52 +0200 Subject: [PATCH 10/16] Build contracts before Ponder entrypoints --- packages/ponder/package.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ponder/package.json b/packages/ponder/package.json index 51302e276..af0048c3d 100644 --- a/packages/ponder/package.json +++ b/packages/ponder/package.json @@ -4,11 +4,12 @@ "private": true, "type": "module", "scripts": { - "dev": "node ./scripts/devWithRecovery.mjs", - "dev:raw": "ponder dev --disable-ui", - "dev:ui": "ponder dev", - "start": "node ./scripts/start.mjs", - "codegen": "ponder codegen", + "build:contracts": "yarn workspace @rateloop/contracts build", + "dev": "yarn build:contracts && node ./scripts/devWithRecovery.mjs", + "dev:raw": "yarn build:contracts && ponder dev --disable-ui", + "dev:ui": "yarn build:contracts && ponder dev", + "start": "yarn build:contracts && node ./scripts/start.mjs", + "codegen": "yarn build:contracts && ponder codegen", "serve": "ponder serve", "check-types": "tsc --noEmit", "test": "vitest run", From 8e1ad5a5b3e97ff420c75f3900a3003e7920d4e1 Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 17:47:00 +0200 Subject: [PATCH 11/16] Sync Certora math harness with reveal count --- .../foundry/certora/harnesses/MathHarness.sol | 13 +++++++------ packages/foundry/certora/specs/Math.spec | 15 ++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/foundry/certora/harnesses/MathHarness.sol b/packages/foundry/certora/harnesses/MathHarness.sol index 830236c23..17745b2d0 100644 --- a/packages/foundry/certora/harnesses/MathHarness.sol +++ b/packages/foundry/certora/harnesses/MathHarness.sol @@ -36,12 +36,13 @@ contract MathHarness { return RewardMath.calculatePositiveScoreSpreadWeight(rbtsWeight, scoreBps, meanScoreBps); } - function calculateNegativeScoreSpreadForfeit(uint256 stakeAmount, uint16 scoreBps, uint16 meanScoreBps) - external - pure - returns (uint256) - { - return RewardMath.calculateNegativeScoreSpreadForfeit(stakeAmount, scoreBps, meanScoreBps); + function calculateNegativeScoreSpreadForfeit( + uint256 stakeAmount, + uint16 scoreBps, + uint16 meanScoreBps, + uint256 revealedCount + ) external pure returns (uint256) { + return RewardMath.calculateNegativeScoreSpreadForfeit(stakeAmount, scoreBps, meanScoreBps, revealedCount); } // splitPool returns a 3-tuple; expose each share separately so the spec can diff --git a/packages/foundry/certora/specs/Math.spec b/packages/foundry/certora/specs/Math.spec index 1dc7b7f9e..80fb9eb20 100644 --- a/packages/foundry/certora/specs/Math.spec +++ b/packages/foundry/certora/specs/Math.spec @@ -19,7 +19,7 @@ methods { function calculateRating(uint256, uint256) external returns (uint16) envfree; function calculateVoterReward(uint256, uint256, uint256) external returns (uint256) envfree; function calculatePositiveScoreSpreadWeight(uint256, uint16, uint16) external returns (uint256) envfree; - function calculateNegativeScoreSpreadForfeit(uint256, uint16, uint16) external returns (uint256) envfree; + function calculateNegativeScoreSpreadForfeit(uint256, uint16, uint16, uint256) external returns (uint256) envfree; function splitPoolVoter(uint256) external returns (uint256) envfree; function splitPoolPlatform(uint256) external returns (uint256) envfree; function splitPoolTreasury(uint256) external returns (uint256) envfree; @@ -67,15 +67,20 @@ rule voterRewardZeroWhenNoWinners(uint256 effectiveStake, uint256 voterPool) { /// Below-mean forfeiture is capped by the raw stake: a voter can never forfeit /// more than they staked. -rule negativeForfeitCappedByStake(uint256 stakeAmount, uint16 scoreBps, uint16 meanScoreBps) { - uint256 forfeit = calculateNegativeScoreSpreadForfeit(stakeAmount, scoreBps, meanScoreBps); +rule negativeForfeitCappedByStake(uint256 stakeAmount, uint16 scoreBps, uint16 meanScoreBps, uint256 revealedCount) { + uint256 forfeit = calculateNegativeScoreSpreadForfeit(stakeAmount, scoreBps, meanScoreBps, revealedCount); assert forfeit <= stakeAmount; } /// No forfeiture when the score is at or above the round mean. -rule negativeForfeitZeroAtOrAboveMean(uint256 stakeAmount, uint16 scoreBps, uint16 meanScoreBps) { +rule negativeForfeitZeroAtOrAboveMean( + uint256 stakeAmount, + uint16 scoreBps, + uint16 meanScoreBps, + uint256 revealedCount +) { require scoreBps >= meanScoreBps; - uint256 forfeit = calculateNegativeScoreSpreadForfeit(stakeAmount, scoreBps, meanScoreBps); + uint256 forfeit = calculateNegativeScoreSpreadForfeit(stakeAmount, scoreBps, meanScoreBps, revealedCount); assert forfeit == 0; } From b2404ff8c4fee7611b65292436dbe36fce1ccddd Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 17:47:10 +0200 Subject: [PATCH 12/16] Check rejected correlation roots before source work --- packages/foundry/contracts/ClusterPayoutOracle.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/foundry/contracts/ClusterPayoutOracle.sol b/packages/foundry/contracts/ClusterPayoutOracle.sol index cf5cca6d7..d73bdd0bf 100644 --- a/packages/foundry/contracts/ClusterPayoutOracle.sol +++ b/packages/foundry/contracts/ClusterPayoutOracle.sol @@ -259,6 +259,7 @@ contract ClusterPayoutOracle is IClusterPayoutOracle, AccessControl, ReentrancyG if (existing.status != SnapshotStatus.None && existing.status != SnapshotStatus.Rejected) { revert SnapshotExists(); } + if (rejectedCorrelationEpochRoots[epochId][clusterRoot]) revert InvalidSnapshot(); (bytes32 coverageDigest, bytes32 sourceSetDigest) = _requireCorrelationEpochSourcesReady(epochId, fromRoundId, toRoundId, sourceRefs); bytes32 proposalDigest = _correlationEpochDigest( @@ -273,10 +274,7 @@ contract ClusterPayoutOracle is IClusterPayoutOracle, AccessControl, ReentrancyG ); if (rejectedCorrelationEpochSnapshotDigests[epochId][proposalDigest]) revert InvalidSnapshot(); // L-Oracle-4: block identical re-proposal of a previously rejected clusterRoot. - if ( - rejectedCorrelationEpochRoots[epochId][clusterRoot] - || rejectedCorrelationEpochRootKeys[correlationEpochRootKey(sourceSetDigest, clusterRoot)] - ) { + if (rejectedCorrelationEpochRootKeys[correlationEpochRootKey(sourceSetDigest, clusterRoot)]) { revert InvalidSnapshot(); } address proposalTimeSnapshotProposer = frontendRegistry.snapshotProposerForFrontend(frontendOperator); From dd0896bceec947c358424bd4b583044cfb478c9d Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 18:50:59 +0200 Subject: [PATCH 13/16] Avoid E2E workspace build races --- .github/workflows/e2e.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 51aa58dda..ecafc96c5 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -120,12 +120,6 @@ jobs: echo "✓ Anvil ready" yarn deploy - - name: Start Ponder indexer - env: - PONDER_NETWORK: hardhat - PONDER_RPC_URL_31337: http://127.0.0.1:8545 - run: yarn ponder:dev & - - name: Build Next.js env: RATELOOP_E2E_PRODUCTION_BUILD: "true" @@ -137,6 +131,12 @@ jobs: NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: 3a8170812b534d0ff9d794f19a901d64 run: yarn workspace @rateloop/nextjs build + - name: Start Ponder indexer + env: + PONDER_NETWORK: hardhat + PONDER_RPC_URL_31337: http://127.0.0.1:8545 + run: yarn ponder:dev & + - name: Start Next.js (production) env: RATELOOP_E2E_PRODUCTION_BUILD: "true" @@ -280,12 +280,6 @@ jobs: echo "✓ Anvil ready" yarn deploy - - name: Start Ponder indexer - env: - PONDER_NETWORK: hardhat - PONDER_RPC_URL_31337: http://127.0.0.1:8545 - run: yarn ponder:dev & - - name: Build Next.js env: RATELOOP_E2E_PRODUCTION_BUILD: "true" @@ -297,6 +291,12 @@ jobs: NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: 3a8170812b534d0ff9d794f19a901d64 run: yarn workspace @rateloop/nextjs build + - name: Start Ponder indexer + env: + PONDER_NETWORK: hardhat + PONDER_RPC_URL_31337: http://127.0.0.1:8545 + run: yarn ponder:dev & + - name: Start Next.js (production) env: RATELOOP_E2E_PRODUCTION_BUILD: "true" From 6af2a1cd221699d6481fab51b81003cd73e3f19c Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 19:26:26 +0200 Subject: [PATCH 14/16] Avoid E2E Ponder contract rebuilds --- .github/workflows/e2e.yaml | 4 ++-- packages/ponder/package.json | 3 +++ packages/ponder/scripts/devWithRecovery.mjs | 14 ++++++++++---- packages/ponder/scripts/devWithRecovery.test.ts | 7 +++++++ .../ponder/scripts/ensureContractsArtifacts.mjs | 16 ++++++++++++++++ 5 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 packages/ponder/scripts/ensureContractsArtifacts.mjs diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index ecafc96c5..a97bbbec7 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -135,7 +135,7 @@ jobs: env: PONDER_NETWORK: hardhat PONDER_RPC_URL_31337: http://127.0.0.1:8545 - run: yarn ponder:dev & + run: yarn workspace @rateloop/ponder dev:built-contracts & - name: Start Next.js (production) env: @@ -295,7 +295,7 @@ jobs: env: PONDER_NETWORK: hardhat PONDER_RPC_URL_31337: http://127.0.0.1:8545 - run: yarn ponder:dev & + run: yarn workspace @rateloop/ponder dev:built-contracts & - name: Start Next.js (production) env: diff --git a/packages/ponder/package.json b/packages/ponder/package.json index af0048c3d..15bc052f2 100644 --- a/packages/ponder/package.json +++ b/packages/ponder/package.json @@ -5,8 +5,11 @@ "type": "module", "scripts": { "build:contracts": "yarn workspace @rateloop/contracts build", + "ensure:contracts": "node ./scripts/ensureContractsArtifacts.mjs", "dev": "yarn build:contracts && node ./scripts/devWithRecovery.mjs", + "dev:built-contracts": "node ./scripts/devWithRecovery.mjs dev:raw:built-contracts", "dev:raw": "yarn build:contracts && ponder dev --disable-ui", + "dev:raw:built-contracts": "yarn ensure:contracts && ponder dev --disable-ui", "dev:ui": "yarn build:contracts && ponder dev", "start": "yarn build:contracts && node ./scripts/start.mjs", "codegen": "yarn build:contracts && ponder codegen", diff --git a/packages/ponder/scripts/devWithRecovery.mjs b/packages/ponder/scripts/devWithRecovery.mjs index 50f9c721e..5bb6c197e 100644 --- a/packages/ponder/scripts/devWithRecovery.mjs +++ b/packages/ponder/scripts/devWithRecovery.mjs @@ -18,6 +18,7 @@ const SHUTDOWN_STATUS_TIMEOUT_MS = 1_500; const PONDER_PORT_RELEASE_TIMEOUT_MS = 10_000; const PONDER_PORT_RELEASE_POLL_MS = 250; const PONDER_PORT_CHECK_TIMEOUT_MS = 500; +const DEFAULT_DEV_RAW_SCRIPT = "dev:raw"; const PONDER_PORT_FALLBACK_PATTERN = /\bPort (\d{1,5}) was in use, trying port (\d{1,5})\b/g; const PONDER_SERVER_TRANSITION_PATTERN = /\b(Hot reload|Using PGlite database at|Port \d{1,5} was in use, trying port \d{1,5}|Started listening on port \d{1,5})\b/; @@ -180,10 +181,14 @@ async function waitForPonderPortRelease(statusUrl, env = process.env) { return false; } -function runDevRaw() { +export function resolveDevRawScript(argv = process.argv.slice(2)) { + return argv[0]?.trim() || DEFAULT_DEV_RAW_SCRIPT; +} + +function runDevRaw({ scriptName = resolveDevRawScript() } = {}) { return new Promise((resolve, reject) => { const useProcessGroup = process.platform !== "win32"; - const child = spawn("yarn", ["run", "dev:raw"], { + const child = spawn("yarn", ["run", scriptName], { cwd: ponderDir, stdio: ["inherit", "pipe", "pipe"], env: process.env, @@ -331,7 +336,8 @@ function resetPgliteIfPresent() { } async function main() { - const firstRun = await runDevRaw(); + const scriptName = resolveDevRawScript(); + const firstRun = await runDevRaw({ scriptName }); const recoveryReason = getRecoveryReason(firstRun.output, process.env); if (firstRun.code === 0 || firstRun.shutdownRequested || !recoveryReason) { process.exit(firstRun.code); @@ -355,7 +361,7 @@ async function main() { console.warn("\nWarning: Ponder port is still occupied after recovery stop; retrying anyway...\n"); } - const secondRun = await runDevRaw(); + const secondRun = await runDevRaw({ scriptName }); process.exit(secondRun.code); } diff --git a/packages/ponder/scripts/devWithRecovery.test.ts b/packages/ponder/scripts/devWithRecovery.test.ts index 0a2cc2265..48c577e51 100644 --- a/packages/ponder/scripts/devWithRecovery.test.ts +++ b/packages/ponder/scripts/devWithRecovery.test.ts @@ -3,11 +3,18 @@ import { outputIndicatesClosedPglite, outputIndicatesConfiguredPortFallback, outputIndicatesPonderServerTransition, + resolveDevRawScript, shouldRecover, shouldResetPglite, } from "./devWithRecovery.mjs"; describe("devWithRecovery", () => { + test("uses dev:raw by default and accepts an alternate raw dev script", () => { + expect(resolveDevRawScript([])).toBe("dev:raw"); + expect(resolveDevRawScript(["dev:raw:built-contracts"])).toBe("dev:raw:built-contracts"); + expect(resolveDevRawScript([""])).toBe("dev:raw"); + }); + test("recovers from PGlite corruption", () => { const output = "RuntimeError: Aborted()\n@electric-sql/pglite\nInitWalRecovery"; diff --git a/packages/ponder/scripts/ensureContractsArtifacts.mjs b/packages/ponder/scripts/ensureContractsArtifacts.mjs new file mode 100644 index 000000000..c1c94c340 --- /dev/null +++ b/packages/ponder/scripts/ensureContractsArtifacts.mjs @@ -0,0 +1,16 @@ +import { pathToFileURL } from "node:url"; +import { ensureContractsArtifacts } from "./start.mjs"; + +export function runEnsureContractsArtifacts() { + try { + ensureContractsArtifacts(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`[ponder] ${message}`); + process.exitCode = 1; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + runEnsureContractsArtifacts(); +} From 214d9cfb06345257770a624a9c3d3fe8ca61e5fd Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 19:45:26 +0200 Subject: [PATCH 15/16] Extend E2E Ponder readiness timeout --- .github/workflows/e2e.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index a97bbbec7..0fef6349b 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -155,7 +155,7 @@ jobs: echo "✓ Next.js ready" # Wait for Ponder - timeout 120 bash -c 'until curl -sf http://127.0.0.1:42069/status > /dev/null 2>&1; do sleep 2; done' + timeout 300 bash -c 'until curl -sf http://127.0.0.1:42069/status > /dev/null 2>&1; do sleep 2; done' echo "✓ Ponder ready" - name: Warm critical Next.js routes @@ -315,7 +315,7 @@ jobs: echo "✓ Next.js ready" # Wait for Ponder - timeout 120 bash -c 'until curl -sf http://127.0.0.1:42069/status > /dev/null 2>&1; do sleep 2; done' + timeout 300 bash -c 'until curl -sf http://127.0.0.1:42069/status > /dev/null 2>&1; do sleep 2; done' echo "✓ Ponder ready" - name: Warm critical Next.js routes From 9bdd7bf49058798e2aac36715764acc6babd14c2 Mon Sep 17 00:00:00 2001 From: David Hawig Date: Wed, 10 Jun 2026 20:16:59 +0200 Subject: [PATCH 16/16] Use production Ponder launcher in E2E --- .github/workflows/e2e.yaml | 8 ++++++-- packages/ponder/package.json | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 0fef6349b..35c002bb6 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -133,9 +133,11 @@ jobs: - name: Start Ponder indexer env: + DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/rateloop_app + RATELOOP_PONDER_DATABASE_SCHEMA: rateloop_ponder_ci PONDER_NETWORK: hardhat PONDER_RPC_URL_31337: http://127.0.0.1:8545 - run: yarn workspace @rateloop/ponder dev:built-contracts & + run: yarn workspace @rateloop/ponder start:built-contracts --port 42069 & - name: Start Next.js (production) env: @@ -293,9 +295,11 @@ jobs: - name: Start Ponder indexer env: + DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/rateloop_app + RATELOOP_PONDER_DATABASE_SCHEMA: rateloop_ponder_ci PONDER_NETWORK: hardhat PONDER_RPC_URL_31337: http://127.0.0.1:8545 - run: yarn workspace @rateloop/ponder dev:built-contracts & + run: yarn workspace @rateloop/ponder start:built-contracts --port 42069 & - name: Start Next.js (production) env: diff --git a/packages/ponder/package.json b/packages/ponder/package.json index 15bc052f2..001b2efae 100644 --- a/packages/ponder/package.json +++ b/packages/ponder/package.json @@ -12,6 +12,7 @@ "dev:raw:built-contracts": "yarn ensure:contracts && ponder dev --disable-ui", "dev:ui": "yarn build:contracts && ponder dev", "start": "yarn build:contracts && node ./scripts/start.mjs", + "start:built-contracts": "node ./scripts/start.mjs", "codegen": "yarn build:contracts && ponder codegen", "serve": "ponder serve", "check-types": "tsc --noEmit",