Skip to content

Commit

Permalink
feat: validate horizon staking authorization during agent bootup (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
jahabeebs authored Nov 25, 2024
1 parent 6cccd6d commit 6575d77
Show file tree
Hide file tree
Showing 17 changed files with 735 additions and 643 deletions.
1 change: 1 addition & 0 deletions apps/agent/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ protocolProvider:
eboRequestCreator: "0x1234567890123456789012345678901234567890" # EBO Request Creator contract
bondEscalationModule: "0x1234567890123456789012345678901234567890" # Bond Escalation Module contract
horizonAccountingExtension: "0x1234567890123456789012345678901234567890" # Accounting extension contract
horizonStaking: "0x1234567890123456789012345678901234567890" # Horizon Staking contract
accessControl:
serviceProviderAddress: "0x1234567890123456789012345678901234567890" # Service Provider Address

Expand Down
1 change: 1 addition & 0 deletions apps/agent/config.tenderly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ protocolProvider:
eboRequestCreator: "0xa13318684281a820304C164427396385C306d870"
bondEscalationModule: "0x52d7728fE87826FfF51b21b303e2FF7cB04F6Aec"
horizonAccountingExtension: "0xbDAB27D1903da4e18B0D1BE873E18924514E52eC"
horizonStaking: "0x3F53F9f9a5d7F36dCC869f8D2F227499c411c0cf"

blockNumberService:
blockmetaConfig:
Expand Down
3 changes: 2 additions & 1 deletion apps/agent/src/config/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const envSchema = z.object({
DISCORD_WEBHOOK: z.string().optional(),
});

const addressSchema = z.string().refine((address) => isAddress(address));
export const addressSchema = z.string().refine((address) => isAddress(address));

const rpcConfigSchema = z.object({
chainId: z
Expand Down Expand Up @@ -64,6 +64,7 @@ const protocolProviderConfigSchema = z.object({
eboRequestCreator: addressSchema,
bondEscalationModule: addressSchema,
horizonAccountingExtension: addressSchema,
horizonStaking: addressSchema,
}),
accessControl: accessControlSchema.optional(),
});
Expand Down
2 changes: 1 addition & 1 deletion apps/scripts/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ RPC_URLS_L2=["https://l2.rpc.url"]
TRANSACTION_RECEIPT_CONFIRMATIONS=1
TIMEOUT=30000
RETRY_INTERVAL=1000
CONTRACTS_ADDRESSES={"l1ChainId":"eip155:1","l2ChainId":"eip155:42161","oracle":"0x...","epochManager":"0x...","bondEscalationModule":"0x...","horizonAccountingExtension":"0x..."}
CONTRACTS_ADDRESSES={"l1ChainId":"eip155:1","l2ChainId":"eip155:42161","oracle":"0x...","epochManager":"0x...","bondEscalationModule":"0x...","horizonAccountingExtension":"0x...","horizonStaking":"0x..."}
BONDED_RESPONSE_MODULE_ADDRESS=0x...
BOND_ESCALATION_MODULE_ADDRESS=0x...
1 change: 1 addition & 0 deletions apps/scripts/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ vi.stubEnv(
epochManager: "0x0000000000000000000000000000000000000002",
bondEscalationModule: "0x0000000000000000000000000000000000000003",
horizonAccountingExtension: "0x0000000000000000000000000000000000000004",
horizonStaking: "0x0000000000000000000000000000000000000005",
}),
);
vi.stubEnv("BONDED_RESPONSE_MODULE_ADDRESS", "0xBondedResponseModule");
Expand Down
19 changes: 10 additions & 9 deletions apps/scripts/utilities/approveAccountingModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@ const stringToJSONSchema = z.string().transform((str, ctx): Record<string, unkno
});

/**
* Defines a schema for a valid Hex string.
* Defines a schema for a valid Hex address string.
*/
const hexSchema = z.string().refine((val): val is Hex => isHex(val), {
message: "Must be a valid Hex string",
});
const addressSchema = z.string().refine((address) => isAddress(address));

