// SPDX-License-Identifier: MIT pragma solidity 0.8.35; import {Test} from "forge-std/Test.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; import {DemoScript} from "../script/lib/DemoScript.sol"; import {DeployV1} from "../script/DeployV1.s.sol"; import {SeedV1Demo} from "../script/SeedV1Demo.s.sol"; import {SeedBaseSepolia} from "../script/SeedBaseSepolia.s.sol"; import {CheckState} from "../script/CheckState.s.sol"; import {UpgradeV2} from "../script/UpgradeV2.s.sol"; import {TransferV2Demo} from "../script/TransferV2Demo.s.sol"; import {BankV1} from "../src/BankV1.sol"; import {BankV2} from "../src/BankV2.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 UpgradeV2Harness is UpgradeV2 { function assertSnapshotUnchanged(Snapshot calldata before_, Snapshot calldata after_) external pure { _assertSnapshotUnchanged(before_, after_); } function runWithPaths(string calldata activePath, string calldata pendingPath, address sender) external returns (bool upgraded, address implementation) { _requireSupportedChain(block.chainid); return _run(activePath, pendingPath, sender); } } contract TransferV2DemoHarness is TransferV2Demo { function runWithPath(string calldata manifestPath) external { _run(manifestPath); } } contract CheckStateHarness is CheckState { function runWithPath(string calldata manifestPath, string calldata stage) external view { _run(manifestPath, stage); } } contract ScriptPreflightTest is Test { ScriptPreflightHarness internal harness; string internal fixtureDir; address internal constant OWNER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; address internal constant ALICE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; address internal constant BOB = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; 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[7] memory rejected = [uint256(1), uint256(10), uint256(56), uint256(137), uint256(8453), uint256(42161), 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 testBaseManifestRequiresExactPresenterRecipientActors() 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 testBaseSepoliaManifestUsesPublicUrlsAndExactActors() public { string memory path = _writePublicBaseManifest(); vm.chainId(84532); _etchManifestContracts(); DemoScript.Manifest memory manifest = harness.readManifest(path, true); assertEq(manifest.network, "baseSepolia"); assertEq(manifest.rpcUrl, "https://public.invalid"); assertEq(manifest.explorerBaseUrl, "https://sepolia.basescan.org"); assertEq(manifest.actors[0].label, "Presenter"); assertEq(manifest.actors[0].address_, OWNER); assertEq(manifest.actors[1].label, "Recipient"); assertEq(manifest.actors[1].address_, BOB); } 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 testBaseDeployRejectsMismatchedConfiguredSenderBeforeBroadcast() public { string memory path = string.concat(fixtureDir, "/base-deploy-mismatch.json"); vm.chainId(84532); vm.setEnv("SCRIPT_SENDER", vm.toString(OWNER)); vm.setEnv("BASE_SEPOLIA_SENDER", vm.toString(ALICE)); vm.setEnv("BASE_SEPOLIA_RECIPIENT", vm.toString(BOB)); vm.setEnv("BASE_SEPOLIA_PUBLIC_RPC_URL", "https://public.invalid"); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); uint256 nonceBefore = vm.getNonce(OWNER); DeployV1 deployer = new DeployV1(); vm.expectRevert( abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "BASE_SEPOLIA_SENDER", OWNER, ALICE) ); deployer.run(); assertEq(vm.getNonce(OWNER), nonceBefore); } function testBaseDeployWritesPublicRpcExplorerAndPresenterRecipientActors() public { string memory path = string.concat(fixtureDir, "/base-deployed.json"); vm.chainId(84532); _setBaseConfig(OWNER, BOB, "https://public.invalid"); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); (address token, address proxy, address implementation) = new DeployV1().run(); _etchManifestContracts(); DemoScript.Manifest memory manifest = harness.readManifest(path, false); assertEq(manifest.token, token); assertEq(manifest.proxy, proxy); assertEq(manifest.implementation, implementation); assertEq(manifest.owner, OWNER); assertEq(manifest.rpcUrl, "https://public.invalid"); assertEq(manifest.explorerBaseUrl, "https://sepolia.basescan.org"); assertEq(manifest.actors.length, 2); assertEq(manifest.actors[0].label, "Presenter"); assertEq(manifest.actors[0].address_, OWNER); assertEq(manifest.actors[1].label, "Recipient"); assertEq(manifest.actors[1].address_, BOB); } function testBaseDeployRejectsZeroOrSameRecipientBeforeBroadcast() public { vm.chainId(84532); DeployV1 deployer = new DeployV1(); address[2] memory invalidRecipients = [address(0), OWNER]; for (uint256 i; i < invalidRecipients.length; ++i) { _setBaseConfig(OWNER, invalidRecipients[i], "https://public.invalid"); uint256 nonceBefore = vm.getNonce(OWNER); vm.expectRevert(DemoScript.InvalidActorConfiguration.selector); deployer.run(); assertEq(vm.getNonce(OWNER), nonceBefore); } } function testBaseDeployRejectsCredentialBearingPublicRpcBeforeBroadcast() public { vm.chainId(84532); DeployV1 deployer = new DeployV1(); string[3] memory invalidUrls = ["http://public.invalid", "https://user@public.invalid", "https://public.invalid/path?api_key=fixture"]; bytes4 invalidPublicRpcUrl = bytes4(keccak256("InvalidPublicRpcUrl()")); for (uint256 i; i < invalidUrls.length; ++i) { _setBaseConfig(OWNER, BOB, invalidUrls[i]); uint256 nonceBefore = vm.getNonce(OWNER); vm.expectRevert(invalidPublicRpcUrl); deployer.run(); assertEq(vm.getNonce(OWNER), nonceBefore); } } function testSeedV1DemoExecutesExactActOneStateAndCheckStateAcceptsIt() public { (string memory path, DemoScript.Manifest memory manifest) = _deployFixtureNamed("seed-v1"); 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 testSeedBaseSepoliaCreatesExactPresenterDepositState() public { (string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("seed-base"); _setBaseConfig(OWNER, BOB, "https://public.invalid"); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); new SeedBaseSepolia().run(); BankV1 bank = BankV1(manifest.proxy); MockUSDC token = MockUSDC(manifest.token); assertEq(bank.balanceOf(OWNER), 1_000e6); assertEq(bank.balanceOf(BOB), 0); assertEq(bank.totalLiabilities(), 1_000e6); assertEq(token.balanceOf(manifest.proxy), 1_000e6); assertEq(token.balanceOf(OWNER), 0); } function testSeedBaseSepoliaRejectsMismatchedSenderBeforeMintOrBroadcast() public { (string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("seed-base-mismatch"); _setBaseConfig(OWNER, BOB, "https://public.invalid"); vm.setEnv("BASE_SEPOLIA_SENDER", vm.toString(ALICE)); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); SeedBaseSepolia seeder = new SeedBaseSepolia(); vm.expectRevert( abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "BASE_SEPOLIA_SENDER", OWNER, ALICE) ); seeder.run(); assertEq(MockUSDC(manifest.token).totalSupply(), 0); assertEq(BankV1(manifest.proxy).totalLiabilities(), 0); } function testCheckStateRejectsManifestImplementationMismatch() public { (string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("check-mismatch"); vm.writeJson(string.concat('"', vm.toString(manifest.token), '"'), path, ".implementation"); CheckStateHarness checker = new CheckStateHarness(); vm.expectRevert( abi.encodeWithSelector( DemoScript.UnexpectedAddress.selector, "implementation", manifest.token, manifest.implementation ) ); checker.runWithPath(path, "deployed"); } function testUpgradeV2RequiresManifestIdentityCodeOwnerSenderAndVersionOne() public { (string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("upgrade-preflight"); string memory pending = string.concat(fixtureDir, "/upgrade-pending.json"); UpgradeV2Harness upgrader = new UpgradeV2Harness(); vm.expectRevert( abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "SCRIPT_SENDER", manifest.owner, ALICE) ); upgrader.runWithPaths(path, pending, ALICE); vm.writeJson(string.concat('"', vm.toString(manifest.token), '"'), path, ".implementation"); vm.expectRevert( abi.encodeWithSelector( DemoScript.UnexpectedAddress.selector, "implementation", manifest.token, manifest.implementation ) ); upgrader.runWithPaths(path, pending, OWNER); } function testUpgradeV2RejectsUnsupportedChainMissingProxyCodeWrongOwnerAndUnexpectedVersion() public { UpgradeV2Harness upgrader = new UpgradeV2Harness(); vm.chainId(1); vm.expectRevert(abi.encodeWithSelector(DemoScript.UnsupportedChain.selector, uint256(1))); upgrader.runWithPaths("unused", "unused", OWNER); vm.chainId(31337); (string memory missingPath,) = _deployUpgradeFixtureNamed("upgrade-missing-proxy"); vm.writeJson('"0x4000000000000000000000000000000000000004"', missingPath, ".proxy"); vm.expectRevert( abi.encodeWithSelector(DemoScript.MissingCode.selector, "proxy", 0x4000000000000000000000000000000000000004) ); upgrader.runWithPaths(missingPath, string.concat(fixtureDir, "/missing-pending.json"), OWNER); (string memory ownerPath, DemoScript.Manifest memory ownerManifest) = _deployUpgradeFixtureNamed("upgrade-owner"); vm.prank(OWNER); BankV1(ownerManifest.proxy).transferOwnership(ALICE); vm.expectRevert(abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "owner", OWNER, ALICE)); upgrader.runWithPaths(ownerPath, string.concat(fixtureDir, "/owner-pending.json"), ALICE); (string memory versionPath, DemoScript.Manifest memory versionManifest) = _deployUpgradeFixtureNamed("upgrade-version"); vm.mockCall( versionManifest.proxy, abi.encodeWithSelector(BankV1.contractVersion.selector), abi.encode(uint256(3)) ); vm.expectRevert(abi.encodeWithSelector(UpgradeV2.UnexpectedVersion.selector, uint256(3))); upgrader.runWithPaths(versionPath, string.concat(fixtureDir, "/version-pending.json"), OWNER); } function testUpgradeV2PreservesCompleteSnapshotAndWritesOnlyStagingRecord() public { (string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("upgrade-preserve"); _seedState(manifest); string memory beforeManifest = vm.readFile(path); string memory pending = string.concat(fixtureDir, "/upgrade-pending.json"); (bool upgraded, address implementation) = new UpgradeV2Harness().runWithPaths(path, pending, OWNER); assertTrue(upgraded); assertNotEq(implementation, manifest.implementation); assertEq(vm.readFile(path), beforeManifest); BankV2 bankV2 = BankV2(manifest.proxy); assertEq(bankV2.contractVersion(), 2); assertEq(bankV2.owner(), manifest.owner); assertEq(address(bankV2.asset()), manifest.token); assertFalse(bankV2.paused()); assertEq(bankV2.balanceOf(ALICE), 900e6); assertEq(bankV2.balanceOf(BOB), 500e6); assertEq(bankV2.totalLiabilities(), 1_400e6); assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6); string memory marker = vm.readFile(pending); assertEq(vm.parseJsonString(marker, ".mode"), "upgrade"); assertEq(vm.parseJsonAddress(marker, ".implementation"), implementation); assertEq(vm.parseJsonAddress(marker, ".snapshot.proxy"), manifest.proxy); assertEq(vm.parseJsonUint(marker, ".snapshot.balances[1].balance"), 900e6); assertEq(vm.parseJsonUint(marker, ".snapshot.balances[2].balance"), 500e6); } function testUpgradeV2AlreadyActiveWritesVerifiableNoopWithoutChangingImplementation() public { (string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("upgrade-noop"); string memory pending = string.concat(fixtureDir, "/upgrade-pending.json"); UpgradeV2Harness upgrader = new UpgradeV2Harness(); (, address implementation) = upgrader.runWithPaths(path, pending, OWNER); vm.writeJson(string.concat('"', vm.toString(implementation), '"'), path, ".implementation"); uint256 nonceBefore = vm.getNonce(OWNER); (bool upgraded, address observedImplementation) = upgrader.runWithPaths(path, pending, OWNER); assertFalse(upgraded); assertEq(observedImplementation, implementation); assertEq(vm.getNonce(OWNER), nonceBefore); string memory marker = vm.readFile(pending); assertEq(vm.parseJsonString(marker, ".mode"), "noop"); assertEq(vm.parseJsonUint(marker, ".chainId"), 31337); assertEq(vm.parseJsonUint(marker, ".observedBlock"), block.number); assertEq(vm.parseJsonUint(marker, ".ownerNonce"), nonceBefore); assertEq(vm.parseJsonAddress(marker, ".proxy"), manifest.proxy); assertEq(vm.parseJsonAddress(marker, ".implementation"), implementation); } function testSnapshotComparisonRejectsApplicationMutationButAllowsImplementationAndVersionChange() public { UpgradeV2Harness upgradeHarness = new UpgradeV2Harness(); UpgradeV2.Snapshot memory before_ = _snapshot(); UpgradeV2.Snapshot memory after_ = _snapshot(); after_.implementation = address(0x9999); after_.version = 2; upgradeHarness.assertSnapshotUnchanged(before_, after_); for (uint256 mutation; mutation < 10; ++mutation) { before_ = _snapshot(); after_ = _snapshot(); if (mutation == 0) after_.proxy = address(0x9999); else if (mutation == 1) after_.owner = address(0x9999); else if (mutation == 2) after_.asset = address(0x9999); else if (mutation == 3) after_.paused = true; else if (mutation == 4) after_.balances = new uint256[](2); else if (mutation == 5) after_.balances[1] += 1; else if (mutation == 6) after_.liabilities += 1; else if (mutation == 7) after_.reserves += 1; else if (mutation == 8) after_.surplus += 1; else after_.deploymentBlock += 1; vm.expectRevert(); upgradeHarness.assertSnapshotUnchanged(before_, after_); } } function testTransferV2DemoExecutesExactActThreeAndCheckStateAcceptsBothV2Stages() public { (string memory path, DemoScript.Manifest memory manifest) = _deployUpgradeFixtureNamed("transfer-v2"); _seedState(manifest); string memory pending = string.concat(fixtureDir, "/upgrade-pending.json"); (, address implementation) = new UpgradeV2Harness().runWithPaths(path, pending, OWNER); vm.writeJson(string.concat('"', vm.toString(implementation), '"'), path, ".implementation"); new CheckStateHarness().runWithPath(path, "upgraded"); new TransferV2DemoHarness().runWithPath(path); new CheckStateHarness().runWithPath(path, "v2"); BankV2 bankV2 = BankV2(manifest.proxy); assertEq(bankV2.balanceOf(ALICE), 650e6); assertEq(bankV2.balanceOf(BOB), 750e6); assertEq(bankV2.totalLiabilities(), 1_400e6); assertEq(MockUSDC(manifest.token).balanceOf(manifest.proxy), 1_400e6); } function testBaseUpgradeAndTransferPreserveAccountingAndMoveExactBalance() public { (string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("transfer-base"); _setBaseConfig(OWNER, BOB, "https://public.invalid"); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); new SeedBaseSepolia().run(); string memory pending = string.concat(fixtureDir, "/base-upgrade-pending.json"); (, address implementation) = new UpgradeV2Harness().runWithPaths(path, pending, OWNER); vm.writeJson(string.concat('"', vm.toString(implementation), '"'), path, ".implementation"); new TransferV2DemoHarness().runWithPath(path); BankV2 bank = BankV2(manifest.proxy); MockUSDC token = MockUSDC(manifest.token); assertEq(bank.balanceOf(OWNER), 750e6); assertEq(bank.balanceOf(BOB), 250e6); assertEq(bank.totalLiabilities(), 1_000e6); assertEq(token.balanceOf(manifest.proxy), 1_000e6); new CheckStateHarness().runWithPath(path, "invariants"); } function testBaseUpgradeRejectsMismatchedConfiguredSenderBeforeBroadcast() public { (string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("upgrade-base-mismatch"); _setBaseConfig(ALICE, BOB, "https://public.invalid"); string memory pending = string.concat(fixtureDir, "/base-mismatch-pending.json"); UpgradeV2Harness upgrader = new UpgradeV2Harness(); vm.expectRevert( abi.encodeWithSelector(DemoScript.UnexpectedAddress.selector, "BASE_SEPOLIA_SENDER", OWNER, ALICE) ); upgrader.runWithPaths(path, pending, OWNER); assertEq(Upgrades.getImplementationAddress(manifest.proxy), manifest.implementation); } function testBaseCheckStateRejectsConfiguredActorMismatch() public { (string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("check-base-mismatch"); _setBaseConfig(OWNER, ALICE, "https://public.invalid"); CheckStateHarness checker = new CheckStateHarness(); vm.expectRevert( abi.encodeWithSelector( DemoScript.UnexpectedAddress.selector, "Recipient", manifest.actors[1].address_, ALICE ) ); checker.runWithPath(path, "deployed"); } function testBaseTransferRejectsInvalidRecipientBeforeBroadcast() public { (string memory path, DemoScript.Manifest memory manifest) = _deployBaseFixtureNamed("transfer-base-invalid"); _setBaseConfig(OWNER, BOB, "https://public.invalid"); vm.setEnv("DEPLOYMENT_MANIFEST_PATH", path); new SeedBaseSepolia().run(); string memory pending = string.concat(fixtureDir, "/base-transfer-upgrade.json"); (, address implementation) = new UpgradeV2Harness().runWithPaths(path, pending, OWNER); vm.writeJson(string.concat('"', vm.toString(implementation), '"'), path, ".implementation"); _setBaseConfig(OWNER, OWNER, "https://public.invalid"); TransferV2DemoHarness transfer = new TransferV2DemoHarness(); vm.expectRevert(DemoScript.InvalidActorConfiguration.selector); transfer.runWithPath(path); BankV2 bank = BankV2(manifest.proxy); assertEq(bank.balanceOf(OWNER), 1_000e6); assertEq(bank.balanceOf(BOB), 0); } function _snapshot() internal pure returns (UpgradeV2.Snapshot memory snapshot) { snapshot.proxy = PROXY; snapshot.implementation = IMPLEMENTATION; snapshot.owner = OWNER; snapshot.asset = TOKEN; snapshot.paused = false; snapshot.balances = new uint256[](3); snapshot.balances[0] = 10; snapshot.balances[1] = 20; snapshot.balances[2] = 30; snapshot.liabilities = 60; snapshot.reserves = 70; snapshot.surplus = 10; snapshot.deploymentBlock = 1; snapshot.version = 1; } 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) { return _deployFixtureNamed("deployed"); } function _deployFixtureNamed(string memory name) internal returns (string memory path, DemoScript.Manifest memory manifest) { path = string.concat(fixtureDir, "/", name, ".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 _deployUpgradeFixtureNamed(string memory name) internal returns (string memory path, DemoScript.Manifest memory manifest) { vm.chainId(31337); MockUSDC deployedToken = new MockUSDC(OWNER); address deployedProxy = Upgrades.deployUUPSProxy( "BankV1.sol:BankV1", abi.encodeCall(BankV1.initialize, (address(deployedToken), OWNER)) ); manifest.schemaVersion = 1; manifest.network = "anvil"; manifest.chainId = 31337; manifest.deploymentBlock = 1; manifest.rpcUrl = "http://127.0.0.1:8545"; manifest.token = address(deployedToken); manifest.proxy = deployedProxy; manifest.implementation = Upgrades.getImplementationAddress(deployedProxy); manifest.owner = OWNER; manifest.actors = _actors(); path = string.concat(fixtureDir, "/", name, ".json"); vm.writeFile(path, harness.serializeManifest(manifest)); } function _deployBaseFixtureNamed(string memory name) internal returns (string memory path, DemoScript.Manifest memory manifest) { vm.chainId(84532); MockUSDC deployedToken = new MockUSDC(OWNER); address deployedProxy = Upgrades.deployUUPSProxy( "BankV1.sol:BankV1", abi.encodeCall(BankV1.initialize, (address(deployedToken), OWNER)) ); manifest.schemaVersion = 1; manifest.network = "baseSepolia"; manifest.chainId = 84532; manifest.deploymentBlock = 1; manifest.rpcUrl = "https://public.invalid"; manifest.explorerBaseUrl = "https://sepolia.basescan.org"; manifest.token = address(deployedToken); manifest.proxy = deployedProxy; manifest.implementation = Upgrades.getImplementationAddress(deployedProxy); manifest.owner = OWNER; manifest.actors = _baseActors(); path = string.concat(fixtureDir, "/", name, ".json"); vm.writeFile(path, harness.serializeManifest(manifest)); } function _seedState(DemoScript.Manifest memory manifest) internal { MockUSDC deployedToken = MockUSDC(manifest.token); BankV1 deployedBank = BankV1(manifest.proxy); vm.startPrank(OWNER); deployedToken.mint(ALICE, 2_000e6); deployedToken.mint(BOB, 1_000e6); vm.stopPrank(); vm.startPrank(ALICE); deployedToken.approve(manifest.proxy, 1_000e6); deployedBank.deposit(1_000e6); deployedBank.withdraw(100e6); vm.stopPrank(); vm.startPrank(BOB); deployedToken.approve(manifest.proxy, 500e6); deployedBank.deposit(500e6); vm.stopPrank(); } 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,"rpcUrl":"https://public.invalid","explorerBaseUrl":"https://sepolia.basescan.org","token":"', vm.toString(TOKEN), '","proxy":"', vm.toString(PROXY), '","implementation":"', vm.toString(IMPLEMENTATION), '","owner":"', vm.toString(OWNER), '","actors":[{"label":"Presenter","address":"', vm.toString(OWNER), '"},{"label":"Recipient","address":"', vm.toString(BOB), '"}]}' ) ); } 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 _baseActors() internal pure returns (DemoScript.Actor[] memory actors) { actors = new DemoScript.Actor[](2); actors[0] = DemoScript.Actor({label: "Presenter", address_: OWNER}); actors[1] = DemoScript.Actor({label: "Recipient", address_: BOB}); } 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; } function _setBaseConfig(address sender, address recipient, string memory publicRpcUrl) internal { vm.setEnv("SCRIPT_SENDER", vm.toString(sender)); vm.setEnv("BASE_SEPOLIA_SENDER", vm.toString(sender)); vm.setEnv("BASE_SEPOLIA_RECIPIENT", vm.toString(recipient)); vm.setEnv("BASE_SEPOLIA_PUBLIC_RPC_URL", publicRpcUrl); } }