diff --git a/packages/llm-bot/hardhat.config.ts b/packages/llm-bot/hardhat.config.ts new file mode 100644 index 0000000000..90ed556d5c --- /dev/null +++ b/packages/llm-bot/hardhat.config.ts @@ -0,0 +1,22 @@ +import { getHardhatConfig } from "@uma/common"; +import path from "path"; + +// Hardhat plugins used in monitor-v2 package tests. +import "@nomiclabs/hardhat-ethers"; +import "@nomiclabs/hardhat-waffle"; +import "hardhat-deploy"; + +const coreWkdir = path.dirname(require.resolve("@uma/core/package.json")); +const packageWkdir = path.dirname(require.resolve("./package.json")); + +const configOverride = { + paths: { + root: coreWkdir, + sources: `${coreWkdir}/contracts`, + artifacts: `${coreWkdir}/artifacts`, + cache: `${coreWkdir}/cache`, + tests: `${packageWkdir}/test`, + }, +}; + +export default getHardhatConfig(configOverride, coreWkdir); diff --git a/packages/llm-bot/package.json b/packages/llm-bot/package.json new file mode 100644 index 0000000000..fe69e69cd5 --- /dev/null +++ b/packages/llm-bot/package.json @@ -0,0 +1,30 @@ +{ + "name": "@uma/llm-bot", + "version": "1.0.0", + "description": "LLM bots for UMA", + "author": "UMA Team", + "license": "AGPL-3.0-only", + "scripts": { + "build": "tsc --build", + "test": "hardhat test" + }, + "dependencies": { + "@ethersproject/abstract-provider": "^5.4.0", + "@uma/common": "^2.34.0", + "@uma/contracts-node": "^0.4.17", + "@uma/financial-templates-lib": "^2.33.0", + "async-retry": "^1.3.3", + "ethers": "^5.4.2" + }, + "devDependencies": { + "@nomicfoundation/hardhat-network-helpers": "^1.0.8", + "@nomiclabs/hardhat-waffle": "^2.0.5", + "chai": "^4.3.7", + "ethereum-waffle": "^4.0.10", + "sinon": "^15.0.1" + }, + "publishConfig": { + "registry": "https://registry.npmjs.com/", + "access": "public" + } +} diff --git a/packages/monitor-v2/src/llm-bot/OptimisticOracleV2.ts b/packages/llm-bot/src/core/OptimisticOracleV2.ts similarity index 100% rename from packages/monitor-v2/src/llm-bot/OptimisticOracleV2.ts rename to packages/llm-bot/src/core/OptimisticOracleV2.ts diff --git a/packages/monitor-v2/src/llm-bot/common.ts b/packages/llm-bot/src/core/common.ts similarity index 100% rename from packages/monitor-v2/src/llm-bot/common.ts rename to packages/llm-bot/src/core/common.ts diff --git a/packages/monitor-v2/src/llm-bot/examples.ts b/packages/llm-bot/src/core/examples.ts similarity index 100% rename from packages/monitor-v2/src/llm-bot/examples.ts rename to packages/llm-bot/src/core/examples.ts diff --git a/packages/llm-bot/src/utils/constants.ts b/packages/llm-bot/src/utils/constants.ts new file mode 100644 index 0000000000..b695804c9e --- /dev/null +++ b/packages/llm-bot/src/utils/constants.ts @@ -0,0 +1,41 @@ +interface BlockConfig { + oneHour: number; + maxBlockLookBack: number; +} + +/** + * Default configuration for different blockchain networks. + * Each network is identified by its chain ID. + */ +export const blockDefaults: Record = { + "1": { + // Mainnet configuration + oneHour: 300, // Approximate number of blocks mined in one hour (12 seconds per block) + maxBlockLookBack: 20000, // Maximum number of blocks to look back for events + }, + "137": { + // Polygon (Matic) configuration + oneHour: 1800, // Approximate number of blocks mined in one hour (2 seconds per block) + maxBlockLookBack: 3499, // Maximum number of blocks to look back for events + }, + "10": { + // Optimism configuration + oneHour: 1800, // Approximate number of blocks mined in one hour (2 seconds per block) + maxBlockLookBack: 10000, // Maximum number of blocks to look back for events + }, + "42161": { + // Arbitrum configuration + oneHour: 240, // Approximate number of blocks mined in one hour (15 seconds per block) + maxBlockLookBack: 10000, // Maximum number of blocks to look back for events + }, + "43114": { + // Avalanche configuration + oneHour: 1800, // Approximate number of blocks mined in one hour (2 seconds per block) + maxBlockLookBack: 2000, // Maximum number of blocks to look back for events + }, + other: { + // Default configuration for other networks + oneHour: 240, // Approximate number of blocks mined in one hour (15 seconds per block) + maxBlockLookBack: 1000, // Maximum number of blocks to look back for events + }, +}; diff --git a/packages/llm-bot/src/utils/contracts.ts b/packages/llm-bot/src/utils/contracts.ts new file mode 100644 index 0000000000..c8732ab892 --- /dev/null +++ b/packages/llm-bot/src/utils/contracts.ts @@ -0,0 +1,60 @@ +import { ContractName, ERC20Ethers, getAbi, getAddress } from "@uma/contracts-node"; +import { Contract } from "ethers"; +import { Provider } from "@ethersproject/abstract-provider"; +import { utils } from "ethers"; + +export const getContractInstanceWithProvider = async ( + contractName: ContractName, + provider: Provider, + address?: string +): Promise => { + const networkId = (await provider.getNetwork()).chainId; + const contractAddress = address || (await getAddress(contractName, networkId)); + const contractAbi = getAbi(contractName); + return new Contract(contractAddress, contractAbi, provider) as T; +}; + +export const tryHexToUtf8String = (ancillaryData: string): string => { + try { + return utils.toUtf8String(ancillaryData); + } catch (err) { + return ancillaryData; + } +}; + +export const getCurrencyDecimals = async (provider: Provider, currencyAddress: string): Promise => { + const currencyContract = await getContractInstanceWithProvider("ERC20", provider, currencyAddress); + try { + return await currencyContract.decimals(); + } catch (err) { + return 18; + } +}; + +export const getCurrencySymbol = async (provider: Provider, currencyAddress: string): Promise => { + const currencyContract = await getContractInstanceWithProvider("ERC20", provider, currencyAddress); + try { + return await currencyContract.symbol(); + } catch (err) { + // Try to get the symbol as bytes32 (e.g. MKR uses this). + try { + const bytes32SymbolIface = new utils.Interface(["function symbol() view returns (bytes32 symbol)"]); + const bytes32Symbol = await provider.call({ + to: currencyAddress, + data: bytes32SymbolIface.encodeFunctionData("symbol"), + }); + return utils.parseBytes32String(bytes32SymbolIface.decodeFunctionResult("symbol", bytes32Symbol).symbol); + } catch (err) { + return ""; + } + } +}; + +// Gets the topic of an event from its name. In case of overloaded events, the first one found is returned. +export const getEventTopic = (contractName: ContractName, eventName: string): string => { + const contractAbi = getAbi(contractName); + const iface = new utils.Interface(contractAbi); + const eventKey = Object.keys(iface.events).find((key) => iface.events[key].name === eventName); + if (!eventKey) throw new Error(`Event ${eventName} not found in contract ${contractName}`); + return utils.keccak256(utils.toUtf8Bytes(eventKey)); +}; diff --git a/packages/monitor-v2/test/OptimisticOracleV2LLM.ts b/packages/llm-bot/test/OptimisticOracleV2LLM.ts similarity index 99% rename from packages/monitor-v2/test/OptimisticOracleV2LLM.ts rename to packages/llm-bot/test/OptimisticOracleV2LLM.ts index 999e640405..f34af61205 100644 --- a/packages/monitor-v2/test/OptimisticOracleV2LLM.ts +++ b/packages/llm-bot/test/OptimisticOracleV2LLM.ts @@ -5,7 +5,7 @@ import { DisputerStrategy, OptimisticOracleClientV2, OptimisticOracleClientV2FilterDisputeable, -} from "../src/llm-bot/OptimisticOracleV2"; +} from "../src/core/OptimisticOracleV2"; import { defaultOptimisticOracleV2Identifier } from "./constants"; import { optimisticOracleV2Fixture } from "./fixtures/OptimisticOracleV2.Fixture"; import { Signer, hre, toUtf8Bytes } from "./utils"; diff --git a/packages/llm-bot/test/constants.ts b/packages/llm-bot/test/constants.ts new file mode 100644 index 0000000000..eeccfc93e5 --- /dev/null +++ b/packages/llm-bot/test/constants.ts @@ -0,0 +1,24 @@ +import { formatBytes32String, parseUnits } from "./utils"; + +// Constants for DVM2.0. +export const baseSlashAmount = parseUnits("0.001", "ether"); +export const governanceSlashAmount = parseUnits("0", "ether"); +export const emissionRate = parseUnits("0.18", "ether"); +export const unstakeCooldown = 60 * 60 * 24 * 7; // 7 days +export const phaseLength = 60 * 60 * 24; // 1 day +export const gat = parseUnits("5000000", "ether"); +export const spat = parseUnits("0.5", "ether"); +export const maxRolls = 4; +export const maxRequestsPerRound = 1000; +export const minimumWaitTime = 60 * 60 * 24 * 10; // 10 days +export const governorStartingId = 0; +export const governanceProposalBond = parseUnits("5000", "ether"); +export const emergencyQuorum = parseUnits("5000000", "ether"); +export const totalSupply = parseUnits("100000000", "ether"); + +// Constants for Optimistic Oracle V3. +export const defaultLiveness = 7200; +export const defaultCurrency = { name: "Bond", symbol: "BOND", decimals: 18, finalFee: parseUnits("100") }; +export const defaultOptimisticOracleV3Identifier = formatBytes32String("ASSERT_TRUTH"); +export const defaultOptimisticOracleV2Identifier = formatBytes32String("YES_OR_NO_QUERY"); +export const zeroRawValue = { rawValue: "0" }; diff --git a/packages/llm-bot/test/fixtures/DVM2.Fixture.ts b/packages/llm-bot/test/fixtures/DVM2.Fixture.ts new file mode 100644 index 0000000000..b1cdb0c9ff --- /dev/null +++ b/packages/llm-bot/test/fixtures/DVM2.Fixture.ts @@ -0,0 +1,113 @@ +import { addGlobalHardhatTestingAddress, ZERO_ADDRESS } from "@uma/common"; +import { + EmergencyProposerEthers, + FixedSlashSlashingLibraryEthers, + GovernorV2Ethers, + ProposerV2Ethers, + VotingV2Ethers, +} from "@uma/contracts-node"; +import { umaEcosystemFixture } from "./UmaEcosystem.Fixture"; +import { + baseSlashAmount, + emergencyQuorum, + emissionRate, + gat, + governanceProposalBond, + governanceSlashAmount, + governorStartingId, + maxRequestsPerRound, + maxRolls, + minimumWaitTime, + phaseLength, + spat, + totalSupply, + unstakeCooldown, +} from "../constants"; +import { formatBytes32String, getContractFactory, hre, Signer } from "../utils"; + +export interface DVM2Contracts { + votingV2: VotingV2Ethers; + governorV2: GovernorV2Ethers; + proposerV2: ProposerV2Ethers; + emergencyProposer: EmergencyProposerEthers; +} + +export const dvm2Fixture = hre.deployments.createFixture( + async ({ ethers }): Promise => { + return await deployDVM2(ethers); + } +); + +export const deployDVM2 = hre.deployments.createFixture( + async ({ ethers }): Promise => { + // Signer from ethers and hardhat-ethers are not version compatible. + const [deployer] = (await ethers.getSigners()) as Signer[]; + const deployerAddress = await deployer.getAddress(); + + // This fixture is dependent on the UMA ecosystem fixture. Run it first and grab the output. This is used in the + // deployments that follows. + const parentFixture = await umaEcosystemFixture(); + + // Deploy slashing library. + const slashingLibrary = (await (await getContractFactory("FixedSlashSlashingLibrary", deployer)).deploy( + baseSlashAmount, + governanceSlashAmount + )) as FixedSlashSlashingLibraryEthers; + + // Deploying VotingV2 contract requires minting voting tokens for GAT validation. + await parentFixture.votingToken.addMinter(deployerAddress); + await parentFixture.votingToken.mint(deployerAddress, totalSupply); + const votingV2 = (await (await getContractFactory("VotingV2", deployer)).deploy( + emissionRate, + unstakeCooldown, + phaseLength, + maxRolls, + maxRequestsPerRound, + gat, + spat, + parentFixture.votingToken.address, + parentFixture.finder.address, + slashingLibrary.address, + ZERO_ADDRESS + )) as VotingV2Ethers; + + // Deploy GovernorV2 contract. + const governorV2 = (await (await getContractFactory("GovernorV2", deployer)).deploy( + parentFixture.finder.address, + governorStartingId + )) as GovernorV2Ethers; + + // Deploy ProposerV2 contract. + const proposerV2 = (await (await getContractFactory("ProposerV2", deployer)).deploy( + parentFixture.votingToken.address, + governanceProposalBond, + governorV2.address, + parentFixture.finder.address + )) as ProposerV2Ethers; + + // Deploy EmergencyProposer contract. + const emergencyProposer = (await (await getContractFactory("EmergencyProposer", deployer)).deploy( + parentFixture.votingToken.address, + emergencyQuorum, + governorV2.address, + await deployer.getAddress(), + minimumWaitTime + )) as EmergencyProposerEthers; + + // Configure GovernorV2 contract. + await governorV2.resetMember(1, proposerV2.address); + await governorV2.resetMember(2, emergencyProposer.address); + + // Transfer VotingV2 ownership and register it as Oracle with the Finder. + await votingV2.transferOwnership(governorV2.address); + await parentFixture.finder.changeImplementationAddress(formatBytes32String("Oracle"), votingV2.address); + + // Add contracts to global hardhatTestingAddresses. + addGlobalHardhatTestingAddress("VotingV2", votingV2.address); + addGlobalHardhatTestingAddress("GovernorV2", governorV2.address); + addGlobalHardhatTestingAddress("ProposerV2", proposerV2.address); + addGlobalHardhatTestingAddress("EmergencyProposer", emergencyProposer.address); + + return { votingV2, governorV2, proposerV2, emergencyProposer }; + } +); diff --git a/packages/llm-bot/test/fixtures/OptimisticGovernor.Fixture.ts b/packages/llm-bot/test/fixtures/OptimisticGovernor.Fixture.ts new file mode 100644 index 0000000000..df0c9840d4 --- /dev/null +++ b/packages/llm-bot/test/fixtures/OptimisticGovernor.Fixture.ts @@ -0,0 +1,99 @@ +import { addGlobalHardhatTestingAddress } from "@uma/common"; +import { + ExpandedERC20Ethers, + ModuleProxyFactoryEthers, + OptimisticGovernorEthers, + OptimisticGovernorTestEthers, + OptimisticOracleV3Ethers, + TestAvatarEthers, +} from "@uma/contracts-node"; +import { umaEcosystemFixture } from "./UmaEcosystem.Fixture"; +import { defaultCurrency, defaultLiveness, defaultOptimisticOracleV3Identifier } from "../constants"; +import { formatBytes32String, getContractFactory, hre, parseEther, Signer } from "../utils"; + +export interface OptimisticGovernorContracts { + bondToken: ExpandedERC20Ethers; + optimisticOracleV3: OptimisticOracleV3Ethers; + optimisticGovernor: OptimisticGovernorEthers; + avatar: TestAvatarEthers; + moduleProxyFactory: ModuleProxyFactoryEthers; +} + +export const optimisticGovernorFixture = hre.deployments.createFixture( + async ({ ethers }): Promise => { + return await deployOptimisticGovernor(ethers); + } +); + +export const deployOptimisticGovernor = hre.deployments.createFixture( + async ({ ethers }): Promise => { + // Signer from ethers and hardhat-ethers are not version compatible. + const [deployer] = (await ethers.getSigners()) as Signer[]; + + // This fixture is dependent on the UMA ecosystem fixture. Run it first and grab the output. This is used in the + // deployments that follows. + const parentFixture = await umaEcosystemFixture(); + + // Deploy default currency for Optimistic Oracle V3. + const bondToken = (await (await getContractFactory("ExpandedERC20", deployer)).deploy( + defaultCurrency.name, + defaultCurrency.symbol, + defaultCurrency.decimals + )) as ExpandedERC20Ethers; + + // Sets default currency as approved in the UMA collateralWhitelist. + await parentFixture.collateralWhitelist.addToWhitelist(bondToken.address); + + // Sets finalFee for default currency. + await parentFixture.store.setFinalFee(bondToken.address, { rawValue: defaultCurrency.finalFee }); + + // Adds default identifier for Optimistic Oracle V3 to the UMA identifierWhitelist. + await parentFixture.identifierWhitelist.addSupportedIdentifier(defaultOptimisticOracleV3Identifier); + + // Deploy Optimistic Oracle V3 and add it to global hardhatTestingAddresses. + const optimisticOracleV3 = (await (await getContractFactory("OptimisticOracleV3Test", deployer)).deploy( + parentFixture.finder.address, + bondToken.address, + defaultLiveness, + parentFixture.timer.address + )) as OptimisticOracleV3Ethers; + + await parentFixture.finder.changeImplementationAddress( + formatBytes32String("OptimisticOracleV3"), + optimisticOracleV3.address + ); + + addGlobalHardhatTestingAddress("OptimisticOracleV3", optimisticOracleV3.address); + + const avatar = (await (await getContractFactory("TestAvatar", deployer)).deploy()) as TestAvatarEthers; + + // Deploy Optimistic Governor and add it to global hardhatTestingAddresses. + const rules = "https://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi.ipfs.dweb.link/"; + const identifier = formatBytes32String("ZODIAC"); + const identifierTest = formatBytes32String("TEST"); + + await parentFixture.identifierWhitelist.addSupportedIdentifier(identifier); + await parentFixture.identifierWhitelist.addSupportedIdentifier(identifierTest); + + const liveness = 7200; + const optimisticGovernor = (await (await getContractFactory("OptimisticGovernorTest", deployer)).deploy( + parentFixture.finder.address, + avatar.address, + bondToken.address, + parseEther("500"), + rules, + identifier, + liveness, + parentFixture.timer.address + )) as OptimisticGovernorTestEthers; + + await avatar.setModule(optimisticGovernor.address); + + // Deploy ModuleProxyFactory. + const moduleProxyFactory = (await ( + await getContractFactory("ModuleProxyFactory", deployer) + ).deploy()) as ModuleProxyFactoryEthers; + + return { bondToken, optimisticOracleV3, optimisticGovernor, avatar, moduleProxyFactory }; + } +); diff --git a/packages/llm-bot/test/fixtures/OptimisticOracleV2.Fixture.ts b/packages/llm-bot/test/fixtures/OptimisticOracleV2.Fixture.ts new file mode 100644 index 0000000000..0f8d481a67 --- /dev/null +++ b/packages/llm-bot/test/fixtures/OptimisticOracleV2.Fixture.ts @@ -0,0 +1,54 @@ +import { addGlobalHardhatTestingAddress } from "@uma/common"; +import { ExpandedERC20Ethers, OptimisticOracleV2Ethers } from "@uma/contracts-node"; +import { umaEcosystemFixture } from "./UmaEcosystem.Fixture"; +import { defaultCurrency, defaultLiveness, defaultOptimisticOracleV2Identifier } from "../constants"; +import { getContractFactory, hre, Signer } from "../utils"; + +export interface OptimisticOracleV2Contracts { + bondToken: ExpandedERC20Ethers; + optimisticOracleV2: OptimisticOracleV2Ethers; +} + +export const optimisticOracleV2Fixture = hre.deployments.createFixture( + async ({ ethers }): Promise => { + return await deployOptimisticOracleV2(ethers); + } +); + +export const deployOptimisticOracleV2 = hre.deployments.createFixture( + async ({ ethers }): Promise => { + // Signer from ethers and hardhat-ethers are not version compatible. + const [deployer] = (await ethers.getSigners()) as Signer[]; + + // This fixture is dependent on the UMA ecosystem fixture. Run it first and grab the output. This is used in the + // deployments that follows. + const parentFixture = await umaEcosystemFixture(); + + // Deploy default currency for Optimistic Oracle V3. + const bondToken = (await (await getContractFactory("ExpandedERC20", deployer)).deploy( + defaultCurrency.name, + defaultCurrency.symbol, + defaultCurrency.decimals + )) as ExpandedERC20Ethers; + + // Sets default currency as approved in the UMA collateralWhitelist. + await parentFixture.collateralWhitelist.addToWhitelist(bondToken.address); + + // Sets finalFee for default currency. + await parentFixture.store.setFinalFee(bondToken.address, { rawValue: defaultCurrency.finalFee }); + + // Adds default identifier for Optimistic Oracle V2 to the UMA identifierWhitelist. + await parentFixture.identifierWhitelist.addSupportedIdentifier(defaultOptimisticOracleV2Identifier); + + // Deploy Optimistic Oracle V2 and add it to global hardhatTestingAddresses. + const optimisticOracleV2 = (await (await getContractFactory("OptimisticOracleV2", deployer)).deploy( + defaultLiveness, + parentFixture.finder.address, + parentFixture.timer.address + )) as OptimisticOracleV2Ethers; + + addGlobalHardhatTestingAddress("OptimisticOracleV2", optimisticOracleV2.address); + + return { bondToken, optimisticOracleV2 }; + } +); diff --git a/packages/llm-bot/test/fixtures/OptimisticOracleV3.Fixture.ts b/packages/llm-bot/test/fixtures/OptimisticOracleV3.Fixture.ts new file mode 100644 index 0000000000..feda3744c7 --- /dev/null +++ b/packages/llm-bot/test/fixtures/OptimisticOracleV3.Fixture.ts @@ -0,0 +1,53 @@ +import { addGlobalHardhatTestingAddress } from "@uma/common"; +import { ExpandedERC20Ethers, OptimisticOracleV3Ethers } from "@uma/contracts-node"; +import { umaEcosystemFixture } from "./UmaEcosystem.Fixture"; +import { defaultCurrency, defaultLiveness, defaultOptimisticOracleV3Identifier } from "../constants"; +import { getContractFactory, hre, Signer } from "../utils"; + +export interface OptimisticOracleV3Contracts { + bondToken: ExpandedERC20Ethers; + optimisticOracleV3: OptimisticOracleV3Ethers; +} + +export const optimisticOracleV3Fixture = hre.deployments.createFixture( + async ({ ethers }): Promise => { + return await deployOptimisticOracleV3(ethers); + } +); + +export const deployOptimisticOracleV3 = hre.deployments.createFixture( + async ({ ethers }): Promise => { + // Signer from ethers and hardhat-ethers are not version compatible. + const [deployer] = (await ethers.getSigners()) as Signer[]; + + // This fixture is dependent on the UMA ecosystem fixture. Run it first and grab the output. This is used in the + // deployments that follows. + const parentFixture = await umaEcosystemFixture(); + + // Deploy default currency for Optimistic Oracle V3. + const bondToken = (await (await getContractFactory("ExpandedERC20", deployer)).deploy( + defaultCurrency.name, + defaultCurrency.symbol, + defaultCurrency.decimals + )) as ExpandedERC20Ethers; + + // Sets default currency as approved in the UMA collateralWhitelist. + await parentFixture.collateralWhitelist.addToWhitelist(bondToken.address); + + // Sets finalFee for default currency. + await parentFixture.store.setFinalFee(bondToken.address, { rawValue: defaultCurrency.finalFee }); + + // Adds default identifier for Optimistic Oracle V3 to the UMA identifierWhitelist. + await parentFixture.identifierWhitelist.addSupportedIdentifier(defaultOptimisticOracleV3Identifier); + + // Deploy Optimistic Oracle V3 and add it to global hardhatTestingAddresses. + const optimisticOracleV3 = (await (await getContractFactory("OptimisticOracleV3", deployer)).deploy( + parentFixture.finder.address, + bondToken.address, + defaultLiveness + )) as OptimisticOracleV3Ethers; + addGlobalHardhatTestingAddress("OptimisticOracleV3", optimisticOracleV3.address); + + return { bondToken, optimisticOracleV3 }; + } +); diff --git a/packages/llm-bot/test/fixtures/UmaEcosystem.Fixture.ts b/packages/llm-bot/test/fixtures/UmaEcosystem.Fixture.ts new file mode 100644 index 0000000000..59fb8e3489 --- /dev/null +++ b/packages/llm-bot/test/fixtures/UmaEcosystem.Fixture.ts @@ -0,0 +1,79 @@ +import { addGlobalHardhatTestingAddress, ZERO_ADDRESS } from "@uma/common"; +import { + AddressWhitelistEthers, + FinderEthers, + IdentifierWhitelistEthers, + MockOracleAncillaryEthers, + RegistryEthers, + StoreEthers, + TimerEthers, + VotingTokenEthers, +} from "@uma/contracts-node"; +import { zeroRawValue } from "../constants"; +import { formatBytes32String, getContractFactory, hre, Signer } from "../utils"; +import { utils } from "ethers"; + +export interface UmaEcosystemContracts { + finder: FinderEthers; + collateralWhitelist: AddressWhitelistEthers; + identifierWhitelist: IdentifierWhitelistEthers; + registry: RegistryEthers; + store: StoreEthers; + votingToken: VotingTokenEthers; + mockOracle: MockOracleAncillaryEthers; + timer: TimerEthers; +} + +export const umaEcosystemFixture = hre.deployments.createFixture( + async ({ ethers }): Promise => { + // Signer from ethers and hardhat-ethers are not version compatible. + const [deployer] = (await ethers.getSigners()) as Signer[]; + + // Deploy the UMA ecosystem contracts with Mock Oracle. + const finder = (await (await getContractFactory("Finder", deployer)).deploy()) as FinderEthers; + const collateralWhitelist = (await ( + await getContractFactory("AddressWhitelist", deployer) + ).deploy()) as AddressWhitelistEthers; + const identifierWhitelist = (await ( + await getContractFactory("IdentifierWhitelist", deployer) + ).deploy()) as IdentifierWhitelistEthers; + const registry = (await (await getContractFactory("Registry", deployer)).deploy()) as RegistryEthers; + const store = (await (await getContractFactory("Store", deployer)).deploy( + zeroRawValue, + zeroRawValue, + ZERO_ADDRESS + )) as StoreEthers; + const votingToken = (await (await getContractFactory("VotingToken", deployer)).deploy()) as VotingTokenEthers; + const mockOracle = (await (await getContractFactory("MockOracleAncillary", deployer)).deploy( + finder.address, + ZERO_ADDRESS + )) as MockOracleAncillaryEthers; + + const timer = (await (await getContractFactory("Timer", deployer)).deploy()) as TimerEthers; + + // Register the UMA ecosystem contracts with the Finder. + await finder.changeImplementationAddress(formatBytes32String("Store"), store.address); + await finder.changeImplementationAddress(formatBytes32String("Registry"), registry.address); + await finder.changeImplementationAddress(formatBytes32String("CollateralWhitelist"), collateralWhitelist.address); + await finder.changeImplementationAddress(formatBytes32String("IdentifierWhitelist"), identifierWhitelist.address); + await finder.changeImplementationAddress(formatBytes32String("Oracle"), mockOracle.address); + + await collateralWhitelist.whitelist(votingToken.address); + + await store.setFinalFee(votingToken.address, { rawValue: utils.parseEther("1.0") }); + // Add voting token to global hardhatTestingAddresses. + addGlobalHardhatTestingAddress("VotingToken", votingToken.address); + addGlobalHardhatTestingAddress("Store", store.address); + + return { + finder, + collateralWhitelist, + identifierWhitelist, + registry, + store, + votingToken, + mockOracle, + timer, + }; + } +); diff --git a/packages/llm-bot/test/utils.ts b/packages/llm-bot/test/utils.ts new file mode 100644 index 0000000000..fd546e5062 --- /dev/null +++ b/packages/llm-bot/test/utils.ts @@ -0,0 +1,46 @@ +import { Provider } from "@ethersproject/providers"; +import { time as hardhatTime } from "@nomicfoundation/hardhat-network-helpers"; +import { VotePhasesEnum } from "@uma/common"; +import { ContractName, getAbi, getBytecode, VotingV2Ethers } from "@uma/contracts-node"; +import { ContractFactory, ContractTransaction, Signer, utils } from "ethers"; +import hre from "hardhat"; + +export async function getContractFactory(contractName: ContractName, signer?: Signer): Promise { + const contractAbi = getAbi(contractName); + const contractBytecode = getBytecode(contractName); + return new ContractFactory(contractAbi, contractBytecode, signer); +} + +// Get block number from transaction (or 0 if transaction is not mined). +export const getBlockNumberFromTx = async (tx: ContractTransaction): Promise => { + await tx.wait(); + return tx.blockNumber !== undefined ? tx.blockNumber : 0; +}; + +// Moves the voting contract to the first phase of the next round. +export const moveToNextRound = async (voting: VotingV2Ethers): Promise => { + const phase = await voting.getVotePhase(); + const phaseLength = await voting.voteTiming(); + + let timeIncrement; + if (phase.toString() === VotePhasesEnum.COMMIT) { + // Commit phase, so it will take 2 days to move to the next round. + timeIncrement = phaseLength.mul(2); + } else { + // Reveal phase, so it will take 1 day to move to the next round. + timeIncrement = phaseLength; + } + await hardhatTime.increase(timeIncrement); +}; + +export const moveToNextPhase = async (voting: VotingV2Ethers): Promise => { + const phaseLength = await voting.voteTiming(); + const currentRoundId = await voting.getCurrentRoundId(); + const roundEndTime = await voting.getRoundEndTime(currentRoundId); + + await hardhatTime.setNextBlockTimestamp(roundEndTime.sub(phaseLength)); +}; + +export const { formatBytes32String, parseBytes32String, parseUnits, parseEther, toUtf8Bytes, toUtf8String } = utils; + +export { ContractTransaction, hardhatTime, hre, Provider, Signer }; diff --git a/packages/llm-bot/tsconfig.json b/packages/llm-bot/tsconfig.json new file mode 100644 index 0000000000..d5ee104d70 --- /dev/null +++ b/packages/llm-bot/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "commonjs", + "resolveJsonModule": true, + "declaration": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["./src"] +} diff --git a/packages/monitor-v2/test/OptimisticOracleV2Client.ts b/packages/monitor-v2/test/OptimisticOracleV2Client.ts deleted file mode 100644 index acd8aba79b..0000000000 --- a/packages/monitor-v2/test/OptimisticOracleV2Client.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ExpandedERC20Ethers, OptimisticOracleV2Ethers } from "@uma/contracts-node"; -import { assert } from "chai"; -import { OptimisticOracleClientV2 } from "../src/llm-bot/OptimisticOracleV2"; -import { defaultOptimisticOracleV2Identifier } from "./constants"; -import { optimisticOracleV2Fixture } from "./fixtures/OptimisticOracleV2.Fixture"; -import { Signer, hre, toUtf8Bytes } from "./utils"; - -const ethers = hre.ethers; - -describe("OptimisticOracleV2Client", function () { - let bondToken: ExpandedERC20Ethers; - let optimisticOracleV2: OptimisticOracleV2Ethers; - let requester: Signer; - let proposer: Signer; - let disputer: Signer; - let oov2Client: OptimisticOracleClientV2; - - const bond = ethers.utils.parseEther("1000"); - - const question = "This is just a test question"; - const ancillaryData = toUtf8Bytes(question); - - beforeEach(async function () { - // Signer from ethers and hardhat-ethers are not version compatible, thus, we cannot use the SignerWithAddress. - [requester, proposer, disputer] = (await ethers.getSigners()) as Signer[]; - - // Get contract instances. - const optimisticOracleV2Contracts = await optimisticOracleV2Fixture(); - bondToken = optimisticOracleV2Contracts.bondToken; - optimisticOracleV2 = optimisticOracleV2Contracts.optimisticOracleV2; - - oov2Client = new OptimisticOracleClientV2(optimisticOracleV2.provider); - - // Fund proposer and disputer with bond amount and approve Optimistic Oracle V2 to spend bond tokens. - await bondToken.addMinter(await requester.getAddress()); - await bondToken.mint(await proposer.getAddress(), bond); - await bondToken.mint(await disputer.getAddress(), bond); - await bondToken.connect(proposer).approve(optimisticOracleV2.address, bond); - await bondToken.connect(disputer).approve(optimisticOracleV2.address, bond); - }); - it("Fetches price requests", async function () { - const tx = await optimisticOracleV2.requestPrice( - defaultOptimisticOracleV2Identifier, - 0, - ancillaryData, - bondToken.address, - 0 - ); - - const oov2ClientUpdated = await oov2Client.updateWithBlockRange(); - const requests = Array.from(oov2ClientUpdated.requests.values()); - const request = requests[0]; - - assert.equal(requests.length, 1); - assert.equal(request.requester, await requester.getAddress()); - assert.equal(request.identifier, ethers.utils.parseBytes32String(defaultOptimisticOracleV2Identifier)); - assert.equal(request.timestamp, 0); - assert.equal(request.body, question); - assert.equal(request.requestTx, tx.hash); - }); - - it("Handles wrong block range", async function () { - const wrongBlockRange: [number, number] = [100, 99]; - // should fail when calling updateWithBlockRange - try { - await oov2Client.updateWithBlockRange(wrongBlockRange); - assert.fail("Expected function to throw an error, but it did not."); - } catch (error) { - assert.ok(error instanceof Error); - assert.strictEqual(error.message, "Start block number should be less than or equal to end block number"); - } - }); - - it("Handles No Requests Found", async function () { - const latestBlockNumber = await optimisticOracleV2.provider.getBlockNumber(); - const emptyBlockRange: [number, number] = [latestBlockNumber + 1, latestBlockNumber + 10]; - const oov2ClientUpdated = await oov2Client.updateWithBlockRange(emptyBlockRange); - const requests = Array.from(oov2ClientUpdated.requests.values()); - assert.isArray(requests); - assert.isEmpty(requests); - }); -});