/**
* Defines the schema for CONTRACTS_ADDRESSES based on expected structure.
Expand All @@ -36,14 +34,17 @@ const contractsAddressesSchema = z.object({
l2ChainId: z.string().refine((val): val is Caip2ChainId => val.includes(":"), {
message: "l2ChainId must be in the format 'namespace:chainId' (e.g., 'eip155:42161')",
}),
oracle: hexSchema,
epochManager: hexSchema,
bondEscalationModule: hexSchema,
horizonAccountingExtension: hexSchema
oracle: addressSchema,
epochManager: addressSchema,
bondEscalationModule: addressSchema,
horizonAccountingExtension: addressSchema
.optional()
.default("0x0000000000000000000000000000000000000000"),
horizonStaking: addressSchema,
// Default to a zero address because it's not required for the script but required for ProtocolProvider
eboRequestCreator: hexSchema.optional().default("0x0000000000000000000000000000000000000000"),
eboRequestCreator: addressSchema
.optional()
.default("0x0000000000000000000000000000000000000000"),
});

/**
Expand Down
93 changes: 93 additions & 0 deletions packages/automated-dispute/src/abis/horizonStaking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
export const horizonStakingAbi = [
{
type: "function",
name: "addToProvision",
inputs: [
{ name: "serviceProvider", type: "address", internalType: "address" },
{ name: "verifier", type: "address", internalType: "address" },
{ name: "tokens", type: "uint256", internalType: "uint256" },
],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "getProvision",
inputs: [
{ name: "serviceProvider", type: "address", internalType: "address" },
{ name: "verifier", type: "address", internalType: "address" },
],
outputs: [
{
name: "",
type: "tuple",
internalType: "struct IHorizonStaking.Provision",
components: [
{ name: "tokens", type: "uint256", internalType: "uint256" },
{ name: "tokensThawing", type: "uint256", internalType: "uint256" },
{ name: "sharesThawing", type: "uint256", internalType: "uint256" },
{ name: "maxVerifierCut", type: "uint32", internalType: "uint32" },
{ name: "thawingPeriod", type: "uint64", internalType: "uint64" },
{ name: "createdAt", type: "uint64", internalType: "uint64" },
{ name: "maxVerifierCutPending", type: "uint32", internalType: "uint32" },
{ name: "thawingPeriodPending", type: "uint64", internalType: "uint64" },
],
},
],
stateMutability: "view",
},
{
type: "function",
name: "isAuthorized",
inputs: [
{ name: "serviceProvider", type: "address", internalType: "address" },
{ name: "verifier", type: "address", internalType: "address" },
{ name: "operator", type: "address", internalType: "address" },
],
outputs: [{ name: "", type: "bool", internalType: "bool" }],
stateMutability: "view",
},
{
type: "function",
name: "provision",
inputs: [
{ name: "serviceProvider", type: "address", internalType: "address" },
{ name: "verifier", type: "address", internalType: "address" },
{ name: "tokens", type: "uint256", internalType: "uint256" },
{ name: "maxVerifierCut", type: "uint32", internalType: "uint32" },
{ name: "thawingPeriod", type: "uint64", internalType: "uint64" },
],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "slash",
inputs: [
{ name: "serviceProvider", type: "address", internalType: "address" },
{ name: "tokens", type: "uint256", internalType: "uint256" },
{ name: "tokensVerifier", type: "uint256", internalType: "uint256" },
{ name: "verifierDestination", type: "address", internalType: "address" },
],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "stake",
inputs: [{ name: "tokens", type: "uint256", internalType: "uint256" }],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "thaw",
inputs: [
{ name: "serviceProvider", type: "address", internalType: "address" },
{ name: "verifier", type: "address", internalType: "address" },
{ name: "tokens", type: "uint256", internalType: "uint256" },
],
outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }],
stateMutability: "nonpayable",
},
] as const;
1 change: 1 addition & 0 deletions packages/automated-dispute/src/abis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./epochManager.js";
export * from "./eboRequestCreator.js";
export * from "./bondEscalationModule.js";
export * from "./horizonAccountingExtension.js";
export * from "./horizonStaking.js";
1 change: 1 addition & 0 deletions packages/automated-dispute/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const ProtocolContractsNames = [
"eboRequestCreator",
"bondEscalationModule",
"horizonAccountingExtension",
"horizonStaking",
] as const;
13 changes: 11 additions & 2 deletions packages/automated-dispute/src/interfaces/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ export interface IReadProvider {
* @returns A Promise that resolves to the BondEscalation data.
*/
getEscalation(requestId: RequestId): Promise<BondEscalation>;

