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

feat: fetch pledges for a dispute before settling or escalating #82

Merged
merged 6 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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