Skip to content

Commit

Permalink
feat: fetch pledges before disputing or escalating
Browse files Browse the repository at this point in the history
  • Loading branch information
jahabeebs committed Oct 31, 2024
1 parent 7c18d94 commit 7c779a5
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 66 deletions.
19 changes: 18 additions & 1 deletion packages/automated-dispute/src/interfaces/protocolProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { Caip2ChainId } from "@ebo-agent/shared";
import { Address, Block } from "viem";

import type { Dispute, EboEvent, EboEventName, Epoch, Request, Response } from "../types/index.js";
import type {
BondEscalation,
Dispute,
EboEvent,
EboEventName,
Epoch,
Request,
RequestId,
Response,
} from "../types/index.js";
import { ProtocolContractsNames } from "../constants.js";

export type ProtocolContract = (typeof ProtocolContractsNames)[number];
Expand Down Expand Up @@ -55,6 +64,14 @@ export interface IReadProvider {
* @returns A promise that resolves with an array of approved modules for the user.
*/
getApprovedModules(user: Address): Promise<readonly Address[]>;

/**
* Fetches the escalation data for a given request ID.
*
* @param requestId - The ID of the request.
* @returns A Promise that resolves to the BondEscalation data.
*/
getEscalation(requestId: RequestId): Promise<BondEscalation>;
}

/**
Expand Down
43 changes: 43 additions & 0 deletions packages/automated-dispute/src/providers/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { privateKeyToAccount } from "viem/accounts";
import { arbitrum, arbitrumSepolia, mainnet, sepolia } from "viem/chains";

import type {
BondEscalation,
BondEscalationStatus,
Dispute,
DisputeId,
EboEvent,
Expand Down Expand Up @@ -189,6 +191,7 @@ export class ProtocolProvider implements IProtocolProvider {
getAvailableChains: this.getAvailableChains.bind(this),
getAccountingModuleAddress: this.getAccountingModuleAddress.bind(this),
getApprovedModules: this.getApprovedModules.bind(this),
getEscalation: this.getEscalation.bind(this),
};

private createReadClient(
Expand Down Expand Up @@ -1101,4 +1104,44 @@ export class ProtocolProvider implements IProtocolProvider {
throw new TransactionExecutionError("finalize transaction failed");
}
}

/**
* Fetches the escalation data for a given request ID.
*
* @param requestId - The ID of the request.
* @returns A Promise that resolves to the BondEscalation data.
*/
async getEscalation(requestId: RequestId): Promise<BondEscalation> {
const result = await this.bondEscalationContract.read.getEscalation([requestId]);

const bondEscalation: BondEscalation = {
disputeId: result.disputeId,
status: this.decodeBondEscalationStatus(result.status),
amountOfPledgesForDispute: result.amountOfPledgesForDispute,
amountOfPledgesAgainstDispute: result.amountOfPledgesAgainstDispute,
};

return bondEscalation;
}