/**
* @notice Check if an operator is authorized for the caller on a specific verifier/data service.
* @param serviceProvider The service provider on behalf of whom they're claiming to act
* @param verifier The verifier/data service on which they're claiming to act
* @param operator The address to check for auth
* @return Whether the operator is authorized or not
*/
isAuthorized(serviceProvider: Address, verifier: Address, operator: Address): Promise<boolean>;
}

/**
Expand All @@ -90,11 +99,11 @@ export interface IWriteProvider {
* Creates a request on the EBO Request Creator contract.
*
* @param epoch The epoch for which the request is being created.
* @param chains An array of chain identifiers where the request should be created.
* @param chain A chain identifier where the request should be created.
* @throws Will throw an error if the chains array is empty or if the transaction fails.
* @returns A promise that resolves when the request is successfully created.
*/
createRequest(epoch: bigint, chains: Caip2ChainId): Promise<void>;
createRequest(epoch: bigint, chain: Caip2ChainId): Promise<void>;

/**
* Proposes a response to a request.
Expand Down
60 changes: 40 additions & 20 deletions packages/automated-dispute/src/providers/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
eboRequestCreatorAbi,
epochManagerAbi,
horizonAccountingExtensionAbi,
horizonStakingAbi,
oracleAbi,
} from "../abis/index.js";
import {
Expand Down Expand Up @@ -121,6 +122,12 @@ export class ProtocolProvider implements IProtocolProvider {
Address
>;

private horizonStakingContract: GetContractReturnType<
typeof horizonStakingAbi,
typeof this.l2ReadClient,
Address
>;

/**
* Creates a new ProtocolProvider instance
* @param rpcConfig The configuration for the serviceProviderAddress and RPC connections, including URLs, timeout, retry interval, and transaction receipt confirmations
Expand Down Expand Up @@ -177,11 +184,7 @@ export class ProtocolProvider implements IProtocolProvider {
public: this.l2ReadClient,
wallet: this.l2WriteClient,
},
}) as GetContractReturnType<
typeof bondEscalationModuleAbi,
typeof this.l2WriteClient,
Address
>;
});

this.horizonAccountingExtensionContract = getContract({
address: contracts.horizonAccountingExtension,
Expand All @@ -190,11 +193,13 @@ export class ProtocolProvider implements IProtocolProvider {
public: this.l2ReadClient,
wallet: this.l2WriteClient,
},
}) as GetContractReturnType<
typeof horizonAccountingExtensionAbi,
typeof this.l2WriteClient,
Address
>;
});

this.horizonStakingContract = getContract({
address: contracts.horizonStaking,
abi: horizonStakingAbi,
client: this.l2ReadClient,
});
}

public write: IWriteProvider = {
Expand All @@ -217,6 +222,7 @@ export class ProtocolProvider implements IProtocolProvider {
getAccountingModuleAddress: this.getAccountingModuleAddress.bind(this),
getApprovedModules: this.getApprovedModules.bind(this),
getEscalation: this.getEscalation.bind(this),
isAuthorized: this.isAuthorized.bind(this),
};

/**
Expand Down Expand Up @@ -852,11 +858,7 @@ export class ProtocolProvider implements IProtocolProvider {
async getApprovedModules(user: Address): Promise<readonly Address[]> {
const bondAddress = user ?? this.getAccountAddress();

const modules = await this.horizonAccountingExtensionContract.read.approvedModules([
bondAddress,
]);

return modules;
return await this.horizonAccountingExtensionContract.read.approvedModules([bondAddress]);
}

// TODO: waiting for ChainId to be merged for _chains parameter
Expand All @@ -865,16 +867,16 @@ export class ProtocolProvider implements IProtocolProvider {
* and then executing it if the simulation is successful.
*
* @param {bigint} epoch - The epoch for which the request is being created.
* @param {Caip2ChainId} chain - An array of chain identifiers for which the request should be created.
* @param {Caip2ChainId} chain - A chain identifier for which the request should be created.
* @throws {Error} Throws an error if the chains array is empty or if the transaction fails.
* @returns {Promise<void>} A promise that resolves when the request is successfully created.
*/
async createRequest(epoch: bigint, chains: Caip2ChainId): Promise<void> {
async createRequest(epoch: bigint, chain: Caip2ChainId): Promise<void> {
const { request: simulatedRequest } = await this.l2ReadClient.simulateContract({
address: this.eboRequestCreatorContract.address,
abi: eboRequestCreatorAbi,
functionName: "createRequest",
args: [epoch, chains],
args: [epoch, chain],
account: this.l2WriteClient.account,
});

Expand Down Expand Up @@ -1206,13 +1208,31 @@ export class ProtocolProvider implements IProtocolProvider {
async getEscalation(requestId: RequestId): Promise<BondEscalation> {
const result = await this.bondEscalationContract.read.getEscalation([requestId]);

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

return bondEscalation;
/**
* Checks if an operator is authorized for a given service provider and verifier.
*
* @param serviceProvider - The service provider address.
* @param verifier - The verifier address.
* @param operator - The operator address.
* @returns A promise that resolves to a boolean indicating authorization status.
*/
async isAuthorized(
serviceProvider: Address,
verifier: Address,
operator: Address,
): Promise<boolean> {
return await this.horizonStakingContract.read.isAuthorized([
serviceProvider,
verifier,
operator,
]);
}
}
16 changes: 16 additions & 0 deletions packages/automated-dispute/src/services/eboProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ export class EboProcessor {

await this.checkAllModulesApproved();

const serviceProvider = this.protocolProvider.getServiceProviderAddress();
const verifier = this.protocolProvider.getAccountingModuleAddress();
const operator = this.protocolProvider.getAccountAddress();

const isAuth = await this.protocolProvider.isAuthorized(
serviceProvider,
verifier,
operator,
);

if (!isAuth) {
const errorMessage = `Authorization required: Operator ${operator} is not authorized for service provider ${serviceProvider}.`;
this.logger.error(errorMessage);
process.exit(1);
}

await this.sync(); // Bootstrapping

this.eventsInterval = setInterval(async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/automated-dispute/tests/mocks/eboActor.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Mutex } from "async-mutex";
import { Block, pad } from "viem";
import { vi } from "vitest";

import { ProtocolProvider } from "../../src/providers/index.js";
import { ProtocolProvider } from "../../src/index.js";
import { EboActor, EboMemoryRegistry, ProphetCodec } from "../../src/services/index.js";
import {
Dispute,
Expand Down
9 changes: 6 additions & 3 deletions packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { BlockNumberService } from "@ebo-agent/blocknumber";
import { Caip2ChainId, ILogger, NotificationService } from "@ebo-agent/shared";
import { vi } from "vitest";

import { ProtocolProvider } from "../../src/providers/index.js";
import { EboActorsManager, EboProcessor } from "../../src/services/index.js";
import { AccountingModules } from "../../src/types/prophet.js";
import {
AccountingModules,
EboActorsManager,
EboProcessor,
ProtocolProvider,
} from "../../src/index.js";
import {
DEFAULT_MOCKED_PROTOCOL_CONTRACTS,
mockedPrivateKey,
Expand Down
Loading

0 comments on commit 6575d77

Please sign in to comment.