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: validate horizon staking authorization during agent bootup #95

Merged
merged 20 commits into from
Nov 25, 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
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just double checking that we want to exit the process right away

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yepp

}

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