From 54c893fdb73e5bb0d75ff0883f23bc01db092b81 Mon Sep 17 00:00:00 2001 From: golem Date: Fri, 21 Aug 2026 03:00:23 -0600 Subject: [PATCH] fix: enforce optional manifest fields --- script/lib/DemoScript.sol | 14 +++++++++++ test/ScriptPreflight.t.sol | 43 ++++++++++++++++++++++++++++++++ tools/finalize-manifest.mjs | 4 ++- tools/test-finalize-manifest.mjs | 13 ++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/script/lib/DemoScript.sol b/script/lib/DemoScript.sol index 1cc026c..769cd12 100644 --- a/script/lib/DemoScript.sol +++ b/script/lib/DemoScript.sol @@ -116,6 +116,7 @@ abstract contract DemoScript is Script { actors = new Actor[](actorCount); for (uint256 i; i < actorCount; ++i) { string memory index = vm.toString(i); + _assertExactActorSchema(json, index); actors[i].label = vm.parseJsonString(json, string.concat(".actors[", index, "].label")); actors[i].address_ = vm.parseJsonAddress(json, string.concat(".actors[", index, "].address")); } @@ -124,6 +125,19 @@ abstract contract DemoScript is Script { } } + function _assertExactActorSchema(string memory json, string memory index) private view { + string[] memory keys = vm.parseJsonKeys(json, string.concat(".actors[", index, "]")); + bool hasLabel; + bool hasAddress; + if (keys.length != 2) revert InvalidManifestSchema(0); + for (uint256 i; i < keys.length; ++i) { + if (_equals(keys[i], "label")) hasLabel = true; + else if (_equals(keys[i], "address")) hasAddress = true; + else revert InvalidManifestSchema(0); + } + if (!hasLabel || !hasAddress) revert InvalidManifestSchema(0); + } + function _validateActorConfiguration(Manifest memory manifest) internal pure { if (manifest.chainId == ANVIL_CHAIN_ID) { if ( diff --git a/test/ScriptPreflight.t.sol b/test/ScriptPreflight.t.sol index a9a381a..7f1f96b 100644 --- a/test/ScriptPreflight.t.sol +++ b/test/ScriptPreflight.t.sol @@ -225,6 +225,20 @@ contract ScriptPreflightTest is Test { harness.readManifest(path, true); } + function testActorObjectWithExtraKeyIsRejected() public { + string memory path = _writeAnvilManifestWithExtraActorKey("note", "public"); + _etchManifestContracts(); + vm.expectRevert(abi.encodeWithSelector(DemoScript.InvalidManifestSchema.selector, uint256(0))); + harness.readManifest(path, true); + } + + function testActorObjectWithSecretBearingKeyIsRejected() public { + string memory path = _writeAnvilManifestWithExtraActorKey("privateKey", "not-a-key"); + _etchManifestContracts(); + vm.expectRevert(abi.encodeWithSelector(DemoScript.InvalidManifestSchema.selector, uint256(0))); + harness.readManifest(path, true); + } + function testBaseSepoliaManifestUsesCamelCaseAndOmitsUnavailableUrls() public { string memory path = _writePublicBaseManifest(); vm.chainId(84532); @@ -404,6 +418,35 @@ contract ScriptPreflightTest is Test { ); } + function _writeAnvilManifestWithExtraActorKey(string memory key, string memory value) + internal + returns (string memory path) + { + path = string.concat(fixtureDir, "/extra-actor-key.json"); + vm.writeFile( + path, + string.concat( + '{"schemaVersion":1,"network":"anvil","chainId":31337,"deploymentBlock":1,"rpcUrl":"http://127.0.0.1:8545","token":"', + vm.toString(TOKEN), + '","proxy":"', + vm.toString(PROXY), + '","implementation":"', + vm.toString(IMPLEMENTATION), + '","owner":"', + vm.toString(OWNER), + '","actors":[{"label":"owner","address":"', + vm.toString(OWNER), + '","', + key, + '":"', + value, + '"},{"label":"Alice","address":"', + vm.toString(ALICE), + '"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}' + ) + ); + } + function _writePublicBaseManifest() internal returns (string memory path) { path = string.concat(fixtureDir, "/public-base-manifest.json"); vm.writeFile( diff --git a/tools/finalize-manifest.mjs b/tools/finalize-manifest.mjs index 4aa4c1d..47c2b78 100644 --- a/tools/finalize-manifest.mjs +++ b/tools/finalize-manifest.mjs @@ -159,7 +159,9 @@ function assertExactSchema(manifest) { function assertManifestUrls(manifest, spec) { if (manifest.network === "anvil") { - if (manifest.rpcUrl !== spec.rpcUrl) throw new Error("manifest anvil rpcUrl must use the local public endpoint"); + if (Object.hasOwn(manifest, "rpcUrl") && manifest.rpcUrl !== spec.rpcUrl) { + throw new Error("manifest anvil rpcUrl must use the local public endpoint"); + } if (Object.hasOwn(manifest, "explorerBaseUrl")) throw new Error("manifest anvil must omit explorerBaseUrl"); return; } diff --git a/tools/test-finalize-manifest.mjs b/tools/test-finalize-manifest.mjs index d1840fb..7b4e1eb 100644 --- a/tools/test-finalize-manifest.mjs +++ b/tools/test-finalize-manifest.mjs @@ -56,6 +56,19 @@ test("finalizer confirms a nested public actor manifest and preserves omitted Ba }); }); +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()),