fix: validate bridged ABI and malformed logs
This commit is contained in:
@@ -3,11 +3,21 @@ import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const bankRequirements = {
|
||||
functions: ["contractVersion", "paused", "asset", "owner", "totalLiabilities", "balanceOf"],
|
||||
events: ["Deposited", "Withdrawn", "Paused", "Unpaused", "OwnershipTransferred", "Upgraded"],
|
||||
};
|
||||
const tokenRequirements = { functions: ["balanceOf"], events: [] };
|
||||
const bankRequirements = [
|
||||
functionSignature("contractVersion", [], ["uint256"], "pure"),
|
||||
functionSignature("paused", [], ["bool"], "view"),
|
||||
functionSignature("asset", [], ["address"], "view"),
|
||||
functionSignature("owner", [], ["address"], "view"),
|
||||
functionSignature("totalLiabilities", [], ["uint256"], "view"),
|
||||
functionSignature("balanceOf", [parameter("account", "address")], ["uint256"], "view"),
|
||||
eventSignature("Deposited", [parameter("account", "address", true), parameter("amount", "uint256", false)]),
|
||||
eventSignature("Withdrawn", [parameter("account", "address", true), parameter("amount", "uint256", false)]),
|
||||
eventSignature("Paused", [parameter("account", "address", false)]),
|
||||
eventSignature("Unpaused", [parameter("account", "address", false)]),
|
||||
eventSignature("OwnershipTransferred", [parameter("previousOwner", "address", true), parameter("newOwner", "address", true)]),
|
||||
eventSignature("Upgraded", [parameter("implementation", "address", true)]),
|
||||
];
|
||||
const tokenRequirements = [functionSignature("balanceOf", [parameter("account", "address")], ["uint256"], "view")];
|
||||
|
||||
export function extractAbi(artifact, contractName) {
|
||||
if (!artifact || typeof artifact !== "object" || !Array.isArray(artifact.abi)) {
|
||||
@@ -15,16 +25,34 @@ export function extractAbi(artifact, contractName) {
|
||||
}
|
||||
const requirements = contractName === "BankV1" ? bankRequirements : contractName === "MockUSDC" ? tokenRequirements : null;
|
||||
if (!requirements) throw new Error(`unsupported contract artifact ${contractName}`);
|
||||
for (const [type, names] of Object.entries(requirements)) {
|
||||
for (const name of names) {
|
||||
if (!artifact.abi.some((entry) => entry && entry.type === type.slice(0, -1) && entry.name === name)) {
|
||||
throw new Error(`${contractName} ABI is missing required ${type.slice(0, -1)} ${name}`);
|
||||
}
|
||||
for (const requirement of requirements) {
|
||||
const entries = artifact.abi.filter((entry) => entry && entry.type === requirement.type && entry.name === requirement.name);
|
||||
if (entries.length === 0) throw new Error(`${contractName} ABI is missing required ${requirement.type} ${requirement.name}`);
|
||||
if (!entries.some((entry) => matchesSignature(entry, requirement))) {
|
||||
throw new Error(`${contractName} ABI has an invalid signature for ${requirement.name}`);
|
||||
}
|
||||
}
|
||||
return artifact.abi;
|
||||
}
|
||||
|
||||
function parameter(name, type, indexed) { return { name, type, ...(indexed === undefined ? {} : { indexed }) }; }
|
||||
function functionSignature(name, inputs, outputs, stateMutability) {
|
||||
return { type: "function", name, inputs, outputs: outputs.map((type) => parameter("", type)), stateMutability };
|
||||
}
|
||||
function eventSignature(name, inputs) { return { type: "event", name, inputs, anonymous: false }; }
|
||||
function matchesSignature(entry, requirement) {
|
||||
return entry.stateMutability === requirement.stateMutability
|
||||
&& entry.anonymous === requirement.anonymous
|
||||
&& matchesParameters(entry.inputs, requirement.inputs)
|
||||
&& (requirement.outputs === undefined || matchesParameters(entry.outputs, requirement.outputs));
|
||||
}
|
||||
function matchesParameters(actual, expected) {
|
||||
return Array.isArray(actual) && actual.length === expected.length && actual.every((parameter, index) => {
|
||||
const required = expected[index];
|
||||
return parameter && parameter.name === required.name && parameter.type === required.type && parameter.indexed === required.indexed;
|
||||
});
|
||||
}
|
||||
|
||||
export function renderContractsModule(bankV1Abi, mockUsdcAbi) {
|
||||
return `/* This file is generated by tools/sync-web-artifacts.mjs. Do not edit. */\n\nexport const bankV1Abi = ${JSON.stringify(bankV1Abi, null, 2)} as const;\n\nexport const mockUsdcAbi = ${JSON.stringify(mockUsdcAbi, null, 2)} as const;\n`;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,20 @@ import { publishManifest } from "./publish-web-manifest.mjs";
|
||||
const address = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
|
||||
const owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
|
||||
const bankAbi = [
|
||||
...["contractVersion", "paused", "asset", "owner", "totalLiabilities", "balanceOf"].map((name) => ({ type: "function", name, inputs: [], outputs: [], stateMutability: "view" })),
|
||||
...["Deposited", "Withdrawn", "Paused", "Unpaused", "OwnershipTransferred", "Upgraded"].map((name) => ({ type: "event", name, inputs: [], anonymous: false })),
|
||||
{ type: "function", name: "contractVersion", inputs: [], outputs: [{ name: "", type: "uint256" }], stateMutability: "pure" },
|
||||
{ type: "function", name: "paused", inputs: [], outputs: [{ name: "", type: "bool" }], stateMutability: "view" },
|
||||
{ type: "function", name: "asset", inputs: [], outputs: [{ name: "", type: "address" }], stateMutability: "view" },
|
||||
{ type: "function", name: "owner", inputs: [], outputs: [{ name: "", type: "address" }], stateMutability: "view" },
|
||||
{ type: "function", name: "totalLiabilities", inputs: [], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" },
|
||||
{ type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" },
|
||||
{ type: "event", name: "Deposited", inputs: [{ name: "account", type: "address", indexed: true }, { name: "amount", type: "uint256", indexed: false }], anonymous: false },
|
||||
{ type: "event", name: "Withdrawn", inputs: [{ name: "account", type: "address", indexed: true }, { name: "amount", type: "uint256", indexed: false }], anonymous: false },
|
||||
{ type: "event", name: "Paused", inputs: [{ name: "account", type: "address", indexed: false }], anonymous: false },
|
||||
{ type: "event", name: "Unpaused", inputs: [{ name: "account", type: "address", indexed: false }], anonymous: false },
|
||||
{ type: "event", name: "OwnershipTransferred", inputs: [{ name: "previousOwner", type: "address", indexed: true }, { name: "newOwner", type: "address", indexed: true }], anonymous: false },
|
||||
{ type: "event", name: "Upgraded", inputs: [{ name: "implementation", type: "address", indexed: true }], anonymous: false },
|
||||
];
|
||||
const tokenAbi = [{ type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" }];
|
||||
const tokenAbi = [{ type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" }];
|
||||
const manifest = {
|
||||
schemaVersion: 1, network: "anvil", chainId: 31337, deploymentBlock: 3,
|
||||
rpcUrl: "http://127.0.0.1:8545", token: address, proxy: address,
|
||||
@@ -41,6 +51,13 @@ await withFixture(async (root) => {
|
||||
|
||||
// Catches a bridge that exports an ABI missing a V1 contract function or event.
|
||||
assert.throws(() => extractAbi({ abi: bankAbi.filter((entry) => entry.name !== "Withdrawn") }, "BankV1"), /Withdrawn/);
|
||||
// Catches generated calls/log decoders accepting ABI entries with the right name but wrong wire signature.
|
||||
const wrongFunction = structuredClone(bankAbi);
|
||||
wrongFunction.find((entry) => entry.name === "balanceOf").outputs = [];
|
||||
assert.throws(() => extractAbi({ abi: wrongFunction }, "BankV1"), /signature.*balanceOf/i);
|
||||
const wrongEvent = structuredClone(bankAbi);
|
||||
wrongEvent.find((entry) => entry.name === "Deposited").inputs[0].indexed = false;
|
||||
assert.throws(() => extractAbi({ abi: wrongEvent }, "BankV1"), /signature.*Deposited/i);
|
||||
const rendered = renderContractsModule(extractAbi({ abi: bankAbi }, "BankV1"), extractAbi({ abi: tokenAbi }, "MockUSDC"));
|
||||
assert.match(rendered, /export const bankV1Abi = .* as const;/s);
|
||||
assert.match(rendered, /export const mockUsdcAbi = .* as const;/s);
|
||||
|
||||
Reference in New Issue
Block a user