Skip to content

Commit

Permalink
feat: fetch pledges for a dispute before settling or escalating (#82)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes GRT-194

## Description
- Idea is to avoid BondEscalationModule_ShouldBeEscalated by fetching
pledges for a dispute before trying to settle or escalate
  • Loading branch information
jahabeebs authored Nov 4, 2024
1 parent 0e65a1c commit 436a9a8
Show file tree
Hide file tree
Showing 14 changed files with 581 additions and 105 deletions.
356 changes: 352 additions & 4 deletions apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -36,8 +38,8 @@ import {
} from "../../utils/prophet-e2e-scaffold/index.js";
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_SCENARIO_SETUP_TIMEOUT = 120_000;
const E2E_TEST_TIMEOUT = 120_000;

// TODO: it'd be nice to have zod here
const KEYSTORE_PASSWORD = process.env.KEYSTORE_PASSWORD || "";
Expand Down Expand Up @@ -128,7 +130,15 @@ describe.sequential("single agent", () => {
chain: PROTOCOL_L2_CHAIN,
grtHolder: GRT_HOLDER,
grtContractAddress: GRT_CONTRACT_ADDRESS,
grtFundAmount: parseEther("50"),
grtFundAmount: parseEther("5000"),
}),
await setUpAccount({
localRpcUrl: PROTOCOL_L2_LOCAL_URL,
deployedContracts: protocolContracts,
chain: PROTOCOL_L2_CHAIN,
grtHolder: GRT_HOLDER,
grtContractAddress: GRT_CONTRACT_ADDRESS,
grtFundAmount: parseEther("5000"),
}),
];

Expand All @@ -138,7 +148,7 @@ describe.sequential("single agent", () => {
grtAddress: GRT_CONTRACT_ADDRESS,
horizonStakingAddress: HORIZON_STAKING_ADDRESS,
chainsToAdd: [PROTOCOL_L2_CHAIN_ID],
grtProvisionAmount: parseEther("45"),
grtProvisionAmount: parseEther("4500"),
anvilClient: createTestClient({
mode: "anvil",
transport: http(PROTOCOL_L2_LOCAL_URL),
Expand Down Expand Up @@ -625,4 +635,342 @@ describe.sequential("single agent", () => {
expect(requestFinalizedEvent).toBeDefined();
expect(newEpochEvent).toBeDefined();
});

/**
* Given:
* - A single agent A1 operating for chain CHAIN1
* - A request REQ1 for E1, a response RESP1(REQ1) and a dispute DISP1(RESP1)
* - A pledge for DISP1
* - Within dispute window (and tying buffer)
*
* When:
* - A1 considers RESP1 to be correct
*
* Then:
* - A1 pledges against DISP1
* - No one else pledges for/against DISP1
* - A1 escalates dispute
* - `DisputeEscalated(A1.address, DISP1.id, DISP1)`
*/
test.skip("escalate dispute to arbitrator", { timeout: E2E_TEST_TIMEOUT }, async () => {
const logger = Logger.getInstance();

const blockNumberService = new BlockNumberService(
new Map<Caip2ChainId, string[]>([[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 = accounts[1];

// 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<Caip2ChainId, string[]>([
[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: "PledgedAgainstDispute" }),
strict: true,
},
matcher: (log) => {
return log.args._pledger === accounts[0].account.address;
},
pollingIntervalMs: 100,
blockTimeout: initBlock + 1000n,
});

expect(pledgeAgainstEvent).toBeDefined();

// Verify that pledges are equal
const finalEscalation = await protocolProvider.bondEscalationContract.read.getEscalation([
dispute.requestId,
]);

console.info(
"Final pledges - For:",
finalEscalation.amountOfPledgesForDispute.toString(),
"Against:",
finalEscalation.amountOfPledgesAgainstDispute.toString(),
);

expect(finalEscalation.amountOfPledgesForDispute).toBe(
finalEscalation.amountOfPledgesAgainstDispute,
);

const disputeParams = await protocolProvider.bondEscalationContract.read.decodeRequestData([
requestCreatedEvent.args._request.disputeModuleData,
]);

// 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(disputeParams.bondEscalationDeadline) + Number(disputeParams.tyingBuffer) + 3600;
await anvilClient.increaseTime({ seconds: timeToIncrease });
await anvilClient.mine({ blocks: 1 });

// 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 + 5000n,
});

expect(disputeEscalatedEvent).toBeDefined();

const disputeModuleAddress = protocolContracts["BondEscalationModule"];

// Check dispute status updated to "Escalated"
const disputeStatusChangedEvent = await waitForEvent({
client: anvilClient,
filter: {
address: disputeModuleAddress,
fromBlock: initBlock,
event: getAbiItem({ abi: bondEscalationModuleAbi, name: "DisputeStatusChanged" }),
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(disputeStatusChangedEvent).toBeDefined();
});
});
Loading

0 comments on commit 436a9a8

Please sign in to comment.