Initial commit with 🏗️ create-eth @ 2.0.4
This commit is contained in:
68
packages/hardhat/scripts/fetchPriceFromUniswap.ts
Normal file
68
packages/hardhat/scripts/fetchPriceFromUniswap.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ethers } from "hardhat";
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import { config as hardhatConfig } from "hardhat";
|
||||
import { getConfig, updatePriceCache } from "./utils";
|
||||
import { parseEther, formatEther } from "ethers";
|
||||
|
||||
const UNISWAP_V2_PAIR_ABI = [
|
||||
"function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)",
|
||||
"function token0() external view returns (address)",
|
||||
"function token1() external view returns (address)",
|
||||
];
|
||||
|
||||
const DAI_ADDRESS = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
|
||||
const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||
const UNISWAP_V2_FACTORY = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f";
|
||||
const mainnet = hardhatConfig.networks.mainnet;
|
||||
const MAINNET_RPC = "url" in mainnet ? mainnet.url : "";
|
||||
|
||||
export const fetchPriceFromUniswap = async (): Promise<bigint> => {
|
||||
const config = getConfig();
|
||||
const cachedPrice = config.PRICE.CACHEDPRICE;
|
||||
const timestamp = config.PRICE.TIMESTAMP;
|
||||
|
||||
if (Date.now() - timestamp < 1000 * 60 * 60) {
|
||||
return parseEther(cachedPrice.toString());
|
||||
}
|
||||
console.log("Cache expired or missing, fetching fresh price from Uniswap...");
|
||||
|
||||
try {
|
||||
const provider = new ethers.JsonRpcProvider(MAINNET_RPC);
|
||||
const tokenAddress = WETH_ADDRESS; // Always use WETH for mainnet
|
||||
|
||||
// Get pair address from Uniswap V2 Factory
|
||||
const factory = new ethers.Contract(
|
||||
UNISWAP_V2_FACTORY,
|
||||
["function getPair(address tokenA, address tokenB) external view returns (address pair)"],
|
||||
provider,
|
||||
);
|
||||
|
||||
const pairAddress = await factory.getPair(tokenAddress, DAI_ADDRESS);
|
||||
if (pairAddress === ethers.ZeroAddress) {
|
||||
throw new Error("No liquidity pair found");
|
||||
}
|
||||
|
||||
const pairContract = new ethers.Contract(pairAddress, UNISWAP_V2_PAIR_ABI, provider);
|
||||
const [reserves, token0Address] = await Promise.all([pairContract.getReserves(), pairContract.token0()]);
|
||||
|
||||
// Determine which reserve is token and which is DAI
|
||||
const isToken0 = token0Address.toLowerCase() === tokenAddress.toLowerCase();
|
||||
const tokenReserve = isToken0 ? reserves[0] : reserves[1];
|
||||
const daiReserve = isToken0 ? reserves[1] : reserves[0];
|
||||
|
||||
// Calculate price (DAI per token)
|
||||
const price = BigInt(Math.floor((Number(daiReserve) / Number(tokenReserve)) * 1e18));
|
||||
|
||||
// Update cache with fresh price
|
||||
const pricePerEther = parseFloat(formatEther(price));
|
||||
updatePriceCache(pricePerEther, Date.now());
|
||||
console.log(`Fresh price fetched and cached: ${formatEther(price)} ETH`);
|
||||
|
||||
return price;
|
||||
} catch (error) {
|
||||
console.error("Error fetching ETH price from Uniswap: ", error);
|
||||
return parseEther(cachedPrice.toString());
|
||||
}
|
||||
};
|
||||
58
packages/hardhat/scripts/generateAccount.ts
Normal file
58
packages/hardhat/scripts/generateAccount.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
const getValidatedPassword = async () => {
|
||||
while (true) {
|
||||
const pass = await password({ message: "Enter a password to encrypt your private key:" });
|
||||
const confirmation = await password({ message: "Confirm password:" });
|
||||
|
||||
if (pass === confirmation) {
|
||||
return pass;
|
||||
}
|
||||
console.log("❌ Passwords don't match. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const setNewEnvConfig = async (existingEnvConfig = {}) => {
|
||||
console.log("👛 Generating new Wallet\n");
|
||||
const randomWallet = ethers.Wallet.createRandom();
|
||||
|
||||
const pass = await getValidatedPassword();
|
||||
const encryptedJson = await randomWallet.encrypt(pass);
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Generated wallet address:", randomWallet.address, "\n");
|
||||
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
await setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
await setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
136
packages/hardhat/scripts/generateTsAbis.ts
Normal file
136
packages/hardhat/scripts/generateTsAbis.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* DON'T MODIFY OR DELETE THIS SCRIPT (unless you know what you're doing)
|
||||
*
|
||||
* This script generates the file containing the contracts Abi definitions.
|
||||
* These definitions are used to derive the types needed in the custom scaffold-eth hooks, for example.
|
||||
* This script should run as the last deploy script.
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import prettier from "prettier";
|
||||
import { DeployFunction } from "hardhat-deploy/types";
|
||||
|
||||
const generatedContractComment = `
|
||||
/**
|
||||
* This file is autogenerated by Scaffold-ETH.
|
||||
* You should not edit it manually or your changes might be overwritten.
|
||||
*/
|
||||
`;
|
||||
|
||||
const DEPLOYMENTS_DIR = "./deployments";
|
||||
const ARTIFACTS_DIR = "./artifacts";
|
||||
|
||||
function getDirectories(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
}
|
||||
|
||||
function getContractNames(path: string) {
|
||||
return fs
|
||||
.readdirSync(path, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isFile() && dirent.name.endsWith(".json"))
|
||||
.map(dirent => dirent.name.split(".")[0]);
|
||||
}
|
||||
|
||||
function getActualSourcesForContract(sources: Record<string, any>, contractName: string) {
|
||||
for (const sourcePath of Object.keys(sources)) {
|
||||
const sourceName = sourcePath.split("/").pop()?.split(".sol")[0];
|
||||
if (sourceName === contractName) {
|
||||
const contractContent = sources[sourcePath].content as string;
|
||||
const regex = /contract\s+(\w+)\s+is\s+([^{}]+)\{/;
|
||||
const match = contractContent.match(regex);
|
||||
|
||||
if (match) {
|
||||
const inheritancePart = match[2];
|
||||
// Split the inherited contracts by commas to get the list of inherited contracts
|
||||
const inheritedContracts = inheritancePart.split(",").map(contract => `${contract.trim()}.sol`);
|
||||
|
||||
return inheritedContracts;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getInheritedFunctions(sources: Record<string, any>, contractName: string) {
|
||||
const actualSources = getActualSourcesForContract(sources, contractName);
|
||||
const inheritedFunctions = {} as Record<string, any>;
|
||||
|
||||
for (const sourceContractName of actualSources) {
|
||||
const sourcePath = Object.keys(sources).find(key => key.includes(`/${sourceContractName}`));
|
||||
if (sourcePath) {
|
||||
const sourceName = sourcePath?.split("/").pop()?.split(".sol")[0];
|
||||
const { abi } = JSON.parse(fs.readFileSync(`${ARTIFACTS_DIR}/${sourcePath}/${sourceName}.json`).toString());
|
||||
for (const functionAbi of abi) {
|
||||
if (functionAbi.type === "function") {
|
||||
inheritedFunctions[functionAbi.name] = sourcePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inheritedFunctions;
|
||||
}
|
||||
|
||||
function getContractDataFromDeployments() {
|
||||
if (!fs.existsSync(DEPLOYMENTS_DIR)) {
|
||||
throw Error("At least one other deployment script should exist to generate an actual contract.");
|
||||
}
|
||||
const output = {} as Record<string, any>;
|
||||
const chainDirectories = getDirectories(DEPLOYMENTS_DIR);
|
||||
for (const chainName of chainDirectories) {
|
||||
let chainId;
|
||||
try {
|
||||
chainId = fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/.chainId`).toString();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
console.log(`No chainId file found for ${chainName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const contracts = {} as Record<string, any>;
|
||||
for (const contractName of getContractNames(`${DEPLOYMENTS_DIR}/${chainName}`)) {
|
||||
const { abi, address, metadata, receipt } = JSON.parse(
|
||||
fs.readFileSync(`${DEPLOYMENTS_DIR}/${chainName}/${contractName}.json`).toString(),
|
||||
);
|
||||
const inheritedFunctions = metadata ? getInheritedFunctions(JSON.parse(metadata).sources, contractName) : {};
|
||||
contracts[contractName] = { address, abi, inheritedFunctions, deployedOnBlock: receipt?.blockNumber };
|
||||
}
|
||||
output[chainId] = contracts;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the TypeScript contract definition file based on the json output of the contract deployment scripts
|
||||
* This script should be run last.
|
||||
*/
|
||||
const generateTsAbis: DeployFunction = async function () {
|
||||
const TARGET_DIR = "../nextjs/contracts/";
|
||||
const allContractsData = getContractDataFromDeployments();
|
||||
|
||||
const fileContent = Object.entries(allContractsData).reduce((content, [chainId, chainConfig]) => {
|
||||
return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify(chainConfig, null, 2)},`;
|
||||
}, "");
|
||||
|
||||
if (!fs.existsSync(TARGET_DIR)) {
|
||||
fs.mkdirSync(TARGET_DIR);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
`${TARGET_DIR}deployedContracts.ts`,
|
||||
await prettier.format(
|
||||
`${generatedContractComment} import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; \n\n
|
||||
const deployedContracts = {${fileContent}} as const; \n\n export default deployedContracts satisfies GenericContractsDeclaration`,
|
||||
{
|
||||
parser: "typescript",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
console.log(`📝 Updated TypeScript contract definition file on ${TARGET_DIR}deployedContracts.ts`);
|
||||
};
|
||||
|
||||
export default generateTsAbis;
|
||||
72
packages/hardhat/scripts/importAccount.ts
Normal file
72
packages/hardhat/scripts/importAccount.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ethers } from "ethers";
|
||||
import { parse, stringify } from "envfile";
|
||||
import * as fs from "fs";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
const envFilePath = "./.env";
|
||||
|
||||
const getValidatedPassword = async () => {
|
||||
while (true) {
|
||||
const pass = await password({ message: "Enter a password to encrypt your private key:" });
|
||||
const confirmation = await password({ message: "Confirm password:" });
|
||||
|
||||
if (pass === confirmation) {
|
||||
return pass;
|
||||
}
|
||||
console.log("❌ Passwords don't match. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const getWalletFromPrivateKey = async () => {
|
||||
while (true) {
|
||||
const privateKey = await password({ message: "Paste your private key:" });
|
||||
try {
|
||||
const wallet = new ethers.Wallet(privateKey);
|
||||
return wallet;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("❌ Invalid private key format. Please try again.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setNewEnvConfig = async (existingEnvConfig = {}) => {
|
||||
console.log("👛 Importing Wallet\n");
|
||||
|
||||
const wallet = await getWalletFromPrivateKey();
|
||||
|
||||
const pass = await getValidatedPassword();
|
||||
const encryptedJson = await wallet.encrypt(pass);
|
||||
|
||||
const newEnvConfig = {
|
||||
...existingEnvConfig,
|
||||
DEPLOYER_PRIVATE_KEY_ENCRYPTED: encryptedJson,
|
||||
};
|
||||
|
||||
// Store in .env
|
||||
fs.writeFileSync(envFilePath, stringify(newEnvConfig));
|
||||
console.log("\n📄 Encrypted Private Key saved to packages/hardhat/.env file");
|
||||
console.log("🪄 Imported wallet address:", wallet.address, "\n");
|
||||
console.log("⚠️ Make sure to remember your password! You'll need it to decrypt the private key.");
|
||||
};
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(envFilePath)) {
|
||||
// No .env file yet.
|
||||
await setNewEnvConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEnvConfig = parse(fs.readFileSync(envFilePath).toString());
|
||||
if (existingEnvConfig.DEPLOYER_PRIVATE_KEY_ENCRYPTED) {
|
||||
console.log("⚠️ You already have a deployer account. Check the packages/hardhat/.env file");
|
||||
return;
|
||||
}
|
||||
|
||||
await setNewEnvConfig(existingEnvConfig);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
52
packages/hardhat/scripts/listAccount.ts
Normal file
52
packages/hardhat/scripts/listAccount.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { ethers, Wallet } from "ethers";
|
||||
import QRCode from "qrcode";
|
||||
import { config } from "hardhat";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
async function main() {
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = await password({ message: "Enter your password to decrypt the private key:" });
|
||||
let wallet: Wallet;
|
||||
try {
|
||||
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("❌ Failed to decrypt private key. Wrong password?");
|
||||
return;
|
||||
}
|
||||
|
||||
const address = wallet.address;
|
||||
console.log(await QRCode.toString(address, { type: "terminal", small: true }));
|
||||
console.log("Public address:", address, "\n");
|
||||
|
||||
// Balance on each network
|
||||
const availableNetworks = config.networks;
|
||||
for (const networkName in availableNetworks) {
|
||||
try {
|
||||
const network = availableNetworks[networkName];
|
||||
if (!("url" in network)) continue;
|
||||
const provider = new ethers.JsonRpcProvider(network.url);
|
||||
await provider._detectNetwork();
|
||||
const balance = await provider.getBalance(address);
|
||||
console.log("--", networkName, "-- 📡");
|
||||
console.log(" balance:", +ethers.formatEther(balance));
|
||||
console.log(" nonce:", +(await provider.getTransactionCount(address)));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.log("Can't connect to network", networkName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
29
packages/hardhat/scripts/oracle-bot/balances.ts
Normal file
29
packages/hardhat/scripts/oracle-bot/balances.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ethers } from "hardhat";
|
||||
import { formatEther } from "ethers";
|
||||
|
||||
export async function reportBalances() {
|
||||
try {
|
||||
// Get all signers (accounts)
|
||||
const signers = await ethers.getSigners();
|
||||
const oracleNodes = signers.slice(1, 11); // Get oracle node accounts
|
||||
|
||||
// Get the StakingOracle contract
|
||||
const oracleContract = await ethers.getContract("StakingOracle");
|
||||
const oracle = await ethers.getContractAt("StakingOracle", oracleContract.target);
|
||||
|
||||
// Get the ORA token address and create contract instance
|
||||
const oraTokenAddress = await oracle.oracleToken();
|
||||
const oraToken = await ethers.getContractAt("contracts/OracleToken.sol:ORA", oraTokenAddress);
|
||||
|
||||
console.log("\nNode Balances:");
|
||||
for (const node of oracleNodes) {
|
||||
const nodeInfo = await oracle.nodes(node.address);
|
||||
const oraBalance = await oraToken.balanceOf(node.address);
|
||||
console.log(`\nNode ${node.address}:`);
|
||||
console.log(` Staked ETH: ${formatEther(nodeInfo.stakedAmount)} ETH`);
|
||||
console.log(` ORA Balance: ${formatEther(oraBalance)} ORA`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error reporting balances:", error);
|
||||
}
|
||||
}
|
||||
56
packages/hardhat/scripts/oracle-bot/config.json
Normal file
56
packages/hardhat/scripts/oracle-bot/config.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"PRICE": {
|
||||
"CACHEDPRICE": 4000,
|
||||
"TIMESTAMP": 1761680177006
|
||||
},
|
||||
"INTERVALS": {
|
||||
"PRICE_REPORT": 1750,
|
||||
"VALIDATION": 1750
|
||||
},
|
||||
"NODE_CONFIGS": {
|
||||
"default": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0x70997970c51812dc3a010c7d01b50e0d17dc79c8": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0x976ea74026e726554db657fa54763abd0c3a0aa9": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0x90f79bf6eb2c4f870365e785982e1f101e93b906": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0xbcd4042de499d14e55001ccbb24a551f3b954096": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0x14dc79964da2c08b23698b3d3cc7ca32193d9955": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
},
|
||||
"0xa0ee7a142d267c1f36714e4a8f75612f20a79720": {
|
||||
"PROBABILITY_OF_SKIPPING_REPORT": 0,
|
||||
"PRICE_VARIANCE": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/hardhat/scripts/oracle-bot/price.ts
Normal file
16
packages/hardhat/scripts/oracle-bot/price.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getConfig } from "../utils";
|
||||
|
||||
export const getRandomPrice = async (nodeAddress: string, currentPrice: number): Promise<number> => {
|
||||
const config = getConfig();
|
||||
const nodeConfig = config.NODE_CONFIGS[nodeAddress] || config.NODE_CONFIGS.default;
|
||||
|
||||
// Calculate variance range based on the node's PRICE_VARIANCE
|
||||
// PRICE_VARIANCE of 0 means no variance, higher values mean wider range
|
||||
const varianceRange = Math.floor(currentPrice * nodeConfig.PRICE_VARIANCE);
|
||||
|
||||
// Apply variance to the base price
|
||||
const finalPrice = currentPrice + (Math.random() * 2 - 1) * varianceRange;
|
||||
|
||||
// Round to nearest integer
|
||||
return Math.round(finalPrice);
|
||||
};
|
||||
80
packages/hardhat/scripts/oracle-bot/reporting.ts
Normal file
80
packages/hardhat/scripts/oracle-bot/reporting.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { PublicClient } from "viem";
|
||||
import { getRandomPrice } from "./price";
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { getConfig } from "../utils";
|
||||
import { fetchPriceFromUniswap } from "../fetchPriceFromUniswap";
|
||||
import { DeployedContract } from "hardhat-deploy/types";
|
||||
|
||||
const getStakedAmount = async (
|
||||
publicClient: PublicClient,
|
||||
nodeAddress: `0x${string}`,
|
||||
oracleContract: DeployedContract,
|
||||
) => {
|
||||
const nodeInfo = (await publicClient.readContract({
|
||||
address: oracleContract.address as `0x${string}`,
|
||||
abi: oracleContract.abi,
|
||||
functionName: "nodes",
|
||||
args: [nodeAddress],
|
||||
})) as any[];
|
||||
|
||||
const [, stakedAmount] = nodeInfo;
|
||||
return stakedAmount as bigint;
|
||||
};
|
||||
|
||||
export const reportPrices = async (hre: HardhatRuntimeEnvironment) => {
|
||||
const { deployments } = hre;
|
||||
const oracleContract = await deployments.get("StakingOracle");
|
||||
const config = getConfig();
|
||||
const accounts = await hre.viem.getWalletClients();
|
||||
const oracleNodeAccounts = accounts.slice(1, 11);
|
||||
const publicClient = await hre.viem.getPublicClient();
|
||||
|
||||
// Get minimum stake requirement from contract
|
||||
const minimumStake = (await publicClient.readContract({
|
||||
address: oracleContract.address as `0x${string}`,
|
||||
abi: oracleContract.abi,
|
||||
functionName: "MINIMUM_STAKE",
|
||||
args: [],
|
||||
})) as unknown as bigint;
|
||||
|
||||
const currentPrice = Number(await fetchPriceFromUniswap());
|
||||
try {
|
||||
return Promise.all(
|
||||
oracleNodeAccounts.map(async account => {
|
||||
const nodeConfig = config.NODE_CONFIGS[account.account.address] || config.NODE_CONFIGS.default;
|
||||
const shouldReport = Math.random() > nodeConfig.PROBABILITY_OF_SKIPPING_REPORT;
|
||||
const stakedAmount = await getStakedAmount(publicClient, account.account.address, oracleContract);
|
||||
if (stakedAmount < minimumStake) {
|
||||
console.log(`Insufficient stake for ${account.account.address} for price reporting`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (shouldReport) {
|
||||
const price = BigInt(await getRandomPrice(account.account.address, currentPrice));
|
||||
console.log(`Reporting price ${price} from ${account.account.address}`);
|
||||
try {
|
||||
return await account.writeContract({
|
||||
address: oracleContract.address as `0x${string}`,
|
||||
abi: oracleContract.abi,
|
||||
functionName: "reportPrice",
|
||||
args: [price],
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.message && error.message.includes("Not enough stake")) {
|
||||
console.log(
|
||||
`Skipping price report from ${account.account.address} - insufficient stake during execution`,
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
console.log(`Skipping price report from ${account.account.address}`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error reporting prices:", error);
|
||||
}
|
||||
};
|
||||
19
packages/hardhat/scripts/oracle-bot/types.ts
Normal file
19
packages/hardhat/scripts/oracle-bot/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
interface NodeConfig {
|
||||
PROBABILITY_OF_SKIPPING_REPORT: number;
|
||||
PRICE_VARIANCE: number; // Higher number means wider price range
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
PRICE: {
|
||||
CACHEDPRICE: number;
|
||||
TIMESTAMP: number;
|
||||
};
|
||||
INTERVALS: {
|
||||
PRICE_REPORT: number;
|
||||
VALIDATION: number;
|
||||
};
|
||||
NODE_CONFIGS: {
|
||||
[key: string]: NodeConfig;
|
||||
default: NodeConfig;
|
||||
};
|
||||
}
|
||||
79
packages/hardhat/scripts/oracle-bot/validation.ts
Normal file
79
packages/hardhat/scripts/oracle-bot/validation.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
|
||||
const getStakedAmount = async (publicClient: any, nodeAddress: `0x${string}`, oracleContract: any) => {
|
||||
const nodeInfo = (await publicClient.readContract({
|
||||
address: oracleContract.address as `0x${string}`,
|
||||
abi: oracleContract.abi,
|
||||
functionName: "nodes",
|
||||
args: [nodeAddress],
|
||||
})) as any[];
|
||||
|
||||
const [, stakedAmount] = nodeInfo;
|
||||
return stakedAmount as bigint;
|
||||
};
|
||||
|
||||
export const claimRewards = async (hre: HardhatRuntimeEnvironment) => {
|
||||
const { deployments } = hre;
|
||||
const oracleContract = await deployments.get("StakingOracle");
|
||||
const accounts = await hre.viem.getWalletClients();
|
||||
const oracleNodeAccounts = accounts.slice(1, 11);
|
||||
const publicClient = await hre.viem.getPublicClient();
|
||||
|
||||
// Get minimum stake requirement from contract
|
||||
const minimumStake = (await publicClient.readContract({
|
||||
address: oracleContract.address as `0x${string}`,
|
||||
abi: oracleContract.abi,
|
||||
functionName: "MINIMUM_STAKE",
|
||||
args: [],
|
||||
})) as unknown as bigint;
|
||||
|
||||
try {
|
||||
return Promise.all(
|
||||
oracleNodeAccounts.map(async account => {
|
||||
const stakedAmount = await getStakedAmount(publicClient, account.account.address, oracleContract);
|
||||
|
||||
// Only claim rewards if the node has sufficient stake
|
||||
if (stakedAmount >= minimumStake) {
|
||||
try {
|
||||
console.log(`Claiming rewards for ${account.account.address}`);
|
||||
return await account.writeContract({
|
||||
address: oracleContract.address as `0x${string}`,
|
||||
abi: oracleContract.abi,
|
||||
functionName: "claimReward",
|
||||
args: [],
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.message && error.message.includes("No rewards available")) {
|
||||
console.log(`Skipping reward claim for ${account.account.address} - no rewards available`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
console.log(`Skipping reward claim for ${account.account.address} - insufficient stake`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error claiming rewards:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Keep the old validateNodes function for backward compatibility if needed
|
||||
export const validateNodes = async (hre: HardhatRuntimeEnvironment) => {
|
||||
const { deployments } = hre;
|
||||
const [account] = await hre.viem.getWalletClients();
|
||||
const oracleContract = await deployments.get("StakingOracle");
|
||||
|
||||
try {
|
||||
return await account.writeContract({
|
||||
address: oracleContract.address as `0x${string}`,
|
||||
abi: oracleContract.abi,
|
||||
functionName: "slashNodes",
|
||||
args: [],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error validating nodes:", error);
|
||||
}
|
||||
};
|
||||
31
packages/hardhat/scripts/revealPK.ts
Normal file
31
packages/hardhat/scripts/revealPK.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { Wallet } from "ethers";
|
||||
import password from "@inquirer/password";
|
||||
|
||||
async function main() {
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("👀 This will reveal your private key on the console.\n");
|
||||
|
||||
const pass = await password({ message: "Enter your password to decrypt the private key:" });
|
||||
let wallet: Wallet;
|
||||
try {
|
||||
wallet = (await Wallet.fromEncryptedJson(encryptedKey, pass)) as Wallet;
|
||||
} catch {
|
||||
console.log("❌ Failed to decrypt private key. Wrong password?");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\n🔑 Private key:", wallet.privateKey);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
58
packages/hardhat/scripts/runHardhatDeployWithPK.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import { Wallet } from "ethers";
|
||||
import password from "@inquirer/password";
|
||||
import { spawn } from "child_process";
|
||||
import { config } from "hardhat";
|
||||
|
||||
/**
|
||||
* Unencrypts the private key and runs the hardhat deploy command
|
||||
*/
|
||||
async function main() {
|
||||
const networkIndex = process.argv.indexOf("--network");
|
||||
const networkName = networkIndex !== -1 ? process.argv[networkIndex + 1] : config.defaultNetwork;
|
||||
|
||||
if (networkName === "localhost" || networkName === "hardhat") {
|
||||
// Deploy command on the localhost network
|
||||
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
hardhat.on("exit", code => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedKey = process.env.DEPLOYER_PRIVATE_KEY_ENCRYPTED;
|
||||
|
||||
if (!encryptedKey) {
|
||||
console.log("🚫️ You don't have a deployer account. Run `yarn generate` or `yarn account:import` first");
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = await password({ message: "Enter password to decrypt private key:" });
|
||||
|
||||
try {
|
||||
const wallet = await Wallet.fromEncryptedJson(encryptedKey, pass);
|
||||
process.env.__RUNTIME_DEPLOYER_PRIVATE_KEY = wallet.privateKey;
|
||||
|
||||
const hardhat = spawn("hardhat", ["deploy", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
hardhat.on("exit", code => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.error("Failed to decrypt private key. Wrong password?");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
258
packages/hardhat/scripts/runOptimisticBots.ts
Normal file
258
packages/hardhat/scripts/runOptimisticBots.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
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);
|
||||
});
|
||||
688
packages/hardhat/scripts/runStakingOracleBots.ts
Normal file
688
packages/hardhat/scripts/runStakingOracleBots.ts
Normal file
@@ -0,0 +1,688 @@
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import hre from "hardhat";
|
||||
import { sleep, getConfig } from "./utils";
|
||||
import { fetchPriceFromUniswap } from "./fetchPriceFromUniswap";
|
||||
import { parseEther } from "viem";
|
||||
|
||||
const oraTokenAbi = [
|
||||
{
|
||||
type: "function",
|
||||
name: "approve",
|
||||
stateMutability: "nonpayable",
|
||||
inputs: [
|
||||
{ name: "spender", type: "address" },
|
||||
{ name: "amount", type: "uint256" },
|
||||
],
|
||||
outputs: [{ name: "", type: "bool" }],
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "balanceOf",
|
||||
stateMutability: "view",
|
||||
inputs: [{ name: "owner", type: "address" }],
|
||||
outputs: [{ name: "", type: "uint256" }],
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "allowance",
|
||||
stateMutability: "view",
|
||||
inputs: [
|
||||
{ name: "owner", type: "address" },
|
||||
{ name: "spender", type: "address" },
|
||||
],
|
||||
outputs: [{ name: "", type: "uint256" }],
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
name: "transfer",
|
||||
stateMutability: "nonpayable",
|
||||
inputs: [
|
||||
{ name: "to", type: "address" },
|
||||
{ name: "amount", type: "uint256" },
|
||||
],
|
||||
outputs: [{ name: "", type: "bool" }],
|
||||
},
|
||||
] as const;
|
||||
|
||||
type WalletClient = Awaited<ReturnType<typeof hre.viem.getWalletClients>>[number];
|
||||
|
||||
const normalizeNodeInfo = (raw: any) => {
|
||||
const zero = 0n;
|
||||
if (!raw)
|
||||
return {
|
||||
stakedAmount: zero,
|
||||
lastReportedBucket: zero,
|
||||
reportCount: zero,
|
||||
claimedReportCount: zero,
|
||||
firstBucket: zero,
|
||||
active: false,
|
||||
};
|
||||
const get = (idx: number, name: string) => {
|
||||
const byName = raw[name];
|
||||
const byIndex = Array.isArray(raw) ? raw[idx] : undefined;
|
||||
if (typeof byName === "bigint") return byName as bigint;
|
||||
if (typeof byIndex === "bigint") return byIndex as bigint;
|
||||
const val = byName ?? byIndex ?? 0;
|
||||
try {
|
||||
return BigInt(String(val));
|
||||
} catch {
|
||||
return zero;
|
||||
}
|
||||
};
|
||||
return {
|
||||
stakedAmount: get(0, "stakedAmount"),
|
||||
lastReportedBucket: get(1, "lastReportedBucket"),
|
||||
reportCount: get(2, "reportCount"),
|
||||
claimedReportCount: get(3, "claimedReportCount"),
|
||||
firstBucket: get(4, "firstBucket"),
|
||||
active:
|
||||
typeof raw?.active === "boolean"
|
||||
? (raw.active as boolean)
|
||||
: Array.isArray(raw) && typeof raw[5] === "boolean"
|
||||
? (raw[5] as boolean)
|
||||
: false,
|
||||
};
|
||||
};
|
||||
|
||||
// Current base price used by the bot. Initialized once at start from Uniswap
|
||||
// and updated from on-chain contract prices thereafter.
|
||||
let currentPrice: bigint | null = null;
|
||||
|
||||
const stringToBool = (value: string | undefined | null): boolean => {
|
||||
if (!value) return false;
|
||||
const normalized = value.toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
};
|
||||
|
||||
// Feature flag: enable automatic slashing when the AUTO_SLASH environment variable is truthy
|
||||
const AUTO_SLASH: boolean = stringToBool(process.env.AUTO_SLASH);
|
||||
|
||||
const getStakingOracleDeployment = async (runtime: HardhatRuntimeEnvironment) => {
|
||||
const deployment = await runtime.deployments.get("StakingOracle");
|
||||
return {
|
||||
address: deployment.address as `0x${string}`,
|
||||
abi: deployment.abi,
|
||||
deployedBlock: deployment.receipt?.blockNumber ? BigInt(deployment.receipt.blockNumber) : 0n,
|
||||
} as const;
|
||||
};
|
||||
|
||||
const getActiveNodeWalletClients = async (
|
||||
runtime: HardhatRuntimeEnvironment,
|
||||
stakingAddress: `0x${string}`,
|
||||
stakingAbi: any,
|
||||
): Promise<WalletClient[]> => {
|
||||
const accounts = await runtime.viem.getWalletClients();
|
||||
// Filter to only those that are registered (firstBucket != 0)
|
||||
const publicClient = await runtime.viem.getPublicClient();
|
||||
const nodeClients: WalletClient[] = [];
|
||||
for (const client of accounts) {
|
||||
try {
|
||||
const rawNodeInfo = await publicClient.readContract({
|
||||
address: stakingAddress,
|
||||
abi: stakingAbi,
|
||||
functionName: "nodes",
|
||||
args: [client.account.address],
|
||||
});
|
||||
const node = normalizeNodeInfo(rawNodeInfo);
|
||||
if (node.firstBucket !== 0n && node.active) {
|
||||
nodeClients.push(client);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return nodeClients;
|
||||
};
|
||||
|
||||
const findNodeIndex = async (
|
||||
runtime: HardhatRuntimeEnvironment,
|
||||
stakingAddress: `0x${string}`,
|
||||
stakingAbi: any,
|
||||
nodeAddress: `0x${string}`,
|
||||
): Promise<number | null> => {
|
||||
const publicClient = await runtime.viem.getPublicClient();
|
||||
// Iterate indices until out-of-bounds revert
|
||||
try {
|
||||
const addresses = (await publicClient.readContract({
|
||||
address: stakingAddress,
|
||||
abi: stakingAbi,
|
||||
functionName: "getNodeAddresses",
|
||||
args: [],
|
||||
})) as `0x${string}`[];
|
||||
return addresses.findIndex(addr => addr.toLowerCase() === nodeAddress.toLowerCase());
|
||||
} catch {}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getReportIndexForNode = async (
|
||||
publicClient: Awaited<ReturnType<typeof hre.viem.getPublicClient>>,
|
||||
stakingAddress: `0x${string}`,
|
||||
stakingAbi: any,
|
||||
bucketNumber: bigint,
|
||||
nodeAddress: `0x${string}`,
|
||||
fromBlock: bigint,
|
||||
): Promise<number | null> => {
|
||||
try {
|
||||
const events = (await publicClient.getContractEvents({
|
||||
address: stakingAddress,
|
||||
abi: stakingAbi,
|
||||
eventName: "PriceReported",
|
||||
fromBlock,
|
||||
toBlock: "latest",
|
||||
})) as any[];
|
||||
const bucketEvents = events.filter((ev: any) => {
|
||||
const bucket = ev.args?.bucketNumber as bigint | undefined;
|
||||
return bucket !== undefined && bucket === bucketNumber;
|
||||
});
|
||||
const idx = bucketEvents.findIndex((ev: any) => {
|
||||
const reporter = (ev.args?.node as string | undefined) ?? "";
|
||||
return reporter.toLowerCase() === nodeAddress.toLowerCase();
|
||||
});
|
||||
return idx === -1 ? null : idx;
|
||||
} catch (error) {
|
||||
console.warn("Failed to compute report index:", (error as Error).message);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const runCycle = async (runtime: HardhatRuntimeEnvironment) => {
|
||||
try {
|
||||
const { address, abi, deployedBlock } = await getStakingOracleDeployment(runtime);
|
||||
const publicClient = await runtime.viem.getPublicClient();
|
||||
const allWalletClients = await runtime.viem.getWalletClients();
|
||||
const blockNumber = await publicClient.getBlockNumber();
|
||||
console.log(`\n[Block ${blockNumber}] Starting new oracle cycle...`);
|
||||
|
||||
// Read current bucket window and bucket number
|
||||
const [bucketWindow, currentBucket] = await Promise.all([
|
||||
publicClient
|
||||
.readContract({ address, abi, functionName: "BUCKET_WINDOW", args: [] })
|
||||
.then(value => BigInt(String(value))),
|
||||
publicClient
|
||||
.readContract({ address, abi, functionName: "getCurrentBucketNumber", args: [] })
|
||||
.then(value => BigInt(String(value))),
|
||||
]);
|
||||
const previousBucket = currentBucket > 0n ? currentBucket - 1n : 0n;
|
||||
console.log(`BUCKET_WINDOW=${bucketWindow} | currentBucket=${currentBucket}`);
|
||||
|
||||
// Update base price from previous bucket using the RECORDED MEDIAN (not an average of reports).
|
||||
// Fallback to contract's latest price, then to previous cached value.
|
||||
try {
|
||||
const previous = previousBucket;
|
||||
if (previous > 0n) {
|
||||
try {
|
||||
// `getPastPrice(bucket)` returns the recorded median for that bucket (0 if not recorded yet).
|
||||
const pastMedian = await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getPastPrice",
|
||||
args: [previous],
|
||||
});
|
||||
const median = BigInt(String(pastMedian));
|
||||
if (median > 0n) {
|
||||
currentPrice = median;
|
||||
}
|
||||
} catch {
|
||||
// ignore and fall back
|
||||
}
|
||||
|
||||
if (currentPrice === null) {
|
||||
// Fallback to on-chain latest average (previous bucket average)
|
||||
try {
|
||||
const onchain = await publicClient.readContract({ address, abi, functionName: "getLatestPrice", args: [] });
|
||||
currentPrice = BigInt(String(onchain));
|
||||
} catch {
|
||||
// keep prior currentPrice
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// keep prior currentPrice
|
||||
}
|
||||
|
||||
// Load config once per cycle so runtime edits to the config file are picked up
|
||||
const cfg = getConfig();
|
||||
|
||||
// 1) Reporting: each node only once per bucket
|
||||
const nodeWalletClients = await getActiveNodeWalletClients(runtime, address, abi);
|
||||
// Ensure we have an initial price (set once at startup in run())
|
||||
if (currentPrice === null) {
|
||||
currentPrice = await fetchPriceFromUniswap();
|
||||
}
|
||||
const reportTxHashes: `0x${string}`[] = [];
|
||||
for (const client of nodeWalletClients) {
|
||||
try {
|
||||
const rawNodeInfo = await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "nodes",
|
||||
args: [client.account.address],
|
||||
});
|
||||
const node = normalizeNodeInfo(rawNodeInfo);
|
||||
if (node.lastReportedBucket !== currentBucket) {
|
||||
// Determine node config (probability to skip and variance)
|
||||
const nodeCfg = cfg.NODE_CONFIGS[client.account.address.toLowerCase()] || cfg.NODE_CONFIGS.default;
|
||||
const skipProb = Number(nodeCfg.PROBABILITY_OF_SKIPPING_REPORT ?? 0);
|
||||
if (Math.random() < skipProb) {
|
||||
console.log(`Skipping report (by probability) for ${client.account.address}`);
|
||||
continue;
|
||||
}
|
||||
// Compute deviated price as integer math using parts-per-million (ppm)
|
||||
const variancePpm = Math.floor((Number(nodeCfg.PRICE_VARIANCE) || 0) * 1_000_000);
|
||||
const randomPpm = variancePpm > 0 ? Math.floor(Math.random() * (variancePpm * 2 + 1)) - variancePpm : 0;
|
||||
const basePrice = currentPrice!; // derived from previous bucket excluding outliers
|
||||
const delta = (basePrice * BigInt(randomPpm)) / 1_000_000n;
|
||||
const priceToReport = basePrice + delta;
|
||||
|
||||
console.log(
|
||||
`Reporting price for node ${client.account.address} in bucket ${currentBucket} (price=${priceToReport})...`,
|
||||
);
|
||||
const txHash = await client.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "reportPrice",
|
||||
args: [priceToReport],
|
||||
});
|
||||
reportTxHashes.push(txHash as `0x${string}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Skipping report for ${client.account.address}:`, (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for report transactions to be mined so subsequent reads (claiming) see the updated state.
|
||||
if (reportTxHashes.length > 0) {
|
||||
try {
|
||||
await Promise.all(reportTxHashes.map(hash => publicClient.waitForTransactionReceipt({ hash } as any)));
|
||||
} catch (err) {
|
||||
// If waiting fails, continue — claims will be attempted anyway but may not see the latest reports.
|
||||
console.warn("Error while waiting for report tx receipts:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Finalize median automatically when quorum is reached
|
||||
// You can only finalize buckets strictly in the past, so we finalize the *previous* bucket (current - 1).
|
||||
if (previousBucket > 0n) {
|
||||
let medianAlreadyRecorded = false;
|
||||
try {
|
||||
const median = await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getPastPrice",
|
||||
args: [previousBucket],
|
||||
});
|
||||
medianAlreadyRecorded = BigInt(String(median)) > 0n;
|
||||
} catch {
|
||||
medianAlreadyRecorded = false;
|
||||
}
|
||||
|
||||
if (!medianAlreadyRecorded) {
|
||||
try {
|
||||
const activeNodeAddresses = (await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getNodeAddresses",
|
||||
args: [],
|
||||
})) as `0x${string}`[];
|
||||
|
||||
const reportStatuses = await Promise.all(
|
||||
activeNodeAddresses.map(async nodeAddr => {
|
||||
try {
|
||||
const [price] = (await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getSlashedStatus",
|
||||
args: [nodeAddr, previousBucket],
|
||||
})) as [bigint, boolean];
|
||||
return price;
|
||||
} catch {
|
||||
return 0n;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const reportedCount = reportStatuses.reduce((acc, price) => acc + (price > 0n ? 1n : 0n), 0n);
|
||||
const requiredReports =
|
||||
activeNodeAddresses.length === 0 ? 0n : (2n * BigInt(activeNodeAddresses.length) + 2n) / 3n;
|
||||
|
||||
if (activeNodeAddresses.length === 0) {
|
||||
console.log("No active nodes; skipping recordBucketMedian evaluation.");
|
||||
} else if (reportedCount >= requiredReports) {
|
||||
const finalizer = allWalletClients[0];
|
||||
try {
|
||||
await finalizer.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "recordBucketMedian",
|
||||
args: [previousBucket],
|
||||
});
|
||||
console.log(
|
||||
`Recorded median for bucket ${previousBucket} (reports ${reportedCount}/${requiredReports}).`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to record median for bucket ${previousBucket}:`, (err as Error).message);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Skipping median recording for bucket ${previousBucket}; only ${reportedCount}/${requiredReports} reports.`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Unable to evaluate automatic recordBucketMedian:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Slashing: if previous bucket had outliers
|
||||
if (AUTO_SLASH) {
|
||||
try {
|
||||
const outliers = (await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getOutlierNodes",
|
||||
args: [previousBucket],
|
||||
})) as `0x${string}`[];
|
||||
|
||||
if (outliers.length > 0) {
|
||||
console.log(`Found ${outliers.length} outliers in bucket ${previousBucket}, attempting to slash...`);
|
||||
// Use the first wallet (deployer) to slash
|
||||
const slasher = allWalletClients[0];
|
||||
for (const nodeAddr of outliers) {
|
||||
const index = await findNodeIndex(runtime, address, abi, nodeAddr);
|
||||
if (index === null) {
|
||||
console.warn(`Index not found for node ${nodeAddr}, skipping slashing.`);
|
||||
continue;
|
||||
}
|
||||
const reportIndex = await getReportIndexForNode(
|
||||
publicClient,
|
||||
address,
|
||||
abi,
|
||||
previousBucket,
|
||||
nodeAddr,
|
||||
deployedBlock,
|
||||
);
|
||||
if (reportIndex === null) {
|
||||
console.warn(`Report index not found for node ${nodeAddr}, skipping slashing.`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await slasher.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "slashNode",
|
||||
args: [nodeAddr, previousBucket, BigInt(reportIndex), BigInt(index)],
|
||||
});
|
||||
console.log(
|
||||
`Slashed node ${nodeAddr} for bucket ${previousBucket} at indices report=${reportIndex}, node=${index}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to slash ${nodeAddr}:`, (err as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// getOutlierNodes may revert for small sample sizes (e.g., 0 or 1 report)
|
||||
console.log(`Skipping slashing check for bucket ${previousBucket}:`, (err as Error).message);
|
||||
}
|
||||
} else {
|
||||
// Auto-slash disabled by flag
|
||||
console.log(`Auto-slash disabled; skipping slashing for bucket ${previousBucket}`);
|
||||
}
|
||||
|
||||
// 4) Rewards: claim when there are unclaimed reports
|
||||
// Wait a couple seconds after reports have been mined before claiming
|
||||
console.log("Waiting 2s before claiming rewards...");
|
||||
await sleep(2000);
|
||||
for (const client of nodeWalletClients) {
|
||||
try {
|
||||
const rawNodeInfo = await publicClient.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "nodes",
|
||||
args: [client.account.address],
|
||||
});
|
||||
const node = normalizeNodeInfo(rawNodeInfo);
|
||||
if (node.reportCount > node.claimedReportCount) {
|
||||
await client.writeContract({ address, abi, functionName: "claimReward", args: [] });
|
||||
console.log(`Claimed rewards for ${client.account.address}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to claim rewards for ${client.account.address}:`, (err as Error).message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in oracle cycle:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
console.log("Starting oracle bot system...");
|
||||
// Fetch Uniswap price once at startup; subsequent cycles will base price on on-chain reports
|
||||
currentPrice = await fetchPriceFromUniswap();
|
||||
console.log(`Initial base price from Uniswap: ${currentPrice}`);
|
||||
|
||||
// Spin up nodes (fund + approve + register) for local testing if they aren't registered yet.
|
||||
try {
|
||||
const { address, abi } = await getStakingOracleDeployment(hre);
|
||||
const publicClient = await hre.viem.getPublicClient();
|
||||
const accounts = await hre.viem.getWalletClients();
|
||||
// Mirror deploy script: use accounts[1..10] as oracle nodes
|
||||
const nodeAccounts = accounts.slice(1, 11);
|
||||
const deployerClient = accounts[0];
|
||||
|
||||
const [minimumStake, oraTokenAddress] = await Promise.all([
|
||||
publicClient.readContract({ address, abi, functionName: "MINIMUM_STAKE", args: [] }).then(v => BigInt(String(v))),
|
||||
publicClient
|
||||
.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "oracleToken",
|
||||
args: [],
|
||||
})
|
||||
.then(v => v as unknown as `0x${string}`),
|
||||
]);
|
||||
|
||||
// Default bot stake for local simulations (keep it small so it matches the new UX expectations)
|
||||
const defaultStake = parseEther("500");
|
||||
const stakeAmount = minimumStake > defaultStake ? minimumStake : defaultStake;
|
||||
|
||||
// Build an idempotent setup plan based on current on-chain state (so restarts resume cleanly).
|
||||
const snapshots = await Promise.all(
|
||||
nodeAccounts.map(async nodeClient => {
|
||||
const nodeAddress = nodeClient.account.address;
|
||||
const [rawNodeInfo, balance, allowance] = await Promise.all([
|
||||
publicClient
|
||||
.readContract({ address, abi, functionName: "nodes", args: [nodeAddress] })
|
||||
.catch(() => null as any),
|
||||
publicClient.readContract({
|
||||
address: oraTokenAddress,
|
||||
abi: oraTokenAbi,
|
||||
functionName: "balanceOf",
|
||||
args: [nodeAddress],
|
||||
}) as Promise<bigint>,
|
||||
publicClient.readContract({
|
||||
address: oraTokenAddress,
|
||||
abi: oraTokenAbi,
|
||||
functionName: "allowance",
|
||||
args: [nodeAddress, address],
|
||||
}) as Promise<bigint>,
|
||||
]);
|
||||
|
||||
const node = normalizeNodeInfo(rawNodeInfo);
|
||||
const effectiveStake = node.active
|
||||
? await publicClient
|
||||
.readContract({ address, abi, functionName: "getEffectiveStake", args: [nodeAddress] })
|
||||
.then(v => BigInt(String(v)))
|
||||
.catch(() => 0n)
|
||||
: 0n;
|
||||
|
||||
return { nodeClient, nodeAddress, node, effectiveStake, balance, allowance };
|
||||
}),
|
||||
);
|
||||
|
||||
const transfers: { to: `0x${string}`; amount: bigint }[] = [];
|
||||
const perNodeActions: {
|
||||
nodeClient: WalletClient;
|
||||
nodeAddress: `0x${string}`;
|
||||
approveAmount: bigint;
|
||||
kind: "register" | "addStake" | "none";
|
||||
amount: bigint;
|
||||
note: string;
|
||||
}[] = [];
|
||||
|
||||
for (const snap of snapshots) {
|
||||
const { nodeClient, nodeAddress, node, effectiveStake, balance, allowance } = snap;
|
||||
|
||||
if (node.active) {
|
||||
if (effectiveStake < minimumStake) {
|
||||
const needed = minimumStake - effectiveStake;
|
||||
const transferAmount = balance < needed ? needed - balance : 0n;
|
||||
if (transferAmount > 0n) transfers.push({ to: nodeAddress, amount: transferAmount });
|
||||
|
||||
const approveAmount = allowance < needed ? needed : 0n;
|
||||
perNodeActions.push({
|
||||
nodeClient,
|
||||
nodeAddress,
|
||||
approveAmount,
|
||||
kind: "addStake",
|
||||
amount: needed,
|
||||
note: `top up effectiveStake=${effectiveStake} by ${needed}`,
|
||||
});
|
||||
} else {
|
||||
perNodeActions.push({
|
||||
nodeClient,
|
||||
nodeAddress,
|
||||
approveAmount: 0n,
|
||||
kind: "none",
|
||||
amount: 0n,
|
||||
note: "already active (no action)",
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inactive -> fund/approve/register. On restart, we only do the missing pieces.
|
||||
const transferAmount = balance < stakeAmount ? stakeAmount - balance : 0n;
|
||||
if (transferAmount > 0n) transfers.push({ to: nodeAddress, amount: transferAmount });
|
||||
|
||||
const approveAmount = allowance < stakeAmount ? stakeAmount : 0n;
|
||||
perNodeActions.push({
|
||||
nodeClient,
|
||||
nodeAddress,
|
||||
approveAmount,
|
||||
kind: "register",
|
||||
amount: stakeAmount,
|
||||
note: `register with stake=${stakeAmount}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Fund nodes in one burst from deployer using nonce chaining.
|
||||
if (transfers.length > 0) {
|
||||
const deployerNonce = await publicClient.getTransactionCount({ address: deployerClient.account.address });
|
||||
const transferTxs: `0x${string}`[] = [];
|
||||
console.log(`Funding ${transfers.length} node(s) from deployer (burst)...`);
|
||||
for (const [i, t] of transfers.entries()) {
|
||||
const tx = await deployerClient.writeContract({
|
||||
address: oraTokenAddress,
|
||||
abi: oraTokenAbi,
|
||||
functionName: "transfer",
|
||||
nonce: deployerNonce + i,
|
||||
args: [t.to, t.amount],
|
||||
});
|
||||
transferTxs.push(tx as `0x${string}`);
|
||||
}
|
||||
await Promise.all(transferTxs.map(hash => publicClient.waitForTransactionReceipt({ hash })));
|
||||
console.log("Funding burst mined.");
|
||||
}
|
||||
|
||||
// 2) For each node, chain approve -> (register|addStake) with explicit nonces, then wait for all receipts once.
|
||||
const nodeNonces = await Promise.all(
|
||||
perNodeActions.map(a => publicClient.getTransactionCount({ address: a.nodeAddress })),
|
||||
);
|
||||
const nodeTxs: `0x${string}`[] = [];
|
||||
|
||||
for (const [idx, action] of perNodeActions.entries()) {
|
||||
const { nodeClient, nodeAddress, approveAmount, kind, amount, note } = action;
|
||||
let nonce = nodeNonces[idx];
|
||||
|
||||
if (kind === "none") {
|
||||
console.log(`Node ${nodeAddress}: ${note}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Node ${nodeAddress}: ${note}`);
|
||||
|
||||
if (approveAmount > 0n) {
|
||||
const tx = await nodeClient.writeContract({
|
||||
address: oraTokenAddress,
|
||||
abi: oraTokenAbi,
|
||||
functionName: "approve",
|
||||
nonce,
|
||||
args: [address, approveAmount],
|
||||
});
|
||||
nodeTxs.push(tx as `0x${string}`);
|
||||
nonce += 1;
|
||||
}
|
||||
|
||||
if (kind === "register") {
|
||||
const tx = await nodeClient.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "registerNode",
|
||||
nonce,
|
||||
args: [amount],
|
||||
});
|
||||
nodeTxs.push(tx as `0x${string}`);
|
||||
} else if (kind === "addStake") {
|
||||
const tx = await nodeClient.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "addStake",
|
||||
nonce,
|
||||
args: [amount],
|
||||
});
|
||||
nodeTxs.push(tx as `0x${string}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeTxs.length > 0) {
|
||||
console.log(`Waiting for ${nodeTxs.length} node tx(s) to be mined...`);
|
||||
await Promise.all(nodeTxs.map(hash => publicClient.waitForTransactionReceipt({ hash })));
|
||||
console.log("Node setup txs mined.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Node registration step failed:", (err as Error).message);
|
||||
}
|
||||
while (true) {
|
||||
await runCycle(hre);
|
||||
await sleep(12000);
|
||||
}
|
||||
};
|
||||
|
||||
run().catch(error => {
|
||||
console.error("Fatal error in oracle bot system:", error);
|
||||
process.exit(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);
|
||||
});
|
||||
112
packages/hardhat/scripts/runWhitelistOracleBots.ts
Normal file
112
packages/hardhat/scripts/runWhitelistOracleBots.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { ethers } from "hardhat";
|
||||
import { WhitelistOracle } from "../typechain-types";
|
||||
import hre from "hardhat";
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import { fetchPriceFromUniswap } from "./fetchPriceFromUniswap";
|
||||
import { sleep } from "./utils";
|
||||
|
||||
async function getAllOracles() {
|
||||
const [deployer] = await ethers.getSigners();
|
||||
const whitelistContract = await ethers.getContract<WhitelistOracle>("WhitelistOracle", deployer.address);
|
||||
|
||||
const oracleAddresses = [];
|
||||
let index = 0;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const oracle = await whitelistContract.oracles(index);
|
||||
oracleAddresses.push(oracle);
|
||||
index++;
|
||||
}
|
||||
} catch {
|
||||
// When we hit an out-of-bounds error, we've found all oracles
|
||||
console.log(`Found ${oracleAddresses.length} oracles`);
|
||||
}
|
||||
|
||||
return oracleAddresses;
|
||||
}
|
||||
|
||||
function getRandomPrice(basePrice: bigint): bigint {
|
||||
const percentageShifts = [1, 2, 5, 7, 10, 15, 20];
|
||||
const randomIndex = Math.floor(Math.random() * percentageShifts.length);
|
||||
const percentage = BigInt(percentageShifts[randomIndex]);
|
||||
|
||||
const direction = Math.random() < 0.5 ? -1n : 1n;
|
||||
const offset = (basePrice * percentage * direction) / 100n;
|
||||
|
||||
return basePrice + offset;
|
||||
}
|
||||
|
||||
const runCycle = async (hre: HardhatRuntimeEnvironment, basePrice: bigint) => {
|
||||
try {
|
||||
const accounts = await hre.viem.getWalletClients();
|
||||
const simpleOracleFactory = await ethers.getContractFactory("SimpleOracle");
|
||||
const publicClient = await hre.viem.getPublicClient();
|
||||
|
||||
const blockNumber = await publicClient.getBlockNumber();
|
||||
console.log(`\n[Block ${blockNumber}] Starting new whitelist oracle cycle...`);
|
||||
const oracleAddresses = await getAllOracles();
|
||||
if (oracleAddresses.length === 0) {
|
||||
console.log("No oracles found");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const oracleAddress of oracleAddresses) {
|
||||
if (Math.random() < 0.4) {
|
||||
console.log(`Skipping oracle at ${oracleAddress}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const randomPrice = getRandomPrice(basePrice);
|
||||
console.log(`Setting price for oracle at ${oracleAddress} to ${randomPrice}`);
|
||||
|
||||
await accounts[0].writeContract({
|
||||
address: oracleAddress as `0x${string}`,
|
||||
abi: simpleOracleFactory.interface.fragments,
|
||||
functionName: "setPrice",
|
||||
args: [randomPrice],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in oracle cycle:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
async function run() {
|
||||
console.log("Starting whitelist oracle bots...");
|
||||
const basePrice = await fetchPriceFromUniswap();
|
||||
|
||||
while (true) {
|
||||
await runCycle(hre, basePrice);
|
||||
await sleep(4000);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
102
packages/hardhat/scripts/utils.ts
Normal file
102
packages/hardhat/scripts/utils.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Config } from "./oracle-bot/types";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const getConfigPath = (): string => {
|
||||
return path.join(__dirname, "oracle-bot", "config.json");
|
||||
};
|
||||
|
||||
export const getConfig = (): Config => {
|
||||
const configPath = getConfigPath();
|
||||
const configContent = fs.readFileSync(configPath, "utf-8");
|
||||
const config = JSON.parse(configContent) as Config;
|
||||
return config;
|
||||
};
|
||||
|
||||
export const updateConfig = (updates: Partial<Config>): void => {
|
||||
const configPath = getConfigPath();
|
||||
const currentConfig = getConfig();
|
||||
const updatedConfig = { ...currentConfig, ...updates };
|
||||
fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2));
|
||||
};
|
||||
|
||||
export const updatePriceCache = (price: number, timestamp: number): void => {
|
||||
updateConfig({
|
||||
PRICE: {
|
||||
CACHEDPRICE: price,
|
||||
TIMESTAMP: timestamp,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export const QUESTIONS_FOR_OO: string[] = [
|
||||
"Did ETH/USD exceed $3,000 at 00:00 UTC on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did the BTC/ETH ratio fall below 14 on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did Uniswap's TVL exceed $10B on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did the Ethereum Cancun upgrade activate before {MONTH} {DAY}, {YEAR}?",
|
||||
"Did the average gas price on Ethereum exceed 200 gwei on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did Ethereum's staking participation rate exceed 25% on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did Base chain have more than 1M daily transactions on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did the SEC approve a Bitcoin ETF before {MONTH} {DAY}, {YEAR}?",
|
||||
"Did OpenSea's trading volume exceed $500M in {MONTH} {YEAR}?",
|
||||
"Did Farcaster have more than 10K active users on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did ENS domains exceed 5M total registrations before {MONTH} {YEAR}?",
|
||||
"Did the total bridged USDC on Arbitrum exceed $2B on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did Optimism's native token OP increase above $1.50 on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did Aave v3 have higher borrow volume than v2 on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did Compound see more than 1,000 liquidations on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did BTC's 24-hour volume exceed $50B on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did Real Madrid win the UEFA Champions League Final in {YEAR}?",
|
||||
"Did G2 Esports win a major tournament in {MONTH} {YEAR}?",
|
||||
"Did the temperature in New York exceed 35°C on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did it rain more than 50mm in London on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did Tokyo experience an earthquake of magnitude 5.0 or higher in {MONTH} {YEAR}?",
|
||||
"Did the Nasdaq Composite fall more than 3% on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did the S&P 500 set a new all-time high on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did the US unemployment rate drop below 4% in {MONTH} {YEAR}?",
|
||||
"Did the average global temperature for {MONTH} {YEAR} exceed that of the previous year?",
|
||||
"Did gold price exceed $2,200/oz on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did YouTube's most viewed video gain more than 10M new views in {MONTH} {YEAR}?",
|
||||
"Did the population of India officially surpass China according to the UN in {YEAR}?",
|
||||
"Did the UEFA Euro 2024 Final have more than 80,000 attendees in the stadium?",
|
||||
"Did a pigeon successfully complete a 500km race in under 10 hours in {MONTH} {YEAR}?",
|
||||
"Did a goat attend a university graduation ceremony wearing a cap and gown on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did someone eat 100 chicken nuggets in under 10 minutes on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did a cat walk across a live TV weather report in {MONTH} {YEAR}?",
|
||||
"Did a cow escape from a farm and get caught on camera riding a water slide in {YEAR}?",
|
||||
"Did a man legally change his name to 'Bitcoin McMoneyface' on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did a squirrel steal a GoPro and film itself on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did someone cosplay as Shrek and complete a full marathon on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did a group of people attempt to cook the world's largest pancake using a flamethrower?",
|
||||
"Did a man propose using a pizza drone delivery on {MONTH} {DAY}, {YEAR}?",
|
||||
"Did a woman knit a sweater large enough to cover a school bus in {MONTH} {YEAR}?",
|
||||
"Did someone attempt to break the world record for most dad jokes told in 1 hour?",
|
||||
"Did an alpaca accidentally join a Zoom meeting for a tech startup on {MONTH} {DAY}, {YEAR}?",
|
||||
];
|
||||
|
||||
const generateRandomPastDate = (now: Date): Date => {
|
||||
const daysBack = Math.floor(Math.random() * 45) + 1; // 1 - 45 days
|
||||
|
||||
const pastDate = new Date(now);
|
||||
pastDate.setDate(pastDate.getDate() - daysBack);
|
||||
|
||||
return pastDate;
|
||||
};
|
||||
|
||||
const replaceDatePlaceholders = (question: string): string => {
|
||||
const now = new Date();
|
||||
const past = generateRandomPastDate(now);
|
||||
|
||||
return question
|
||||
.replace(/\{DAY\}/g, past.getDate().toString())
|
||||
.replace(/\{MONTH\}/g, past.toLocaleDateString("en-US", { month: "long" }))
|
||||
.replace(/\{YEAR\}/g, past.getFullYear().toString());
|
||||
};
|
||||
|
||||
export const getRandomQuestion = (): string => {
|
||||
const randomIndex = Math.floor(Math.random() * QUESTIONS_FOR_OO.length);
|
||||
const question = QUESTIONS_FOR_OO[randomIndex];
|
||||
return replaceDatePlaceholders(question);
|
||||
};
|
||||
Reference in New Issue
Block a user