/**
* Decodes the BondEscalationStatus enum from the contract.
*
* @param status - The numeric status from the contract.
* @returns The corresponding BondEscalationStatus string.
*/
private decodeBondEscalationStatus(status: number): BondEscalationStatus {
switch (status) {
case 0:
return "Active";
case 1:
return "Resolved";
case 2:
return "Escalated";
case 3:
return "NoResolution";
default:
throw new Error(`Unknown BondEscalationStatus: ${status}`);
}
}
}
47 changes: 21 additions & 26 deletions packages/automated-dispute/src/services/eboActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,13 +421,28 @@ export class EboActor {
try {
this.logger.info(`Settling dispute ${dispute.id}...`);

// OPTIMIZE: check for pledges to potentially save the ShouldBeEscalated error
const escalationData = await this.protocolProvider.read.getEscalation(request.id);

await this.protocolProvider.settleDispute(
request.prophetData,
response.prophetData,
dispute.prophetData,
);
const amountFor = escalationData.amountOfPledgesForDispute;
const amountAgainst = escalationData.amountOfPledgesAgainstDispute;

if (amountFor > amountAgainst) {
await this.protocolProvider.settleDispute(
request.prophetData,
response.prophetData,
dispute.prophetData,
);

this.logger.info(`Dispute ${dispute.id} settled.`);
} else if (amountFor <= amountAgainst) {
await this.protocolProvider.escalateDispute(
request.prophetData,
response.prophetData,
dispute.prophetData,
);

this.logger.info(`Dispute ${dispute.id} escalated.`);
}

this.logger.info(`Dispute ${dispute.id} settled.`);
} catch (err) {
Expand All @@ -440,26 +455,6 @@ export class EboActor {
dispute,
registry: this.registry,
});

err.on("BondEscalationModule_ShouldBeEscalated", async () => {
try {
await this.protocolProvider.escalateDispute(
request.prophetData,
response.prophetData,
dispute.prophetData,
);

this.logger.info(`Dispute ${dispute.id} escalated.`);

await this.errorHandler.handle(err);
} catch (escalationError) {
this.logger.error(
`Failed to escalate dispute ${dispute.id}: ${escalationError}`,
);

throw escalationError;
}
});
} else {
this.logger.error(`Failed to escalate dispute ${dispute.id}: ${err}`);

Expand Down
9 changes: 9 additions & 0 deletions packages/automated-dispute/src/types/prophet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ export interface Response {

export type DisputeStatus = "None" | "Active" | "Escalated" | "Won" | "Lost" | "NoResolution";

export type BondEscalationStatus = "Active" | "Resolved" | "Escalated" | "NoResolution";

export interface BondEscalation {
disputeId: string;
status: BondEscalationStatus;
amountOfPledgesForDispute: bigint;
amountOfPledgesAgainstDispute: bigint;
}

export interface Dispute {
id: DisputeId;
createdAt: {
Expand Down
10 changes: 8 additions & 2 deletions packages/automated-dispute/tests/mocks/eboActor.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export function buildEboActor(request: Request, logger: ILogger) {
mockedPrivateKey,
);

vi.spyOn(protocolProvider.read, "getEscalation").mockResolvedValue({
disputeId: ("0x" + "03".repeat(32)) as DisputeId,
status: "Active",
amountOfPledgesForDispute: BigInt(10),
amountOfPledgesAgainstDispute: BigInt(5),
});
vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue({
number: BigInt(1),
firstBlockNumber: BigInt(100),
Expand Down Expand Up @@ -167,7 +173,7 @@ export function buildResponse(request: Request, attributes: Partial<Response> =
};

const baseResponse: Response = {
id: "0x0111111111111111111111111111111111111111" as ResponseId,
id: attributes.id || (("0x" + "02".repeat(32)) as ResponseId),
createdAt: {
timestamp: (request.createdAt.timestamp + 1n) as UnixTimestamp,
blockNumber: request.createdAt.blockNumber + 1n,
Expand Down Expand Up @@ -195,7 +201,7 @@ export function buildDispute(
attributes: Partial<Dispute> = {},
): Dispute {
const baseDispute: Dispute = {
id: "0x01" as DisputeId,
id: attributes.id || (("0x" + "03".repeat(32)) as DisputeId),
status: "Active",
createdAt: {
timestamp: (response.createdAt.timestamp + 1n) as UnixTimestamp,
Expand Down
72 changes: 46 additions & 26 deletions packages/automated-dispute/tests/services/eboActor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
PastEventEnqueueError,
RequestMismatch,
} from "../../src/exceptions/index.js";
import { EboEvent, ErrorContext, Request, RequestId, ResponseId } from "../../src/types/index.js";
import { EboEvent, Request, RequestId, ResponseId } from "../../src/types/index.js";
import mocks from "../mocks/index.js";
import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "../services/eboActor/fixtures.js";

Expand Down Expand Up @@ -489,42 +489,30 @@ describe("EboActor", () => {
});

describe("settleDispute", () => {
it("escalates dispute when BondEscalationModule_ShouldBeEscalated error occurs", async () => {
it("escalates dispute when amountOfPledgesForDispute <= amountOfPledgesAgainstDispute", async () => {
const { actor, protocolProvider } = mocks.buildEboActor(request, logger);
const response = mocks.buildResponse(request);
const dispute = mocks.buildDispute(request, response);

const shouldBeEscalatedName = "BondEscalationModule_ShouldBeEscalated";

const customError = new CustomContractError(shouldBeEscalatedName, {
shouldNotify: false,
shouldReenqueue: false,
shouldTerminate: false,
vi.spyOn(protocolProvider.read, "getEscalation").mockResolvedValue({
disputeId: dispute.id,
status: "Active",
amountOfPledgesForDispute: BigInt(5),
amountOfPledgesAgainstDispute: BigInt(10),
});

vi.spyOn(protocolProvider, "settleDispute").mockRejectedValue(customError);

const escalateDisputeMock = vi
.spyOn(protocolProvider, "escalateDispute")
.mockResolvedValue();

vi.spyOn(customError, "on").mockImplementation((err, errorHandler) => {
expect(err).toMatch(shouldBeEscalatedName);
expect(errorHandler).toBeTypeOf("function");

errorHandler({} as ErrorContext);

expect(escalateDisputeMock).toHaveBeenCalledWith(
request.prophetData,
response.prophetData,
dispute.prophetData,
);

return customError;
});

await actor["settleDispute"](request, response, dispute);

expect(escalateDisputeMock).toHaveBeenCalledWith(
request.prophetData,
response.prophetData,
dispute.prophetData,
);

expect(logger.info).toHaveBeenCalledWith(`Dispute ${dispute.id} escalated.`);
});

Expand All @@ -534,12 +522,44 @@ describe("EboActor", () => {
const dispute = mocks.buildDispute(request, response);

const settleError = new Error("SettleDispute failed");
vi.spyOn(protocolProvider.read, "getEscalation").mockResolvedValue({
disputeId: dispute.id,
status: "Active",
amountOfPledgesForDispute: BigInt(10),
amountOfPledgesAgainstDispute: BigInt(5),
});

vi.spyOn(protocolProvider, "settleDispute").mockRejectedValue(settleError);

await expect(actor["settleDispute"](request, response, dispute)).rejects.toThrow(
settleError,
);
});

it("settles dispute when amountOfPledgesForDispute > amountOfPledgesAgainstDispute", async () => {
const { actor, protocolProvider } = mocks.buildEboActor(request, logger);
const response = mocks.buildResponse(request);
const dispute = mocks.buildDispute(request, response);

vi.spyOn(protocolProvider.read, "getEscalation").mockResolvedValue({
disputeId: dispute.id,
status: "Active",
amountOfPledgesForDispute: BigInt(10),
amountOfPledgesAgainstDispute: BigInt(5),
});

const settleDisputeMock = vi
.spyOn(protocolProvider, "settleDispute")
.mockResolvedValue();

await actor["settleDispute"](request, response, dispute);

expect(settleDisputeMock).toHaveBeenCalledWith(
request.prophetData,
response.prophetData,
dispute.prophetData,
);

expect(logger.info).toHaveBeenCalledWith(`Dispute ${dispute.id} settled.`);
});
});
});
10 changes: 5 additions & 5 deletions packages/automated-dispute/tests/services/eboActor/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
RequestId,
Response,
ResponseId,
} from "../../../src/types/prophet";
} from "../../../src/types/prophet.js";

export const mockedPrivateKey =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
Expand All @@ -19,12 +19,12 @@ export const DEFAULT_MOCKED_PROTOCOL_CONTRACTS: ProtocolContractsAddresses = {
oracle: "0x1234560000000000000000000000000000000000" as Address,
epochManager: "0x6543210000000000000000000000000000000000" as Address,
eboRequestCreator: "0x9999990000000000000000000000000000000000" as Address,
bondEscalationModule: "0x1a2b3c" as Address,
bondEscalationModule: "0x1a2b3c0000000000000000000000000000000000" as Address,
horizonAccountingExtension: "0x9999990000000000000000000000000000000000" as Address,
};

export const DEFAULT_MOCKED_RESPONSE_DATA: Response = {
id: "0x1234567890123456789012345678901234567890" as ResponseId,
id: ("0x" + "02".repeat(32)) as ResponseId,
createdAt: {
timestamp: 1625097600n as UnixTimestamp,
blockNumber: 1n,
Expand Down Expand Up @@ -68,7 +68,7 @@ const DEFAULT_REQUEST_MODULES_DATA = {
};

export const DEFAULT_MOCKED_REQUEST_CREATED_DATA: Request = {
id: "0x01" as RequestId,
id: ("0x" + "01".repeat(32)) as RequestId,
createdAt: {
timestamp: BigInt(Date.UTC(2024, 0, 1, 0, 0, 0, 0) / 1000) as UnixTimestamp,
blockNumber: 1n,
Expand Down Expand Up @@ -103,7 +103,7 @@ export const DEFAULT_MOCKED_REQUEST_CREATED_DATA: Request = {
};

export const DEFAULT_MOCKED_DISPUTE_DATA: Dispute = {
id: "0x3456789012345678901234567890123456789012" as DisputeId,
id: ("0x" + "03".repeat(32)) as DisputeId,
createdAt: 1625097800n,
status: "Active",
prophetData: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("onDisputeStatusUpdated", () => {
blockNumber: 1n,
logIndex: 1,
metadata: {
disputeId: "0x01" as DisputeId,
disputeId: dispute.id,
status: "Lost",
dispute: dispute.prophetData,
blockNumber: 1n,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { UnixTimestamp } from "@ebo-agent/shared";
import { describe, expect, it, vi } from "vitest";

import { DisputeWithoutResponse } from "../../../src/exceptions/index.js";
import { ResponseId } from "../../../src/types/prophet.js";
import { DisputeId, ResponseId } from "../../../src/types/prophet.js";
import mocks from "../../mocks";
import { DEFAULT_MOCKED_REQUEST_CREATED_DATA } from "./fixtures";

Expand All @@ -14,8 +14,9 @@ describe("EboActor", () => {
const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA;
const { disputeModuleData } = request.decodedData;

const responseToSettle = mocks.buildResponse(request, { id: "0x10" as ResponseId });

const responseToSettle = mocks.buildResponse(request, {
id: ("0x" + "02".repeat(32)) as ResponseId,
});
const disputeToSettle = mocks.buildDispute(request, responseToSettle, {
createdAt: {
timestamp: 1n as UnixTimestamp,
Expand Down Expand Up @@ -64,6 +65,13 @@ describe("EboActor", () => {

const newBlockNumber = disputeDeadline + 1n;

vi.spyOn(protocolProvider.read, "getEscalation").mockResolvedValue({
disputeId: ("0x" + "03".repeat(32)) as DisputeId,
status: "Active",
amountOfPledgesForDispute: BigInt(10),
amountOfPledgesAgainstDispute: BigInt(5),
});

await actor.onLastBlockUpdated(newBlockNumber as UnixTimestamp);

expect(mockSettleDispute).toHaveBeenCalledWith(
Expand Down
Loading

0 comments on commit 7c779a5

Please sign in to comment.