Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move llm bots to separate package #4629

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/llm-bot/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -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);
30 changes: 30 additions & 0 deletions packages/llm-bot/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
41 changes: 41 additions & 0 deletions packages/llm-bot/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -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<string, BlockConfig> = {
"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
},
};
60 changes: 60 additions & 0 deletions packages/llm-bot/src/utils/contracts.ts
Original file line number Diff line number Diff line change
@@ -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 <T extends Contract>(
contractName: ContractName,
provider: Provider,
address?: string
): Promise<T> => {
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<number> => {
const currencyContract = await getContractInstanceWithProvider<ERC20Ethers>("ERC20", provider, currencyAddress);
try {
return await currencyContract.decimals();
} catch (err) {
return 18;
}
};

export const getCurrencySymbol = async (provider: Provider, currencyAddress: string): Promise<string> => {
const currencyContract = await getContractInstanceWithProvider<ERC20Ethers>("ERC20", provider, currencyAddress);
try {
return await currencyContract.symbol();
} catch (err) {
// Try to get the symbol as bytes32 (e.g. MKR uses this).
try {
const bytes32SymbolIface = new utils.Interface(["function symbol() view returns (bytes32 symbol)"]);
const bytes32Symbol = await provider.call({
to: currencyAddress,
data: bytes32SymbolIface.encodeFunctionData("symbol"),
});
return utils.parseBytes32String(bytes32SymbolIface.decodeFunctionResult("symbol", bytes32Symbol).symbol);
} catch (err) {
return "";
}
}
};

// 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));
};
29 changes: 29 additions & 0 deletions packages/llm-bot/src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { TenderlySimulationResult } from "@uma/common";

const optimisticOracleV2UIBaseUrl = "https://oracle.uma.xyz";
const testnetOptimisticOracleV2UIBaseUrl = "https://testnet.oracle.uma.xyz";

// monitor-v2 package is only using Optimistic Oracle V3, so currently there is no need to generalize this.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: outdated comment

export const generateOOv3UILink = (transactionHash: string, eventIndex: number, chainId?: number): string => {
// Currently testnet UI supports only goerli, so assume any other chain is production.
const baseUrl = chainId === 5 ? testnetOptimisticOracleV2UIBaseUrl : optimisticOracleV2UIBaseUrl;
return `<${baseUrl}/?transactionHash=${transactionHash}&eventIndex=${eventIndex}|View in UI>`;
};

export const createSnapshotProposalLink = (baseUrl: string, space: string, proposalId: string): string => {
return `<${baseUrl}/#/${space}/proposal/${proposalId}|Snapshot UI>`;
};

export const createTenderlySimulationLink = (simulationResult?: TenderlySimulationResult): string => {
if (simulationResult === undefined) {
return "No Tenderly simulation available";
} else if (simulationResult.status) {
return `<${simulationResult.resultUrl.url}|Tenderly simulation successful${
!simulationResult.resultUrl.public ? " (private)" : ""
}>`;
} else {
return `<${simulationResult.resultUrl.url}|Tenderly simulation reverted${
!simulationResult.resultUrl.public ? " (private)" : ""
}>`;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was a little surprised to see this code here? Is this used (or placed to be used) in the LLC bit codebase? If so, no problem, just wanted to call it out!

};
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
24 changes: 24 additions & 0 deletions packages/llm-bot/test/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { formatBytes32String, parseUnits } from "./utils";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are some/all of these test utilities files copied? If so, could you comment that in the PR so we know which to review as fresh code?


// 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" };
113 changes: 113 additions & 0 deletions packages/llm-bot/test/fixtures/DVM2.Fixture.ts
Original file line number Diff line number Diff line change
@@ -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<DVM2Contracts> => {
return await deployDVM2(ethers);
}
);

export const deployDVM2 = hre.deployments.createFixture(
async ({ ethers }): Promise<DVM2Contracts> => {
// 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 };
}
);
Loading