// SPDX-License-Identifier: MIT pragma solidity 0.8.35; import {Test} from "forge-std/Test.sol"; import {DemoScript} from "../script/lib/DemoScript.sol"; import {DeployV1} from "../script/DeployV1.s.sol"; import {SeedV1Demo} from "../script/SeedV1Demo.s.sol"; import {CheckState} from "../script/CheckState.s.sol"; import {BankV1} from "../src/BankV1.sol"; import {MockUSDC} from "../src/MockUSDC.sol"; contract ScriptPreflightHarness is DemoScript { function requireSupportedChain(uint256 chainId) external pure { _requireSupportedChain(chainId); } function deriveLocalActor(uint256 chainId, uint32 index) external pure returns (address actor) { (, actor) = _deriveLocalActor(chainId, index); } function readManifest(string calldata path, bool active) external view returns (Manifest memory) { return _readManifest(path, active); } function requireCode(string calldata label, address target) external view { _requireCode(label, target); } function serializeManifest(Manifest calldata manifest) external returns (string memory) { return _serializeManifest(manifest); } function educationalWarning() external pure returns (string memory) { return _educationalWarning(); } } contract ScriptPreflightTest is Test { ScriptPreflightHarness internal harness; string internal fixtureDir; address internal constant OWNER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; address internal constant ALICE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; address internal constant TOKEN = 0x1000000000000000000000000000000000000001; address internal constant PROXY = 0x2000000000000000000000000000000000000002; address internal constant IMPLEMENTATION = 0x3000000000000000000000000000000000000003; function setUp() public { harness = new ScriptPreflightHarness(); fixtureDir = string.concat(vm.projectRoot(), "/deployments/test-script-preflight"); vm.createDir(fixtureDir, true); } function testSupportedChainsAreAccepted() public view { harness.requireSupportedChain(31337); harness.requireSupportedChain(84532); } function testUnsupportedChainsAreRejectedBeforeBroadcast() public { uint256[3] memory rejected = [uint256(1), uint256(8453), uint256(7777777)]; for (uint256 i; i < rejected.length; ++i) { vm.expectRevert(abi.encodeWithSelector(DemoScript.UnsupportedChain.selector, rejected[i])); harness.requireSupportedChain(rejected[i]); } } function testLocalActorsDeriveOnlyOnAnvil() public { assertEq(harness.deriveLocalActor(31337, 0), OWNER); assertEq(harness.deriveLocalActor(31337, 1), ALICE); vm.expectRevert(abi.encodeWithSelector(DemoScript.UnsupportedChain.selector, uint256(84532))); harness.deriveLocalActor(84532, 0); } function testMissingManifestIsRejected() public { vm.expectRevert(); harness.readManifest(string.concat(fixtureDir, "/missing.json"), false); } function testInvalidManifestIsRejected() public { string memory path = string.concat(fixtureDir, "/invalid.json"); vm.writeFile(path, "not-json"); vm.expectRevert(); harness.readManifest(path, false); } function testWrongManifestChainIsRejected() public { string memory path = _writeManifest(84532, 1, TOKEN, PROXY, IMPLEMENTATION); vm.chainId(31337); vm.expectRevert( abi.encodeWithSelector(DemoScript.ManifestChainMismatch.selector, uint256(31337), uint256(84532)) ); harness.readManifest(path, true); } function testZeroManifestAddressIsRejected() public { string memory path = _writeManifest(31337, 1, address(0), PROXY, IMPLEMENTATION); vm.chainId(31337); vm.expectRevert(abi.encodeWithSelector(DemoScript.MissingCode.selector, "token", address(0))); harness.readManifest(path, true); } function testAddressWithoutCodeIsRejected() public { vm.expectRevert(abi.encodeWithSelector(DemoScript.MissingCode.selector, "token", TOKEN)); harness.requireCode("token", TOKEN); } function testPendingManifestMayUseDeploymentBlockZero() public { string memory path = _writeManifest(31337, 0, TOKEN, PROXY, IMPLEMENTATION); vm.chainId(31337); vm.etch(TOKEN, hex"00"); vm.etch(PROXY, hex"00"); vm.etch(IMPLEMENTATION, hex"00"); DemoScript.Manifest memory manifest = harness.readManifest(path, false); assertEq(manifest.deploymentBlock, 0); } function testActiveManifestRejectsDeploymentBlockZero() public { string memory path = _writeManifest(31337, 0, TOKEN, PROXY, IMPLEMENTATION); vm.chainId(31337); vm.etch(TOKEN, hex"00"); vm.etch(PROXY, hex"00"); vm.etch(IMPLEMENTATION, hex"00"); vm.expectRevert(DemoScript.InvalidDeploymentBlock.selector); harness.readManifest(path, true); } function testAnvilManifestRejectsUnexpectedActorObjectCount() public { string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION); _etchManifestContracts(); vm.writeJson( string.concat( '[{"label":"owner","address":"', vm.toString(OWNER), '"},{"label":"Alice","address":"', vm.toString(ALICE), '"}]' ), path, ".actors" ); vm.expectRevert(); harness.readManifest(path, true); } function testAnvilManifestRejectsUnexpectedActorLabels() public { string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION); _etchManifestContracts(); vm.writeJson( string.concat( '[{"label":"owner","address":"', vm.toString(OWNER), '"},{"label":"Mallory","address":"', vm.toString(ALICE), '"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]' ), path, ".actors" ); vm.expectRevert(DemoScript.InvalidActorConfiguration.selector); harness.readManifest(path, true); } function testAnvilManifestRequiresOwnerAtActorZero() public { string memory path = _writeManifest(31337, 1, TOKEN, PROXY, IMPLEMENTATION); _etchManifestContracts(); vm.writeJson( string.concat( '[{"label":"owner","address":"', vm.toString(ALICE), '"},{"label":"Alice","address":"', vm.toString(OWNER), '"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]' ), path, ".actors" ); vm.expectRevert(DemoScript.InvalidActorConfiguration.selector); harness.readManifest(path, true); } function testBaseManifestRequiresOnlyOwnerActor() public { string memory path = _writePublicBaseManifest(); vm.chainId(84532); _etchManifestContracts(); vm.writeJson( string.concat( '[{"label":"owner","address":"', vm.toString(OWNER), '"},{"label":"Alice","address":"', vm.toString(ALICE), '"}]' ), path, ".actors" ); vm.expectRevert(DemoScript.InvalidActorConfiguration.selector); harness.readManifest(path, true); } function testEducationalWarningIsExact() public view { assertEq(harness.educationalWarning(), unicode"Educational demo — mock token — never use real funds."); } function testNestedActorsRoundTripThroughManifestSerialization() public { string memory path = _writePublicAnvilManifest(); _etchManifestContracts(); DemoScript.Manifest memory manifest = harness.readManifest(path, true); assertEq(manifest.actors[0].label, "owner"); assertEq(manifest.actors[1].address_, ALICE); string memory serialized = harness.serializeManifest(manifest); assertFalse(_contains(serialized, "actorLabels")); assertFalse(_contains(serialized, "explorerUrl")); vm.writeFile(path, serialized); DemoScript.Manifest memory roundTrip = harness.readManifest(path, true); assertEq(roundTrip.actors[2].label, "Bob"); assertEq(roundTrip.actors[2].address_, 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC); } function testLegacyParallelActorManifestIsRejected() public { string memory path = _writeLegacyParallelActorManifest(); _etchManifestContracts(); vm.expectRevert(abi.encodeWithSelector(DemoScript.InvalidManifestSchema.selector, uint256(0))); 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); _etchManifestContracts(); DemoScript.Manifest memory manifest = harness.readManifest(path, true); assertEq(manifest.network, "baseSepolia"); assertEq(manifest.rpcUrl, ""); assertEq(manifest.explorerBaseUrl, ""); assertEq(manifest.actors[0].label, "owner"); assertEq(manifest.actors[0].address_, OWNER); } function testSerializedManifestContainsPublicAddressesAndNoSecrets() public { DemoScript.Manifest memory manifest = DemoScript.Manifest({ schemaVersion: 1, network: "anvil", chainId: 31337, deploymentBlock: 0, rpcUrl: "http://127.0.0.1:8545", explorerBaseUrl: "", token: TOKEN, proxy: PROXY, implementation: IMPLEMENTATION, owner: OWNER, actors: _actors() }); string memory json = harness.serializeManifest(manifest); assertTrue(_contains(json, vm.toString(TOKEN))); assertTrue(_contains(json, vm.toString(PROXY))); assertTrue(_contains(json, vm.toString(IMPLEMENTATION))); assertTrue(_contains(json, vm.toString(OWNER))); assertFalse(_contains(json, "test test test test test test test test test test test junk")); assertFalse(_contains(json, "PRIVATE_KEY")); assertFalse(_contains(json, "MNEMONIC")); assertFalse(_contains(json, "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")); } function testDeployV1CreatesInitializedProxyAndPublicPendingManifest() public { string memory path = string.concat(fixtureDir, "/deployed.json"); vm.chainId(31337); vm.setEnv("SCRIPT_SENDER", vm.toString(OWNER)); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); DeployV1 deployer = new DeployV1(); (address token, address proxy, address implementation) = deployer.run(); DemoScript.Manifest memory manifest = harness.readManifest(path, false); assertEq(manifest.token, token); assertEq(manifest.proxy, proxy); assertEq(manifest.implementation, implementation); assertEq(BankV1(proxy).owner(), OWNER); assertEq(address(BankV1(proxy).asset()), token); assertEq(BankV1(proxy).contractVersion(), 1); assertEq(manifest.actors[0].label, "owner"); assertEq(manifest.actors[1].label, "Alice"); assertEq(manifest.actors[2].label, "Bob"); assertEq(manifest.actors[0].address_, OWNER); assertEq(manifest.actors[1].address_, ALICE); assertEq(manifest.actors[2].address_, 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC); } function testSeedV1DemoExecutesExactActOneStateAndCheckStateAcceptsIt() public { (string memory path, DemoScript.Manifest memory manifest) = _deployFixture(); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); new SeedV1Demo().run(); BankV1 bank = BankV1(manifest.proxy); assertEq(bank.balanceOf(manifest.actors[1].address_), 900e6); assertEq(bank.balanceOf(manifest.actors[2].address_), 500e6); assertEq(bank.totalLiabilities(), 1_400e6); assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6); assertEq(MockUSDC(manifest.token).balanceOf(manifest.actors[1].address_), 1_100e6); assertEq(MockUSDC(manifest.token).balanceOf(manifest.actors[2].address_), 500e6); vm.setEnv("DEMO_EXPECTED_STAGE", "v1"); new CheckState().run(); } function testCheckStateRejectsManifestImplementationMismatch() public { (string memory path, DemoScript.Manifest memory manifest) = _deployFixture(); vm.writeJson(string.concat('"', vm.toString(manifest.token), '"'), path, ".implementation"); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); vm.setEnv("DEMO_EXPECTED_STAGE", "deployed"); CheckState checker = new CheckState(); vm.expectRevert( abi.encodeWithSelector( DemoScript.UnexpectedAddress.selector, "implementation", manifest.token, manifest.implementation ) ); checker.run(); } function _writeManifest( uint256 chainId, uint256 deploymentBlock, address token, address proxy, address implementation ) internal returns (string memory path) { path = string.concat(fixtureDir, "/manifest.json"); string memory json = string.concat( '{"schemaVersion":1,"network":"anvil","chainId":', vm.toString(chainId), ',"deploymentBlock":', vm.toString(deploymentBlock), ',"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), '"},{"label":"Alice","address":"', vm.toString(ALICE), '"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}' ); vm.writeFile(path, json); } function _deployFixture() internal returns (string memory path, DemoScript.Manifest memory manifest) { path = string.concat(fixtureDir, "/deployed.json"); vm.chainId(31337); vm.setEnv("SCRIPT_SENDER", vm.toString(OWNER)); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); new DeployV1().run(); vm.writeJson("1", path, ".deploymentBlock"); manifest = harness.readManifest(path, true); } function _writePublicAnvilManifest() internal returns (string memory path) { path = string.concat(fixtureDir, "/public-anvil-manifest.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), '"},{"label":"Alice","address":"', vm.toString(ALICE), '"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}' ) ); } function _writeLegacyParallelActorManifest() internal returns (string memory path) { path = string.concat(fixtureDir, "/legacy-parallel-actors.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), '","actorLabels":["owner","Alice","Bob"],"actors":[{"label":"owner","address":"', vm.toString(OWNER), '"},{"label":"Alice","address":"', vm.toString(ALICE), '"},{"label":"Bob","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}]}' ) ); } 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( path, string.concat( '{"schemaVersion":1,"network":"baseSepolia","chainId":84532,"deploymentBlock":1,"token":"', vm.toString(TOKEN), '","proxy":"', vm.toString(PROXY), '","implementation":"', vm.toString(IMPLEMENTATION), '","owner":"', vm.toString(OWNER), '","actors":[{"label":"owner","address":"', vm.toString(OWNER), '"}]}' ) ); } function _etchManifestContracts() internal { vm.etch(TOKEN, hex"00"); vm.etch(PROXY, hex"00"); vm.etch(IMPLEMENTATION, hex"00"); } function _actors() internal pure returns (DemoScript.Actor[] memory actors) { actors = new DemoScript.Actor[](3); actors[0] = DemoScript.Actor({label: "owner", address_: OWNER}); actors[1] = DemoScript.Actor({label: "Alice", address_: ALICE}); actors[2] = DemoScript.Actor({label: "Bob", address_: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC}); } function _contains(string memory haystack, string memory needle) internal pure returns (bool) { bytes memory h = bytes(haystack); bytes memory n = bytes(needle); if (n.length > h.length) return false; for (uint256 i; i + n.length <= h.length; ++i) { bool match_ = true; for (uint256 j; j < n.length; ++j) { if (h[i + j] != n[j]) { match_ = false; break; } } if (match_) return true; } return false; } }