Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/diamond-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@perfect-abstractions/compose-cli": patch
---

Add a shared Diamond introspection adapter for ERC-8153 and ERC-2535, and use it for inspect and bytecode validation.
37 changes: 37 additions & 0 deletions cli/src/adapters/IDiamondAdapter/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/** The four introspection functions shared by ERC-8153 and ERC-2535 diamonds. */
export const DIAMOND_INSPECT_ABI = [
{
name: "facets",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{
type: "tuple[]",
components: [
{ name: "facet", type: "address" },
{ name: "functionSelectors", type: "bytes4[]" },
],
}],
},
{
name: "facetAddresses",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ type: "address[]" }],
},
{
name: "facetFunctionSelectors",
type: "function",
stateMutability: "view",
inputs: [{ name: "facet", type: "address" }],
outputs: [{ type: "bytes4[]" }],
},
{
name: "facetAddress",
type: "function",
stateMutability: "view",
inputs: [{ name: "selector", type: "bytes4" }],
outputs: [{ type: "address" }],
},
] as const;
35 changes: 35 additions & 0 deletions cli/src/adapters/IDiamondAdapter/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { IRPCAdapter } from "../IRPCAdapter/interface";
import { DIAMOND_INSPECT_ABI } from "./abi";
import type { IDiamondAdapter } from "./interface";

/** Adapt generic RPC reads to the common diamond introspection interface. */
export function createDiamondAdapter(rpc: IRPCAdapter): IDiamondAdapter {
return {
facets: (diamond, options) => rpc.readContract({
address: diamond,
abi: DIAMOND_INSPECT_ABI,
functionName: "facets",
blockNumber: options?.blockNumber,
}, { verifyCode: true }),
facetAddresses: (diamond, options) => rpc.readContract({
address: diamond,
abi: DIAMOND_INSPECT_ABI,
functionName: "facetAddresses",
blockNumber: options?.blockNumber,
}, { verifyCode: true }),
facetFunctionSelectors: (diamond, facet, options) => rpc.readContract({
address: diamond,
abi: DIAMOND_INSPECT_ABI,
functionName: "facetFunctionSelectors",
args: [facet],
blockNumber: options?.blockNumber,
}, { verifyCode: true }),
facetAddress: (diamond, selector, options) => rpc.readContract({
address: diamond,
abi: DIAMOND_INSPECT_ABI,
functionName: "facetAddress",
args: [selector],
blockNumber: options?.blockNumber,
}, { verifyCode: true }),
};
}
20 changes: 20 additions & 0 deletions cli/src/adapters/IDiamondAdapter/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Address, Hex } from "viem";

/** An entry returned by the shared ERC-8153/ERC-2535 diamond introspection ABI. */
export type DiamondFacet = {
facet: Address;
functionSelectors: Hex[];
};

/** Pin related introspection reads to the same chain state when needed. */
export type DiamondReadOptions = {
blockNumber?: bigint;
};

