Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/monitor-v2/src/bot-oo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -36,6 +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 `"<txHash>:<logIndex>"` 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 `"<txHash>:<logIndex>"` 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

Expand All @@ -45,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:
Expand Down
47 changes: 45 additions & 2 deletions packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -21,11 +21,52 @@ function chunk<T>(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 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++;
if (!settleIncludeList) skippedIds.push(id);
}
}

logger.debug({
at: "OOv2Bot",
message: "Applied include/exclude proposal filter",
mode: settleIncludeList ? "include" : "exclude",
listSize: (settleIncludeList ?? settleExcludeList)?.size,
kept: kept.length,
skipped,
...(settleIncludeList ? {} : { skippedIds }),
});

return kept;
}

export async function settleOOv2Requests(
logger: typeof Logger,
params: MonitoringParams,
gasEstimator: GasEstimator
): Promise<void> {
if (params.oracleType === "ManagedOptimisticOracleV2" && !params.settleIncludeList && !params.settleExcludeList) {
throw new Error("Managed OOv2 settlement requires an include or exclude list");
}

const oo = await getContractInstanceWithProvider<OptimisticOracleV2Ethers>(
"OptimisticOracleV2",
params.provider,
Expand Down Expand Up @@ -99,7 +140,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;
Expand Down
1 change: 1 addition & 0 deletions packages/monitor-v2/src/bot-oo/SettleRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
83 changes: 79 additions & 4 deletions packages/monitor-v2/src/bot-oo/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,79 @@ 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";
export type OracleType =
| "OptimisticOracle"
| "SkinnyOptimisticOracle"
| "OptimisticOracleV2"
| "ManagedOptimisticOracleV2";

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 "<txHash>:<logIndex>" 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).
export function parseProposalIdList(value: string | undefined, envName: string): Set<string> | 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 "<txHash>:<logIndex>" strings`);
}
if (!Array.isArray(parsed)) {
throw new Error(`${envName} must be a JSON array of "<txHash>:<logIndex>" strings`);
}

const ids = parsed.map((entry) => {
if (typeof entry !== "string") throw new Error(`${envName} entries must be "<txHash>:<logIndex>" strings`);
const match = entry.match(PROPOSAL_ID_REGEX);
if (!match) throw new Error(`Invalid ${envName} entry "${entry}"; expected "<txHash>:<logIndex>"`);
return proposalEventId(match[1], Number(match[2]));
Comment thread
md0x marked this conversation as resolved.
});

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<string>; settleExcludeList?: Set<string> } {
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<string>()
: configuredIncludeList;

return { settleIncludeList, settleExcludeList };
}

export interface BotModes {
settleRequestsEnabled: boolean;
settleOnlyDisputed: boolean; // Supported for OptimisticOracleV2 (incl. ManagedOOv2); ignored for OOv1 and SkinnyOO.
Expand All @@ -27,6 +89,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 ("<txHash>:<logIndex>"). 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.
settleIncludeList?: Set<string>;
settleExcludeList?: Set<string>;
}

export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise<MonitoringParams> => {
Expand All @@ -42,13 +109,17 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise<Moni

if (!env.ORACLE_TYPE)
throw new Error(
"ORACLE_TYPE must be defined in env (OptimisticOracle, SkinnyOptimisticOracle, or OptimisticOracleV2)"
"ORACLE_TYPE must be defined in env (OptimisticOracle, SkinnyOptimisticOracle, OptimisticOracleV2, or ManagedOptimisticOracleV2)"
);
const oracleType = env.ORACLE_TYPE as OracleType;

if (!["OptimisticOracle", "SkinnyOptimisticOracle", "OptimisticOracleV2"].includes(oracleType)) {
if (
!["OptimisticOracle", "SkinnyOptimisticOracle", "OptimisticOracleV2", "ManagedOptimisticOracleV2"].includes(
oracleType
)
) {
throw new Error(
`Invalid ORACLE_TYPE: ${oracleType}. Must be OptimisticOracle, SkinnyOptimisticOracle, or OptimisticOracleV2`
`Invalid ORACLE_TYPE: ${oracleType}. Must be OptimisticOracle, SkinnyOptimisticOracle, OptimisticOracleV2, or ManagedOptimisticOracleV2`
);
}

Expand All @@ -65,6 +136,8 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise<Moni
DEFAULT_SETTLE_MIN_PROPOSAL_AGE_SECONDS
);

const { settleIncludeList, settleExcludeList } = parseSettleProposalIdLists(env, oracleType);

return {
...base,
botModes,
Expand All @@ -74,6 +147,8 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise<Moni
executionDeadline,
settleBatchSize,
settleMinProposalAgeSeconds,
settleIncludeList,
settleExcludeList,
};
};

Expand Down
5 changes: 5 additions & 0 deletions packages/monitor-v2/src/bot-oo/requestKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ export const requestKey = (args: RequestKeyArgs): string =>
[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}`;
Loading