import { deployments, ethers } from "hardhat"; import hre from "hardhat"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { getRandomQuestion, sleep } from "./utils"; import { WalletClient } from "@nomicfoundation/hardhat-viem/types"; import { Deployment } from "hardhat-deploy/types"; import { zeroAddress } from "viem"; import { OptimisticOracle } from "../typechain-types"; const isHalfTimePassed = (assertion: any, currentTimestamp: bigint) => { const startTime: bigint = assertion.startTime; const endTime: bigint = assertion.endTime; const halfTimePassed = (endTime - startTime) / 2n; return currentTimestamp > startTime && startTime + halfTimePassed < currentTimestamp; }; const stopTrackingAssertion = ( accountToAssertionIds: Record, account: WalletClient, assertionId: bigint, ) => { accountToAssertionIds[account.account.address] = accountToAssertionIds[account.account.address].filter( id => id !== assertionId, ); }; const canPropose = (assertion: any, currentTimestamp: bigint) => { const rangeOfSeconds = [10n, 20n, 30n, 40n, 50n, 60n, 70n, 80n, 90n, 100n]; const randomSeconds = rangeOfSeconds[Math.floor(Math.random() * rangeOfSeconds.length)]; return assertion.proposer === zeroAddress && currentTimestamp > assertion.startTime + randomSeconds; }; const createAssertions = async ( optimisticDeployment: Deployment, optimisticOracle: OptimisticOracle, otherAccounts: WalletClient[], accountToAssertionIds: Record, ) => { const minReward = ethers.parseEther("0.01"); let nextAssertionId = await optimisticOracle.nextAssertionId(); for (const account of otherAccounts) { const assertionIds = accountToAssertionIds[account.account.address]; if (assertionIds.length === 0 && Math.random() < 0.5) { await account.writeContract({ address: optimisticDeployment.address as `0x${string}`, abi: optimisticDeployment.abi, functionName: "assertEvent", args: [getRandomQuestion(), 0n, 0n], value: minReward + (1n * 10n ** 18n * BigInt(Math.floor(Math.random() * 100))) / 100n, }); console.log(`✅ created assertion ${nextAssertionId}`); // Track the assertion for 80% of cases; otherwise, leave it untracked so it will expire if (Math.random() < 0.8) { accountToAssertionIds[account.account.address].push(nextAssertionId); } nextAssertionId++; } } }; const proposeAssertions = async ( trueResponder: WalletClient, falseResponder: WalletClient, randomResponder: WalletClient, optimisticDeployment: Deployment, optimisticOracle: OptimisticOracle, currentTimestamp: bigint, otherAccounts: WalletClient[], accountToAssertionIds: Record, ) => { for (const account of otherAccounts) { const assertionIds = accountToAssertionIds[account.account.address]; if (assertionIds.length !== 0) { for (const assertionId of assertionIds) { const assertion = await optimisticOracle.assertions(assertionId); if (canPropose(assertion, currentTimestamp)) { const randomness = Math.random(); if (randomness < 0.25) { await trueResponder.writeContract({ address: optimisticDeployment.address as `0x${string}`, abi: optimisticDeployment.abi, functionName: "proposeOutcome", args: [assertionId, true], value: assertion.bond, }); console.log(`✅ proposed outcome=true for assertion ${assertionId}`); } else if (randomness < 0.5) { await falseResponder.writeContract({ address: optimisticDeployment.address as `0x${string}`, abi: optimisticDeployment.abi, functionName: "proposeOutcome", args: [assertionId, false], value: assertion.bond, }); console.log(`❌ proposed outcome=false for assertion ${assertionId} `); } else if (randomness < 0.9) { const outcome = Math.random() < 0.5; await randomResponder.writeContract({ address: optimisticDeployment.address as `0x${string}`, abi: optimisticDeployment.abi, functionName: "proposeOutcome", args: [assertionId, outcome], value: assertion.bond, }); console.log(`${outcome ? "✅" : "❌"} proposed outcome=${outcome} for assertion ${assertionId}`); // if randomly wallet proposed, then remove the assertion from the account (No need to track and dispute) stopTrackingAssertion(accountToAssertionIds, account, assertionId); } } } } } }; const disputeAssertions = async ( trueResponder: WalletClient, falseResponder: WalletClient, optimisticDeployment: Deployment, optimisticOracle: OptimisticOracle, currentTimestamp: bigint, accountToAssertionIds: Record, otherAccounts: WalletClient[], ) => { for (const account of otherAccounts) { const assertionIds = accountToAssertionIds[account.account.address]; for (const assertionId of assertionIds) { const assertion = await optimisticOracle.assertions(assertionId); if ( assertion.proposer.toLowerCase() === trueResponder.account.address.toLowerCase() && isHalfTimePassed(assertion, currentTimestamp) ) { await falseResponder.writeContract({ address: optimisticDeployment.address as `0x${string}`, abi: optimisticDeployment.abi, functionName: "disputeOutcome", args: [assertionId], value: assertion.bond, }); console.log(`⚔️ disputed assertion ${assertionId}`); // if disputed, then remove the assertion from the account stopTrackingAssertion(accountToAssertionIds, account, assertionId); } else if ( assertion.proposer.toLowerCase() === falseResponder.account.address.toLowerCase() && isHalfTimePassed(assertion, currentTimestamp) ) { await trueResponder.writeContract({ address: optimisticDeployment.address as `0x${string}`, abi: optimisticDeployment.abi, functionName: "disputeOutcome", args: [assertionId], value: assertion.bond, }); console.log(`⚔️ disputed assertion ${assertionId}`); // if disputed, then remove the assertion from the account stopTrackingAssertion(accountToAssertionIds, account, assertionId); } } } }; let currentAction = 0; const runCycle = async ( hre: HardhatRuntimeEnvironment, accountToAssertionIds: Record, accounts: WalletClient[], ) => { try { const trueResponder = accounts[0]; const falseResponder = accounts[1]; const randomResponder = accounts[2]; const otherAccounts = accounts.slice(3); const optimisticDeployment = await deployments.get("OptimisticOracle"); const optimisticOracle = await ethers.getContractAt("OptimisticOracle", optimisticDeployment.address); const publicClient = await hre.viem.getPublicClient(); // get current timestamp const latestBlock = await publicClient.getBlock(); const currentTimestamp = latestBlock.timestamp; // also track thex of the account start from the third account if (currentAction === 0) { console.log(`\n📝 === CREATING ASSERTIONS PHASE ===`); await createAssertions(optimisticDeployment, optimisticOracle, otherAccounts, accountToAssertionIds); } else if (currentAction === 1) { console.log(`\n🎯 === PROPOSING OUTCOMES PHASE ===`); await proposeAssertions( trueResponder, falseResponder, randomResponder, optimisticDeployment, optimisticOracle, currentTimestamp, otherAccounts, accountToAssertionIds, ); } else if (currentAction === 2) { console.log(`\n⚔️ === DISPUTING ASSERTIONS PHASE ===`); await disputeAssertions( trueResponder, falseResponder, optimisticDeployment, optimisticOracle, currentTimestamp, accountToAssertionIds, otherAccounts, ); } currentAction = (currentAction + 1) % 3; } catch (error) { console.error("Error in oracle cycle:", error); throw error; } }; async function run() { console.log("Starting optimistic oracle bots..."); const accountToAssertionIds: Record = {}; const accounts = (await hre.viem.getWalletClients()).slice(0, 8); for (const account of accounts) { accountToAssertionIds[account.account.address] = []; } while (true) { await runCycle(hre, accountToAssertionIds, accounts); await sleep(3000); } } run().catch(error => { console.error(error); process.exitCode = 1; }); // Handle process termination signals process.on("SIGINT", async () => { console.log("\nReceived SIGINT (Ctrl+C). Cleaning up..."); process.exit(0); }); process.on("SIGTERM", async () => { console.log("\nReceived SIGTERM. Cleaning up..."); process.exit(0); }); // Handle uncaught exceptions process.on("uncaughtException", async error => { console.error("Uncaught Exception:", error); process.exit(1); }); // Handle unhandled promise rejections process.on("unhandledRejection", async (reason, promise) => { console.error("Unhandled Rejection at:", promise, "reason:", reason); process.exit(1); });