259 lines
9.3 KiB
TypeScript
259 lines
9.3 KiB
TypeScript
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<string, bigint[]>,
|
||
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<string, bigint[]>,
|
||
) => {
|
||
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<string, bigint[]>,
|
||
) => {
|
||
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<string, bigint[]>,
|
||
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<string, bigint[]>,
|
||
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<string, bigint[]> = {};
|
||
|
||
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);
|
||
});
|