diff --git a/packages/monitor-v2/src/bot-oo/README.md b/packages/monitor-v2/src/bot-oo/README.md index 644a5b66c0..f99aa61f31 100644 --- a/packages/monitor-v2/src/bot-oo/README.md +++ b/packages/monitor-v2/src/bot-oo/README.md @@ -36,9 +36,8 @@ node ./packages/monitor-v2/dist/bot-oo/index.js - `SETTLE_DELAY`: Lookback period in seconds to detect settleable requests (default `300`). - `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, 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. +- `SETTLE_ONLY_DISPUTED`: When `true`, standard `OptimisticOracleV2` only settles resolved disputes (`false` by default). Managed OOv2 always preserves normal resolved-dispute settlement and uses its include list for other requests. Ignored for `OptimisticOracle` and `SkinnyOptimisticOracle`. +- `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. For standard OOv2 the bot settles **only** these proposals. For Managed OOv2, listed proposals are fetched directly and added to the normal resolved-dispute candidates. Managed OOv2 defaults to an empty include list, which prevents all non-resolved proposals from settling while leaving resolved disputes unaffected. OOv2 contract types only — setting it for another `ORACLE_TYPE` throws at startup. Example: `["0xabc...def:5"]`. ## Behavior diff --git a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts index fb01d879d5..5259825f1c 100644 --- a/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts +++ b/packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts @@ -21,51 +21,69 @@ 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( +// Applies the standard OOv2 include list. Managed OOv2 applies it after checking request state so resolved disputes +// preserve their normal settlement behavior. +function filterByIncludeList( logger: typeof Logger, params: MonitoringParams, requests: ProposePriceEvent[] ): ProposePriceEvent[] { - const { settleIncludeList, settleExcludeList } = params; - if (!settleIncludeList && !settleExcludeList) return requests; + const { settleIncludeList } = params; + if (!settleIncludeList) return requests; const kept: ProposePriceEvent[] = []; - 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); + if (settleIncludeList.has(id)) kept.push(req); else { skipped++; - if (!settleIncludeList) skippedIds.push(id); } } logger.debug({ at: "OOv2Bot", - message: "Applied include/exclude proposal filter", - mode: settleIncludeList ? "include" : "exclude", - listSize: (settleIncludeList ?? settleExcludeList)?.size, + message: "Applied proposal include filter", + listSize: settleIncludeList.size, kept: kept.length, skipped, - ...(settleIncludeList ? {} : { skippedIds }), }); return kept; } +async function getManagedIncludedProposals( + oo: OptimisticOracleV2Ethers, + includedProposalIds: Set +): Promise { + const transactionHashes = [...new Set([...includedProposalIds].map((id) => id.split(":")[0]))]; + const receipts = await Promise.all( + transactionHashes.map((transactionHash) => oo.provider.getTransactionReceipt(transactionHash)) + ); + const blockNumbers = [...new Set(receipts.flatMap((receipt) => (receipt ? [receipt.blockNumber] : [])))]; + const proposals = ( + await Promise.all( + blockNumbers.map((blockNumber) => oo.queryFilter(oo.filters.ProposePrice(), blockNumber, blockNumber)) + ) + ).flat(); + const proposalsById = new Map( + proposals.map((proposal) => [proposalEventId(proposal.transactionHash, proposal.logIndex), proposal] as const) + ); + + return [...includedProposalIds].map((id) => { + const proposal = proposalsById.get(id); + if (!proposal) throw new Error(`Could not find included ProposePrice event ${id} on ${oo.address}`); + return proposal; + }); +} + export async function settleOOv2Requests( logger: typeof Logger, 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"); - } + if (params.oracleType === "ManagedOptimisticOracleV2" && !params.settleIncludeList) + throw new Error("Managed OOv2 settlement requires an include list"); const oo = await getContractInstanceWithProvider( "OptimisticOracleV2", @@ -138,11 +156,41 @@ export async function settleOOv2Requests( throw error; } + if (params.oracleType === "ManagedOptimisticOracleV2" && params.settleIncludeList) { + try { + const includedProposals = await getManagedIncludedProposals(oo, params.settleIncludeList); + const proposalsById = new Map( + proposals.map((proposal) => [proposalEventId(proposal.transactionHash, proposal.logIndex), proposal] as const) + ); + for (const proposal of includedProposals) { + proposalsById.set(proposalEventId(proposal.transactionHash, proposal.logIndex), proposal); + } + proposals = [...proposalsById.values()]; + logger.debug({ + at: "OOv2Bot", + message: "Queried included ProposePrice events", + requested: params.settleIncludeList.size, + count: includedProposals.length, + }); + } catch (error) { + // Manual candidates fail closed, but their lookup must not block historical disputed settlements. + logger.error({ + at: "OOv2Bot", + message: "Failed querying included ProposePrice events", + requested: params.settleIncludeList.size, + ...getSettleTxErrorLogFields(error), + }); + } + } + const settledKeys = new Set(settlements.map((e) => requestKey(e.args))); const unsettledRequests = proposals.filter((e) => !settledKeys.has(requestKey(e.args))); - const requestsToSettle = filterByIncludeExclude(logger, params, unsettledRequests); + const requestsToSettle = + params.oracleType === "ManagedOptimisticOracleV2" + ? unsettledRequests + : filterByIncludeList(logger, params, unsettledRequests); const requestsToSettleTxCount = params.settleBatchSize > 1 ? Math.ceil(requestsToSettle.length / params.settleBatchSize) : requestsToSettle.length; @@ -198,15 +246,27 @@ export async function settleOOv2Requests( } } - // When settleOnlyDisputed is enabled, check on-chain state and skip undisputed requests. - if (params.botModes.settleOnlyDisputed) { + // Managed OOv2 always preserves normal disputed settlement; its include list governs non-resolved requests. + if (params.oracleType === "ManagedOptimisticOracleV2" || params.botModes.settleOnlyDisputed) { const state = await oo.getState( req.args.requester, req.args.identifier, req.args.timestamp, req.args.ancillaryData ); - if (state !== STATE_RESOLVED) { + if (params.oracleType === "ManagedOptimisticOracleV2" && state !== STATE_RESOLVED) { + const id = proposalEventId(req.transactionHash, req.logIndex); + if (!params.settleIncludeList?.has(id)) { + logger.debug({ + at: "OOv2Bot", + message: "Skipping non-resolved Managed OOv2 request outside settlement include list", + requestKey: requestKey(req.args), + proposalEventId: id, + state, + }); + return null; + } + } else if (params.oracleType !== "ManagedOptimisticOracleV2" && state !== STATE_RESOLVED) { logger.debug({ at: "OOv2Bot", message: "Skipping non-disputed request (settleOnlyDisputed)", diff --git a/packages/monitor-v2/src/bot-oo/common.ts b/packages/monitor-v2/src/bot-oo/common.ts index 651d8fadc8..685a602699 100644 --- a/packages/monitor-v2/src/bot-oo/common.ts +++ b/packages/monitor-v2/src/bot-oo/common.ts @@ -23,8 +23,7 @@ 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/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). +// An explicit empty array is accepted; oracle-specific support is validated separately. export function parseProposalIdList(value: string | undefined, envName: string): Set | undefined { if (value === undefined || value.trim() === "") return undefined; @@ -48,37 +47,27 @@ export function parseProposalIdList(value: string | undefined, envName: string): return new Set(ids); } -// 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 configuredIncludeList = parseProposalIdList(env.SETTLE_INCLUDE_LIST, "SETTLE_INCLUDE_LIST"); - const settleExcludeList = parseProposalIdList(env.SETTLE_EXCLUDE_LIST, "SETTLE_EXCLUDE_LIST"); - 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; +// Parses SETTLE_INCLUDE_LIST for OOv2 contracts. For Managed OOv2 it governs non-resolved requests only; normal +// resolved-dispute settlement is always preserved. +export function parseSettleIncludeList(env: NodeJS.ProcessEnv, oracleType: OracleType): Set | undefined { + // Fail fast on the removed setting so stale deployments cannot silently fall back to settling every proposal. + if (env.SETTLE_EXCLUDE_LIST !== undefined) + throw new Error("SETTLE_EXCLUDE_LIST is not supported; use SETTLE_INCLUDE_LIST"); - return { settleIncludeList, settleExcludeList }; + const configuredIncludeList = parseProposalIdList(env.SETTLE_INCLUDE_LIST, "SETTLE_INCLUDE_LIST"); + if (configuredIncludeList && oracleType !== "OptimisticOracleV2" && oracleType !== "ManagedOptimisticOracleV2") + throw new Error("SETTLE_INCLUDE_LIST is only supported for OptimisticOracleV2 and ManagedOptimisticOracleV2"); + + // Default Managed OOv2 settlement to an empty include list so missing configuration cannot settle non-resolved + // proposals. + return oracleType === "ManagedOptimisticOracleV2" && configuredIncludeList === undefined + ? new Set() + : configuredIncludeList; } export interface BotModes { settleRequestsEnabled: boolean; - settleOnlyDisputed: boolean; // Supported for OptimisticOracleV2 (incl. ManagedOOv2); ignored for OOv1 and SkinnyOO. + settleOnlyDisputed: boolean; // Supported for standard OOv2; ignored for OOv1, SkinnyOO, and Managed OOv2. } export interface MonitoringParams extends BaseMonitoringParams { @@ -89,11 +78,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 (":"). 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 is invalid for ManagedOptimisticOracleV2. + // Proposal event ids (":"). Standard OOv2 applies the include filter to all proposals. Managed + // OOv2 applies it only to non-resolved proposals; resolved disputes follow normal settlement. settleIncludeList?: Set; - settleExcludeList?: Set; } export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise => { @@ -136,7 +123,7 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise ); // 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). +// identifier used by the settlement include list (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 29a9441879..f1dd8cd216 100644 --- a/packages/monitor-v2/test/OptimisticOracleV2Bot.ts +++ b/packages/monitor-v2/test/OptimisticOracleV2Bot.ts @@ -6,8 +6,9 @@ import { TimerEthers, } from "@uma/contracts-node"; import { spyLogIncludes, spyLogLevel, GasEstimator } from "@uma/financial-templates-lib"; +import { BlockFinder } from "@uma/sdk"; import { assert } from "chai"; -import { OracleType, parseProposalIdList, parseSettleProposalIdLists } from "../src/bot-oo/common"; +import { OracleType, parseProposalIdList, parseSettleIncludeList } from "../src/bot-oo/common"; import { proposalEventId } from "../src/bot-oo/requestKey"; import { settleRequests } from "../src/bot-oo/SettleRequests"; import { defaultLiveness, defaultOptimisticOracleV2Identifier } from "./constants"; @@ -472,6 +473,15 @@ describe("OptimisticOracleV2Bot", function () { settleRequestsEnabled: false, settleOnlyDisputed: true, }); + params.settleIncludeList = new Set(); + 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, "Standard OOv2 include lists must still filter resolved disputes"); + + spy.resetHistory(); + params.settleIncludeList = undefined; await gasEstimator.update(); await settleRequests(logger, params, gasEstimator); @@ -481,43 +491,65 @@ describe("OptimisticOracleV2Bot", function () { 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(), + for (const listMode of ["empty include", "invalid include"] as const) { + it(`Managed OOv2 settles disputed requests when using the ${listMode} list`, async function () { + await ( + await optimisticOracleV2.requestPrice( defaultOptimisticOracleV2Identifier, 0, ancillaryData, - ethers.utils.parseEther("1") + bondToken.address, + 0 ) - ).wait(); + ).wait(); - await advanceTimerPastLiveness(timer, getReceiptBlockNumber(proposeReceipt), defaultLiveness); + await ( + await optimisticOracleV2 + .connect(proposer) + .proposePrice( + await requester.getAddress(), + defaultOptimisticOracleV2Identifier, + 0, + ancillaryData, + ethers.utils.parseEther("1") + ) + ).wait(); - const { spy, logger } = makeSpyLogger(); - const params = await createParams("ManagedOptimisticOracleV2", optimisticOracleV2.address); - params.settleExcludeList = new Set([getProposalEventId(proposeReceipt)]); - await gasEstimator.update(); - await settleRequests(logger, params, gasEstimator); + await ( + await optimisticOracleV2 + .connect(disputer) + .disputePrice(await requester.getAddress(), defaultOptimisticOracleV2Identifier, 0, ancillaryData) + ).wait(); - 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)]); - }); + const pending = await mockOracle.getPendingQueries(); + const last = getLast(pending, "Expected a pending DVM query"); + await ( + await mockOracle.pushPrice(last.identifier, last.time, last.ancillaryData, ethers.utils.parseEther("1")) + ).wait(); + + const { spy, logger } = makeSpyLogger(); + const params = await createParams("ManagedOptimisticOracleV2", optimisticOracleV2.address); + if (listMode === "empty include") { + params.settleIncludeList = new Set(); + } else { + 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, 1, "Include lists must not block normal disputed settlement"); + + if (listMode === "invalid include") { + assert.isTrue( + spy.getCalls().some((call) => call.lastArg?.message === "Failed querying included ProposePrice events"), + "Invalid direct includes should be logged without blocking disputed settlement" + ); + } + }); + } - it("Settles only proposals in the include list", async function () { + it("Uses the include list for non-disputed Managed OOv2 proposals", async function () { await ( await optimisticOracleV2.requestPrice(defaultOptimisticOracleV2Identifier, 0, ancillaryData, bondToken.address, 0) ).wait(); @@ -536,23 +568,27 @@ describe("OptimisticOracleV2Bot", function () { await advanceTimerPastLiveness(timer, getReceiptBlockNumber(proposeReceipt), defaultLiveness); - // An include list that does not contain the proposal: nothing settles. + // An empty include list settles nothing. { const { spy, logger } = makeSpyLogger(); const params = await createParams("ManagedOptimisticOracleV2", optimisticOracleV2.address); - params.settleIncludeList = new Set([proposalEventId(`0x${"0".repeat(64)}`, 0)]); + params.settleIncludeList = new Set(); 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"); - const filterLog = getLast( - spy.getCalls().filter((call) => call.lastArg?.message === "Applied include/exclude proposal filter"), - "Expected include/exclude filter log" + const skipLog = getLast( + spy + .getCalls() + .filter( + (call) => + call.lastArg?.message === "Skipping non-resolved Managed OOv2 request outside settlement include list" + ), + "Expected include list skip log" ).lastArg; - assert.equal(filterLog.skipped, 1); - assert.notProperty(filterLog, "skippedIds"); + assert.equal(skipLog.proposalEventId, getProposalEventId(proposeReceipt)); } // An include list containing the proposal: it settles. @@ -560,6 +596,10 @@ describe("OptimisticOracleV2Bot", function () { const { spy, logger } = makeSpyLogger(); const params = await createParams("ManagedOptimisticOracleV2", optimisticOracleV2.address); params.settleIncludeList = new Set([getProposalEventId(proposeReceipt)]); + params.botModes.settleOnlyDisputed = true; + // Exclude the proposal from the historical event range to exercise the direct include-list lookup. + params.timeLookback = 0; + params.blockFinder = new BlockFinder(params.provider.getBlock.bind(params.provider), undefined, params.chainId); await gasEstimator.update(); await settleRequests(logger, params, gasEstimator); @@ -568,44 +608,42 @@ describe("OptimisticOracleV2Bot", function () { } }); - it("Accepts explicit empty include/exclude lists", async function () { - // Templated deployments commonly render optional list env vars as "[]"; this must not throw. + it("Parses an explicit empty proposal include list", async function () { 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); }); it("Defaults Managed OOv2 settlements to an empty include list", async function () { - const { settleIncludeList, settleExcludeList } = parseSettleProposalIdLists( - {} as NodeJS.ProcessEnv, - "ManagedOptimisticOracleV2" - ); + const settleIncludeList = parseSettleIncludeList({} as NodeJS.ProcessEnv, "ManagedOptimisticOracleV2"); assert.instanceOf(settleIncludeList, Set); assert.equal(settleIncludeList?.size, 0); - assert.isUndefined(settleExcludeList); - const explicitSettleAll = parseSettleProposalIdLists( - { SETTLE_EXCLUDE_LIST: "[]" } as NodeJS.ProcessEnv, - "ManagedOptimisticOracleV2" - ); - assert.isUndefined(explicitSettleAll.settleIncludeList); - assert.instanceOf(explicitSettleAll.settleExcludeList, Set); - assert.equal(explicitSettleAll.settleExcludeList?.size, 0); + const standardOOv2 = parseSettleIncludeList({} as NodeJS.ProcessEnv, "OptimisticOracleV2"); + assert.isUndefined(standardOOv2); + }); - const standardOOv2 = parseSettleProposalIdLists({} as NodeJS.ProcessEnv, "OptimisticOracleV2"); - assert.isUndefined(standardOOv2.settleIncludeList); - assert.isUndefined(standardOOv2.settleExcludeList); + it("Rejects the removed exclude list setting for every oracle type", async function () { + const oracleTypes: OracleType[] = [ + "OptimisticOracle", + "SkinnyOptimisticOracle", + "OptimisticOracleV2", + "ManagedOptimisticOracleV2", + ]; + for (const value of ["", "[]", JSON.stringify([`0x${"0".repeat(64)}:0`])]) { + for (const oracleType of oracleTypes) { + assert.throws( + () => parseSettleIncludeList({ SETTLE_EXCLUDE_LIST: value } as NodeJS.ProcessEnv, oracleType), + /SETTLE_EXCLUDE_LIST is not supported/ + ); + } + } }); - it("Rejects Managed OOv2 settlement params without an include or exclude list", async function () { + it("Rejects Managed OOv2 settlement params without an include list", async function () { const { logger } = makeSpyLogger(); const params = await createParams("ManagedOptimisticOracleV2", optimisticOracleV2.address); params.settleIncludeList = undefined; - params.settleExcludeList = undefined; let error: unknown; try { @@ -615,32 +653,26 @@ describe("OptimisticOracleV2Bot", function () { } assert.instanceOf(error, Error); - assert.match((error as Error).message, /Managed OOv2 settlement requires an include or exclude list/); + assert.match((error as Error).message, /Managed OOv2 settlement requires an include list/); }); - it("Rejects include/exclude lists for non-OOv2 oracle types", async function () { - // Only the OOv2 settler applies these lists; silently ignoring them would settle proposals the operator - // intended to skip, so startup must fail instead. - const env = { SETTLE_EXCLUDE_LIST: JSON.stringify([`0x${"0".repeat(64)}:0`]) } as NodeJS.ProcessEnv; + it("Rejects include lists for non-OOv2 oracle types", async function () { + const env = { SETTLE_INCLUDE_LIST: JSON.stringify([`0x${"0".repeat(64)}:0`]) } as NodeJS.ProcessEnv; assert.throws( - () => parseSettleProposalIdLists(env, "OptimisticOracle"), + () => parseSettleIncludeList(env, "OptimisticOracle"), /only supported for OptimisticOracleV2 and ManagedOptimisticOracleV2/ ); assert.throws( - () => parseSettleProposalIdLists(env, "SkinnyOptimisticOracle"), + () => parseSettleIncludeList(env, "SkinnyOptimisticOracle"), /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; - assert.doesNotThrow(() => parseSettleProposalIdLists(emptyExcludeEnv, "OptimisticOracle")); + assert.doesNotThrow(() => parseSettleIncludeList(env, "OptimisticOracleV2")); + assert.doesNotThrow(() => parseSettleIncludeList(env, "ManagedOptimisticOracleV2")); // 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"), + () => parseSettleIncludeList(emptyIncludeEnv, "OptimisticOracle"), /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 94d5814d6e..67321ffbe0 100644 --- a/packages/monitor-v2/test/helpers/monitoring.ts +++ b/packages/monitor-v2/test/helpers/monitoring.ts @@ -43,7 +43,7 @@ export async function makeMonitoringParamsOO( contractAddress, settleBatchSize: 1, settleMinProposalAgeSeconds: 0, - ...(oracleType === "ManagedOptimisticOracleV2" ? { settleExcludeList: new Set() } : {}), + ...(oracleType === "ManagedOptimisticOracleV2" ? { settleIncludeList: new Set() } : {}), }; }