/** Read-only diamond introspection boundary. */
export interface IDiamondAdapter {
facets(diamond: Address, options?: DiamondReadOptions): Promise<DiamondFacet[]>;
facetAddresses(diamond: Address, options?: DiamondReadOptions): Promise<Address[]>;
facetFunctionSelectors(diamond: Address, facet: Address, options?: DiamondReadOptions): Promise<Hex[]>;
facetAddress(diamond: Address, selector: Hex, options?: DiamondReadOptions): Promise<Address>;
}
19 changes: 5 additions & 14 deletions cli/src/modules/bytecodeValidation/module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getAddress, type Address } from "viem";
import { getAddress } from "viem";
import type {
BytecodeValidationReport,
IBytecodeValidatorAdapter,
} from "../../adapters/IBytecodeValidatorAdapter/interface";
import type { IRPCAdapter } from "../../adapters/IRPCAdapter/interface";
import type { IDiamondAdapter } from "../../adapters/IDiamondAdapter/interface";
import type { ComposeContext, ModuleState } from "../../context/types";
import { DIAMOND_INSPECT_ABI } from "../inspect/diamondInspectAbi";
import type { VirtualStorageLayoutRecord } from "../validation/types";
import type {
BytecodeValidationSummary,
Expand All @@ -14,14 +14,10 @@ import type {

type DeploymentDependencies = {
rpc: IRPCAdapter;
diamond: IDiamondAdapter;
validator: IBytecodeValidatorAdapter;
};

type InspectedFacet = {
facet: Address;
functionSelectors: `0x${string}`[];
};

function storageRecords(ctx: ComposeContext): VirtualStorageLayoutRecord[] {
return (ctx.param.virtualStorageRecords as VirtualStorageLayoutRecord[] | undefined) ?? [];
}
Expand Down Expand Up @@ -80,14 +76,9 @@ export const BytecodeValidationModule = {
const chainKey = String(ctx.param.chainKey);
const diamondAddress = getAddress(String(ctx.param.diamondAddress));
const blockNumber = await dependencies.rpc.getBlockNumber();
let rawFacets: InspectedFacet[];
let rawFacets;
try {
rawFacets = await dependencies.rpc.readContract<InspectedFacet[]>({
address: diamondAddress,
abi: DIAMOND_INSPECT_ABI,
functionName: "facets",
blockNumber,
}, { verifyCode: true });
rawFacets = await dependencies.diamond.facets(diamondAddress, { blockNumber });
} catch (error) {
const message = error instanceof Error ? error.message : "Diamond introspection failed.";
ctx.state.bytecodeDeploymentValidation = {
Expand Down
22 changes: 0 additions & 22 deletions cli/src/modules/inspect/diamondInspectAbi.ts

This file was deleted.

70 changes: 24 additions & 46 deletions cli/src/modules/inspect/module.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,46 @@
import path from "node:path";
import { isAddress, type Address, type Hex } from "viem";
import { isAddress, type Address } from "viem";
import { ComposeContext } from "../../context/types";
import { DependencyKey } from "../../resolver/dependencyKey";
import { DependencyResolver } from "../../resolver/dependencyResolver";
import { resolveChainConfig } from "../../utils/chainConfig";
import type { IDiamondAdapter } from "../../adapters/IDiamondAdapter/interface";
import type { ResolvedChainConfig } from "../../utils/chainConfig";
import { findFileAncestor } from "../../utils/files";
import { RPCAdapterError } from "../../adapters/IRPCAdapter/errors";
import { showInspect } from "./output";
import { DIAMOND_INSPECT_ABI } from "./diamondInspectAbi";
import { toFacetInfo } from "./facetFormatter";
import { mergeProjectSignatures } from "./selectorDecoder";
import type { InspectResult, FacetInfo } from "./types";

/** Validate the command address before resolving chain dependencies. */
export function inspectAddress(value: unknown): Address {
if (typeof value !== "string" || !isAddress(value, { strict: false })) {
throw new RPCAdapterError(
"RPC_INVALID_ADDRESS",
`Invalid diamond address: ${String(value ?? "")}`,
{ operation: "inspect" },
);
}
return value as Address;
}

export const InspectModule = {
/**
* Inspects an on-chain Diamond and displays its facets and selectors.
*
* Validates the diamond address, resolves the RPC adapter for the target
* chain, fetches facets via Diamond introspection, and decodes each selector
* Validates the diamond address, fetches facets via the resolved Diamond
* adapter, and decodes each selector
* using a combination of common signatures and project ABI files.
*
* @param ctx - The compose context with `address` and optional `chain` params.
* @param diamond - Resolved Diamond adapter for the selected chain.
* @param chain - Resolved chain identity for the result.
* @returns The updated context with inspect result stored in
* `ctx.state.inspect` as {@link ModuleState}\<{@link InspectResult}\>.
* @throws {RPCAdapterError} If the address is invalid or no contract code is
* found.
*/
async inspect(ctx: ComposeContext): Promise<ComposeContext> {
const addressValue = ctx.param.address;
if (typeof addressValue !== "string" || !isAddress(addressValue, { strict: false })) {
throw new RPCAdapterError(
"RPC_INVALID_ADDRESS",
`Invalid diamond address: ${String(addressValue ?? "")}`,
{ operation: "inspect" },
);
}
const diamondAddress = addressValue as Address;

const chainKey = typeof ctx.param.chain === "string" ? ctx.param.chain : "local";
const configuredChain = await resolveChainConfig({ chainKey });

const dependencies = await DependencyResolver.resolve([{
key: DependencyKey.RPC,
params: { chainKey: configuredChain.chainKey },
}]);
const rpc = dependencies[DependencyKey.RPC];
if (!rpc) throw new Error("RPC dependency was not resolved");

const code = await rpc.getCode(diamondAddress);
if (!code || code === "0x") {
throw new RPCAdapterError(
"RPC_CONTRACT_NOT_FOUND",
`No contract code found at ${diamondAddress}`,
{ operation: "inspect", chainId: configuredChain.chainId },
);
}

const rawFacets = await rpc.readContract<
{ facet: Address; functionSelectors: Hex[] }[]
>({
address: diamondAddress,
abi: DIAMOND_INSPECT_ABI,
functionName: "facets",
});
async inspect(ctx: ComposeContext, diamond: IDiamondAdapter, chain: ResolvedChainConfig): Promise<ComposeContext> {
const diamondAddress = inspectAddress(ctx.param.address);
const rawFacets = await diamond.facets(diamondAddress);

const composePath = await findFileAncestor(process.cwd(), "compose.json");
const projectRoot = composePath ? path.dirname(composePath) : null;
Expand All @@ -74,8 +52,8 @@ export const InspectModule = {

const result: InspectResult = {
diamond: diamondAddress,
chainKey: configuredChain.chainKey,
chainId: configuredChain.chainId,
chainKey: chain.chainKey,
chainId: chain.chainId,
facets,
};

Expand Down
13 changes: 11 additions & 2 deletions cli/src/pipelines/bytecodeValidationPipeline.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ComposeContext, ModuleState } from "../context/types";
import type { IBytecodeValidatorAdapter } from "../adapters/IBytecodeValidatorAdapter/interface";
import type { IRPCAdapter } from "../adapters/IRPCAdapter/interface";
import type { IDiamondAdapter } from "../adapters/IDiamondAdapter/interface";
import { BytecodeValidationModule } from "../modules/bytecodeValidation/module";
import { DependencyKey } from "../resolver/dependencyKey";
import { DependencyResolver } from "../resolver/dependencyResolver";
Expand All @@ -27,13 +28,21 @@ export const BytecodeValidationPipeline = {
projectRoot: String(ctx.param.projectRoot),
},
},
{
key: DependencyKey.Diamond,
params: {
chainKey: ctx.param.chainKey,
projectRoot: String(ctx.param.projectRoot),
},
},
{ key: DependencyKey.BytecodeValidator },
]);
const rpc = dependencies[DependencyKey.RPC] as IRPCAdapter | undefined;
const diamond = dependencies[DependencyKey.Diamond] as IDiamondAdapter | undefined;
const validator = dependencies[DependencyKey.BytecodeValidator] as IBytecodeValidatorAdapter | undefined;
if (!rpc || !validator) throw new Error("Bytecode validation dependencies were not resolved.");
if (!rpc || !diamond || !validator) throw new Error("Bytecode validation dependencies were not resolved.");

ctx = await BytecodeValidationModule.validateDeployment(ctx, { rpc, validator });
ctx = await BytecodeValidationModule.validateDeployment(ctx, { rpc, diamond, validator });
const state = ctx.state.bytecodeDeploymentValidation as ModuleState;
if (!state.success) {
ctx.status = {
Expand Down
16 changes: 14 additions & 2 deletions cli/src/pipelines/inspectPipeline.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { ComposeContext } from "../context/types";
import { InspectModule } from "../modules/inspect/module";
import { InspectModule, inspectAddress } from "../modules/inspect/module";
import { DependencyKey } from "../resolver/dependencyKey";
import { DependencyResolver } from "../resolver/dependencyResolver";
import { resolveChainConfig } from "../utils/chainConfig";

/** Diamond inspect pipeline for querying on-chain facets and selectors. */
export const InspectPipeline = {
async execute(ctx: ComposeContext): Promise<ComposeContext> {
return InspectModule.inspect(ctx);
inspectAddress(ctx.param.address);
const chainKey = typeof ctx.param.chain === "string" ? ctx.param.chain : "local";
const chain = await resolveChainConfig({ chainKey });
const dependencies = await DependencyResolver.resolve([{
key: DependencyKey.Diamond,
params: { chainKey: chain.chainKey },
}]);
const diamond = dependencies[DependencyKey.Diamond];
if (!diamond) throw new Error("Diamond dependency was not resolved");
return InspectModule.inspect(ctx, diamond, chain);
},
};
1 change: 1 addition & 0 deletions cli/src/resolver/dependencyKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export enum DependencyKey {
Hashing = "hashing",
RPC = "rpc",
Diamond = "diamond",
Foundry = "foundry",
Hardhat = "hardhat",
BytecodeValidator = "bytecodeValidator",
Expand Down
5 changes: 5 additions & 0 deletions cli/src/resolver/dependencyRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { foundryAdapter } from "../adapters/IFrameworkAdapter/foundryAdapter/ada
import { hardhatAdapter } from "../adapters/IFrameworkAdapter/hardhatAdapter/adapter";
import type { IRPCAdapter } from "../adapters/IRPCAdapter/interface";
import { createRPCAdapter } from "../adapters/IRPCAdapter/adapter";
import type { IDiamondAdapter } from "../adapters/IDiamondAdapter/interface";
import { createDiamondAdapter } from "../adapters/IDiamondAdapter/adapter";
import { resolveChainConfig } from "../utils/chainConfig";
import { DependencyKey } from "./dependencyKey";
import { BytecodeValidatorAdapter } from "../adapters/IBytecodeValidatorAdapter/adapter";
Expand All @@ -29,6 +31,7 @@ export type DependencyFactory<T = unknown> = (
export type DependencyMap = {
[DependencyKey.Hashing]: IHashingAdapter;
[DependencyKey.RPC]: IRPCAdapter;
[DependencyKey.Diamond]: IDiamondAdapter;
[DependencyKey.Foundry]: IFrameworkAdapter;
[DependencyKey.Hardhat]: IFrameworkAdapter;
[DependencyKey.BytecodeValidator]: IBytecodeValidatorAdapter;
Expand All @@ -52,6 +55,8 @@ export const DependencyRegistry: {
const resolved = await resolveChainConfig(params as RPCDependencyParams | undefined);
return createRPCAdapter({ rpcUrl: resolved.rpcUrl, chainId: resolved.chainId });
},
[DependencyKey.Diamond]: async (params) =>
createDiamondAdapter(await DependencyRegistry[DependencyKey.RPC](params)),
[DependencyKey.Foundry]: () => foundryAdapter,
[DependencyKey.Hardhat]: () => hardhatAdapter,
[DependencyKey.BytecodeValidator]: () => BytecodeValidatorAdapter,
Expand Down
Loading
Loading