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
5 changes: 2 additions & 3 deletions packages/monitor-v2/src/bot-oo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `"<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.
- `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 `"<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. 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

Expand Down
102 changes: 81 additions & 21 deletions packages/monitor-v2/src/bot-oo/SettleOOv2Requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,51 +21,69 @@ 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(
// 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<string>
): Promise<ProposePriceEvent[]> {
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<void> {
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<OptimisticOracleV2Ethers>(
"OptimisticOracleV2",
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)",
Expand Down
54 changes: 20 additions & 34 deletions packages/monitor-v2/src/bot-oo/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ function getNonNegativeNumber(value: string | undefined, defaultValue: number):

// 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).
// An explicit empty array is accepted; oracle-specific support is validated separately.
export function parseProposalIdList(value: string | undefined, envName: string): Set<string> | undefined {
if (value === undefined || value.trim() === "") return undefined;

Expand All @@ -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<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;
// 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<string> | 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<string>()
: 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 {
Expand All @@ -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 ("<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.
// Proposal event ids ("<txHash>:<logIndex>"). 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<string>;
settleExcludeList?: Set<string>;
}

export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise<MonitoringParams> => {
Expand Down Expand Up @@ -136,7 +123,7 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise<Moni
DEFAULT_SETTLE_MIN_PROPOSAL_AGE_SECONDS
);

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

return {
...base,
Expand All @@ -148,7 +135,6 @@ export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise<Moni
settleBatchSize,
settleMinProposalAgeSeconds,
settleIncludeList,
settleExcludeList,
};
};

Expand Down
2 changes: 1 addition & 1 deletion packages/monitor-v2/src/bot-oo/requestKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export const requestKey = (args: RequestKeyArgs): string =>
);

// 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}`;
Loading