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 0a383f1..24e05b6 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 @@ -12,11 +12,13 @@ import { Account, Address, createTestClient, + encodeFunctionData, getAbiItem, Hex, http, keccak256, padHex, + parseAbi, parseAbiItem, parseEther, publicActions, @@ -176,8 +178,6 @@ describe.sequential("single agent", () => { process: agent, configPath: tmpConfigFile, }); - } else { - console.warn(`Warning: ${tmpConfigFile} does not exist. Skipping agent kill.`); } }); @@ -387,6 +387,7 @@ describe.sequential("single agent", () => { * - `EBOFinalityModule.newEpoch(E1, CHAIN1, RESP2.response)` * - `OracleRequestFinalized(REQ1.id, RESP2.id, A1.address)` */ + // TODO: DisputeStatusUpdated not triggered, dispute immediately becomes 'Resolved' test.skip("dispute response and propose a new one", { timeout: E2E_TEST_TIMEOUT }, async () => { if ( !accounts || @@ -452,6 +453,12 @@ describe.sequential("single agent", () => { console.log("protocolProvider", protocolProvider); + console.log("Approving bond escalation module..."); + await protocolProvider.write.approveModule( + protocolContracts["BondEscalationModule"] as Address, + ); + console.log("Bond escalation module approved."); + const anvilClient = createTestClient({ mode: "anvil", account: GRT_HOLDER, @@ -461,6 +468,26 @@ describe.sequential("single agent", () => { .extend(publicActions) .extend(walletActions); + console.log("Approving GRT spending for bond escalation module..."); + const approveGrtTx = await anvilClient.sendTransaction({ + account: accounts[0].account, + to: GRT_CONTRACT_ADDRESS, + data: encodeFunctionData({ + abi: parseAbi(["function approve(address spender, uint256 amount)"]), + args: [protocolContracts["BondEscalationModule"], parseEther("1000")], // Approve a large amount + }), + }); + await anvilClient.waitForTransactionReceipt({ hash: approveGrtTx }); + console.log("GRT spending approved"); + + // Verify the approval + // const allowance = await anvilClient.readContract({ + // address: GRT_CONTRACT_ADDRESS, + // abi: parseAbi(["function allowance(address,address) returns (uint256)"]), + // functionName: "allowance", + // args: [accounts[0].account.address, protocolContracts["BondEscalationModule"]], + // }); + // Set epoch length to a big enough epoch length as in sepolia is way too short at the moment await setEpochLength({ length: 100_000n, @@ -472,8 +499,10 @@ describe.sequential("single agent", () => { const initBlock = await anvilClient.getBlockNumber(); const currentEpoch = await protocolProvider.getCurrentEpoch(); - console.log("currentEpoch", currentEpoch); - console.log("initBlock", initBlock); + const correctResponse = await blockNumberService.getEpochBlockNumber( + currentEpoch.startTimestamp, + PROTOCOL_L2_CHAIN_ID, + ); const accessControl = { user: accounts[0].account.address as Address, @@ -502,11 +531,6 @@ describe.sequential("single agent", () => { console.log(requestCreatedEvent, "requestCreatedEvent"); console.log(currentEpoch.startTimestamp, "currentEpoch.startTimestamp"); - const correctResponse = await blockNumberService.getEpochBlockNumber( - currentEpoch.startTimestamp, - PROTOCOL_L2_CHAIN_ID, - ); - console.log("correctResponse", correctResponse); // A1 proposes a response RESP1(REQ1) @@ -539,12 +563,6 @@ describe.sequential("single agent", () => { throw new Error("ResponseProposed event not found"); } - // Define accessControl for Agent A1 - const agentAccessControl = { - user: accounts[0].account.address as Address, - data: "0x" as Address, - }; - agent = spawnAgent({ configPath: tmpConfigFile, config: { @@ -604,14 +622,13 @@ describe.sequential("single agent", () => { }, }); - console.log("disputing response"); - console.log(requestCreatedEvent.args._request, "requestCreatedEvent.args._request"); - console.log( - badResponseProposedEvent.args._response, - "badResponseProposedEvent.args._response", - ); + const agentAccessControl = { + user: accounts[0].account.address as Address, + data: "0x" as Address, + }; // A1 disputes RESP1 with DISP1 and provides accessControl + console.log("Disputing bad response..."); await protocolProvider.disputeResponse( requestCreatedEvent.args._request, badResponseProposedEvent.args._response, @@ -646,7 +663,41 @@ describe.sequential("single agent", () => { throw new Error("ResponseDisputed event not found"); } - // A1 proposes a correct response RESP2 + console.log("Checking dispute state immediately after creation..."); + const initialEscalation = await protocolProvider.read.getEscalation( + requestCreatedEvent.args._requestId, + ); + console.log("Initial dispute state:", initialEscalation); + + // if (initialEscalation.status !== "Active") { + // throw new Error(`Unexpected initial dispute status: ${initialEscalation.status}`); + // } + + // Immediately pledge for the dispute + // console.log("Pledging for dispute..."); + // try { + // await protocolProvider.write.pledgeForDispute( + // requestCreatedEvent.args._request, + // { + // disputer: accounts[0].account.address, + // proposer: accounts[0].account.address, + // responseId: badResponseProposedEvent.args._responseId as ResponseId, + // requestId: requestCreatedEvent.args._requestId as RequestId, + // } + // ); + // console.log("Pledge successful"); + // } catch (error) { + // console.error("Pledge error:", error); + // throw error; + // } + + // // Verify pledge was successful + // const postPledgeEscalation = await protocolProvider.read.getEscalation( + // requestCreatedEvent.args._requestId + // ); + // console.log("Dispute state after pledge:", postPledgeEscalation); + + // Propose correct response await protocolProvider.proposeResponse( requestCreatedEvent.args._request, { @@ -656,9 +707,6 @@ describe.sequential("single agent", () => { }, agentAccessControl, ); - - console.log("correctResponse", correctResponse); - const correctResponseProposedEvent = await waitForEvent({ client: anvilClient, filter: { @@ -682,91 +730,105 @@ describe.sequential("single agent", () => { blockTimeout: initBlock + 1000n, }); - console.log("correctResponseProposedEvent", correctResponseProposedEvent); + // Advance time + console.log("Advancing time by 1 week..."); + await anvilClient.increaseTime({ seconds: 60 * 60 * 24 * 7 }); + + console.log("correctResponse", correctResponse); expect(correctResponseProposedEvent).toBeDefined(); if (correctResponseProposedEvent === undefined) { throw new Error("CorrectResponseProposed event not found"); } - console.log("Advancing blockchain time by 4 weeks to trigger settlement..."); - await anvilClient.increaseTime({ seconds: 60 * 60 * 24 * 7 * 4 }); - await anvilClient.mine({ blocks: 1 }); - console.log("Blockchain time advanced."); - - const disputeSettledEvent = await waitForEvent({ - client: anvilClient, - filter: { - address: protocolContracts["Oracle"], - fromBlock: initBlock, - event: getAbiItem({ abi: oracleAbi, name: "DisputeStatusUpdated" }), - strict: true, - }, - matcher: (log) => { - console.log(log, "log"); - console.log(log.args._status, "log.args._status"); - console.log(ProphetCodec.decodeDisputeStatus(log.args._status), "decoded status"); - console.log(log.args._disputeId, "log.args._disputeId"); - - const status = ProphetCodec.decodeDisputeStatus(log.args._status); - - return ( - log.args._disputeId === badResponseDisputedEvent.args._disputeId && - status === "Won" - ); - }, - pollingIntervalMs: 100, - blockTimeout: initBlock + 1000n, - }); - - console.log("disputeSettledEvent", disputeSettledEvent); - - expect(disputeSettledEvent).toBeDefined(); - - const [requestFinalizedEvent, newEpochEventFinal] = await Promise.all([ - waitForEvent({ - client: anvilClient, - filter: { - address: protocolContracts["Oracle"], - fromBlock: initBlock, - event: getAbiItem({ abi: oracleAbi, name: "OracleRequestFinalized" }), - strict: true, - }, - matcher: (log) => { - return ( - log.args._requestId === requestCreatedEvent.args._requestId && - log.args._responseId === correctResponseProposedEvent.args._responseId - ); - }, - pollingIntervalMs: 100, - blockTimeout: initBlock + 1000n, - }), - waitForEvent({ - client: anvilClient, - filter: { - address: protocolContracts["EBOFinalityModule"], - fromBlock: initBlock, - event: newEventAbi, - strict: true, - }, - matcher: (log) => { - return ( - console.log(log.args._chainId, "log.args._chainId"), - console.log( - keccak256(toHex(ARBITRUM_SEPOLIA_ID)), - "keccak256(toHex(ARBITRUM_SEPOLIA_ID))", - ), - log.args._chainId === keccak256(toHex(ARBITRUM_SEPOLIA_ID)) && - log.args._epoch === currentEpoch.number - ); - }, - pollingIntervalMs: 100, - blockTimeout: initBlock + 1000n, - }), - ]); - - expect(requestFinalizedEvent).toBeDefined(); - expect(newEpochEventFinal).toBeDefined(); + const midSettlementState = await protocolProvider.read.getEscalation( + requestCreatedEvent.args._requestId, + ); + console.log("Dispute state after time advance:", midSettlementState); + + // if (midSettlementState.status === "Active") { + // console.log("Triggering settlement..."); + // await protocolProvider.write.settleDispute( + // requestCreatedEvent.args._request, + // badResponseProposedEvent.args._response, + // { + // disputer: accounts[0].account.address, + // proposer: accounts[0].account.address, + // responseId: badResponseProposedEvent.args._responseId as ResponseId, + // requestId: requestCreatedEvent.args._requestId as RequestId, + // } + // ); + // console.log("Settlement transaction submitted"); + // } + // // Wait for DisputeStatusUpdated event + // const disputeSettledEvent = await waitForEvent({ + // client: anvilClient, + // filter: { + // address: protocolContracts["Oracle"], + // fromBlock: initBlock, + // event: getAbiItem({ abi: oracleAbi, name: "DisputeStatusUpdated" }), + // strict: true, + // }, + // matcher: (log) => { + // console.log("Dispute settled event log:", log); + // const status = ProphetCodec.decodeDisputeStatus(log.args._status); + // console.log("Decoded status:", status); + + // return ( + // log.args._disputeId === badResponseDisputedEvent.args._disputeId && + // status === "Won" + // ); + // }, + // pollingIntervalMs: 100, + // blockTimeout: initBlock + 1000n, + // }); + + // expect(disputeSettledEvent).toBeDefined(); + + // const [requestFinalizedEvent, newEpochEventFinal] = await Promise.all([ + // waitForEvent({ + // client: anvilClient, + // filter: { + // address: protocolContracts["Oracle"], + // fromBlock: initBlock, + // event: getAbiItem({ abi: oracleAbi, name: "OracleRequestFinalized" }), + // strict: true, + // }, + // matcher: (log) => { + // return ( + // log.args._requestId === requestCreatedEvent.args._requestId && + // log.args._responseId === correctResponseProposedEvent.args._responseId + // ); + // }, + // pollingIntervalMs: 100, + // blockTimeout: initBlock + 1000n, + // }), + // waitForEvent({ + // client: anvilClient, + // filter: { + // address: protocolContracts["EBOFinalityModule"], + // fromBlock: initBlock, + // event: newEventAbi, + // strict: true, + // }, + // matcher: (log) => { + // return ( + // console.log(log.args._chainId, "log.args._chainId"), + // console.log( + // keccak256(toHex(ARBITRUM_SEPOLIA_ID)), + // "keccak256(toHex(ARBITRUM_SEPOLIA_ID))", + // ), + // log.args._chainId === keccak256(toHex(ARBITRUM_SEPOLIA_ID)) && + // log.args._epoch === currentEpoch.number + // ); + // }, + // pollingIntervalMs: 100, + // blockTimeout: initBlock + 1000n, + // }), + // ]); + + // expect(requestFinalizedEvent).toBeDefined(); + // expect(newEpochEventFinal).toBeDefined(); }); /** @@ -785,23 +847,15 @@ describe.sequential("single agent", () => { * - A1 escalates dispute * - `DisputeEscalated(A1.address, DISP1.id, DISP1)` */ + // TODO: disputeResponse reverts due to OracleAccessController_AccessModuleNotApproved() test.skip("escalate dispute to arbitrator", { timeout: E2E_TEST_TIMEOUT }, async () => { const logger = Logger.getInstance(); - if ( - !accounts || - accounts[0] === undefined || - accounts[0]?.privateKey === undefined || - accounts[1] === undefined || - accounts[1]?.privateKey === undefined - ) { - throw new Error("Accounts not found"); - } const blockNumberService = new BlockNumberService( new Map([[PROTOCOL_L2_CHAIN_ID, [PROTOCOL_L2_LOCAL_URL]]]), { baseUrl: new URL("http://not.needed/"), - bearerToken: "secret-token", + bearerToken: "not.needed", bearerTokenExpirationWindow: 1000, servicePaths: { block: "/block", @@ -831,13 +885,11 @@ describe.sequential("single agent", () => { }, }, { - bondEscalationModule: protocolContracts["BondEscalationModule"] as Address, - eboRequestCreator: protocolContracts["EBORequestCreator"] as Address, + bondEscalationModule: protocolContracts["BondEscalationModule"], + eboRequestCreator: protocolContracts["EBORequestCreator"], epochManager: EPOCH_MANAGER_ADDRESS, - oracle: protocolContracts["Oracle"] as Address, - horizonAccountingExtension: protocolContracts[ - "HorizonAccountingExtension" - ] as Address, + oracle: protocolContracts["Oracle"], + horizonAccountingExtension: protocolContracts["HorizonAccountingExtension"], horizonStaking: HORIZON_STAKING_ADDRESS, }, accounts[0].privateKey, @@ -866,11 +918,6 @@ describe.sequential("single agent", () => { const initBlock = await anvilClient.getBlockNumber(); const currentEpoch = await protocolProvider.getCurrentEpoch(); - const accessControl = { - user: accounts[0].account.address as Address, - data: "0x" as Address, - }; - // A1 creates a request REQ1 for Epoch1 await protocolProvider.createRequest(currentEpoch.number, PROTOCOL_L2_CHAIN_ID); @@ -885,16 +932,19 @@ describe.sequential("single agent", () => { pollingIntervalMs: 100, blockTimeout: initBlock + 1000n, }); + expect(requestCreatedEvent).toBeDefined(); - if (requestCreatedEvent === undefined) { - throw new Error("RequestCreated event not found"); - } const correctResponse = await blockNumberService.getEpochBlockNumber( currentEpoch.startTimestamp, PROTOCOL_L2_CHAIN_ID, ); + const accessControl = { + user: accounts[0].account.address as Address, + data: "0x" as Address, + }; + // A1 proposes a response RESP1(REQ1) await protocolProvider.proposeResponse( requestCreatedEvent.args._request, @@ -919,30 +969,55 @@ describe.sequential("single agent", () => { }); expect(responseProposedEvent).toBeDefined(); - if (responseProposedEvent === undefined) { - throw new Error("ResponseProposed event not found"); - } // Setting up a second account to act as the disputer and pledger const account2 = accounts[1]; - // Define accessControl for Account2 - const account2AccessControl = { - user: accounts[0].account.address as Address, - data: "0x" as Address, + // 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"], + horizonStaking: HORIZON_STAKING_ADDRESS, + }, + account2.privateKey, + undefined, + logger, + 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 RESP1, creating DISP1 - await protocolProvider.disputeResponse( + // Account2 disputes the response + await protocolProviderAccount2.disputeResponse( requestCreatedEvent.args._request, responseProposedEvent.args._response, - { - disputer: account2.account.address, - proposer: accounts[0].account.address, - responseId: responseProposedEvent.args._responseId as ResponseId, - requestId: requestCreatedEvent.args._requestId as RequestId, - }, - account2AccessControl, + dispute, + accessControl, ); const responseDisputedEvent = await waitForEvent({ @@ -961,30 +1036,20 @@ describe.sequential("single agent", () => { }); expect(responseDisputedEvent).toBeDefined(); - if (responseDisputedEvent === undefined) { - throw new Error("ResponseDisputed event not found"); - } // Account2 pledges for DISP1 - await protocolProvider.pledgeForDispute(requestCreatedEvent.args._request, { - disputer: account2.account.address, - proposer: accounts[0].account.address, - responseId: responseProposedEvent.args._responseId as ResponseId, - requestId: requestCreatedEvent.args._requestId as RequestId, - }); + await protocolProviderAccount2.pledgeForDispute(requestCreatedEvent.args._request, dispute); - // Start the agent A1 with updated ProtocolProvider and accessControl + // Start the agent A1 agent = spawnAgent({ configPath: tmpConfigFile, config: { protocolProvider: { contracts: { - oracle: protocolContracts["Oracle"] as Address, - bondEscalationModule: protocolContracts["BondEscalationModule"] as Address, - eboRequestCreator: protocolContracts["EBORequestCreator"] as Address, - horizonAccountingExtension: protocolContracts[ - "HorizonAccountingExtension" - ] as Address, + oracle: protocolContracts["Oracle"], + bondEscalationModule: protocolContracts["BondEscalationModule"], + eboRequestCreator: protocolContracts["EBORequestCreator"], + horizonAccountingExtension: protocolContracts["HorizonAccountingExtension"], epochManager: EPOCH_MANAGER_ADDRESS, horizonStaking: HORIZON_STAKING_ADDRESS, }, @@ -1002,7 +1067,6 @@ describe.sequential("single agent", () => { retryInterval: 500, }, }, - privateKey: accounts[0].privateKey, }, blockNumberService: { blockmetaConfig: { @@ -1016,8 +1080,8 @@ describe.sequential("single agent", () => { }, processor: { accountingModules: { - responseModule: protocolContracts["BondedResponseModule"] as Address, - escalationModule: protocolContracts["BondEscalationModule"] as Address, + responseModule: protocolContracts["BondedResponseModule"], + escalationModule: protocolContracts["BondEscalationModule"], }, msBetweenChecks: 3000, }, @@ -1054,7 +1118,7 @@ describe.sequential("single agent", () => { // Verify that pledges are equal const finalEscalation = await protocolProvider.bondEscalationContract.read.getEscalation([ - requestCreatedEvent.args._requestId, + dispute.requestId, ]); console.info( @@ -1072,12 +1136,6 @@ describe.sequential("single agent", () => { requestCreatedEvent.args._request.disputeModuleData, ]); - // Define accessControl for escalation - const escalationAccessControl = { - user: accounts[0].account.address as Address, - data: "0x" as Address, - }; - // Increase time to pass both the dispute window and tying buffer // The additional 3600 seconds is arbitrary and ensures that the agent A1 has enough time to escalate // Note: Number casting is acceptable here because dispute parameters will not exceed Number.MAX_SAFE_INTEGER. @@ -1086,23 +1144,7 @@ describe.sequential("single agent", () => { await anvilClient.increaseTime({ seconds: timeToIncrease }); await anvilClient.mine({ blocks: 1 }); - // A1 escalates the dispute - await protocolProvider.escalateDispute( - requestCreatedEvent.args._request, - { - proposer: accounts[0].account.address, - requestId: requestCreatedEvent.args._requestId as RequestId, - response: ProphetCodec.encodeResponse({ block: correctResponse }), - }, - { - disputer: accounts[0].account.address, - proposer: accounts[0].account.address, - responseId: responseDisputedEvent.args._responseId as ResponseId, - requestId: requestCreatedEvent.args._requestId as RequestId, - }, - escalationAccessControl, - ); - + // Wait for A1 to escalate the dispute const disputeEscalatedEvent = await waitForEvent({ client: anvilClient, filter: { @@ -1162,6 +1204,7 @@ describe.sequential("single agent", () => { * - The request REQ1 is finalized with the response RESP1. * - No additional response is created for the same content as RESP1. */ + // TODO: BlockNumberService not initialized when calling getCurrentEpoch() test.skip( "pledge against dispute and finalize with same response", { timeout: E2E_TEST_TIMEOUT }, @@ -1172,7 +1215,7 @@ describe.sequential("single agent", () => { new Map([[PROTOCOL_L2_CHAIN_ID, [PROTOCOL_L2_LOCAL_URL]]]), { baseUrl: new URL("http://not.needed/"), - bearerToken: "secret-token", + bearerToken: "not.needed", bearerTokenExpirationWindow: 1000, servicePaths: { block: "/block", @@ -1183,7 +1226,7 @@ describe.sequential("single agent", () => { notifier, ); - // Instantiate ProtocolProvider for Account1 + // Set up ProtocolProvider with account[0] (Agent A1) const protocolProvider = new ProtocolProvider( { l1: { @@ -1202,17 +1245,14 @@ describe.sequential("single agent", () => { }, }, { - bondEscalationModule: protocolContracts["BondEscalationModule"] as Address, - eboRequestCreator: protocolContracts["EBORequestCreator"] as Address, + bondEscalationModule: protocolContracts["BondEscalationModule"], + eboRequestCreator: protocolContracts["EBORequestCreator"], epochManager: EPOCH_MANAGER_ADDRESS, - oracle: protocolContracts["Oracle"] as Address, - horizonAccountingExtension: protocolContracts[ - "HorizonAccountingExtension" - ] as Address, + oracle: protocolContracts["Oracle"], + horizonAccountingExtension: protocolContracts["HorizonAccountingExtension"], horizonStaking: HORIZON_STAKING_ADDRESS, }, accounts[0].privateKey, - undefined, logger, blockNumberService, ); @@ -1238,12 +1278,6 @@ describe.sequential("single agent", () => { const initBlock = await anvilClient.getBlockNumber(); const currentEpoch = await protocolProvider.getCurrentEpoch(); - // Define accessControl as required by the updated ProtocolProvider methods - const accessControl = { - user: accounts[0].account.address as Address, - data: "0x" as Address, - }; - // Agent A1 creates a request REQ1 for Epoch E1 await protocolProvider.createRequest(currentEpoch.number, PROTOCOL_L2_CHAIN_ID); @@ -1260,15 +1294,18 @@ describe.sequential("single agent", () => { blockTimeout: initBlock + 1000n, }); expect(requestCreatedEvent).toBeDefined(); - if (requestCreatedEvent === undefined) { - throw new Error("RequestCreated event not found"); - } // Agent A1 proposes a response RESP1(REQ1) const correctResponseBlock = await blockNumberService.getEpochBlockNumber( currentEpoch.startTimestamp, PROTOCOL_L2_CHAIN_ID, ); + + const accessControl = { + user: accounts[0].account.address as Address, + data: "0x" as Address, + }; + await protocolProvider.proposeResponse( requestCreatedEvent.args._request, { @@ -1295,30 +1332,51 @@ describe.sequential("single agent", () => { blockTimeout: initBlock + 1000n, }); expect(responseProposedEvent).toBeDefined(); - if (responseProposedEvent === undefined) { - throw new Error("ResponseProposed event not found"); - } // Setting up a second account to act as the disputer const account2 = accounts[1]; - - // Define accessControl for Account2 - const account2AccessControl = { - user: accounts[0].account.address, - data: "0x", - }; + 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"], + horizonStaking: HORIZON_STAKING_ADDRESS, + }, + account2.privateKey, + logger, + blockNumberService, + ); // Account2 disputes RESP1, creating DISP1 - await protocolProvider.disputeResponse( + const dispute = { + disputer: account2.account.address, + proposer: accounts[0].account.address, + responseId: responseProposedEvent.args._responseId as ResponseId, + requestId: requestCreatedEvent.args._requestId as RequestId, + }; + await protocolProviderAccount2.disputeResponse( requestCreatedEvent.args._request, responseProposedEvent.args._response, - { - disputer: account2.account.address, - proposer: accounts[0].account.address, - responseId: responseProposedEvent.args._responseId as ResponseId, - requestId: requestCreatedEvent.args._requestId as RequestId, - }, - account2AccessControl, + dispute, + accessControl, ); // Wait for the ResponseDisputed event @@ -1337,9 +1395,6 @@ describe.sequential("single agent", () => { blockTimeout: initBlock + 1000n, }); expect(responseDisputedEvent).toBeDefined(); - if (responseDisputedEvent === undefined) { - throw new Error("ResponseDisputed event not found"); - } // Start the agent A1 agent = spawnAgent({ @@ -1347,14 +1402,11 @@ describe.sequential("single agent", () => { config: { protocolProvider: { contracts: { - oracle: protocolContracts["Oracle"] as Address, - bondEscalationModule: protocolContracts[ - "BondEscalationModule" - ] as Address, - eboRequestCreator: protocolContracts["EBORequestCreator"] as Address, - horizonAccountingExtension: protocolContracts[ - "HorizonAccountingExtension" - ] as Address, + oracle: protocolContracts["Oracle"], + bondEscalationModule: protocolContracts["BondEscalationModule"], + eboRequestCreator: protocolContracts["EBORequestCreator"], + horizonAccountingExtension: + protocolContracts["HorizonAccountingExtension"], epochManager: EPOCH_MANAGER_ADDRESS, horizonStaking: HORIZON_STAKING_ADDRESS, }, @@ -1394,7 +1446,6 @@ describe.sequential("single agent", () => { env: { PROTOCOL_PROVIDER_PRIVATE_KEY: accounts[0].privateKey, PROTOCOL_PROVIDER_L1_RPC_URLS: [PROTOCOL_L2_LOCAL_URL], - // Using the same RPC due to Anvil's arbitrum block number bug PROTOCOL_PROVIDER_L2_RPC_URLS: [PROTOCOL_L2_LOCAL_URL], BLOCK_NUMBER_BLOCKMETA_TOKEN: "not.needed", BLOCK_NUMBER_RPC_URLS_MAP: new Map([ @@ -1404,14 +1455,7 @@ describe.sequential("single agent", () => { }); // Wait for A1 to pledge against DISP1 - await protocolProvider.pledgeAgainstDispute(requestCreatedEvent.args._request, { - disputer: account2.account.address, - proposer: accounts[0].account.address, - responseId: responseDisputedEvent.args._responseId as ResponseId, - requestId: requestCreatedEvent.args._requestId as RequestId, - }); - - const pledgeAgainstEvent = await waitForEvent({ + const pledgedAgainstEvent = await waitForEvent({ client: anvilClient, filter: { address: protocolContracts["BondEscalationModule"], @@ -1423,13 +1467,15 @@ describe.sequential("single agent", () => { strict: true, }, matcher: (log) => { - return log.args._pledger === accounts[0].account.address; + return ( + log.args._pledger === accounts[0].account.address && + log.args._disputeId === responseDisputedEvent.args._disputeId + ); }, pollingIntervalMs: 100, blockTimeout: initBlock + 1000n, }); - - expect(pledgeAgainstEvent).toBeDefined(); + expect(pledgedAgainstEvent).toBeDefined(); // Fetch dispute module data to get deadlines const request = requestCreatedEvent.args._request; @@ -1438,63 +1484,22 @@ describe.sequential("single agent", () => { request.disputeModuleData, ]); - // Define accessControl for escalation - const escalationAccessControl = { - user: accounts[0].account.address as Address, - data: "0x" as Address, - }; - - // Increase time to pass both the dispute window and tying buffer - // The additional 3600 seconds is arbitrary and ensures that the agent A1 has enough time to escalate - // Note: Number casting is acceptable here because dispute parameters will not exceed Number.MAX_SAFE_INTEGER. const timeToIncrease = Number(disputeModuleData.bondEscalationDeadline) + Number(disputeModuleData.tyingBuffer) + 3600; + + // Adding extra time to ensure deadlines are passed await anvilClient.increaseTime({ seconds: timeToIncrease }); await anvilClient.mine({ blocks: 1 }); - // A1 escalates the dispute - await protocolProvider.escalateDispute( - requestCreatedEvent.args._request, - { - proposer: accounts[0].account.address, - requestId: requestCreatedEvent.args._requestId as RequestId, - response: ProphetCodec.encodeResponse({ block: correctResponse }), - }, - { - disputer: account2.account.address, - proposer: accounts[0].account.address, - responseId: responseDisputedEvent.args._responseId as ResponseId, - requestId: requestCreatedEvent.args._requestId as RequestId, - }, - escalationAccessControl, - ); - - 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 + 5000n, - }); - - expect(disputeEscalatedEvent).toBeDefined(); - - const disputeModuleAddress = protocolContracts["BondEscalationModule"]; + logger.info("Time increased, waiting for Agent A1 to settle the dispute..."); - // Check dispute status updated to "Escalated" + // Wait for A1 to settle DISP1 and it ends with status "Lost" const disputeStatusChangedEvent = await waitForEvent({ client: anvilClient, filter: { - address: disputeModuleAddress, + address: protocolContracts["BondEscalationModule"], fromBlock: initBlock, event: getAbiItem({ abi: bondEscalationModuleAbi, @@ -1503,17 +1508,73 @@ describe.sequential("single agent", () => { strict: true, }, matcher: (log) => { - const status = ProphetCodec.decodeDisputeStatus(log.args._status); return ( log.args._disputeId === responseDisputedEvent.args._disputeId && - status === "Escalated" + ProphetCodec.decodeDisputeStatus(log.args._status) === "Lost" ); }, pollingIntervalMs: 100, blockTimeout: initBlock + 1000n, }); - expect(disputeStatusChangedEvent).toBeDefined(); + + // Wait for A1 to finalize REQ1 with RESP1 + const [oracleRequestFinalizedEvent, newEpochEvent] = await Promise.all([ + waitForEvent({ + client: anvilClient, + filter: { + address: protocolContracts["Oracle"], + fromBlock: initBlock, + event: getAbiItem({ abi: oracleAbi, name: "OracleRequestFinalized" }), + strict: true, + }, + matcher: (log) => { + return ( + log.args._requestId === requestCreatedEvent.args._requestId && + log.args._responseId === responseProposedEvent.args._responseId + ); + }, + pollingIntervalMs: 100, + blockTimeout: initBlock + 1000n, + }), + waitForEvent({ + client: anvilClient, + filter: { + address: protocolContracts["EBOFinalityModule"], + fromBlock: initBlock, + event: newEventAbi, + strict: true, + }, + matcher: (log) => { + return ( + log.args._chainId === keccak256(toHex(PROTOCOL_L2_CHAIN_ID)) && + log.args._epoch === currentEpoch.number + ); + }, + pollingIntervalMs: 100, + blockTimeout: initBlock + 1000n, + }), + ]); + expect(oracleRequestFinalizedEvent).toBeDefined(); + expect(newEpochEvent).toBeDefined(); + + // Verify that A1 does NOT create a new response with the same content as RESP1 + const latestBlock = await anvilClient.getBlockNumber(); + const responseProposedEvents = await anvilClient.getLogs({ + address: protocolContracts["Oracle"], + fromBlock: initBlock, + toBlock: latestBlock, + event: getAbiItem({ abi: oracleAbi, name: "ResponseProposed" }), + strict: true, + }); + // Filter out the initial response proposed by A1 by checking both logIndex and blockNumber + const newResponseProposedEvents = responseProposedEvents.filter((event) => { + return !( + event.logIndex === responseProposedEvent.logIndex && + event.blockNumber === responseProposedEvent.blockNumber + ); + }); + expect(newResponseProposedEvents.length).toBe(0); }, ); });