From f69c71efb79a09b673cea61ad7776e1e925d1746 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Mon, 1 Jun 2026 15:06:53 +0000 Subject: [PATCH 1/6] feat(bot-oo): add include/exclude proposal lists to OOv2 settler Adds SETTLE_INCLUDE_LIST and SETTLE_EXCLUDE_LIST env vars to the unified OO settlement bot, scoped to OptimisticOracleV2. Proposals are matched by their ProposePrice event identifier (:). When an include list is set the bot settles only those proposals (takes precedence over the exclude list); otherwise proposals in the exclude list are skipped. Signed-off-by: Pablo Maldonado --- packages/monitor-v2/src/bot-oo/README.md | 2 + .../src/bot-oo/SettleOOv2Requests.ts | 39 ++++++++- packages/monitor-v2/src/bot-oo/common.ts | 38 +++++++++ packages/monitor-v2/src/bot-oo/requestKey.ts | 5 ++ .../monitor-v2/test/OptimisticOracleV2Bot.ts | 80 +++++++++++++++++++ 5 files changed, 162 insertions(+), 2 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index 9a655f7616..38d4a13f56 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -36,6 +36,8 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `SETTLE_MIN_PROPOSAL_AGE_SECONDS`: Minimum proposal age in seconds before settling OOv2 requests (default `8100`, set `0` to disable). - `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). Supported for `OptimisticOracleV2` (including `ManagedOptimisticOracleV2`); ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. +- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). When set, the bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`. `OptimisticOracleV2` only. Example: `["0xabc...def:5"]`. +- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. Ignored when `SETTLE_INCLUDE_LIST` is set. `OptimisticOracleV2` only. ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts index 8458dc1688..dd4da88946 100644 --- a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts +++ b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts @@ -7,7 +7,7 @@ import { ethers } from "ethers"; import { computeEventSearch } from "../bot-utils/events"; import { logSettleRequest } from "./BotLogger"; import { getContractInstanceWithProvider, Logger, MonitoringParams, OptimisticOracleV2Ethers } from "./common"; -import { requestKey } from "./requestKey"; +import { proposalEventId, requestKey } from "./requestKey"; import type { GasEstimator } from "@uma/financial-templates-lib"; import { getSettleTxErrorLogFields, getSettleTxErrorLogLevel } from "../bot-utils/errors"; @@ -21,6 +21,39 @@ function chunk(arr: T[], size: number): T[][] { return chunks; } +// Applies the include/exclude proposal lists. The include list is exclusive and takes precedence: when set, only its +// proposals are settled. Otherwise proposals in the exclude list are skipped. Proposals are matched by the +// transaction hash and log index of their ProposePrice event. +function filterByIncludeExclude( + logger: typeof Logger, + params: MonitoringParams, + requests: ProposePriceEvent[] +): ProposePriceEvent[] { + const { settleIncludeList, settleExcludeList } = params; + if (!settleIncludeList && !settleExcludeList) return requests; + + const kept: ProposePriceEvent[] = []; + const skipped: string[] = []; + for (const req of requests) { + const id = proposalEventId(req.transactionHash, req.logIndex); + const allowed = settleIncludeList ? settleIncludeList.has(id) : !settleExcludeList?.has(id); + if (allowed) kept.push(req); + else skipped.push(id); + } + + logger.debug({ + at: "OOv2Bot", + message: "Applied include/exclude proposal filter", + mode: settleIncludeList ? "include" : "exclude", + listSize: (settleIncludeList ?? settleExcludeList)?.size, + kept: kept.length, + skipped: skipped.length, + skippedIds: skipped, + }); + + return kept; +} + export async function settleOOv2Requests( logger: typeof Logger, params: MonitoringParams, @@ -99,7 +132,9 @@ export async function settleOOv2Requests( const settledKeys = new Set(settlements.map((e) => requestKey(e.args))); - const requestsToSettle = proposals.filter((e) => !settledKeys.has(requestKey(e.args))); + const unsettledRequests = proposals.filter((e) => !settledKeys.has(requestKey(e.args))); + + const requestsToSettle = filterByIncludeExclude(logger, params, unsettledRequests); const requestsToSettleTxCount = params.settleBatchSize > 1 ? Math.ceil(requestsToSettle.length / params.settleBatchSize) : requestsToSettle.length; diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index faf1ca56d2..7f3f6f61db 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -3,17 +3,45 @@ export { Logger } from "@uma/financial-templates-lib"; export { computeEventSearch } from "../bot-utils/events"; export { getContractInstanceWithProvider } from "../utils/contracts"; import { BaseMonitoringParams, startupLogLevel as baseStartup, initBaseMonitoringParams } from "../bot-utils/base"; +import { proposalEventId } from "./requestKey"; export type OracleType = "OptimisticOracle" | "SkinnyOptimisticOracle" | "OptimisticOracleV2"; const DEFAULT_SETTLE_MIN_PROPOSAL_AGE_SECONDS = 2 * 60 * 60 + 15 * 60; +const PROPOSAL_ID_REGEX = /^(0x[0-9a-fA-F]{64}):(\d+)$/; + function getNonNegativeNumber(value: string | undefined, defaultValue: number): number { if (value === undefined) return defaultValue; const parsed = Number(value); return Number.isFinite(parsed) ? Math.max(0, parsed) : defaultValue; } +// Parses a JSON array of ":" strings into a normalized set of proposal event ids. +// Returns undefined when the env var is unset/empty so the bot keeps its default (settle everything) behavior. +function parseProposalIdList(value: string | undefined, envName: string): Set | undefined { + if (value === undefined || value.trim() === "") return undefined; + + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + throw new Error(`${envName} must be a JSON array of ":" strings`); + } + if (!Array.isArray(parsed) || parsed.length === 0) { + throw new Error(`${envName} must be a non-empty JSON array of ":" strings`); + } + + const ids = parsed.map((entry) => { + if (typeof entry !== "string") throw new Error(`${envName} entries must be ":" strings`); + const match = entry.match(PROPOSAL_ID_REGEX); + if (!match) throw new Error(`Invalid ${envName} entry "${entry}"; expected ":"`); + return proposalEventId(match[1], Number(match[2])); + }); + + return new Set(ids); +} + export interface BotModes { settleRequestsEnabled: boolean; settleOnlyDisputed: boolean; // Supported for OptimisticOracleV2 (incl. ManagedOOv2); ignored for OOv1 and SkinnyOO. @@ -27,6 +55,11 @@ export interface MonitoringParams extends BaseMonitoringParams { executionDeadline?: number; // Timestamp in sec for when to stop settling, defaults to 4 minutes from now in serverless settleBatchSize: number; // Number of settle calls to batch via multicall (requires MultiCaller on contract), defaults to 1 settleMinProposalAgeSeconds: number; // Minimum proposal age before settlement, defaults to 2h15m + // Include/exclude lists of proposal event ids (":"). OptimisticOracleV2 only. + // When settleIncludeList is set, only those proposals are settled (it takes precedence over the exclude list). + // Otherwise, proposals in settleExcludeList are skipped. Both undefined means settle everything (default). + settleIncludeList?: Set; + settleExcludeList?: Set; } export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise => { @@ -65,6 +98,9 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise [args.requester, args.identifier, args.timestamp, args.ancillaryData] ) ); + +// Identifies a proposal by the transaction hash and log index of its ProposePrice event. This is the +// identifier used by the include/exclude settlement lists (matches how proposals are referenced in the explorer). +export const proposalEventId = (transactionHash: string, logIndex: number): string => + `${transactionHash.toLowerCase()}:${logIndex}`; diff --git a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts index e65a7b7d24..8fb0b28afa 100644 --- a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts +++ b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts @@ -8,6 +8,7 @@ import { import { spyLogIncludes, spyLogLevel, GasEstimator } from "@uma/financial-templates-lib"; import { assert } from "chai"; import { OracleType } from "../src/bot-oo/common"; +import { proposalEventId } from "../src/bot-oo/requestKey"; import { settleRequests } from "../src/bot-oo/SettleRequests"; import { defaultLiveness, defaultOptimisticOracleV2Identifier } from "./constants"; import { optimisticOracleV2Fixture } from "./fixtures/OptimisticOracleV2.Fixture"; @@ -36,6 +37,12 @@ const getLast = (items: T[], message: string) => { return item; }; +const getProposalEventId = (receipt: { events?: { event?: string; transactionHash: string; logIndex: number }[] }) => { + const event = receipt.events?.find((e) => e.event === "ProposePrice"); + if (event === undefined) throw new Error("Expected a ProposePrice event in the receipt"); + return proposalEventId(event.transactionHash, event.logIndex); +}; + describe("OptimisticOracleV2Bot", function () { let bondToken: ExpandedERC20Ethers; let optimisticOracleV2: OptimisticOracleV2Ethers; @@ -473,4 +480,77 @@ describe("OptimisticOracleV2Bot", function () { .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"); }); + + it("Skips proposals in the exclude list", 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(); + + await advanceTimerPastLiveness(timer, getReceiptBlockNumber(proposeReceipt), defaultLiveness); + + const { spy, logger } = makeSpyLogger(); + const params = await createParams("OptimisticOracleV2", optimisticOracleV2.address); + params.settleExcludeList = new Set([getProposalEventId(proposeReceipt)]); + 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, "Excluded proposal should not be settled"); + }); + + it("Settles only proposals in the include list", 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(); + + await advanceTimerPastLiveness(timer, getReceiptBlockNumber(proposeReceipt), defaultLiveness); + + // An include list that does not contain the proposal: nothing settles. + { + const { spy, logger } = makeSpyLogger(); + const params = await createParams("OptimisticOracleV2", optimisticOracleV2.address); + params.settleIncludeList = new Set([proposalEventId(`0x${"0".repeat(64)}`, 0)]); + 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, "Proposal absent from the include list should not be settled"); + } + + // An include list containing the proposal: it settles. + { + const { spy, logger } = makeSpyLogger(); + const params = await createParams("OptimisticOracleV2", optimisticOracleV2.address); + params.settleIncludeList = new Set([getProposalEventId(proposeReceipt)]); + await gasEstimator.update(); + await settleRequests(logger, params, gasEstimator); + + const settlementLogs = spy.getCalls().filter((call) => call.lastArg?.message === "Price Request Settled ✅"); + assert.equal(settlementLogs.length, 1, "Proposal present in the include list should be settled"); + } + }); }); From 70eecdfaeb5296201f19a2e154e08a9aff8bd1a3 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 3 Jun 2026 10:23:58 +0000 Subject: [PATCH 2/6] fix(bot-oo): accept explicit empty settle include/exclude lists An explicit [] no longer throws at startup: an empty exclude list behaves like unset, and an empty include list settles nothing (the include list is exclusive). Matches the OG_WHITELIST/ASSERTION_BLACKLIST convention of accepting empty arrays. Signed-off-by: Pablo Maldonado --- packages/monitor-v2/src/bot-oo/README.md | 4 ++-- packages/monitor-v2/src/bot-oo/common.ts | 8 +++++--- packages/monitor-v2/test/OptimisticOracleV2Bot.ts | 13 ++++++++++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index 38d4a13f56..dcf9379554 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -36,8 +36,8 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `SETTLE_MIN_PROPOSAL_AGE_SECONDS`: Minimum proposal age in seconds before settling OOv2 requests (default `8100`, set `0` to disable). - `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). Supported for `OptimisticOracleV2` (including `ManagedOptimisticOracleV2`); ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. -- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). When set, the bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`. `OptimisticOracleV2` only. Example: `["0xabc...def:5"]`. -- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. Ignored when `SETTLE_INCLUDE_LIST` is set. `OptimisticOracleV2` only. +- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). When set, the bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`, so an explicit empty array (`[]`) settles **nothing**; leave it unset for default behavior. `OptimisticOracleV2` only. Example: `["0xabc...def:5"]`. +- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. An empty array (`[]`) behaves the same as unset. Ignored when `SETTLE_INCLUDE_LIST` is set. `OptimisticOracleV2` only. ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index 7f3f6f61db..0b008551f9 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -19,7 +19,9 @@ function getNonNegativeNumber(value: string | undefined, defaultValue: number): // Parses a JSON array of ":" strings into a normalized set of proposal event ids. // Returns undefined when the env var is unset/empty so the bot keeps its default (settle everything) behavior. -function parseProposalIdList(value: string | undefined, envName: string): Set | undefined { +// An explicit empty array is accepted: an empty exclude list behaves like unset, while an empty include list +// means settle nothing (the include list is exclusive). +export function parseProposalIdList(value: string | undefined, envName: string): Set | undefined { if (value === undefined || value.trim() === "") return undefined; let parsed: unknown; @@ -28,8 +30,8 @@ function parseProposalIdList(value: string | undefined, envName: string): Set:" strings`); } - if (!Array.isArray(parsed) || parsed.length === 0) { - throw new Error(`${envName} must be a non-empty JSON array of ":" strings`); + if (!Array.isArray(parsed)) { + throw new Error(`${envName} must be a JSON array of ":" strings`); } const ids = parsed.map((entry) => { diff --git a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts index 8fb0b28afa..9d41ef0326 100644 --- a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts +++ b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts @@ -7,7 +7,7 @@ import { } from "@uma/contracts-node"; import { spyLogIncludes, spyLogLevel, GasEstimator } from "@uma/financial-templates-lib"; import { assert } from "chai"; -import { OracleType } from "../src/bot-oo/common"; +import { OracleType, parseProposalIdList } from "../src/bot-oo/common"; import { proposalEventId } from "../src/bot-oo/requestKey"; import { settleRequests } from "../src/bot-oo/SettleRequests"; import { defaultLiveness, defaultOptimisticOracleV2Identifier } from "./constants"; @@ -553,4 +553,15 @@ describe("OptimisticOracleV2Bot", function () { assert.equal(settlementLogs.length, 1, "Proposal present in the include list should be settled"); } }); + + it("Accepts explicit empty include/exclude lists", async function () { + // Templated deployments commonly render optional list env vars as "[]"; this must not throw. + const includeList = parseProposalIdList("[]", "SETTLE_INCLUDE_LIST"); + assert.instanceOf(includeList, Set); + assert.equal(includeList?.size, 0); + + const excludeList = parseProposalIdList("[]", "SETTLE_EXCLUDE_LIST"); + assert.instanceOf(excludeList, Set); + assert.equal(excludeList?.size, 0); + }); }); From 0f6930bc4d70cb144d66462089aaafe38e66f38c Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 3 Jun 2026 14:19:18 +0000 Subject: [PATCH 3/6] fix(bot-oo): throw when settle lists are set for non-OOv2 oracle types Only the OOv2 settler applies SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST; silently ignoring them on OptimisticOracle/SkinnyOptimisticOracle would settle proposals the operator intended to skip, so startup now fails fast instead. Signed-off-by: Pablo Maldonado --- packages/monitor-v2/src/bot-oo/README.md | 4 ++-- packages/monitor-v2/src/bot-oo/common.ts | 17 +++++++++++++++-- .../monitor-v2/test/OptimisticOracleV2Bot.ts | 14 +++++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index dcf9379554..2b250b9e5d 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -36,8 +36,8 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `SETTLE_MIN_PROPOSAL_AGE_SECONDS`: Minimum proposal age in seconds before settling OOv2 requests (default `8100`, set `0` to disable). - `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). Supported for `OptimisticOracleV2` (including `ManagedOptimisticOracleV2`); ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. -- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). When set, the bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`, so an explicit empty array (`[]`) settles **nothing**; leave it unset for default behavior. `OptimisticOracleV2` only. Example: `["0xabc...def:5"]`. -- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. An empty array (`[]`) behaves the same as unset. Ignored when `SETTLE_INCLUDE_LIST` is set. `OptimisticOracleV2` only. +- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). When set, the bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`, so an explicit empty array (`[]`) settles **nothing**; leave it unset for default behavior. `OptimisticOracleV2` only — setting it for another `ORACLE_TYPE` throws at startup. Example: `["0xabc...def:5"]`. +- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. An empty array (`[]`) behaves the same as unset. Ignored when `SETTLE_INCLUDE_LIST` is set. `OptimisticOracleV2` only — setting it for another `ORACLE_TYPE` throws at startup. ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index 0b008551f9..b5ad158a8b 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -44,6 +44,20 @@ export function parseProposalIdList(value: string | undefined, envName: string): return new Set(ids); } +// Parses SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST and throws when either is set for a non-OOv2 oracle type: +// only the OOv2 settler applies them, so silently ignoring a list would settle proposals the operator +// intended to skip. +export function parseSettleProposalIdLists( + env: NodeJS.ProcessEnv, + oracleType: OracleType +): { settleIncludeList?: Set; settleExcludeList?: Set } { + const settleIncludeList = parseProposalIdList(env.SETTLE_INCLUDE_LIST, "SETTLE_INCLUDE_LIST"); + const settleExcludeList = parseProposalIdList(env.SETTLE_EXCLUDE_LIST, "SETTLE_EXCLUDE_LIST"); + if ((settleIncludeList || settleExcludeList) && oracleType !== "OptimisticOracleV2") + throw new Error("SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST are only supported for OptimisticOracleV2"); + return { settleIncludeList, settleExcludeList }; +} + export interface BotModes { settleRequestsEnabled: boolean; settleOnlyDisputed: boolean; // Supported for OptimisticOracleV2 (incl. ManagedOOv2); ignored for OOv1 and SkinnyOO. @@ -100,8 +114,7 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise parseSettleProposalIdLists(env, "OptimisticOracle"), /only supported for OptimisticOracleV2/); + assert.throws( + () => parseSettleProposalIdLists(env, "SkinnyOptimisticOracle"), + /only supported for OptimisticOracleV2/ + ); + assert.doesNotThrow(() => parseSettleProposalIdLists(env, "OptimisticOracleV2")); + }); }); From da90162dbbdc3ea07116c93c6930a239a5023839 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Fri, 5 Jun 2026 09:06:25 +0000 Subject: [PATCH 4/6] fix(bot-oo): accept empty settle exclude list for non-OOv2 oracle types An empty exclude list skips nothing and behaves the same as unset, so it no longer trips the non-OOv2 startup throw. An empty include list still throws there since "settle nothing" cannot be honored outside OOv2. Signed-off-by: Pablo Maldonado --- packages/monitor-v2/src/bot-oo/README.md | 2 +- packages/monitor-v2/src/bot-oo/common.ts | 5 +++-- packages/monitor-v2/test/OptimisticOracleV2Bot.ts | 11 +++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index 2b250b9e5d..cc1f56925c 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -37,7 +37,7 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `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). Supported for `OptimisticOracleV2` (including `ManagedOptimisticOracleV2`); ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. - `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). When set, the bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`, so an explicit empty array (`[]`) settles **nothing**; leave it unset for default behavior. `OptimisticOracleV2` only — setting it for another `ORACLE_TYPE` throws at startup. Example: `["0xabc...def:5"]`. -- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. An empty array (`[]`) behaves the same as unset. Ignored when `SETTLE_INCLUDE_LIST` is set. `OptimisticOracleV2` only — setting it for another `ORACLE_TYPE` throws at startup. +- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. An empty array (`[]`) behaves the same as unset, including for non-OOv2 oracle types. Ignored when `SETTLE_INCLUDE_LIST` is set. `OptimisticOracleV2` only — setting a non-empty list for another `ORACLE_TYPE` throws at startup. ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index b5ad158a8b..01e89efef4 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -46,14 +46,15 @@ export function parseProposalIdList(value: string | undefined, envName: string): // Parses SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST and throws when either is set for a non-OOv2 oracle type: // only the OOv2 settler applies them, so silently ignoring a list would settle proposals the operator -// intended to skip. +// intended to skip. An empty exclude list skips nothing and behaves the same as unset, so it is accepted +// for any oracle type; an empty include list means "settle nothing", which non-OOv2 cannot honor. export function parseSettleProposalIdLists( env: NodeJS.ProcessEnv, oracleType: OracleType ): { settleIncludeList?: Set; settleExcludeList?: Set } { const settleIncludeList = parseProposalIdList(env.SETTLE_INCLUDE_LIST, "SETTLE_INCLUDE_LIST"); const settleExcludeList = parseProposalIdList(env.SETTLE_EXCLUDE_LIST, "SETTLE_EXCLUDE_LIST"); - if ((settleIncludeList || settleExcludeList) && oracleType !== "OptimisticOracleV2") + if ((settleIncludeList || settleExcludeList?.size) && oracleType !== "OptimisticOracleV2") throw new Error("SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST are only supported for OptimisticOracleV2"); return { settleIncludeList, settleExcludeList }; } diff --git a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts index 39954eedde..66b8120e18 100644 --- a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts +++ b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts @@ -575,5 +575,16 @@ describe("OptimisticOracleV2Bot", function () { /only supported for OptimisticOracleV2/ ); assert.doesNotThrow(() => parseSettleProposalIdLists(env, "OptimisticOracleV2")); + + // An empty exclude list skips nothing and behaves the same as unset, so it must not block startup. + const emptyExcludeEnv = { SETTLE_EXCLUDE_LIST: "[]" } as NodeJS.ProcessEnv; + assert.doesNotThrow(() => parseSettleProposalIdLists(emptyExcludeEnv, "OptimisticOracle")); + + // An empty include list means "settle nothing", which non-OOv2 cannot honor, so it must still throw. + const emptyIncludeEnv = { SETTLE_INCLUDE_LIST: "[]" } as NodeJS.ProcessEnv; + assert.throws( + () => parseSettleProposalIdLists(emptyIncludeEnv, "OptimisticOracle"), + /only supported for OptimisticOracleV2/ + ); }); }); From 5de2aef4962d6c30c67b3db5b4d602cdc7491621 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Fri, 24 Jul 2026 12:12:44 +0100 Subject: [PATCH 5/6] fix(bot-oo): fail closed for managed OOv2 Signed-off-by: Pablo Maldonado --- packages/monitor-v2/src/bot-oo/README.md | 10 ++-- .../src/bot-oo/SettleOOv2Requests.ts | 4 ++ .../monitor-v2/src/bot-oo/SettleRequests.ts | 1 + packages/monitor-v2/src/bot-oo/common.ts | 53 ++++++++++++------ .../monitor-v2/test/OptimisticOracleV2Bot.ts | 55 +++++++++++++++++-- .../monitor-v2/test/helpers/monitoring.ts | 1 + 6 files changed, 98 insertions(+), 26 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index cc1f56925c..6002431cb0 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -5,6 +5,7 @@ The unified Optimistic Oracle bot performs off-chain actions related to multiple - **OptimisticOracle V1** (deprecated but still deployed) - **SkinnyOptimisticOracle** (gas-optimized version) - **OptimisticOracle V2** (current standard) +- **ManagedOptimisticOracle V2** (uses the OptimisticOracle V2 settlement interface) > Warning: This bot does not support SkinnyOptimisticOracleV2. That contract has a distinct interface from SkinnyOptimisticOracle and is intentionally out of scope here. @@ -16,7 +17,7 @@ node ./packages/monitor-v2/dist/bot-oo/index.js ## Required Environment Variables -- `ORACLE_TYPE`: Oracle contract type (`OptimisticOracle`, `SkinnyOptimisticOracle`, or `OptimisticOracleV2`) +- `ORACLE_TYPE`: Oracle contract type (`OptimisticOracle`, `SkinnyOptimisticOracle`, `OptimisticOracleV2`, or `ManagedOptimisticOracleV2`) - `ORACLE_ADDRESS`: Address of the Oracle contract to monitor ## Standard Environment Variables @@ -36,8 +37,8 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `SETTLE_MIN_PROPOSAL_AGE_SECONDS`: Minimum proposal age in seconds before settling OOv2 requests (default `8100`, set `0` to disable). - `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). Supported for `OptimisticOracleV2` (including `ManagedOptimisticOracleV2`); ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. -- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). When set, the bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`, so an explicit empty array (`[]`) settles **nothing**; leave it unset for default behavior. `OptimisticOracleV2` only — setting it for another `ORACLE_TYPE` throws at startup. Example: `["0xabc...def:5"]`. -- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. An empty array (`[]`) behaves the same as unset, including for non-OOv2 oracle types. Ignored when `SETTLE_INCLUDE_LIST` is set. `OptimisticOracleV2` only — setting a non-empty list for another `ORACLE_TYPE` throws at startup. +- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). The bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`, so an explicit empty array (`[]`) settles **nothing**. When neither list is configured, `OptimisticOracleV2` preserves the existing settle-all behavior while `ManagedOptimisticOracleV2` defaults to an empty include list and settles nothing. OOv2 contract types only — setting it for another `ORACLE_TYPE` throws at startup. Example: `["0xabc...def:5"]`. +- `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. Ignored when `SETTLE_INCLUDE_LIST` is set. An explicit empty array (`[]`) opts in to settling every eligible proposal. OOv2 contract types only — setting a non-empty list for another `ORACLE_TYPE` throws at startup. ## Behavior @@ -47,7 +48,8 @@ The bot operates with a unified dispatcher pattern: settleRequests() ├── OptimisticOracle → settleOOv1Requests() ├── SkinnyOptimisticOracle → settleSkinnyOORequests() -└── OptimisticOracleV2 → settleOOv2Requests() +├── OptimisticOracleV2 → settleOOv2Requests() +└── ManagedOptimisticOracleV2 → settleOOv2Requests() ``` For each Oracle type: diff --git a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts index dd4da88946..abd2ed7c99 100644 --- a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts +++ b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts @@ -59,6 +59,10 @@ export async function settleOOv2Requests( params: MonitoringParams, gasEstimator: GasEstimator ): Promise { + if (params.oracleType === "ManagedOptimisticOracleV2" && !params.settleIncludeList && !params.settleExcludeList) { + throw new Error("Managed OOv2 settlement requires an include or exclude list"); + } + const oo = await getContractInstanceWithProvider( "OptimisticOracleV2", params.provider, diff --git a/packages/monitor-v2/src/bot-oo/SettleRequests.ts b/packages/monitor-v2/src/bot-oo/SettleRequests.ts index f3e31469e2..2a06a5c282 100644 --- a/packages/monitor-v2/src/bot-oo/SettleRequests.ts +++ b/packages/monitor-v2/src/bot-oo/SettleRequests.ts @@ -22,6 +22,7 @@ export async function settleRequests( case "SkinnyOptimisticOracle": return settleSkinnyOORequests(logger, params, gasEstimator); case "OptimisticOracleV2": + case "ManagedOptimisticOracleV2": return settleOOv2Requests(logger, params, gasEstimator); default: throw new Error(`Unsupported oracle type: ${params.oracleType}`); diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index 01e89efef4..651d8fadc8 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -5,7 +5,11 @@ export { getContractInstanceWithProvider } from "../utils/contracts"; import { BaseMonitoringParams, startupLogLevel as baseStartup, initBaseMonitoringParams } from "../bot-utils/base"; import { proposalEventId } from "./requestKey"; -export type OracleType = "OptimisticOracle" | "SkinnyOptimisticOracle" | "OptimisticOracleV2"; +export type OracleType = + | "OptimisticOracle" + | "SkinnyOptimisticOracle" + | "OptimisticOracleV2" + | "ManagedOptimisticOracleV2"; const DEFAULT_SETTLE_MIN_PROPOSAL_AGE_SECONDS = 2 * 60 * 60 + 15 * 60; @@ -18,9 +22,9 @@ function getNonNegativeNumber(value: string | undefined, defaultValue: number): } // Parses a JSON array of ":" strings into a normalized set of proposal event ids. -// Returns undefined when the env var is unset/empty so the bot keeps its default (settle everything) behavior. -// An explicit empty array is accepted: an empty exclude list behaves like unset, while an empty include list -// means settle nothing (the include list is exclusive). +// Returns undefined when the env var is unset/blank. +// An explicit empty array is accepted: an empty exclude list opts Managed OOv2 into settling everything, while an +// empty include list means settle nothing (the include list is exclusive). export function parseProposalIdList(value: string | undefined, envName: string): Set | undefined { if (value === undefined || value.trim() === "") return undefined; @@ -44,18 +48,31 @@ export function parseProposalIdList(value: string | undefined, envName: string): return new Set(ids); } -// Parses SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST and throws when either is set for a non-OOv2 oracle type: -// only the OOv2 settler applies them, so silently ignoring a list would settle proposals the operator -// intended to skip. An empty exclude list skips nothing and behaves the same as unset, so it is accepted -// for any oracle type; an empty include list means "settle nothing", which non-OOv2 cannot honor. +// Parses SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST for OOv2 contracts. An empty exclude list is accepted for any oracle +// type and explicitly opts Managed OOv2 into settling everything; an empty include list means "settle nothing", +// which non-OOv2 oracle types cannot honor. export function parseSettleProposalIdLists( env: NodeJS.ProcessEnv, oracleType: OracleType ): { settleIncludeList?: Set; settleExcludeList?: Set } { - const settleIncludeList = parseProposalIdList(env.SETTLE_INCLUDE_LIST, "SETTLE_INCLUDE_LIST"); + const configuredIncludeList = parseProposalIdList(env.SETTLE_INCLUDE_LIST, "SETTLE_INCLUDE_LIST"); const settleExcludeList = parseProposalIdList(env.SETTLE_EXCLUDE_LIST, "SETTLE_EXCLUDE_LIST"); - if ((settleIncludeList || settleExcludeList?.size) && oracleType !== "OptimisticOracleV2") - throw new Error("SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST are only supported for OptimisticOracleV2"); + if ( + (configuredIncludeList || settleExcludeList?.size) && + oracleType !== "OptimisticOracleV2" && + oracleType !== "ManagedOptimisticOracleV2" + ) + throw new Error( + "SETTLE_INCLUDE_LIST/SETTLE_EXCLUDE_LIST are only supported for OptimisticOracleV2 and ManagedOptimisticOracleV2" + ); + + // Default Managed OOv2 settlement to an empty include list so missing configuration cannot settle every proposal. + // An explicit empty exclude list remains the opt-in for settling everything. + const settleIncludeList = + oracleType === "ManagedOptimisticOracleV2" && configuredIncludeList === undefined && settleExcludeList === undefined + ? new Set() + : configuredIncludeList; + return { settleIncludeList, settleExcludeList }; } @@ -72,9 +89,9 @@ export interface MonitoringParams extends BaseMonitoringParams { executionDeadline?: number; // Timestamp in sec for when to stop settling, defaults to 4 minutes from now in serverless settleBatchSize: number; // Number of settle calls to batch via multicall (requires MultiCaller on contract), defaults to 1 settleMinProposalAgeSeconds: number; // Minimum proposal age before settlement, defaults to 2h15m - // Include/exclude lists of proposal event ids (":"). OptimisticOracleV2 only. + // Include/exclude lists of proposal event ids (":"). OOv2 contract types only. // When settleIncludeList is set, only those proposals are settled (it takes precedence over the exclude list). - // Otherwise, proposals in settleExcludeList are skipped. Both undefined means settle everything (default). + // Otherwise, proposals in settleExcludeList are skipped. Both undefined is invalid for ManagedOptimisticOracleV2. settleIncludeList?: Set; settleExcludeList?: Set; } @@ -92,13 +109,17 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise parseSettleProposalIdLists(env, "OptimisticOracle"), /only supported for OptimisticOracleV2/); + assert.throws( + () => parseSettleProposalIdLists(env, "OptimisticOracle"), + /only supported for OptimisticOracleV2 and ManagedOptimisticOracleV2/ + ); assert.throws( () => parseSettleProposalIdLists(env, "SkinnyOptimisticOracle"), - /only supported for OptimisticOracleV2/ + /only supported for OptimisticOracleV2 and ManagedOptimisticOracleV2/ ); assert.doesNotThrow(() => parseSettleProposalIdLists(env, "OptimisticOracleV2")); + assert.doesNotThrow(() => parseSettleProposalIdLists(env, "ManagedOptimisticOracleV2")); // An empty exclude list skips nothing and behaves the same as unset, so it must not block startup. const emptyExcludeEnv = { SETTLE_EXCLUDE_LIST: "[]" } as NodeJS.ProcessEnv; @@ -584,7 +627,7 @@ describe("OptimisticOracleV2Bot", function () { const emptyIncludeEnv = { SETTLE_INCLUDE_LIST: "[]" } as NodeJS.ProcessEnv; assert.throws( () => parseSettleProposalIdLists(emptyIncludeEnv, "OptimisticOracle"), - /only supported for OptimisticOracleV2/ + /only supported for OptimisticOracleV2 and ManagedOptimisticOracleV2/ ); }); }); diff --git a/packages/monitor-v2/test/helpers/monitoring.ts b/packages/monitor-v2/test/helpers/monitoring.ts index 79ae0b1ec9..94d5814d6e 100644 --- a/packages/monitor-v2/test/helpers/monitoring.ts +++ b/packages/monitor-v2/test/helpers/monitoring.ts @@ -43,6 +43,7 @@ export async function makeMonitoringParamsOO( contractAddress, settleBatchSize: 1, settleMinProposalAgeSeconds: 0, + ...(oracleType === "ManagedOptimisticOracleV2" ? { settleExcludeList: new Set() } : {}), }; } From cd10d1dd056c487525cf572d7eacdeb199d2240a Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Fri, 24 Jul 2026 14:49:48 +0100 Subject: [PATCH 6/6] fix(bot-oo): bound settlement filter logging Signed-off-by: Pablo Maldonado --- packages/monitor-v2/src/bot-oo/README.md | 2 +- .../monitor-v2/src/bot-oo/SettleOOv2Requests.ts | 12 ++++++++---- packages/monitor-v2/test/OptimisticOracleV2Bot.ts | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index 6002431cb0..644a5b66c0 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -37,7 +37,7 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `SETTLE_MIN_PROPOSAL_AGE_SECONDS`: Minimum proposal age in seconds before settling OOv2 requests (default `8100`, set `0` to disable). - `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). Supported for `OptimisticOracleV2` (including `ManagedOptimisticOracleV2`); ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. -- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers (the transaction hash and log index of the `ProposePrice` event). The bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`, so an explicit empty array (`[]`) settles **nothing**. When neither list is configured, `OptimisticOracleV2` preserves the existing settle-all behavior while `ManagedOptimisticOracleV2` defaults to an empty include list and settles nothing. OOv2 contract types only — setting it for another `ORACLE_TYPE` throws at startup. Example: `["0xabc...def:5"]`. +- `SETTLE_INCLUDE_LIST`: JSON array of `":"` proposal identifiers, where `logIndex` is the block-level index of the `ProposePrice` log (for example, ethers' `event.logIndex`), not its position within the transaction. The bot settles **only** these proposals. Takes precedence over `SETTLE_EXCLUDE_LIST`, so an explicit empty array (`[]`) settles **nothing**. When neither list is configured, `OptimisticOracleV2` preserves the existing settle-all behavior while `ManagedOptimisticOracleV2` defaults to an empty include list and settles nothing. OOv2 contract types only — setting it for another `ORACLE_TYPE` throws at startup. Example: `["0xabc...def:5"]`. - `SETTLE_EXCLUDE_LIST`: JSON array of `":"` proposal identifiers to **skip**. Ignored when `SETTLE_INCLUDE_LIST` is set. An explicit empty array (`[]`) opts in to settling every eligible proposal. OOv2 contract types only — setting a non-empty list for another `ORACLE_TYPE` throws at startup. ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts index abd2ed7c99..fb01d879d5 100644 --- a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts +++ b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts @@ -33,12 +33,16 @@ function filterByIncludeExclude( if (!settleIncludeList && !settleExcludeList) return requests; const kept: ProposePriceEvent[] = []; - const skipped: string[] = []; + const skippedIds: string[] = []; + let skipped = 0; for (const req of requests) { const id = proposalEventId(req.transactionHash, req.logIndex); const allowed = settleIncludeList ? settleIncludeList.has(id) : !settleExcludeList?.has(id); if (allowed) kept.push(req); - else skipped.push(id); + else { + skipped++; + if (!settleIncludeList) skippedIds.push(id); + } } logger.debug({ @@ -47,8 +51,8 @@ function filterByIncludeExclude( mode: settleIncludeList ? "include" : "exclude", listSize: (settleIncludeList ?? settleExcludeList)?.size, kept: kept.length, - skipped: skipped.length, - skippedIds: skipped, + skipped, + ...(settleIncludeList ? {} : { skippedIds }), }); return kept; diff --git a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts index a32d5da52c..29a9441879 100644 --- a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts +++ b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts @@ -508,6 +508,13 @@ describe("OptimisticOracleV2Bot", function () { const settlementLogs = spy.getCalls().filter((call) => call.lastArg?.message === "Price Request Settled ✅"); assert.equal(settlementLogs.length, 0, "Excluded proposal should not be settled"); + + const filterLog = getLast( + spy.getCalls().filter((call) => call.lastArg?.message === "Applied include/exclude proposal filter"), + "Expected include/exclude filter log" + ).lastArg; + assert.equal(filterLog.skipped, 1); + assert.deepEqual(filterLog.skippedIds, [getProposalEventId(proposeReceipt)]); }); it("Settles only proposals in the include list", async function () { @@ -539,6 +546,13 @@ describe("OptimisticOracleV2Bot", function () { const settlementLogs = spy.getCalls().filter((call) => call.lastArg?.message === "Price Request Settled ✅"); assert.equal(settlementLogs.length, 0, "Proposal absent from the include list should not be settled"); + + const filterLog = getLast( + spy.getCalls().filter((call) => call.lastArg?.message === "Applied include/exclude proposal filter"), + "Expected include/exclude filter log" + ).lastArg; + assert.equal(filterLog.skipped, 1); + assert.notProperty(filterLog, "skippedIds"); } // An include list containing the proposal: it settles.