503 lines
24 KiB
JavaScript
503 lines
24 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
import { UPGRADED_TOPIC, atomicWrite, finalizeDeployment, finalizeUpgrade, preflightDeploy, runFinalizeCli, validateManifest } from "./finalize-manifest.mjs";
|
|
import { runSelectCli, selectManifest } from "./select-manifest.mjs";
|
|
|
|
const TOKEN = "0x1000000000000000000000000000000000000001";
|
|
const PROXY = "0x2000000000000000000000000000000000000002";
|
|
const IMPLEMENTATION = "0x3000000000000000000000000000000000000003";
|
|
const V2_IMPLEMENTATION = "0x4000000000000000000000000000000000000004";
|
|
const OWNER = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
|
|
const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
|
|
const EDUCATIONAL_WARNING = "Educational demo — mock token — never use real funds.";
|
|
const DECLARED_CREATE_HASH = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
const DECLARED_CALL_HASH = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
|
|
test("preflight rejects an existing target canonical manifest with the safe recovery command", async () => {
|
|
await withFixture(async (root) => {
|
|
await writeJson(join(root, "deployments", "anvil.json"), manifest());
|
|
await assert.rejects(
|
|
() => preflightDeploy({ root, network: "anvil" }),
|
|
/make reset-local/
|
|
);
|
|
await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "baseSepolia", chainId: 84532 }));
|
|
await assert.rejects(
|
|
() => preflightDeploy({ root, network: "baseSepolia" }),
|
|
/make archive-base-manifest/
|
|
);
|
|
});
|
|
});
|
|
|
|
test("direct manifest CLIs print the exact educational warning", async () => {
|
|
await withFixture(async (root) => {
|
|
const finalizeLogs = [];
|
|
await runFinalizeCli(["preflight-deploy", "anvil"], { root, log: (line) => finalizeLogs.push(line) });
|
|
assert.deepEqual(finalizeLogs, [EDUCATIONAL_WARNING]);
|
|
|
|
await writeJson(join(root, "deployments", "anvil.json"), manifest());
|
|
const selectLogs = [];
|
|
await runSelectCli(["anvil"], { root, log: (line) => selectLogs.push(line) });
|
|
assert.deepEqual(selectLogs, [EDUCATIONAL_WARNING]);
|
|
});
|
|
});
|
|
|
|
test("finalizer confirms the exact Base public URLs and Presenter/Recipient actors", async () => {
|
|
await withFixture(async (root) => {
|
|
const pending = manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0 });
|
|
await writeJson(join(root, "deployments", "pending.json"), pending);
|
|
await writeBroadcast(root, pending);
|
|
|
|
const output = await finalizeDeployment({ root, rpc: fakeRpc({ chainId: "0x14a34" }) });
|
|
assert.deepEqual(output.manifest, { ...pending, deploymentBlock: 42 });
|
|
assert.equal(output.manifest.rpcUrl, "https://sepolia.base.org");
|
|
assert.equal(output.manifest.explorerBaseUrl, "https://sepolia.basescan.org");
|
|
assert.deepEqual(output.manifest.actors, [
|
|
{ label: "Presenter", address: OWNER },
|
|
{ label: "Recipient", address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
test("finalizer accepts an Anvil manifest with omitted optional rpcUrl and preserves its absence", async () => {
|
|
await withFixture(async (root) => {
|
|
const pending = manifest({ deploymentBlock: 0 });
|
|
delete pending.rpcUrl;
|
|
await writeJson(join(root, "deployments", "pending.json"), pending);
|
|
await writeBroadcast(root, pending);
|
|
|
|
const output = await finalizeDeployment({ root, rpc: fakeRpc() });
|
|
assert.equal(Object.hasOwn(output.manifest, "rpcUrl"), false);
|
|
assert.equal(Object.hasOwn(await readJson(output.path), "rpcUrl"), false);
|
|
});
|
|
});
|
|
|
|
test("validator rejects the legacy parallel actor and explorer schema", () => {
|
|
assert.throws(
|
|
() => validateManifest(legacyManifest()),
|
|
/unknown field|missing required field/
|
|
);
|
|
});
|
|
|
|
test("finalizer writes only a receipt-confirmed manifest and preserves active until selection", async () => {
|
|
await withFixture(async (root) => {
|
|
const pending = manifest({ deploymentBlock: 0 });
|
|
await writeJson(join(root, "deployments", "pending.json"), pending);
|
|
await writeJson(join(root, "deployments", "active.json"), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 88 }));
|
|
await writeBroadcast(root, pending, { hash: "0xaaa", contractAddress: PROXY });
|
|
|
|
const output = await finalizeDeployment({ root, rpc: fakeRpc() });
|
|
assert.equal(output.path, join(root, "deployments", "anvil.json"));
|
|
assert.deepEqual(await readJson(output.path), { ...pending, deploymentBlock: 42 });
|
|
assert.deepEqual(await readJson(join(root, "deployments", "active.json")), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 88 }));
|
|
});
|
|
});
|
|
|
|
test("finalizer rejects invalid receipt, transaction, RPC, code, slot, and secret data without touching confirmed files", async () => {
|
|
const cases = [
|
|
["failed receipt", { rpc: fakeRpc({ receipt: { status: "0x0", blockNumber: "0x2a" } }) }, /not successful/],
|
|
["missing receipt", { rpc: fakeRpc({ receipt: null }) }, /missing receipt/],
|
|
["ambiguous proxy transaction", { broadcast: { extraProxy: true } }, /exactly one/],
|
|
["partial broadcast", { broadcast: { omitProxy: true } }, /exactly one/],
|
|
["non-creation proxy transaction", { broadcast: { transactionType: "CALL" } }, /exactly one/],
|
|
["wrong chain", { rpc: fakeRpc({ chainId: "0x14a34" }) }, /chain ID/],
|
|
["missing code", { rpc: fakeRpc({ missingCode: TOKEN }) }, /has no code/],
|
|
["implementation slot mismatch", { rpc: fakeRpc({ slot: TOKEN }) }, /implementation slot/],
|
|
["secret-bearing pending manifest", { pending: manifest({ rpcUrl: "https://user:password@example.invalid" }) }, /credential|secret|endpoints/i],
|
|
["mnemonic in actor label", { pending: manifest({ deploymentBlock: 0, actors: localActors("test test test test test test test test test test test junk") }) }, /prohibited/i],
|
|
["private key in actor label", { pending: manifest({ deploymentBlock: 0, actors: localActors("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") }) }, /prohibited/i],
|
|
["credential in RPC path", { pending: manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0, rpcUrl: "https://sepolia.base.org/v1/secret-token" }) }, /public URL|prohibited/i],
|
|
["unknown manifest field", { pending: manifest({ deploymentBlock: 0, harmlessLookingField: "not allowed" }) }, /unknown field/i],
|
|
];
|
|
|
|
for (const [name, options, expected] of cases) {
|
|
await withFixture(async (root) => {
|
|
const pending = options.pending ?? manifest({ deploymentBlock: 0 });
|
|
await writeJson(join(root, "deployments", "pending.json"), pending);
|
|
await writeJson(join(root, "deployments", "anvil.json"), manifest({ deploymentBlock: 7 }));
|
|
await writeJson(join(root, "deployments", "active.json"), manifest({ deploymentBlock: 8 }));
|
|
await writeBroadcast(root, pending, options.broadcast);
|
|
const beforeCanonical = await readFile(join(root, "deployments", "anvil.json"));
|
|
const beforeActive = await readFile(join(root, "deployments", "active.json"));
|
|
|
|
await assert.rejects(() => finalizeDeployment({ root, rpc: options.rpc ?? fakeRpc() }), expected, name);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), beforeCanonical, name);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive, name);
|
|
});
|
|
}
|
|
});
|
|
|
|
test("finalizer failure leaves an initially absent canonical manifest absent", async () => {
|
|
await withFixture(async (root) => {
|
|
const pending = manifest({ deploymentBlock: 0, actors: localActors("MNEMONIC") });
|
|
await writeJson(join(root, "deployments", "pending.json"), pending);
|
|
await writeJson(join(root, "deployments", "active.json"), manifest({ deploymentBlock: 8 }));
|
|
await writeBroadcast(root, pending);
|
|
|
|
await assert.rejects(() => finalizeDeployment({ root, rpc: fakeRpc() }), /prohibited/i);
|
|
await assert.rejects(() => access(join(root, "deployments", "anvil.json")));
|
|
});
|
|
});
|
|
|
|
test("upgrade finalizer verifies receipt, event, slot, artifact-driven state and changes only implementation", async () => {
|
|
await withUpgradeFixture(async (root, active) => {
|
|
const before = structuredClone(active);
|
|
const output = await finalizeUpgrade({ root, rpc: fakeUpgradeRpc() });
|
|
|
|
assert.equal(output.mode, "upgrade");
|
|
assert.equal(output.upgradeBlock, 90);
|
|
assert.deepEqual(output.manifest, { ...before, implementation: V2_IMPLEMENTATION });
|
|
for (const name of ["anvil.json", "active.json"]) {
|
|
const confirmed = await readJson(join(root, "deployments", name));
|
|
assert.deepEqual(confirmed, { ...before, implementation: V2_IMPLEMENTATION });
|
|
assert.deepEqual({ ...confirmed, implementation: before.implementation }, before);
|
|
}
|
|
await assert.rejects(() => access(join(root, "deployments", "upgrade-pending.json")));
|
|
});
|
|
});
|
|
|
|
test("upgrade finalizer rejects proxy, deployment-block, and actor identity mutation without changing confirmed files", async () => {
|
|
for (const [name, mutate] of [
|
|
["proxy", (pending) => { pending.proxy = TOKEN; }],
|
|
["deployment block", (pending) => { pending.deploymentBlock += 1; }],
|
|
["actor", (pending) => { pending.snapshot.balances[1].address = OWNER; }],
|
|
]) {
|
|
await withUpgradeFixture(async (root) => {
|
|
const pendingPath = join(root, "deployments", "upgrade-pending.json");
|
|
const pending = await readJson(pendingPath); mutate(pending); await writeJson(pendingPath, pending);
|
|
const beforeCanonical = await readFile(join(root, "deployments", "anvil.json"));
|
|
const beforeActive = await readFile(join(root, "deployments", "active.json"));
|
|
await assert.rejects(() => finalizeUpgrade({ root, rpc: fakeUpgradeRpc() }), /proxy|deployment|actor|identity/i, name);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), beforeCanonical);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive);
|
|
await access(pendingPath);
|
|
});
|
|
}
|
|
});
|
|
|
|
test("upgrade finalizer leaves confirmed files untouched for failed receipt, missing event, live mismatch, and unknown mode", async () => {
|
|
const cases = [
|
|
["failed receipt", fakeUpgradeRpc({ receiptStatus: "0x0" }), null, /successful/],
|
|
["zero receipt block", fakeUpgradeRpc({ receiptBlock: "0x0" }), null, /nonzero/],
|
|
["missing Upgraded event", fakeUpgradeRpc({ omitUpgradeLog: true }), null, /Upgraded/],
|
|
["wrong live version", fakeUpgradeRpc({ version: 1n }), null, /version/],
|
|
["slot mismatch", fakeUpgradeRpc({ slot: IMPLEMENTATION }), null, /slot/],
|
|
["owner changed", fakeUpgradeRpc({ owner: TOKEN }), null, /owner/],
|
|
["unknown mode", fakeUpgradeRpc(), (pending) => { pending.mode = "mystery"; }, /mode/],
|
|
];
|
|
for (const [name, rpc, mutate, expected] of cases) {
|
|
await withUpgradeFixture(async (root) => {
|
|
const pendingPath = join(root, "deployments", "upgrade-pending.json");
|
|
if (mutate) { const pending = await readJson(pendingPath); mutate(pending); await writeJson(pendingPath, pending); }
|
|
const beforeCanonical = await readFile(join(root, "deployments", "anvil.json"));
|
|
const beforeActive = await readFile(join(root, "deployments", "active.json"));
|
|
await assert.rejects(() => finalizeUpgrade({ root, rpc }), expected, name);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), beforeCanonical);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive);
|
|
await access(pendingPath);
|
|
});
|
|
}
|
|
});
|
|
|
|
test("noop finalizer ignores stale broadcast history and leaves confirmed manifests byte-for-byte unchanged", async () => {
|
|
await withUpgradeFixture(async (root, active) => {
|
|
active.implementation = V2_IMPLEMENTATION;
|
|
await writeJson(join(root, "deployments", "anvil.json"), active);
|
|
await writeJson(join(root, "deployments", "active.json"), active);
|
|
await writeJson(join(root, "deployments", "upgrade-pending.json"), {
|
|
mode: "noop", chainId: 31337, observedBlock: 80, ownerNonce: 5,
|
|
proxy: PROXY, implementation: V2_IMPLEMENTATION,
|
|
});
|
|
await writeJson(join(root, "broadcast", "UpgradeV2.s.sol", "31337", "run-latest.json"), {
|
|
transactions: [{ hash: "0xstale", transaction: { to: TOKEN } }], receipts: [{ status: "0x0" }],
|
|
});
|
|
const beforeCanonical = await readFile(join(root, "deployments", "anvil.json"));
|
|
const beforeActive = await readFile(join(root, "deployments", "active.json"));
|
|
|
|
const output = await finalizeUpgrade({ root, rpc: fakeUpgradeRpc() });
|
|
|
|
assert.equal(output.mode, "noop");
|
|
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), beforeCanonical);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive);
|
|
await assert.rejects(() => access(join(root, "deployments", "upgrade-pending.json")));
|
|
});
|
|
});
|
|
|
|
test("noop finalizer rejects a regressed block or changed owner nonce without touching confirmed state", async () => {
|
|
for (const [name, rpc] of [["block", fakeUpgradeRpc({ blockNumber: 79 })], ["nonce", fakeUpgradeRpc({ nonce: 6 })]]) {
|
|
await withUpgradeFixture(async (root, active) => {
|
|
active.implementation = V2_IMPLEMENTATION;
|
|
await writeJson(join(root, "deployments", "anvil.json"), active);
|
|
await writeJson(join(root, "deployments", "active.json"), active);
|
|
await writeJson(join(root, "deployments", "upgrade-pending.json"), {
|
|
mode: "noop", chainId: 31337, observedBlock: 80, ownerNonce: 5,
|
|
proxy: PROXY, implementation: V2_IMPLEMENTATION,
|
|
});
|
|
const before = await readFile(join(root, "deployments", "active.json"));
|
|
await assert.rejects(() => finalizeUpgrade({ root, rpc }), /block|nonce/i, name);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), before);
|
|
await access(join(root, "deployments", "upgrade-pending.json"));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("selection atomically replaces active with only a valid named canonical manifest", async () => {
|
|
await withFixture(async (root) => {
|
|
const anvil = manifest({ deploymentBlock: 31 });
|
|
const base = manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 32 });
|
|
await writeJson(join(root, "deployments", "anvil.json"), anvil);
|
|
await writeJson(join(root, "deployments", "base-sepolia.json"), base);
|
|
|
|
await selectManifest({ root, network: "anvil" });
|
|
const anvilBytes = await readFile(join(root, "deployments", "anvil.json"));
|
|
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), anvilBytes);
|
|
await selectManifest({ root, network: "baseSepolia" });
|
|
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), anvilBytes);
|
|
assert.deepEqual(await readJson(join(root, "deployments", "active.json")), base);
|
|
const baseBytes = await readFile(join(root, "deployments", "base-sepolia.json"));
|
|
await selectManifest({ root, network: "anvil" });
|
|
assert.deepEqual(await readFile(join(root, "deployments", "base-sepolia.json")), baseBytes);
|
|
|
|
await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0 }));
|
|
const beforeActive = await readFile(join(root, "deployments", "active.json"));
|
|
await assert.rejects(() => selectManifest({ root, network: "baseSepolia" }), /deploymentBlock/);
|
|
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive);
|
|
});
|
|
});
|
|
|
|
test("atomic writes stage a same-directory temporary file before renaming it into place", async () => {
|
|
const target = "/tmp/deployments/active.json";
|
|
const calls = [];
|
|
const io = {
|
|
writeFile: async (path, contents) => calls.push(["write", path, contents]),
|
|
rename: async (source, destination) => calls.push(["rename", source, destination]),
|
|
};
|
|
|
|
await atomicWrite(target, "confirmed", io);
|
|
|
|
assert.equal(calls[0][0], "write");
|
|
assert.equal(dirname(calls[0][1]), dirname(target));
|
|
assert.notEqual(calls[0][1], target);
|
|
assert.match(calls[0][1], /\.json$/);
|
|
assert.deepEqual(calls[1], ["rename", calls[0][1], target]);
|
|
});
|
|
|
|
test("atomic writes remove the ignored staging file when rename fails", async () => {
|
|
const target = "/tmp/deployments/active.json";
|
|
const calls = [];
|
|
const io = {
|
|
writeFile: async (path) => calls.push(["write", path]),
|
|
rename: async () => { throw new Error("rename failed"); },
|
|
rm: async (path) => calls.push(["rm", path]),
|
|
};
|
|
|
|
await assert.rejects(() => atomicWrite(target, "confirmed", io), /rename failed/);
|
|
assert.deepEqual(calls[1], ["rm", calls[0][1]]);
|
|
});
|
|
|
|
function manifest(overrides = {}) {
|
|
const baseSepolia = overrides.network === "baseSepolia";
|
|
return {
|
|
schemaVersion: 1,
|
|
network: baseSepolia ? "baseSepolia" : "anvil",
|
|
chainId: baseSepolia ? 84532 : 31337,
|
|
deploymentBlock: 1,
|
|
...(baseSepolia
|
|
? { rpcUrl: "https://sepolia.base.org", explorerBaseUrl: "https://sepolia.basescan.org" }
|
|
: { rpcUrl: "http://127.0.0.1:8545" }),
|
|
token: TOKEN,
|
|
proxy: PROXY,
|
|
implementation: IMPLEMENTATION,
|
|
owner: OWNER,
|
|
actors: baseSepolia
|
|
? [
|
|
{ label: "Presenter", address: OWNER },
|
|
{ label: "Recipient", address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" },
|
|
]
|
|
: localActors(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function legacyManifest(overrides = {}) {
|
|
return {
|
|
schemaVersion: 1,
|
|
network: "anvil",
|
|
chainId: 31337,
|
|
deploymentBlock: 1,
|
|
rpcUrl: "http://127.0.0.1:8545",
|
|
explorerUrl: "",
|
|
token: TOKEN,
|
|
proxy: PROXY,
|
|
implementation: IMPLEMENTATION,
|
|
owner: OWNER,
|
|
actorLabels: ["owner", "Alice", "Bob"],
|
|
actors: [OWNER, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function localActors(aliceLabel = "Alice") {
|
|
return [
|
|
{ label: "owner", address: OWNER },
|
|
{ label: aliceLabel, address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" },
|
|
{ label: "Bob", address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" },
|
|
];
|
|
}
|
|
|
|
async function withFixture(fn) {
|
|
const root = await mkdtemp(join(tmpdir(), "uups-finalizer-"));
|
|
try {
|
|
await import("node:fs/promises").then(({ mkdir }) => mkdir(join(root, "deployments"), { recursive: true }));
|
|
await fn(root);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function writeBroadcast(root, pending, options = {}) {
|
|
const directory = join(root, "broadcast", "DeployV1.s.sol", String(pending.chainId));
|
|
await import("node:fs/promises").then(({ mkdir }) => mkdir(directory, { recursive: true }));
|
|
const transactions = options.omitProxy
|
|
? [{ hash: "0xbbb", transactionType: "CREATE", contractAddress: pending.token }]
|
|
: [{ hash: "0xaaa", transactionType: options.transactionType ?? "CREATE", contractAddress: pending.proxy }];
|
|
if (options.extraProxy) transactions.push({ hash: "0xccc", transactionType: "CREATE", contractAddress: pending.proxy });
|
|
await writeJson(join(directory, "run-latest.json"), { transactions });
|
|
}
|
|
|
|
function fakeRpc(overrides = {}) {
|
|
return async (method, params) => {
|
|
if (method === "eth_chainId") return overrides.chainId ?? "0x7a69";
|
|
if (method === "eth_getTransactionReceipt") return Object.hasOwn(overrides, "receipt") ? overrides.receipt : { status: "0x1", blockNumber: "0x2a" };
|
|
if (method === "eth_getCode") return params[0].toLowerCase() === overrides.missingCode?.toLowerCase() ? "0x" : "0x6000";
|
|
if (method === "eth_getStorageAt") {
|
|
assert.equal(params[1], IMPLEMENTATION_SLOT);
|
|
return `0x000000000000000000000000${(overrides.slot ?? IMPLEMENTATION).slice(2)}`;
|
|
}
|
|
throw new Error(`unexpected RPC method: ${method}`);
|
|
};
|
|
}
|
|
|
|
const selectors = {
|
|
"owner()": "11111111",
|
|
"asset()": "22222222",
|
|
"paused()": "33333333",
|
|
"balanceOf(address)": "44444444",
|
|
"totalLiabilities()": "55555555",
|
|
"contractVersion()": "66666666",
|
|
};
|
|
|
|
async function withUpgradeFixture(fn) {
|
|
await withFixture(async (root) => {
|
|
const active = manifest();
|
|
await writeJson(join(root, "deployments", "anvil.json"), active);
|
|
await writeJson(join(root, "deployments", "active.json"), active);
|
|
await writeJson(join(root, "deployments", "upgrade-pending.json"), upgradePending(active));
|
|
await writeUpgradeArtifacts(root);
|
|
await writeUpgradeBroadcast(root);
|
|
await fn(root, active);
|
|
});
|
|
}
|
|
|
|
function upgradePending(active) {
|
|
return {
|
|
mode: "upgrade",
|
|
network: active.network,
|
|
chainId: active.chainId,
|
|
token: active.token,
|
|
proxy: active.proxy,
|
|
previousImplementation: active.implementation,
|
|
implementation: V2_IMPLEMENTATION,
|
|
owner: active.owner,
|
|
deploymentBlock: active.deploymentBlock,
|
|
snapshot: {
|
|
proxy: active.proxy,
|
|
implementation: active.implementation,
|
|
owner: active.owner,
|
|
asset: active.token,
|
|
paused: false,
|
|
balances: active.actors.map((actor, index) => ({ address: actor.address, balance: index === 1 ? 900_000_000 : index === 2 ? 500_000_000 : 0 })),
|
|
liabilities: 1_400_000_000,
|
|
reserves: 1_400_000_000,
|
|
surplus: 0,
|
|
deploymentBlock: active.deploymentBlock,
|
|
version: 1,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function writeUpgradeArtifacts(root) {
|
|
const bankDirectory = join(root, "out", "BankV2.sol");
|
|
const tokenDirectory = join(root, "out", "MockUSDC.sol");
|
|
const { mkdir } = await import("node:fs/promises");
|
|
await mkdir(bankDirectory, { recursive: true });
|
|
await mkdir(tokenDirectory, { recursive: true });
|
|
await writeJson(join(bankDirectory, "BankV2.json"), { methodIdentifiers: selectors });
|
|
await writeJson(join(tokenDirectory, "MockUSDC.json"), { methodIdentifiers: { "balanceOf(address)": selectors["balanceOf(address)"] } });
|
|
}
|
|
|
|
async function writeUpgradeBroadcast(root) {
|
|
const directory = join(root, "broadcast", "UpgradeV2.s.sol", "31337");
|
|
const { mkdir } = await import("node:fs/promises");
|
|
await mkdir(directory, { recursive: true });
|
|
await writeJson(join(directory, "run-latest.json"), {
|
|
transactions: [
|
|
{ hash: DECLARED_CREATE_HASH, transactionType: "CREATE", transaction: { to: null } },
|
|
{ hash: DECLARED_CALL_HASH, transactionType: "CALL", transaction: { to: PROXY } },
|
|
],
|
|
});
|
|
}
|
|
|
|
function fakeUpgradeRpc(overrides = {}) {
|
|
const balanceByAddress = new Map([
|
|
[OWNER.toLowerCase(), 0n],
|
|
["0x70997970c51812dc3a010c7d01b50e0d17dc79c8", 900_000_000n],
|
|
["0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", 500_000_000n],
|
|
]);
|
|
return async (method, params) => {
|
|
if (method === "eth_chainId") return "0x7a69";
|
|
if (method === "eth_blockNumber") return hexQuantity(overrides.blockNumber ?? 100);
|
|
if (method === "eth_getTransactionCount") return hexQuantity(overrides.nonce ?? 5);
|
|
if (method === "eth_getCode") return "0x6000";
|
|
if (method === "eth_getStorageAt") return wordForRpc(overrides.slot ?? V2_IMPLEMENTATION);
|
|
if (method === "eth_getTransactionByHash") {
|
|
return { hash: params[0], to: params[0] === DECLARED_CREATE_HASH ? PROXY : null };
|
|
}
|
|
if (method === "eth_getTransactionReceipt") return {
|
|
status: overrides.receiptStatus ?? "0x1",
|
|
blockNumber: overrides.receiptBlock ?? "0x5a",
|
|
logs: overrides.omitUpgradeLog || params[0] === DECLARED_CALL_HASH
|
|
? [] : [{ address: PROXY, topics: [UPGRADED_TOPIC, wordForRpc(V2_IMPLEMENTATION)], data: "0x" }],
|
|
};
|
|
if (method === "eth_call") {
|
|
const call = params[0];
|
|
const selector = call.data.slice(2, 10);
|
|
if (selector === selectors["owner()"]) return wordForRpc(overrides.owner ?? OWNER);
|
|
if (selector === selectors["asset()"]) return wordForRpc(TOKEN);
|
|
if (selector === selectors["paused()"]) return uintWord(0n);
|
|
if (selector === selectors["totalLiabilities()"]) return uintWord(1_400_000_000n);
|
|
if (selector === selectors["contractVersion()"]) return uintWord(overrides.version ?? 2n);
|
|
if (selector === selectors["balanceOf(address)"]) {
|
|
if (call.to.toLowerCase() === TOKEN.toLowerCase()) return uintWord(1_400_000_000n);
|
|
return uintWord(balanceByAddress.get(`0x${call.data.slice(-40)}`.toLowerCase()) ?? 0n);
|
|
}
|
|
}
|
|
throw new Error(`unexpected upgrade RPC method: ${method}`);
|
|
};
|
|
}
|
|
|
|
function wordForRpc(address) { return `0x${"0".repeat(24)}${address.slice(2).toLowerCase()}`; }
|
|
function uintWord(value) { return `0x${BigInt(value).toString(16).padStart(64, "0")}`; }
|
|
function hexQuantity(value) { return `0x${Number(value).toString(16)}`; }
|
|
|
|
async function writeJson(path, value) {
|
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
async function readJson(path) {
|
|
return JSON.parse(await readFile(path, "utf8"));
|
|
}
|