From 1ac15df98334568d2401c93298a4a132ea8e2f6f Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Sun, 29 Mar 2026 21:00:46 +0200 Subject: [PATCH 1/4] feat(monitor-v2): add SETTLE_ONLY_DISPUTED option to settlement bot When enabled, the bot only settles requests that were disputed and resolved via the DVM, skipping undisputed proposals that expired naturally. For OOv2/ManagedOOv2, uses on-chain getState() to check dispute status. For SkinnyOO, filters using already-queried DisputePrice events. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/monitor-v2/src/bot-oo/README.md | 1 + .../src/bot-oo/SettleOOv2Requests.ts | 22 ++++++ .../src/bot-oo/SettleSkinnyOORequests.ts | 16 +++- packages/monitor-v2/src/bot-oo/common.ts | 2 + .../monitor-v2/test/OptimisticOracleV2Bot.ts | 76 +++++++++++++++++++ .../monitor-v2/test/helpers/monitoring.ts | 3 +- 6 files changed, 118 insertions(+), 2 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index e58748de04..784ab55e1c 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -34,6 +34,7 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `GAS_LIMIT_MULTIPLIER`: Percent multiplier on estimated gas (default `150`). - `SETTLE_DELAY`: Lookback period in seconds to detect settleable requests (default `300`). - `SETTLE_TIMEOUT`: Timeout in seconds for submitting settlement transactions in serverless mode (default `240`). +- `SETTLE_ONLY_DISPUTED`: When `true`, only settle requests that have been disputed (`false` by default). ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts index 701738696e..21aa3df566 100644 --- a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts +++ b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts @@ -116,8 +116,30 @@ export async function settleOOv2Requests( const signerAddress = await params.signer.getAddress(); + // State.Resolved = 5: disputed and DVM price is available (settleable after dispute). + const STATE_RESOLVED = 5; + const settleableRequestsPromises = requestsToSettle.map(async (req) => { try { + // When settleOnlyDisputed is enabled, check on-chain state and skip undisputed requests. + if (params.botModes.settleOnlyDisputed) { + const state = await oo.getState( + req.args.requester, + req.args.identifier, + req.args.timestamp, + req.args.ancillaryData + ); + if (state !== STATE_RESOLVED) { + logger.debug({ + at: "OOv2Bot", + message: "Skipping non-disputed request (settleOnlyDisputed)", + requestKey: requestKey(req.args), + state, + }); + return null; + } + } + await oo.callStatic.settle(req.args.requester, req.args.identifier, req.args.timestamp, req.args.ancillaryData, { blockTag: params.settleableCheckBlock, from: signerAddress, diff --git a/packages/monitor-v2/src/bot-oo/SettleSkinnyOORequests.ts b/packages/monitor-v2/src/bot-oo/SettleSkinnyOORequests.ts index bb117de186..21a2e7c0b6 100644 --- a/packages/monitor-v2/src/bot-oo/SettleSkinnyOORequests.ts +++ b/packages/monitor-v2/src/bot-oo/SettleSkinnyOORequests.ts @@ -80,10 +80,24 @@ export async function settleSkinnyOORequests( disputes.forEach(pushIfLatest); // Exclude already settled requests. - const candidatesToSettle: SkinnyEvent[] = Array.from(byKey.entries()) + let candidatesToSettle: SkinnyEvent[] = Array.from(byKey.entries()) .filter(([key]) => !settledKeys.has(key)) .map(([, evt]) => evt); + // When settleOnlyDisputed is enabled, restrict to requests that have a DisputePrice event. + if (params.botModes.settleOnlyDisputed) { + const disputedKeys = new Set(disputes.map((e) => requestKey(toRequestKeyArgs(e.args)))); + const beforeCount = candidatesToSettle.length; + candidatesToSettle = candidatesToSettle.filter((e) => disputedKeys.has(requestKey(toRequestKeyArgs(e.args)))); + logger.debug({ + at: "SkinnyOOBot", + message: "Filtered to disputed-only requests", + before: beforeCount, + after: candidatesToSettle.length, + skipped: beforeCount - candidatesToSettle.length, + }); + } + const settleableRequestsPromises = candidatesToSettle.map(async (req) => { try { // ProposePrice event carries the request struct at args[4] diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index c274b74b10..e875f2f97a 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -8,6 +8,7 @@ export type OracleType = "OptimisticOracle" | "SkinnyOptimisticOracle" | "Optimi export interface BotModes { settleRequestsEnabled: boolean; + settleOnlyDisputed: boolean; } export interface MonitoringParams extends BaseMonitoringParams { @@ -24,6 +25,7 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise call.lastArg?.message === "Price Request Settled ✅"); assert.equal(subsequentLogs.length, 0, "No settlement logs should be generated on subsequent runs"); }); + + it("settleOnlyDisputed skips undisputed expired proposals", async function () { + await ( + await optimisticOracleV2.requestPrice(defaultOptimisticOracleV2Identifier, 0, ancillaryData, bondToken.address, 0) + ).wait(); + + const proposeReceipt = await ( + await optimisticOracleV2 + .connect(proposer) + .proposePrice( + await requester.getAddress(), + defaultOptimisticOracleV2Identifier, + 0, + ancillaryData, + ethers.utils.parseEther("1") + ) + ).wait(); + + // Move timer past liveness — request is settleable but was never disputed. + await advanceTimerPastLiveness(timer, proposeReceipt.blockNumber!, defaultLiveness); + + const { spy, logger } = makeSpyLogger(); + const params = await makeMonitoringParamsOO("OptimisticOracleV2", optimisticOracleV2.address, { + settleRequestsEnabled: false, + settleOnlyDisputed: true, + }); + await gasEstimator.update(); + await settleRequests(logger, params, gasEstimator); + + const settlementLogs = spy.getCalls().filter((call) => call.lastArg?.message === "Price Request Settled ✅"); + assert.equal(settlementLogs.length, 0, "Undisputed request should not be settled when settleOnlyDisputed is true"); + }); + + it("settleOnlyDisputed settles disputed request once DVM resolved", async function () { + await ( + await optimisticOracleV2.requestPrice(defaultOptimisticOracleV2Identifier, 0, ancillaryData, bondToken.address, 0) + ).wait(); + + await ( + await optimisticOracleV2 + .connect(proposer) + .proposePrice( + await requester.getAddress(), + defaultOptimisticOracleV2Identifier, + 0, + ancillaryData, + ethers.utils.parseEther("1") + ) + ).wait(); + + await ( + await optimisticOracleV2 + .connect(disputer) + .disputePrice(await requester.getAddress(), defaultOptimisticOracleV2Identifier, 0, ancillaryData) + ).wait(); + + // Resolve in DVM via MockOracle. + const pending = await mockOracle.getPendingQueries(); + const last = pending[pending.length - 1]!; + await ( + await mockOracle.pushPrice(last.identifier, last.time, last.ancillaryData, ethers.utils.parseEther("1")) + ).wait(); + + const { spy, logger } = makeSpyLogger(); + const params = await makeMonitoringParamsOO("OptimisticOracleV2", optimisticOracleV2.address, { + settleRequestsEnabled: false, + settleOnlyDisputed: true, + }); + await gasEstimator.update(); + await settleRequests(logger, params, gasEstimator); + + const settledIndex = spy + .getCalls() + .findIndex((c) => c.lastArg?.message === "Price Request Settled ✅" && c.lastArg?.at === "OOv2Bot"); + assert.isAbove(settledIndex, -1, "Disputed request should be settled when settleOnlyDisputed is true"); + }); }); diff --git a/packages/monitor-v2/test/helpers/monitoring.ts b/packages/monitor-v2/test/helpers/monitoring.ts index 8c014894fe..a019757d68 100644 --- a/packages/monitor-v2/test/helpers/monitoring.ts +++ b/packages/monitor-v2/test/helpers/monitoring.ts @@ -24,7 +24,8 @@ export async function makeMonitoringParamsOO( const [signer] = await ethers.getSigners(); const defaultBotModes: BotModesOO = { settleRequestsEnabled: false, - } as BotModesOO; + settleOnlyDisputed: false, + }; const mergedBotModes = { ...defaultBotModes, ...botModes } as BotModesOO; From 797f946e121ceda13d2f6924ae76033e81acff00 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 30 Mar 2026 01:17:57 +0200 Subject: [PATCH 2/4] refactor: restrict SETTLE_ONLY_DISPUTED to OOv2 only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the SkinnyOO filtering — the option now only applies to OptimisticOracleV2 and ManagedOptimisticOracleV2. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/bot-oo/SettleSkinnyOORequests.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/SettleSkinnyOORequests.ts b/packages/monitor-v2/src/bot-oo/SettleSkinnyOORequests.ts index 21a2e7c0b6..bb117de186 100644 --- a/packages/monitor-v2/src/bot-oo/SettleSkinnyOORequests.ts +++ b/packages/monitor-v2/src/bot-oo/SettleSkinnyOORequests.ts @@ -80,24 +80,10 @@ export async function settleSkinnyOORequests( disputes.forEach(pushIfLatest); // Exclude already settled requests. - let candidatesToSettle: SkinnyEvent[] = Array.from(byKey.entries()) + const candidatesToSettle: SkinnyEvent[] = Array.from(byKey.entries()) .filter(([key]) => !settledKeys.has(key)) .map(([, evt]) => evt); - // When settleOnlyDisputed is enabled, restrict to requests that have a DisputePrice event. - if (params.botModes.settleOnlyDisputed) { - const disputedKeys = new Set(disputes.map((e) => requestKey(toRequestKeyArgs(e.args)))); - const beforeCount = candidatesToSettle.length; - candidatesToSettle = candidatesToSettle.filter((e) => disputedKeys.has(requestKey(toRequestKeyArgs(e.args)))); - logger.debug({ - at: "SkinnyOOBot", - message: "Filtered to disputed-only requests", - before: beforeCount, - after: candidatesToSettle.length, - skipped: beforeCount - candidatesToSettle.length, - }); - } - const settleableRequestsPromises = candidatesToSettle.map(async (req) => { try { // ProposePrice event carries the request struct at args[4] From 2df4800b1dcce47080247e82eaeef8a839fe8060 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 30 Mar 2026 01:19:50 +0200 Subject: [PATCH 3/4] docs: clarify SETTLE_ONLY_DISPUTED only supports OOv2 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/monitor-v2/src/bot-oo/README.md | 2 +- packages/monitor-v2/src/bot-oo/common.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index 784ab55e1c..94fcf39fdd 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -34,7 +34,7 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `GAS_LIMIT_MULTIPLIER`: Percent multiplier on estimated gas (default `150`). - `SETTLE_DELAY`: Lookback period in seconds to detect settleable requests (default `300`). - `SETTLE_TIMEOUT`: Timeout in seconds for submitting settlement transactions in serverless mode (default `240`). -- `SETTLE_ONLY_DISPUTED`: When `true`, only settle requests that have been disputed (`false` by default). +- `SETTLE_ONLY_DISPUTED`: When `true`, only settle requests that have been disputed (`false` by default). Only supported for `OptimisticOracleV2`; ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index e875f2f97a..0d8c01a40d 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -8,7 +8,7 @@ export type OracleType = "OptimisticOracle" | "SkinnyOptimisticOracle" | "Optimi export interface BotModes { settleRequestsEnabled: boolean; - settleOnlyDisputed: boolean; + settleOnlyDisputed: boolean; // Only supported for OptimisticOracleV2; ignored for OOv1 and SkinnyOO. } export interface MonitoringParams extends BaseMonitoringParams { From 615abfd1d404a9720062f2a550705eabcb6005aa Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 30 Mar 2026 01:20:38 +0200 Subject: [PATCH 4/4] docs: mention ManagedOptimisticOracleV2 support in SETTLE_ONLY_DISPUTED Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/monitor-v2/src/bot-oo/README.md | 2 +- packages/monitor-v2/src/bot-oo/common.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index 94fcf39fdd..acb6f294d6 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -34,7 +34,7 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `GAS_LIMIT_MULTIPLIER`: Percent multiplier on estimated gas (default `150`). - `SETTLE_DELAY`: Lookback period in seconds to detect settleable requests (default `300`). - `SETTLE_TIMEOUT`: Timeout in seconds for submitting settlement transactions in serverless mode (default `240`). -- `SETTLE_ONLY_DISPUTED`: When `true`, only settle requests that have been disputed (`false` by default). Only supported for `OptimisticOracleV2`; ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. +- `SETTLE_ONLY_DISPUTED`: When `true`, only settle requests that have been disputed (`false` by default). Supported for `OptimisticOracleV2` (including `ManagedOptimisticOracleV2`); ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index 0d8c01a40d..7651510560 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -8,7 +8,7 @@ export type OracleType = "OptimisticOracle" | "SkinnyOptimisticOracle" | "Optimi export interface BotModes { settleRequestsEnabled: boolean; - settleOnlyDisputed: boolean; // Only supported for OptimisticOracleV2; ignored for OOv1 and SkinnyOO. + settleOnlyDisputed: boolean; // Supported for OptimisticOracleV2 (incl. ManagedOOv2); ignored for OOv1 and SkinnyOO. } export interface MonitoringParams extends BaseMonitoringParams {