mirror of
https://github.com/luxfi/uma.git
synced 2026-07-27 05:11:41 +00:00
feat: settle assertions bot (#4501)
Signed-off-by: Pablo Maldonado <pablo@umaproject.org>
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user