feat: settle assertions bot (#4501)

Signed-off-by: Pablo Maldonado <pablo@umaproject.org>
This commit is contained in:
Pablo Maldonado
2023-04-05 15:20:19 +01:00
committed by GitHub
parent 1aa637cb17
commit 9957caee3e
14 changed files with 1229 additions and 926 deletions
+1 -1
View File
@@ -31,7 +31,7 @@
"devDependencies": {
"@antora/cli": "^2.1.2",
"@antora/site-generator-default": "^2.1.2",
"@typechain/ethers-v5": "^7.0.1",
"@typechain/ethers-v5": "^7.1.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"babel-eslint": "10.0.1",
+1 -1
View File
@@ -25,7 +25,7 @@ export async function getGckmsSigner(): Promise<Wallet> {
return new Wallet(privateKeys[0]); // GCKMS retrieveGckmsKeys returns multiple keys. For now we only support 1.
}
function getMnemonicSigner() {
export function getMnemonicSigner(): Wallet {
if (!process.env.MNEMONIC) throw new Error(`Wallet mnemonic selected but no MNEMONIC env set!`);
return Wallet.fromMnemonic(process.env.MNEMONIC);
}
@@ -0,0 +1,44 @@
import { createEtherscanLinkMarkdown, createFormatFunction } from "@uma/common";
import { BigNumber, utils } from "ethers";
import { Logger } from "./common";
import type { MonitoringParams } from "./common";
import { getCurrencyDecimals, getCurrencySymbol, tryHexToUtf8String } from "../utils/contracts";
export async function logSettleAssertion(
logger: typeof Logger,
assertion: {
tx: string;
assertionId: string;
claim: string;
bond: BigNumber;
identifier: string;
currency: string;
settlementResolution: boolean;
},
params: MonitoringParams
): Promise<void> {
const currencyDecimals = await getCurrencyDecimals(params.provider, assertion.currency);
const currencySymbol = await getCurrencySymbol(params.provider, assertion.currency);
logger.warn({
at: "OOv3Bot",
message: "Assertion Settled ✅",
mrkdwn:
"Assertion with ID " +
assertion.assertionId +
" settled in transaction " +
createEtherscanLinkMarkdown(assertion.tx, params.chainId) +
". Claim: " +
tryHexToUtf8String(assertion.claim) +
". Settlement Resolution: " +
assertion.settlementResolution +
". Identifier: " +
utils.parseBytes32String(assertion.identifier) +
". Bond: " +
createFormatFunction(2, 2, false, currencyDecimals)(assertion.bond.toString()) +
" " +
currencySymbol +
".",
notificationPath: "optimistic-oracle",
});
}
@@ -0,0 +1,30 @@
# Optimistic Oracle V3 Bot
Optimistic Oracle V3 bots can run off-chain actions related to Optimistic Oracle V3 contracts.
The main entry point to Optimistic Oracle V3 monitor bots is running:
```
node ./packages/monitor-v2/dist/bot-oo-v3/index.js
```
All the configuration should be provided with following environment variables:
- `CHAIN_ID` is network number.
- `NODE_URLS_X` is an array of RPC node URLs replacing `X` in variable name with network number from `CHAIN_ID`.
- `NODE_URL_X` is a single RPC node URL replacing `X` in variable name with network number from `CHAIN_ID`. This is
considered only if matching `NODE_URLS_X` is not provided.
- `MNEMONIC` is a mnemonic for a wallet that has enough funds to pay for transactions.
- `NODE_RETRIES` is the number of retries to make when a node request fails (defaults to `2`).
- `NODE_RETRY_DELAY` is the delay in seconds between retries (defaults to `1`).
- `NODE_TIMEOUT` is the timeout in seconds for node requests (defaults to `60`).
- `RUN_FREQUENCY` is the frequency in seconds at which the bot should run (defaults to `60`).
- `SETTLEMENTS_ENABLED` is boolean enabling/disabling settlement of assertions (`false` by default).
- `BOT_IDENTIFIER` identifies the application name in the logs.
- `SLACK_CONFIG` is a JSON object containing `defaultWebHookUrl` for the default Slack webhook URL.
- `WARMING_UP_BLOCK_LOOKBACK`(Optional) is the number of blocks to look back from the current block to warm up the bot's event state.
See default values in blockDefaults in index.ts
- `BLOCK_LOOKBACK`(Optional) is the number of blocks to look back from the current block to look for past events.
See default values in blockDefaults in index.ts
- `MAX_BLOCK_LOOKBACK`(Optional) is the maximum number of blocks to look back per query.
See default values in blockDefaults in index.ts
@@ -0,0 +1,64 @@
import { getContractInstanceWithProvider, Logger, MonitoringParams, OptimisticOracleV3Ethers } from "./common";
import { logSettleAssertion } from "./BotLogger";
import { paginatedEventQuery } from "../utils/EventUtils";
import {
AssertionSettledEvent,
AssertionMadeEvent,
} from "@uma/contracts-node/dist/packages/contracts-node/typechain/core/ethers/OptimisticOracleV3";
export async function settleAssertions(logger: typeof Logger, params: MonitoringParams): Promise<void> {
const oo = await getContractInstanceWithProvider<OptimisticOracleV3Ethers>("OptimisticOracleV3", params.provider);
const currentBlockNumber = await params.provider.getBlockNumber();
const loopBack = params.firstRun ? params.warmingUpBlockLookback : params.blockLookback;
const searchConfig = {
fromBlock: currentBlockNumber - loopBack < 0 ? 0 : currentBlockNumber - loopBack,
toBlock: currentBlockNumber,
maxBlockLookBack: params.maxBlockLookBack,
};
const assertions = await paginatedEventQuery<AssertionMadeEvent>(oo, oo.filters.AssertionMade(), searchConfig);
const assertionsSettled = await paginatedEventQuery<AssertionSettledEvent>(
oo,
oo.filters.AssertionSettled(),
searchConfig
);
const assertionsSettledIds = new Set(assertionsSettled.map((assertion) => assertion.args.assertionId));
const assertionsToSettle = assertions.filter(
(assertion) => !assertionsSettledIds.has(assertion.args.assertionId)
) as AssertionMadeEvent[];
const setteableAssertions: AssertionMadeEvent[] = [];
for (const assertion of assertionsToSettle) {
try {
await oo.callStatic.settleAndGetAssertionResult(assertion.args.assertionId);
setteableAssertions.push(assertion);
console.log(`Assertion ${assertion.args.assertionId} is setteable.`);
} catch (err) {
console.log(`Assertion ${assertion.args.assertionId} is not setteable yet.`);
}
}
for (const assertion of setteableAssertions) {
const tx = await oo.connect(params.signer).settleAssertion(assertion.args.assertionId);
const receipt = await tx.wait();
const event = receipt.events?.find((e) => e.event === "AssertionSettled");
await logSettleAssertion(
logger,
{
tx: tx.hash,
assertionId: assertion.args.assertionId,
claim: assertion.args.claim,
bond: assertion.args.bond,
identifier: assertion.args.identifier,
currency: assertion.args.currency,
settlementResolution: event?.args?.settlementResolution,
},
params
);
}
}
+115
View File
@@ -0,0 +1,115 @@
import { getMnemonicSigner, getRetryProvider } from "@uma/common";
import { Signer } from "ethers";
import type { Provider } from "@ethersproject/abstract-provider";
export { OptimisticOracleV3Ethers } from "@uma/contracts-node";
export { Logger } from "@uma/financial-templates-lib";
export { getContractInstanceWithProvider } from "../utils/contracts";
export interface BotModes {
settleAssertionsEnabled: boolean;
}
export interface BlockRange {
start: number;
end: number;
}
export interface MonitoringParams {
provider: Provider;
chainId: number;
runFrequency: number;
botModes: BotModes;
signer: Signer;
warmingUpBlockLookback: number;
blockLookback: number;
maxBlockLookBack: number;
firstRun?: boolean;
}
const blockDefaults = {
"1": {
// Mainnet
hour: 300, // 12 seconds per block
day: 7200,
maxBlockLookBack: 20000,
},
"137": {
// Polygon
hour: 1800, // 2 seconds per block
day: 43200,
maxBlockLookBack: 3499,
},
"10": {
// Optimism
hour: 1800, // 2 seconds per block
day: 43200,
maxBlockLookBack: 10000,
},
"42161": {
// Arbitrum
hour: 240, // 15 seconds per block
day: 5760,
maxBlockLookBack: 10000,
},
"43114": {
// Avalanche
hour: 1800, // 2 seconds per block
day: 43200,
maxBlockLookBack: 2000,
},
other: {
hour: 240, // 15 seconds per block
day: 5760,
maxBlockLookBack: 1000,
},
};
export const initMonitoringParams = async (env: NodeJS.ProcessEnv): Promise<MonitoringParams> => {
if (!env.CHAIN_ID) throw new Error("CHAIN_ID must be defined in env");
const chainId = Number(env.CHAIN_ID);
// Creating provider will check for other chainId specific env variables.
const provider = getRetryProvider(chainId) as Provider;
// Throws if MNEMONIC env var is not defined.
const signer = (getMnemonicSigner() as Signer).connect(provider);
// Default to 1 minute run frequency.
const runFrequency = env.RUN_FREQUENCY ? Number(env.RUN_FREQUENCY) : 60;
const botModes = {
settleAssertionsEnabled: env.SETTLEMENTS_ENABLED === "true",
};
const blockLookback =
Number(env.WARMING_UP_BLOCK_LOOKBACK) ||
blockDefaults[chainId.toString() as keyof typeof blockDefaults]?.hour ||
blockDefaults.other.hour;
const warmingUpBlockLookback =
Number(env.BLOCK_LOOKBACK) ||
blockDefaults[chainId.toString() as keyof typeof blockDefaults]?.day ||
blockDefaults.other.day;
const maxBlockLookBack =
Number(env.MAX_BLOCK_LOOKBACK) ||
blockDefaults[chainId.toString() as keyof typeof blockDefaults]?.maxBlockLookBack ||
blockDefaults.other.maxBlockLookBack;
return {
provider,
chainId,
runFrequency,
botModes,
signer,
warmingUpBlockLookback,
blockLookback,
maxBlockLookBack,
};
};
export const startupLogLevel = (params: MonitoringParams): "debug" | "info" => {
return params.runFrequency === 0 ? "debug" : "info";
};
@@ -0,0 +1,57 @@
import { delay } from "@uma/financial-templates-lib";
import { BotModes, initMonitoringParams, Logger, startupLogLevel } from "./common";
import { settleAssertions } from "./SettleAssertions";
export {
AssertionSettledEvent,
AssertionMadeEvent,
} from "@uma/contracts-node/dist/packages/contracts-node/typechain/core/ethers/OptimisticOracleV3";
const logger = Logger;
async function main() {
const params = await initMonitoringParams(process.env);
logger[startupLogLevel(params)]({
at: "OOv3Bot",
message: "Optimistic Oracle V3 Bot started 🤖",
botModes: params.botModes,
});
const cmds = {
settleAssertionsEnabled: settleAssertions,
};
let firstRun = true;
for (;;) {
const runCmds = Object.entries(cmds)
.filter(([mode]) => params.botModes[mode as keyof BotModes])
.map(([, cmd]) => cmd(logger, { ...params, firstRun }));
await Promise.all(runCmds);
firstRun = false;
if (params.runFrequency !== 0) {
await delay(params.runFrequency);
} else {
await delay(5); // Set a delay to let the transports flush fully.
break;
}
}
}
main().then(
() => {
process.exit(0);
},
async (error) => {
logger.error({
at: "OOv3Bot",
message: "Optimistic Oracle V3 Bot execution error🚨",
error,
});
// Wait 5 seconds to allow logger to flush.
await delay(5);
process.exit(1);
}
);
@@ -1,8 +1,9 @@
import { createEtherscanLinkMarkdown, createFormatFunction } from "@uma/common";
import { utils } from "ethers";
import { getCurrencyDecimals, getCurrencySymbol, Logger, OptimisticOracleV3Ethers, tryHexToUtf8String } from "./common";
import { Logger, OptimisticOracleV3Ethers } from "./common";
import type { MonitoringParams } from "./common";
import { getCurrencyDecimals, getCurrencySymbol, tryHexToUtf8String } from "../utils/contracts";
export async function logAssertion(
logger: typeof Logger,
@@ -1,8 +1,5 @@
import { getRetryProvider } from "@uma/common";
import { ERC20Ethers } from "@uma/contracts-node";
import { delay } from "@uma/financial-templates-lib";
import { utils } from "ethers";
import { getContractInstanceWithProvider } from "../utils/contracts";
import type { Provider } from "@ethersproject/abstract-provider";
@@ -79,39 +76,3 @@ export const waitNextBlockRange = async (params: MonitoringParams): Promise<Bloc
export const startupLogLevel = (params: MonitoringParams): "debug" | "info" => {
return params.pollingDelay === 0 ? "debug" : "info";
};
export const tryHexToUtf8String = (ancillaryData: string): string => {
try {
return utils.toUtf8String(ancillaryData);
} catch (err) {
return ancillaryData;
}
};
export const getCurrencyDecimals = async (provider: Provider, currencyAddress: string): Promise<number> => {
const currencyContract = await getContractInstanceWithProvider<ERC20Ethers>("ERC20", provider, currencyAddress);
try {
return await currencyContract.decimals();
} catch (err) {
return 18;
}
};
export const getCurrencySymbol = async (provider: Provider, currencyAddress: string): Promise<string> => {
const currencyContract = await getContractInstanceWithProvider<ERC20Ethers>("ERC20", provider, currencyAddress);
try {
return await currencyContract.symbol();
} catch (err) {
// Try to get the symbol as bytes32 (e.g. MKR uses this).
try {
const bytes32SymbolIface = new utils.Interface(["function symbol() view returns (bytes32 symbol)"]);
const bytes32Symbol = await provider.call({
to: currencyAddress,
data: bytes32SymbolIface.encodeFunctionData("symbol"),
});
return utils.parseBytes32String(bytes32SymbolIface.decodeFunctionResult("symbol", bytes32Symbol).symbol);
} catch (err) {
return "";
}
}
};
+103
View File
@@ -0,0 +1,103 @@
import { delay } from "@uma/financial-templates-lib";
import { Promise } from "bluebird";
import { Contract, Event, EventFilter } from "ethers";
const defaultConcurrency = 200;
const maxRetries = 3;
const retrySleepTime = 10;
export interface EventSearchConfig {
fromBlock: number;
toBlock: number;
maxBlockLookBack?: number;
concurrency?: number;
}
export async function paginatedEventQuery<T extends Event>(
contract: Contract,
filter: EventFilter,
searchConfig: EventSearchConfig,
retryCount = 0
): Promise<Array<T>> {
// If the max block look back is set to 0 then we dont need to do any pagination and can query over the whole range.
if (searchConfig.maxBlockLookBack === 0)
return (await contract.queryFilter(filter, searchConfig.fromBlock, searchConfig.toBlock)) as Array<T>;
// Compute the number of queries needed. If there is no maxBlockLookBack set then we can execute the whole query in
// one go. Else, the number of queries is the range over which we are searching, divided by the maxBlockLookBack,
// rounded up. This gives us the number of queries we need to execute to traverse the whole block range.
const paginatedRanges = getPaginatedBlockRanges(searchConfig);
try {
return (
(
await Promise.map(paginatedRanges, ([fromBlock, toBlock]) => contract.queryFilter(filter, fromBlock, toBlock), {
concurrency: typeof searchConfig.concurrency == "number" ? searchConfig.concurrency : defaultConcurrency,
})
)
.flat()
// Filter events by block number because ranges can include blocks that are outside the range specified for caching reasons.
.filter(
(event: Event) => event.blockNumber >= searchConfig.fromBlock && event.blockNumber <= searchConfig.toBlock
) as Array<T>
);
} catch (error) {
if (retryCount < maxRetries) {
await delay(retrySleepTime);
return await paginatedEventQuery(contract, filter, searchConfig, retryCount + 1);
} else throw error;
}
}
/**
* @dev Warning: this is a specialized function!! Its functionality is not obvious.
* This function attempts to return block ranges to repeat ranges as much as possible. To do so, it may include blocks that
* are outside the provided range. The guarantee is that it will always include _at least_ the blocks requested.
* @param eventSearchConfig contains fromBlock, toBlock, and maxBlockLookBack.
* The range is inclusive, so the results will include events in the fromBlock and in the toBlock.
* maxBlockLookback defined the maximum number of blocks to search. Because the range is inclusive, the maximum diff
* in the returned pairs is maxBlockLookBack - 1. This is a bit non-intuitive here, but this is meant so that this
* parameter more closely aligns with the more commonly understood definition of a max query range that node providers
* use.
* @returns an array of disjoint fromBlock, toBlock ranges that should be queried. These cover at least the entire
* input range, but can include blocks outside of the desired range, so results should be filtered. Results
* are ordered from smallest to largest.
*/
export function getPaginatedBlockRanges({
fromBlock,
toBlock,
maxBlockLookBack,
}: EventSearchConfig): [number, number][] {
// If the maxBlockLookBack is undefined, we can look back as far as we like. Just return the entire range.
if (maxBlockLookBack === undefined) return [[fromBlock, toBlock]];
// If the fromBlock is > toBlock, then return no ranges.
if (fromBlock > toBlock) return [];
// A maxBlockLookBack of 0 is not allowed.
if (maxBlockLookBack <= 0) throw new Error("Cannot set maxBlockLookBack <= 0");
// Floor the requestedFromBlock to the nearest smaller multiple of the maxBlockLookBack to enhance caching.
// This means that a range like 5 - 45 with a maxBlockLookBack of 20 would look like:
// 0-19, 20-39, 40-45.
// This allows us to get the max number of repeated node queries. The maximum number of "nonstandard" queries per
// call of this function is 1.
const flooredStartBlock = Math.floor(fromBlock / maxBlockLookBack) * maxBlockLookBack;
// Note: range is inclusive, so we have to add one to the number of blocks to query.
const iterations = Math.ceil((toBlock + 1 - flooredStartBlock) / maxBlockLookBack);
const ranges: [number, number][] = [];
for (let i = 0; i < iterations; i++) {
// Each inner range start is just a multiple of the maxBlockLookBack added to the start block.
const innerFromBlock = flooredStartBlock + maxBlockLookBack * i;
// The innerFromBlock is just the max range from the innerFromBlock or the outer toBlock, whichever is smaller.
// The end block should never be larger than the outer toBlock. This is to avoid querying blocks that are in the
// future.
const innerToBlock = Math.min(innerFromBlock + maxBlockLookBack - 1, toBlock);
ranges.push([innerFromBlock, innerToBlock]);
}
return ranges;
}
+38 -1
View File
@@ -1,6 +1,7 @@
import { ContractName, getAbi, getAddress } from "@uma/contracts-node";
import { ContractName, ERC20Ethers, getAbi, getAddress } from "@uma/contracts-node";
import { Contract } from "ethers";
import { Provider } from "@ethersproject/abstract-provider";
import { utils } from "ethers";
export const getContractInstanceWithProvider = async <T extends Contract>(
contractName: ContractName,
@@ -12,3 +13,39 @@ export const getContractInstanceWithProvider = async <T extends Contract>(
const contractAbi = getAbi(contractName);
return new Contract(contractAddress, contractAbi, provider) as T;
};
export const tryHexToUtf8String = (ancillaryData: string): string => {
try {
return utils.toUtf8String(ancillaryData);
} catch (err) {
return ancillaryData;
}
};
export const getCurrencyDecimals = async (provider: Provider, currencyAddress: string): Promise<number> => {
const currencyContract = await getContractInstanceWithProvider<ERC20Ethers>("ERC20", provider, currencyAddress);
try {
return await currencyContract.decimals();
} catch (err) {
return 18;
}
};
export const getCurrencySymbol = async (provider: Provider, currencyAddress: string): Promise<string> => {
const currencyContract = await getContractInstanceWithProvider<ERC20Ethers>("ERC20", provider, currencyAddress);
try {
return await currencyContract.symbol();
} catch (err) {
// Try to get the symbol as bytes32 (e.g. MKR uses this).
try {
const bytes32SymbolIface = new utils.Interface(["function symbol() view returns (bytes32 symbol)"]);
const bytes32Symbol = await provider.call({
to: currencyAddress,
data: bytes32SymbolIface.encodeFunctionData("symbol"),
});
return utils.parseBytes32String(bytes32SymbolIface.decodeFunctionResult("symbol", bytes32Symbol).symbol);
} catch (err) {
return "";
}
}
};
@@ -53,8 +53,7 @@ describe("OptimisticGovernorMonitor", function () {
transactionsExecutedEnabled: false,
proposalExecutedEnabled: false,
proposalDeletedEnabled: false,
setBondEnabled: false,
setCollateralEnabled: false,
setCollateralAndBondEnabled: false,
setRulesEnabled: false,
setLivenessEnabled: false,
setIdentifierEnabled: false,
@@ -315,7 +314,7 @@ describe("OptimisticGovernorMonitor", function () {
assert.equal(spyLogLevel(spy, 0), "warn");
assert.equal(spy.getCall(0).lastArg.notificationPath, "optimistic-governor");
const newEscalationManager = await random.getAddress();
const newEscalationManager = await timer.address; // Just use the timer address as a random contract address.
const setEscalationManagerTx = await optimisticGovernor.connect(ogOwner).setEscalationManager(newEscalationManager);
const setEscalationManagerBlockNumber = await getBlockNumberFromTx(setEscalationManagerTx);
@@ -0,0 +1,154 @@
import type { Provider } from "@ethersproject/abstract-provider";
import { ExpandedERC20Ethers, MockOracleAncillaryEthers, OptimisticOracleV3Ethers } from "@uma/contracts-node";
import { createNewLogger, spyLogIncludes, spyLogLevel, SpyTransport } from "@uma/financial-templates-lib";
import { assert } from "chai";
import sinon from "sinon";
import { BotModes, MonitoringParams } from "../src/bot-oo-v3/common";
import { settleAssertions } from "../src/bot-oo-v3/SettleAssertions";
import { defaultLiveness } from "./constants";
import { optimisticOracleV3Fixture } from "./fixtures/OptimisticOracleV3.Fixture";
import { umaEcosystemFixture } from "./fixtures/UmaEcosystem.Fixture";
import { getBlockNumberFromTx, hardhatTime, hre, Signer, toUtf8Bytes, toUtf8String } from "./utils";
const ethers = hre.ethers;
// Create monitoring params for single block to pass to monitor modules.
const createMonitoringParams = async (): Promise<MonitoringParams> => {
// get hardhat signer
const [signer] = await ethers.getSigners();
// Bot modes are not used as we are calling monitor modules directly.
const botModes: BotModes = {
settleAssertionsEnabled: false,
};
return {
provider: ethers.provider as Provider,
chainId: (await ethers.provider.getNetwork()).chainId,
runFrequency: 60,
botModes,
signer,
warmingUpBlockLookback: 2000,
blockLookback: 2000,
maxBlockLookBack: 1000,
firstRun: true,
};
};
describe("OptimisticOracleV3Bot", function () {
let mockOracle: MockOracleAncillaryEthers;
let bondToken: ExpandedERC20Ethers;
let optimisticOracleV3: OptimisticOracleV3Ethers;
let deployer: Signer;
let asserter: Signer;
let disputer: Signer;
const claim = toUtf8Bytes("This is just a test claim");
beforeEach(async function () {
// Signer from ethers and hardhat-ethers are not version compatible, thus, we cannot use the SignerWithAddress.
[deployer, asserter, disputer] = (await ethers.getSigners()) as Signer[];
// Get contract instances.
const umaContracts = await umaEcosystemFixture();
mockOracle = umaContracts.mockOracle;
const optimisticOracleV3Contracts = await optimisticOracleV3Fixture();
bondToken = optimisticOracleV3Contracts.bondToken;
optimisticOracleV3 = optimisticOracleV3Contracts.optimisticOracleV3;
// Fund asserter and disputer with minimum bond amount and approve Optimistic Oracle V3 to spend bond tokens.
const minimumBondAmount = await optimisticOracleV3.getMinimumBond(bondToken.address);
await bondToken.addMinter(await deployer.getAddress());
await bondToken.mint(await asserter.getAddress(), minimumBondAmount);
await bondToken.mint(await disputer.getAddress(), minimumBondAmount);
await bondToken.connect(asserter).approve(optimisticOracleV3.address, minimumBondAmount);
await bondToken.connect(disputer).approve(optimisticOracleV3.address, minimumBondAmount);
});
it("Settle assertion happy path", async function () {
// Make assertion.
const assertionTx = await optimisticOracleV3
.connect(asserter)
.assertTruthWithDefaults(claim, await asserter.getAddress());
// const assertionId = await getAssertionId(assertionTx, optimisticOracleV3);
const assertionBlockNumber = await getBlockNumberFromTx(assertionTx);
const assertionMadeEvent = (
await optimisticOracleV3.queryFilter(optimisticOracleV3.filters.AssertionMade(), assertionBlockNumber)
)[0];
// Call monitorAssertions directly for the block when the assertion was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await settleAssertions(spyLogger, await createMonitoringParams());
// No logs should be generated as there are no assertions to settle.
assert.isNull(spy.getCall(0));
// move time forward to the execution time.
await hardhatTime.increase(defaultLiveness);
await settleAssertions(spyLogger, await createMonitoringParams());
// When calling monitoring module directly there should be only one log (index 0) with the assertion caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "OOv3Bot");
assert.equal(spy.getCall(0).lastArg.message, "Assertion Settled ✅");
assert.equal(spyLogLevel(spy, 0), "warn");
assert.isTrue(spyLogIncludes(spy, 0, assertionMadeEvent.args.assertionId));
assert.isTrue(spyLogIncludes(spy, 0, toUtf8String(claim)));
assert.isTrue(spyLogIncludes(spy, 0, "Settlement Resolution: true"));
assert.equal(spy.getCall(0).lastArg.notificationPath, "optimistic-oracle");
spy.resetHistory();
await settleAssertions(spyLogger, await createMonitoringParams());
// There should be no logs as there are no assertions to settle.
assert.isNull(spy.getCall(0));
});
it("Settle assertion with dispute", async function () {
// Make assertion.
const assertionTx = await optimisticOracleV3
.connect(asserter)
.assertTruthWithDefaults(claim, await asserter.getAddress());
// const assertionId = await getAssertionId(assertionTx, optimisticOracleV3);
const assertionBlockNumber = await getBlockNumberFromTx(assertionTx);
const assertionMadeEvent = (
await optimisticOracleV3.queryFilter(optimisticOracleV3.filters.AssertionMade(), assertionBlockNumber)
)[0];
// Dispute assertion.
const disputeTx = await optimisticOracleV3
.connect(disputer)
.disputeAssertion(assertionMadeEvent.args.assertionId, await disputer.getAddress());
// Get oracle request from the first PriceRequestAdded event in the dispute transaction.
const oracleRequest = (
await mockOracle.queryFilter(mockOracle.filters.PriceRequestAdded(), disputeTx.blockNumber, disputeTx.blockNumber)
)[0].args;
// Call monitorAssertions directly for the block when the assertion was made.
const spy = sinon.spy();
const spyLogger = createNewLogger([new SpyTransport({}, { spy: spy })]);
await settleAssertions(spyLogger, await createMonitoringParams());
// No logs should be generated as there are no assertions to settle.
assert.isNull(spy.getCall(0));
// Resolve assertion as false.
await mockOracle
.connect(disputer)
.pushPrice(oracleRequest.identifier, oracleRequest.time, oracleRequest.ancillaryData, 0);
await settleAssertions(spyLogger, await createMonitoringParams());
// When calling monitoring module directly there should be only one log (index 0) with the assertion caught by spy.
assert.equal(spy.getCall(0).lastArg.at, "OOv3Bot");
assert.equal(spy.getCall(0).lastArg.message, "Assertion Settled ✅");
assert.equal(spyLogLevel(spy, 0), "warn");
assert.isTrue(spyLogIncludes(spy, 0, assertionMadeEvent.args.assertionId));
assert.isTrue(spyLogIncludes(spy, 0, toUtf8String(claim)));
assert.isTrue(spyLogIncludes(spy, 0, "Settlement Resolution: false"));
assert.equal(spy.getCall(0).lastArg.notificationPath, "optimistic-oracle");
spy.resetHistory();
await settleAssertions(spyLogger, await createMonitoringParams());
// There should be no logs as there are no assertions to settle.
assert.isNull(spy.getCall(0));
});
});
+618 -880
View File
File diff suppressed because it is too large Load Diff