fix: harden Base manifest recovery
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
# UUPS Bank V1 Demo
|
# UUPS Bank V1 Demo
|
||||||
|
|
||||||
|
> Educational demo — mock token — never use real funds.
|
||||||
|
|
||||||
This repository is the prepared starting point for a live upgradeability lesson. It deploys a local V1 bank that custodies a six-decimal mock ERC-20, records customer balances behind an ERC-1967 proxy, and presents the result in a read-only operations console.
|
This repository is the prepared starting point for a live upgradeability lesson. It deploys a local V1 bank that custodies a six-decimal mock ERC-20, records customer balances behind an ERC-1967 proxy, and presents the result in a read-only operations console.
|
||||||
|
|
||||||
> **Trust boundary:** MockUSDC has no value. These contracts are educational and unaudited; real deposits must never be sent here. The owner can pause customer actions and install arbitrary future logic. UUPS mistakes can corrupt state or permanently brick upgradeability. A real custody product requires professional audits, operational key controls, multisig or timelocked governance, incident procedures, legal advice, and jurisdiction-specific compliance work.
|
> **Trust boundary:** MockUSDC has no value. These contracts are educational and unaudited; real deposits must never be sent here. The owner can pause customer actions and install arbitrary future logic. UUPS mistakes can corrupt state or permanently brick upgradeability. A real custody product requires professional audits, operational key controls, multisig or timelocked governance, incident procedures, legal advice, and jurisdiction-specific compliance work.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Presenter Runbook: Prepared V1 and Live UUPS Upgrade
|
# Presenter Runbook: Prepared V1 and Live UUPS Upgrade
|
||||||
|
|
||||||
|
> Educational demo — mock token — never use real funds.
|
||||||
|
|
||||||
## Preflight and rehearsal
|
## Preflight and rehearsal
|
||||||
|
|
||||||
Rehearse once from a clean disposable branch created at the `demo-start` tag. Allow 25 minutes: 3 minutes for preflight, 5 for Act 1, 10 for the Codex change and verification, 4 for Act 3, and 3 for questions. Keep two terminals visible and open the browser only after Vite reports ready.
|
Rehearse once from a clean disposable branch created at the `demo-start` tag. Allow 25 minutes: 3 minutes for preflight, 5 for Act 1, 10 for the Codex change and verification, 4 for Act 3, and 3 for questions. Keep two terminals visible and open the browser only after Vite reports ready.
|
||||||
|
|||||||
@@ -363,8 +363,15 @@ function assertManifestUrls(manifest, spec) {
|
|||||||
if (Object.hasOwn(manifest, "explorerBaseUrl")) throw new Error("manifest anvil must omit explorerBaseUrl");
|
if (Object.hasOwn(manifest, "explorerBaseUrl")) throw new Error("manifest anvil must omit explorerBaseUrl");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const field of OPTIONAL_MANIFEST_FIELDS) {
|
for (const field of ["rpcUrl", "explorerBaseUrl"]) {
|
||||||
if (Object.hasOwn(manifest, field)) assertPublicUrl(manifest[field], field);
|
if (!Object.hasOwn(manifest, field)) throw new Error(`manifest baseSepolia is missing ${field}`);
|
||||||
|
assertPublicUrl(manifest[field], field);
|
||||||
|
}
|
||||||
|
if (new URL(manifest.rpcUrl).protocol !== "https:") {
|
||||||
|
throw new Error("manifest Base Sepolia rpcUrl must use HTTPS");
|
||||||
|
}
|
||||||
|
if (manifest.explorerBaseUrl !== "https://sepolia.basescan.org") {
|
||||||
|
throw new Error("manifest Base Sepolia explorerBaseUrl must use the BaseScan root");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,14 +405,13 @@ function assertActorConfiguration(manifest) {
|
|||||||
throw new Error("manifest anvil actors must match the documented local actor configuration");
|
throw new Error("manifest anvil actors must match the documented local actor configuration");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const configured = actors.length === 2 && actors[0].label === "Presenter" && actors[1].label === "Recipient";
|
if (actors.length !== 2 || actors[0].label !== "Presenter" || actors[1].label !== "Recipient") {
|
||||||
const legacy = actors.length === 1 && actors[0].label === "owner"
|
throw new Error("manifest baseSepolia actors must contain exactly Presenter and Recipient");
|
||||||
&& !Object.hasOwn(manifest, "rpcUrl") && !Object.hasOwn(manifest, "explorerBaseUrl");
|
|
||||||
if (!configured && !legacy) {
|
|
||||||
throw new Error("manifest baseSepolia actors must be Presenter and Recipient");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (actors[0].address.toLowerCase() !== owner.toLowerCase()) throw new Error("manifest owner must be actor zero");
|
if (actors[0].address.toLowerCase() !== owner.toLowerCase()) {
|
||||||
|
throw new Error(`manifest owner must be the ${network === "baseSepolia" ? "Presenter" : "first actor"}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function rejectProhibitedStringValues(value, path = "") {
|
function rejectProhibitedStringValues(value, path = "") {
|
||||||
|
|||||||
@@ -35,11 +35,20 @@ export function validatePublicManifest(manifest) {
|
|||||||
assertActors(manifest.actors);
|
assertActors(manifest.actors);
|
||||||
for (const field of optionalFields) if (Object.hasOwn(manifest, field)) assertPublicUrl(manifest[field], field);
|
for (const field of optionalFields) if (Object.hasOwn(manifest, field)) assertPublicUrl(manifest[field], field);
|
||||||
if (manifest.network === "baseSepolia") {
|
if (manifest.network === "baseSepolia") {
|
||||||
if (Object.hasOwn(manifest, "rpcUrl") && new URL(manifest.rpcUrl).protocol !== "https:") {
|
for (const field of ["rpcUrl", "explorerBaseUrl"]) {
|
||||||
|
if (!Object.hasOwn(manifest, field)) throw new Error(`manifest baseSepolia is missing ${field}`);
|
||||||
|
}
|
||||||
|
if (new URL(manifest.rpcUrl).protocol !== "https:") {
|
||||||
throw new Error("manifest Base Sepolia rpcUrl must use HTTPS");
|
throw new Error("manifest Base Sepolia rpcUrl must use HTTPS");
|
||||||
}
|
}
|
||||||
if (Object.hasOwn(manifest, "explorerBaseUrl") && manifest.explorerBaseUrl !== "https://sepolia.basescan.org") {
|
if (manifest.explorerBaseUrl !== "https://sepolia.basescan.org") {
|
||||||
throw new Error("manifest Base Sepolia explorerBaseUrl must use BaseScan");
|
throw new Error("manifest Base Sepolia explorerBaseUrl must use the BaseScan root");
|
||||||
|
}
|
||||||
|
if (manifest.actors.length !== 2 || manifest.actors[0].label !== "Presenter" || manifest.actors[1].label !== "Recipient") {
|
||||||
|
throw new Error("manifest baseSepolia actors must contain exactly Presenter and Recipient");
|
||||||
|
}
|
||||||
|
if (manifest.owner.toLowerCase() !== manifest.actors[0].address.toLowerCase()) {
|
||||||
|
throw new Error("manifest owner must be the Presenter");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { access, readFile, rename } from "node:fs/promises";
|
import { link, readFile, rm } from "node:fs/promises";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export async function selectManifest({ root = process.cwd(), network }) {
|
|||||||
return { source, active };
|
return { source, active };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function archiveManifest({ root = process.cwd(), network, now = new Date() }) {
|
export async function archiveManifest({ root = process.cwd(), network, now = new Date(), io = {} }) {
|
||||||
if (network !== "baseSepolia") throw new Error("only baseSepolia may be archived");
|
if (network !== "baseSepolia") throw new Error("only baseSepolia may be archived");
|
||||||
const spec = networkSpec(network);
|
const spec = networkSpec(network);
|
||||||
const source = join(root, "deployments", spec.canonical);
|
const source = join(root, "deployments", spec.canonical);
|
||||||
@@ -23,16 +23,20 @@ export async function archiveManifest({ root = process.cwd(), network, now = new
|
|||||||
const timestamp = now.toISOString().replace(/[-:.]/g, "");
|
const timestamp = now.toISOString().replace(/[-:.]/g, "");
|
||||||
if (!/^\d{8}T\d{9}Z$/.test(timestamp)) throw new Error("archive timestamp is invalid");
|
if (!/^\d{8}T\d{9}Z$/.test(timestamp)) throw new Error("archive timestamp is invalid");
|
||||||
const target = join(root, "deployments", `base-sepolia.${timestamp}.json`);
|
const target = join(root, "deployments", `base-sepolia.${timestamp}.json`);
|
||||||
|
const operations = { link, rm, ...io };
|
||||||
try {
|
try {
|
||||||
await access(target);
|
await operations.link(source, target);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "ENOENT") {
|
if (error.code === "EEXIST") throw new Error("refusing to overwrite an existing Base Sepolia archive");
|
||||||
await rename(source, target);
|
|
||||||
return { source, path: target };
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new Error("refusing to overwrite an existing Base Sepolia archive");
|
try {
|
||||||
|
await operations.rm(source);
|
||||||
|
} catch (error) {
|
||||||
|
await operations.rm(target, { force: true }).catch(() => {});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return { source, path: target };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runSelectCli(argv, { root = process.cwd(), log = console.log } = {}) {
|
export async function runSelectCli(argv, { root = process.cwd(), log = console.log } = {}) {
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ import { pathToFileURL } from "node:url";
|
|||||||
|
|
||||||
const repositoryRoot = process.argv[2];
|
const repositoryRoot = process.argv[2];
|
||||||
const { archiveManifest, selectManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/select-manifest.mjs")));
|
const { archiveManifest, selectManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/select-manifest.mjs")));
|
||||||
const { publishManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/publish-web-manifest.mjs")));
|
const { validateManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/finalize-manifest.mjs")));
|
||||||
|
const { publishManifest, validatePublicManifest } = await import(pathToFileURL(join(repositoryRoot, "tools/publish-web-manifest.mjs")));
|
||||||
const root = await mkdtemp(join(tmpdir(), "uups-base-config-"));
|
const root = await mkdtemp(join(tmpdir(), "uups-base-config-"));
|
||||||
const deployments = join(root, "deployments");
|
const deployments = join(root, "deployments");
|
||||||
const webManifest = join(root, "web/public/deployment.json");
|
const webManifest = join(root, "web/public/deployment.json");
|
||||||
@@ -157,14 +158,29 @@ try {
|
|||||||
assert.equal(published.rpcUrl, "https://public.invalid/rpc");
|
assert.equal(published.rpcUrl, "https://public.invalid/rpc");
|
||||||
assert.equal(published.explorerBaseUrl, "https://sepolia.basescan.org");
|
assert.equal(published.explorerBaseUrl, "https://sepolia.basescan.org");
|
||||||
assert.equal((await readFile(webManifest, "utf8")).includes("terminal.invalid"), false);
|
assert.equal((await readFile(webManifest, "utf8")).includes("terminal.invalid"), false);
|
||||||
for (const invalid of [
|
const without = (field) => {
|
||||||
{ ...base, rpcUrl: "http://public.invalid/rpc" },
|
const manifest = { ...base };
|
||||||
{ ...base, explorerBaseUrl: "https://example.invalid" },
|
delete manifest[field];
|
||||||
]) {
|
return manifest;
|
||||||
|
};
|
||||||
|
const invalidCases = [
|
||||||
|
["missing rpcUrl", without("rpcUrl"), /rpcUrl/],
|
||||||
|
["missing explorerBaseUrl", without("explorerBaseUrl"), /explorerBaseUrl/],
|
||||||
|
["wrong actor count", { ...base, actors: [base.actors[0]] }, /exactly Presenter and Recipient|actor configuration/],
|
||||||
|
["wrong actor labels", { ...base, actors: [{ ...base.actors[0], label: "Owner" }, base.actors[1]] }, /Presenter and Recipient|actor configuration/],
|
||||||
|
["owner is not Presenter", { ...base, owner: recipient }, /owner.*Presenter|first actor/],
|
||||||
|
["HTTP public RPC", { ...base, rpcUrl: "http://public.invalid/rpc" }, /HTTPS/],
|
||||||
|
["credential-bearing public RPC", { ...base, rpcUrl: "https://user@public.invalid/rpc" }, /credentials|prohibited secret/],
|
||||||
|
["wrong BaseScan root", { ...base, explorerBaseUrl: "https://example.invalid" }, /BaseScan/],
|
||||||
|
];
|
||||||
|
for (const [label, invalid, rejection] of invalidCases) {
|
||||||
|
assert.throws(() => validateManifest(invalid), rejection, `confirmation accepted ${label}`);
|
||||||
|
assert.throws(() => validatePublicManifest(invalid), rejection, `publication validation accepted ${label}`);
|
||||||
await writeFile(join(deployments, "active.json"), `${JSON.stringify(invalid)}\n`);
|
await writeFile(join(deployments, "active.json"), `${JSON.stringify(invalid)}\n`);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => publishManifest({ activePath: join(deployments, "active.json"), outputPath: webManifest }),
|
() => publishManifest({ activePath: join(deployments, "active.json"), outputPath: webManifest }),
|
||||||
/HTTPS|BaseScan/,
|
rejection,
|
||||||
|
`publication accepted ${label}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await writeFile(join(deployments, "active.json"), activeBefore);
|
await writeFile(join(deployments, "active.json"), activeBefore);
|
||||||
@@ -179,6 +195,42 @@ try {
|
|||||||
await assert.rejects(() => access(join(deployments, "base-sepolia.json")));
|
await assert.rejects(() => access(join(deployments, "base-sepolia.json")));
|
||||||
assert.deepEqual(await readFile(join(deployments, "active.json")), activeBefore);
|
assert.deepEqual(await readFile(join(deployments, "active.json")), activeBefore);
|
||||||
assert.deepEqual(await readFile(join(deployments, "anvil.json")), anvilBefore);
|
assert.deepEqual(await readFile(join(deployments, "anvil.json")), anvilBefore);
|
||||||
|
|
||||||
|
const canonicalPath = join(deployments, "base-sepolia.json");
|
||||||
|
const canonicalBytes = Buffer.from(`${JSON.stringify(base)}\n`);
|
||||||
|
const existingBytes = Buffer.from("existing archive bytes\n");
|
||||||
|
const collisionDate = new Date("2026-08-21T12:35:56.789Z");
|
||||||
|
const collisionPath = join(deployments, "base-sepolia.20260821T123556789Z.json");
|
||||||
|
await writeFile(canonicalPath, canonicalBytes);
|
||||||
|
await writeFile(collisionPath, existingBytes);
|
||||||
|
await assert.rejects(
|
||||||
|
() => archiveManifest({ root, network: "baseSepolia", now: collisionDate }),
|
||||||
|
/existing Base Sepolia archive/,
|
||||||
|
);
|
||||||
|
assert.deepEqual(await readFile(canonicalPath), canonicalBytes);
|
||||||
|
assert.deepEqual(await readFile(collisionPath), existingBytes);
|
||||||
|
|
||||||
|
const boundaryDate = new Date("2026-08-21T12:36:56.789Z");
|
||||||
|
const boundaryPath = join(deployments, "base-sepolia.20260821T123656789Z.json");
|
||||||
|
const boundaryBytes = Buffer.from("archive created by racing process\n");
|
||||||
|
await assert.rejects(
|
||||||
|
() => archiveManifest({
|
||||||
|
root,
|
||||||
|
network: "baseSepolia",
|
||||||
|
now: boundaryDate,
|
||||||
|
io: {
|
||||||
|
link: async (_source, target) => {
|
||||||
|
await writeFile(target, boundaryBytes);
|
||||||
|
const error = new Error("collision at link boundary");
|
||||||
|
error.code = "EEXIST";
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/existing Base Sepolia archive/,
|
||||||
|
);
|
||||||
|
assert.deepEqual(await readFile(canonicalPath), canonicalBytes);
|
||||||
|
assert.deepEqual(await readFile(boundaryPath), boundaryBytes);
|
||||||
await assert.rejects(() => archiveManifest({ root, network: "anvil" }), /baseSepolia/);
|
await assert.rejects(() => archiveManifest({ root, network: "anvil" }), /baseSepolia/);
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true });
|
await rm(root, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ test("direct manifest CLIs print the exact educational warning", async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("finalizer confirms a nested public actor manifest and preserves omitted Base URLs", async () => {
|
test("finalizer confirms the exact Base public URLs and Presenter/Recipient actors", async () => {
|
||||||
await withFixture(async (root) => {
|
await withFixture(async (root) => {
|
||||||
const pending = manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0 });
|
const pending = manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0 });
|
||||||
await writeJson(join(root, "deployments", "pending.json"), pending);
|
await writeJson(join(root, "deployments", "pending.json"), pending);
|
||||||
@@ -53,9 +53,12 @@ test("finalizer confirms a nested public actor manifest and preserves omitted Ba
|
|||||||
|
|
||||||
const output = await finalizeDeployment({ root, rpc: fakeRpc({ chainId: "0x14a34" }) });
|
const output = await finalizeDeployment({ root, rpc: fakeRpc({ chainId: "0x14a34" }) });
|
||||||
assert.deepEqual(output.manifest, { ...pending, deploymentBlock: 42 });
|
assert.deepEqual(output.manifest, { ...pending, deploymentBlock: 42 });
|
||||||
assert.equal(Object.hasOwn(output.manifest, "rpcUrl"), false);
|
assert.equal(output.manifest.rpcUrl, "https://sepolia.base.org");
|
||||||
assert.equal(Object.hasOwn(output.manifest, "explorerBaseUrl"), false);
|
assert.equal(output.manifest.explorerBaseUrl, "https://sepolia.basescan.org");
|
||||||
assert.deepEqual(output.manifest.actors, [{ label: "owner", address: OWNER }]);
|
assert.deepEqual(output.manifest.actors, [
|
||||||
|
{ label: "Presenter", address: OWNER },
|
||||||
|
{ label: "Recipient", address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,6 +148,7 @@ test("upgrade finalizer verifies receipt, event, slot, artifact-driven state and
|
|||||||
const output = await finalizeUpgrade({ root, rpc: fakeUpgradeRpc() });
|
const output = await finalizeUpgrade({ root, rpc: fakeUpgradeRpc() });
|
||||||
|
|
||||||
assert.equal(output.mode, "upgrade");
|
assert.equal(output.mode, "upgrade");
|
||||||
|
assert.equal(output.upgradeBlock, 90);
|
||||||
assert.deepEqual(output.manifest, { ...before, implementation: V2_IMPLEMENTATION });
|
assert.deepEqual(output.manifest, { ...before, implementation: V2_IMPLEMENTATION });
|
||||||
for (const name of ["anvil.json", "active.json"]) {
|
for (const name of ["anvil.json", "active.json"]) {
|
||||||
const confirmed = await readJson(join(root, "deployments", name));
|
const confirmed = await readJson(join(root, "deployments", name));
|
||||||
@@ -177,6 +181,7 @@ test("upgrade finalizer rejects proxy, deployment-block, and actor identity muta
|
|||||||
test("upgrade finalizer leaves confirmed files untouched for failed receipt, missing event, live mismatch, and unknown mode", async () => {
|
test("upgrade finalizer leaves confirmed files untouched for failed receipt, missing event, live mismatch, and unknown mode", async () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
["failed receipt", fakeUpgradeRpc({ receiptStatus: "0x0" }), null, /successful/],
|
["failed receipt", fakeUpgradeRpc({ receiptStatus: "0x0" }), null, /successful/],
|
||||||
|
["zero receipt block", fakeUpgradeRpc({ receiptBlock: "0x0" }), null, /nonzero/],
|
||||||
["missing Upgraded event", fakeUpgradeRpc({ omitUpgradeLog: true }), null, /Upgraded/],
|
["missing Upgraded event", fakeUpgradeRpc({ omitUpgradeLog: true }), null, /Upgraded/],
|
||||||
["wrong live version", fakeUpgradeRpc({ version: 1n }), null, /version/],
|
["wrong live version", fakeUpgradeRpc({ version: 1n }), null, /version/],
|
||||||
["slot mismatch", fakeUpgradeRpc({ slot: IMPLEMENTATION }), null, /slot/],
|
["slot mismatch", fakeUpgradeRpc({ slot: IMPLEMENTATION }), null, /slot/],
|
||||||
@@ -300,12 +305,19 @@ function manifest(overrides = {}) {
|
|||||||
network: baseSepolia ? "baseSepolia" : "anvil",
|
network: baseSepolia ? "baseSepolia" : "anvil",
|
||||||
chainId: baseSepolia ? 84532 : 31337,
|
chainId: baseSepolia ? 84532 : 31337,
|
||||||
deploymentBlock: 1,
|
deploymentBlock: 1,
|
||||||
...(baseSepolia ? {} : { rpcUrl: "http://127.0.0.1:8545" }),
|
...(baseSepolia
|
||||||
|
? { rpcUrl: "https://sepolia.base.org", explorerBaseUrl: "https://sepolia.basescan.org" }
|
||||||
|
: { rpcUrl: "http://127.0.0.1:8545" }),
|
||||||
token: TOKEN,
|
token: TOKEN,
|
||||||
proxy: PROXY,
|
proxy: PROXY,
|
||||||
implementation: IMPLEMENTATION,
|
implementation: IMPLEMENTATION,
|
||||||
owner: OWNER,
|
owner: OWNER,
|
||||||
actors: baseSepolia ? [{ label: "owner", address: OWNER }] : localActors(),
|
actors: baseSepolia
|
||||||
|
? [
|
||||||
|
{ label: "Presenter", address: OWNER },
|
||||||
|
{ label: "Recipient", address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" },
|
||||||
|
]
|
||||||
|
: localActors(),
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -456,7 +468,7 @@ function fakeUpgradeRpc(overrides = {}) {
|
|||||||
}
|
}
|
||||||
if (method === "eth_getTransactionReceipt") return {
|
if (method === "eth_getTransactionReceipt") return {
|
||||||
status: overrides.receiptStatus ?? "0x1",
|
status: overrides.receiptStatus ?? "0x1",
|
||||||
blockNumber: "0x5a",
|
blockNumber: overrides.receiptBlock ?? "0x5a",
|
||||||
logs: overrides.omitUpgradeLog || params[0] === DECLARED_CALL_HASH
|
logs: overrides.omitUpgradeLog || params[0] === DECLARED_CALL_HASH
|
||||||
? [] : [{ address: PROXY, topics: [UPGRADED_TOPIC, wordForRpc(V2_IMPLEMENTATION)], data: "0x" }],
|
? [] : [{ address: PROXY, topics: [UPGRADED_TOPIC, wordForRpc(V2_IMPLEMENTATION)], data: "0x" }],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user