diff --git a/apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts b/apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts index e676a14..b01ab8d 100644 --- a/apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts +++ b/apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts @@ -3,6 +3,8 @@ import fs from "fs"; import path from "path"; import { oracleAbi, ProphetCodec, ProtocolProvider } from "@ebo-agent/automated-dispute"; import { RequestId } from "@ebo-agent/automated-dispute/dist/types/prophet.js"; +import { bondEscalationModuleAbi } from "@ebo-agent/automated-dispute/src/abis/index.js"; +import { ResponseId } from "@ebo-agent/automated-dispute/src/types/index.js"; import { BlockNumberService } from "@ebo-agent/blocknumber"; import { Caip2ChainId, Logger } from "@ebo-agent/shared"; import { CreateServerReturnType } from "prool"; @@ -37,7 +39,7 @@ import { import { killAgent, spawnAgent } from "../../utils/prophet-e2e-scaffold/spawnAgent.js"; const E2E_SCENARIO_SETUP_TIMEOUT = 60_000; -const E2E_TEST_TIMEOUT = 30_000; +const E2E_TEST_TIMEOUT = 60_000; // TODO: it'd be nice to have zod here const KEYSTORE_PASSWORD = process.env.KEYSTORE_PASSWORD || ""; @@ -625,4 +627,337 @@ describe.sequential("single agent", () => { expect(requestFinalizedEvent).toBeDefined(); expect(newEpochEvent).toBeDefined(); }); + + test("escalate dispute to arbitrator", { timeout: E2E_TEST_TIMEOUT }, async () => { + const logger = Logger.getInstance(); + + const blockNumberService = new BlockNumberService( + new Map([[PROTOCOL_L2_CHAIN_ID, [PROTOCOL_L2_LOCAL_URL]]]), + { + baseUrl: new URL("http://not.needed/"), + bearerToken: "not.needed", + bearerTokenExpirationWindow: 1000, + servicePaths: { + block: "/block", + blockByTime: "/blockByTime", + }, + }, + logger, + ); + + // Set up the protocol provider with account[0] + const protocolProvider = new ProtocolProvider( + { + l1: { + chainId: PROTOCOL_L2_CHAIN_ID, + urls: [PROTOCOL_L2_LOCAL_URL], + transactionReceiptConfirmations: 1, + timeout: 1_000, + retryInterval: 500, + }, + l2: { + chainId: PROTOCOL_L2_CHAIN_ID, + urls: [PROTOCOL_L2_LOCAL_URL], + transactionReceiptConfirmations: 1, + timeout: 1_000, + retryInterval: 500, + }, + }, + { + bondEscalationModule: protocolContracts["BondEscalationModule"], + eboRequestCreator: protocolContracts["EBORequestCreator"], + epochManager: EPOCH_MANAGER_ADDRESS, + oracle: protocolContracts["Oracle"], + horizonAccountingExtension: protocolContracts["HorizonAccountingExtension"], + }, + accounts[0].privateKey, + blockNumberService, + ); + + const anvilClient = createTestClient({ + mode: "anvil", + account: GRT_HOLDER, + chain: PROTOCOL_L2_CHAIN, + transport: http(PROTOCOL_L2_LOCAL_URL), + }) + .extend(publicActions) + .extend(walletActions); + + // Set epoch length to a big enough epoch length + await setEpochLength({ + length: 100_000n, + client: anvilClient, + epochManagerAddress: EPOCH_MANAGER_ADDRESS, + governorAddress: GOVERNOR_ADDRESS, + }); + + const initBlock = await anvilClient.getBlockNumber(); + const currentEpoch = await protocolProvider.getCurrentEpoch(); + + // A1 creates a request REQ1 for Epoch1 + await protocolProvider.createRequest(currentEpoch.number, PROTOCOL_L2_CHAIN_ID); + + const requestCreatedEvent = await waitForEvent({ + client: anvilClient, + filter: { + address: protocolContracts["Oracle"], + fromBlock: initBlock, + event: getAbiItem({ abi: oracleAbi, name: "RequestCreated" }), + strict: true, + }, + pollingIntervalMs: 100, + blockTimeout: initBlock + 1000n, + }); + + expect(requestCreatedEvent).toBeDefined(); + + const correctResponse = await blockNumberService.getEpochBlockNumber( + currentEpoch.startTimestamp, + PROTOCOL_L2_CHAIN_ID, + ); + + // A1 proposes a response RESP1(REQ1) + await protocolProvider.proposeResponse(requestCreatedEvent.args._request, { + proposer: accounts[0].account.address, + requestId: requestCreatedEvent.args._requestId as RequestId, + response: ProphetCodec.encodeResponse({ block: correctResponse }), + }); + + const responseProposedEvent = await waitForEvent({ + client: anvilClient, + filter: { + address: protocolContracts["Oracle"], + fromBlock: initBlock, + event: getAbiItem({ abi: oracleAbi, name: "ResponseProposed" }), + strict: true, + }, + pollingIntervalMs: 100, + blockTimeout: initBlock + 1000n, + }); + + expect(responseProposedEvent).toBeDefined(); + + // Setting up a second account to act as the disputer and pledger + const account2 = await setUpAccount({ + localRpcUrl: PROTOCOL_L2_LOCAL_URL, + chain: PROTOCOL_L2_CHAIN, + deployedContracts: protocolContracts, + grtHolder: GRT_HOLDER, + grtContractAddress: GRT_CONTRACT_ADDRESS, + grtFundAmount: parseEther("50"), + }); + + // Impersonate the arbitrator address + await anvilClient.impersonateAccount({ address: ARBITRATOR_ADDRESS }); + // TODO: issue funding wallet + // console.log(`Adding 1 ETH to arbitrator ${ARBITRATOR_ADDRESS}...`); + // const fundArbitratorTxHash = await anvilClient.sendTransaction({ + // account: GRT_HOLDER, + // to: ARBITRATOR_ADDRESS, + // value: parseEther('1'), + // }); + // await anvilClient.waitForTransactionReceipt({ hash: fundArbitratorTxHash }); + // console.log(`Arbitrator funded.`); + + // Setting up Prophet for account2 + await setUpProphet({ + accounts: [account2.account], + arbitratorAddress: ARBITRATOR_ADDRESS, + grtAddress: GRT_CONTRACT_ADDRESS, + horizonStakingAddress: HORIZON_STAKING_ADDRESS, + chainsToAdd: [PROTOCOL_L2_CHAIN_ID], + grtProvisionAmount: parseEther("45"), + anvilClient: createTestClient({ + mode: "anvil", + transport: http(PROTOCOL_L2_LOCAL_URL), + chain: PROTOCOL_L2_CHAIN, + }) + .extend(publicActions) + .extend(walletActions), + deployedContracts: protocolContracts, + }); + + // Account2 disputes RESP1, creating DISP1 + const protocolProviderAccount2 = new ProtocolProvider( + { + l1: { + chainId: PROTOCOL_L2_CHAIN_ID, + urls: [PROTOCOL_L2_LOCAL_URL], + transactionReceiptConfirmations: 1, + timeout: 1_000, + retryInterval: 500, + }, + l2: { + chainId: PROTOCOL_L2_CHAIN_ID, + urls: [PROTOCOL_L2_LOCAL_URL], + transactionReceiptConfirmations: 1, + timeout: 1_000, + retryInterval: 500, + }, + }, + { + bondEscalationModule: protocolContracts["BondEscalationModule"], + eboRequestCreator: protocolContracts["EBORequestCreator"], + epochManager: EPOCH_MANAGER_ADDRESS, + oracle: protocolContracts["Oracle"], + horizonAccountingExtension: protocolContracts["HorizonAccountingExtension"], + }, + account2.privateKey, + blockNumberService, + ); + + const dispute = { + disputer: account2.account.address, + proposer: accounts[0].account.address, + responseId: responseProposedEvent.args._responseId as ResponseId, + requestId: requestCreatedEvent.args._requestId as RequestId, + }; + + // Account2 disputes the response + await protocolProviderAccount2.disputeResponse( + requestCreatedEvent.args._request, + responseProposedEvent.args._response, + dispute, + ); + + const responseDisputedEvent = await waitForEvent({ + client: anvilClient, + filter: { + address: protocolContracts["Oracle"], + fromBlock: initBlock, + event: getAbiItem({ abi: oracleAbi, name: "ResponseDisputed" }), + strict: true, + }, + matcher: (log) => { + return log.args._responseId === responseProposedEvent.args._responseId; + }, + pollingIntervalMs: 100, + blockTimeout: initBlock + 1000n, + }); + + expect(responseDisputedEvent).toBeDefined(); + + // Account2 pledges for DISP1 + await protocolProviderAccount2.pledgeForDispute(requestCreatedEvent.args._request, dispute); + + // Start the agent A1 + agent = spawnAgent({ + configPath: tmpConfigFile, + config: { + protocolProvider: { + contracts: { + oracle: protocolContracts["Oracle"], + bondEscalationModule: protocolContracts["BondEscalationModule"], + eboRequestCreator: protocolContracts["EBORequestCreator"], + horizonAccountingExtension: protocolContracts["HorizonAccountingExtension"], + epochManager: EPOCH_MANAGER_ADDRESS, + }, + rpcsConfig: { + l1: { + chainId: PROTOCOL_L2_CHAIN_ID, + transactionReceiptConfirmations: 1, + timeout: 1_000, + retryInterval: 500, + }, + l2: { + chainId: PROTOCOL_L2_CHAIN_ID, + transactionReceiptConfirmations: 1, + timeout: 1_000, + retryInterval: 500, + }, + }, + }, + blockNumberService: { + blockmetaConfig: { + baseUrl: new URL("http://not.needed/"), + bearerTokenExpirationWindow: 1000, + servicePaths: { + block: "/block", + blockByTime: "/blockByTime", + }, + }, + }, + processor: { + accountingModules: { + responseModule: protocolContracts["BondedResponseModule"], + escalationModule: protocolContracts["BondEscalationModule"], + }, + msBetweenChecks: 3000, + }, + }, + env: { + PROTOCOL_PROVIDER_PRIVATE_KEY: accounts[0].privateKey, + PROTOCOL_PROVIDER_L1_RPC_URLS: [PROTOCOL_L2_LOCAL_URL], + PROTOCOL_PROVIDER_L2_RPC_URLS: [PROTOCOL_L2_LOCAL_URL], + BLOCK_NUMBER_BLOCKMETA_TOKEN: "not.needed", + BLOCK_NUMBER_RPC_URLS_MAP: new Map([ + [PROTOCOL_L2_CHAIN_ID, [PROTOCOL_L2_LOCAL_URL]], + ]), + DISCORD_BOT_TOKEN: "", + DISCORD_CHANNEL_ID: "", + }, + }); + + // Wait for A1 to pledge against DISP1 + const pledgeAgainstEvent = await waitForEvent({ + client: anvilClient, + filter: { + address: protocolContracts["BondEscalationModule"], + fromBlock: initBlock, + event: getAbiItem({ abi: bondEscalationModuleAbi, name: "PledgeAgainstDispute" }), + strict: true, + }, + matcher: (log) => { + // TODO: no pledger? + return log.args._pledger === accounts[0].account.address; + }, + pollingIntervalMs: 100, + blockTimeout: initBlock + 1000n, + }); + + expect(pledgeAgainstEvent).toBeDefined(); + + // Increase time to pass the dispute window and tying buffer + await anvilClient.increaseTime({ seconds: 60 * 60 * 24 * 7 * 4 }); + + // Wait for A1 to escalate the dispute + const disputeEscalatedEvent = await waitForEvent({ + client: anvilClient, + filter: { + address: protocolContracts["Oracle"], + fromBlock: initBlock, + event: getAbiItem({ abi: oracleAbi, name: "DisputeEscalated" }), + strict: true, + }, + matcher: (log) => { + return log.args._caller === accounts[0].account.address; + }, + pollingIntervalMs: 100, + blockTimeout: initBlock + 1000n, + }); + + expect(disputeEscalatedEvent).toBeDefined(); + + // Check dispute status updated to "Escalated" + const disputeStatusUpdatedEvent = await waitForEvent({ + client: anvilClient, + filter: { + address: protocolContracts["Oracle"], + fromBlock: initBlock, + event: getAbiItem({ abi: oracleAbi, name: "DisputeStatusUpdated" }), + strict: true, + }, + matcher: (log) => { + const status = ProphetCodec.decodeDisputeStatus(log.args._status); + return ( + log.args._disputeId === responseDisputedEvent.args._disputeId && + status === "Escalated" + ); + }, + pollingIntervalMs: 100, + blockTimeout: initBlock + 1000n, + }); + + expect(disputeStatusUpdatedEvent).toBeDefined(); + }); }); diff --git a/apps/agent/test/e2e/utils/prophet-e2e-scaffold/eboCore.ts b/apps/agent/test/e2e/utils/prophet-e2e-scaffold/eboCore.ts index e347a16..c07c5a8 100644 --- a/apps/agent/test/e2e/utils/prophet-e2e-scaffold/eboCore.ts +++ b/apps/agent/test/e2e/utils/prophet-e2e-scaffold/eboCore.ts @@ -427,7 +427,7 @@ async function addEboRequestCreatorChains( }); await Promise.all( - chainsToAdd.map(async (chainId, index) => { + chainsToAdd.map(async (chainId) => { console.log(`Adding ${chainId} to EBORequestCreator...`); const addChainTxHash = await client.sendTransaction({ @@ -439,7 +439,6 @@ async function addEboRequestCreatorChains( args: [chainId], functionName: "addChain", }), - nonce: index, }); await client.waitForTransactionReceipt({