From 8f2cac467c699d06f3e13f74c25ec5db55a80550 Mon Sep 17 00:00:00 2001 From: David Meister Date: Sun, 16 Aug 2026 13:02:59 +0000 Subject: [PATCH 1/8] feat(migration-registry): settable appliedAt via MigrationRegistryV2 Baseline commit of in-progress work: V2 interface, concrete registry, libs, generated snapshots, deploy suites and tests. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/manual-sol-artifacts.yaml | 15 +- script/Build.sol | 7 +- src/abstract/RegistryDeploySuites.sol | 79 +- src/concrete/MigrationRegistryV2.sol | 206 ++++ .../candidate/MigrationRegistryV2.sol | 26 + src/interface/IMigrationRegistryV2.sol | 397 +++++++ src/lib/LibMigrationRegistryV2.sol | 186 ++++ src/lib/LibMigrationRegistryV2Deploy.sol | 20 + src/lib/LibMigrationRegistryV2Released.sol | 31 + test/concrete/MockMigrationApplierV2.sol | 47 + .../concrete/MigrationRegistryV2Applied.t.sol | 180 ++++ .../MigrationRegistryV2ApplyMigration.t.sol | 999 ++++++++++++++++++ .../concrete/MigrationRegistryV2Head.t.sol | 179 ++++ test/src/lib/LibMigrationRegistryV2.t.sol | 540 ++++++++++ 14 files changed, 2892 insertions(+), 20 deletions(-) create mode 100644 src/concrete/MigrationRegistryV2.sol create mode 100644 src/generated/candidate/MigrationRegistryV2.sol create mode 100644 src/interface/IMigrationRegistryV2.sol create mode 100644 src/lib/LibMigrationRegistryV2.sol create mode 100644 src/lib/LibMigrationRegistryV2Deploy.sol create mode 100644 src/lib/LibMigrationRegistryV2Released.sol create mode 100644 test/concrete/MockMigrationApplierV2.sol create mode 100644 test/src/concrete/MigrationRegistryV2Applied.t.sol create mode 100644 test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol create mode 100644 test/src/concrete/MigrationRegistryV2Head.t.sol create mode 100644 test/src/lib/LibMigrationRegistryV2.t.sol diff --git a/.github/workflows/manual-sol-artifacts.yaml b/.github/workflows/manual-sol-artifacts.yaml index a6f6248..2761fa3 100644 --- a/.github/workflows/manual-sol-artifacts.yaml +++ b/.github/workflows/manual-sol-artifacts.yaml @@ -1,10 +1,10 @@ name: Manual sol artifacts # The on-chain deploy, run by hand. This repo carries deployed concretes -# (`AddressRegistry`, `MigrationRegistry`) whose address + codehash consumers -# pin, and `package-release.yaml` cuts a release for a deployment that ALREADY -# exists — rainix-tag-release verifies the live chains against freshly generated -# pins and never broadcasts. So the deploy has to happen first, and separately, -# which is this. +# (`AddressRegistry`, `MigrationRegistry`, `MigrationRegistryV2`) whose address +# + codehash consumers pin, and `package-release.yaml` cuts a release for a +# deployment that ALREADY exists — rainix-tag-release verifies the live chains +# against freshly generated pins and never broadcasts. So the deploy has to +# happen first, and separately, which is this. # # Order is: dispatch this once per suite, confirm `RegistryDeployChainTest` # passes on every supported network, then push the `sol-v*` tag. @@ -19,14 +19,15 @@ on: required: true description: | Which declared suite to broadcast. One dispatch deploys one suite, - because `DEPLOYMENT_SUITE` selects one — a repo with two deployed - contracts is two dispatches. Offered as a choice rather than typed, + because `DEPLOYMENT_SUITE` selects one — a repo with three deployed + contracts is three dispatches. Offered as a choice rather than typed, so a key that no suite declares cannot be dispatched at all; the script still refuses one, naming the valid keys, if this list falls behind the declaration. options: - address-registry - migration-registry + - migration-registry-v2 jobs: deploy: uses: rainlanguage/rainix/.github/workflows/rainix-manual-sol-artifacts.yaml@main diff --git a/script/Build.sol b/script/Build.sol index 1b0d307..6c180fe 100644 --- a/script/Build.sol +++ b/script/Build.sol @@ -86,7 +86,7 @@ contract Build is Script, RegistryDeploySuites { /// Every contract this repo generates deploy pins for, declared ONCE. /// @return contracts The generated contracts. function generatedContracts() internal pure returns (GeneratedContract[] memory contracts) { - contracts = new GeneratedContract[](2); + contracts = new GeneratedContract[](3); contracts[0] = GeneratedContract({ contractName: "AddressRegistry", constantPrefix: "ADDRESS_REGISTRY", candidate: addressRegistryCandidate() }); @@ -95,6 +95,11 @@ contract Build is Script, RegistryDeploySuites { constantPrefix: "MIGRATION_REGISTRY", candidate: migrationRegistryCandidate() }); + contracts[2] = GeneratedContract({ + contractName: "MigrationRegistryV2", + constantPrefix: "MIGRATION_REGISTRY_V2", + candidate: migrationRegistryV2Candidate() + }); } /// @notice Every build: regenerate the rolling snapshots, their alias libs diff --git a/src/abstract/RegistryDeploySuites.sol b/src/abstract/RegistryDeploySuites.sol index a5d40ae..1d1cfb8 100644 --- a/src/abstract/RegistryDeploySuites.sol +++ b/src/abstract/RegistryDeploySuites.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.25; import {DeployCandidate, DeploySuite, RainDeploySuitesBase} from "./RainDeploySuitesBase.sol"; import {AddressRegistry} from "../concrete/AddressRegistry.sol"; import {MigrationRegistry} from "../concrete/MigrationRegistry.sol"; +import {MigrationRegistryV2} from "../concrete/MigrationRegistryV2.sol"; import { CREATION_CODE as ADDRESS_REGISTRY_CREATION_CODE_CANDIDATE, RUNTIME_CODE as ADDRESS_REGISTRY_RUNTIME_CODE_CANDIDATE @@ -13,10 +14,16 @@ import { CREATION_CODE as MIGRATION_REGISTRY_CREATION_CODE_CANDIDATE, RUNTIME_CODE as MIGRATION_REGISTRY_RUNTIME_CODE_CANDIDATE } from "../generated/candidate/MigrationRegistry.sol"; +import { + CREATION_CODE as MIGRATION_REGISTRY_V2_CREATION_CODE_CANDIDATE, + RUNTIME_CODE as MIGRATION_REGISTRY_V2_RUNTIME_CODE_CANDIDATE +} from "../generated/candidate/MigrationRegistryV2.sol"; import {LibAddressRegistryDeploy} from "../lib/LibAddressRegistryDeploy.sol"; import {LibAddressRegistryReleased} from "../lib/LibAddressRegistryReleased.sol"; import {LibMigrationRegistryDeploy} from "../lib/LibMigrationRegistryDeploy.sol"; import {LibMigrationRegistryReleased} from "../lib/LibMigrationRegistryReleased.sol"; +import {LibMigrationRegistryV2Deploy} from "../lib/LibMigrationRegistryV2Deploy.sol"; +import {LibMigrationRegistryV2Released} from "../lib/LibMigrationRegistryV2Released.sol"; /// @title RegistryDeploySuites /// @notice Everything this repo deploys, declared ONCE. @@ -59,29 +66,43 @@ abstract contract RegistryDeploySuites is RainDeploySuitesBase { /// that took the record whole would give another contract's snapshot this /// contract's suite key and collide with its own entry for that tag. /// - /// Both are empty until the first release is cut. The rolling `candidate/` + /// Flattened by a loop over the libs rather than by a run of index + /// arithmetic per lib, so a contract is added by adding one entry to the + /// group list and nothing else here moves. + /// + /// All are empty until the first release is cut. The rolling `candidate/` /// snapshots are not releases and do exist. function releasedSuites() internal pure override returns (DeploySuite[] memory suites) { - DeploySuite[] memory addressRegistry = LibAddressRegistryReleased.releasedSuites(); - DeploySuite[] memory migrationRegistry = LibMigrationRegistryReleased.releasedSuites(); + DeploySuite[][] memory groups = new DeploySuite[][](3); + groups[0] = LibAddressRegistryReleased.releasedSuites(); + groups[1] = LibMigrationRegistryReleased.releasedSuites(); + groups[2] = LibMigrationRegistryV2Released.releasedSuites(); - suites = new DeploySuite[](addressRegistry.length + migrationRegistry.length); - for (uint256 i = 0; i < addressRegistry.length; i++) { - suites[i] = addressRegistry[i]; + uint256 total = 0; + for (uint256 i = 0; i < groups.length; i++) { + total += groups[i].length; } - for (uint256 i = 0; i < migrationRegistry.length; i++) { - suites[addressRegistry.length + i] = migrationRegistry[i]; + + suites = new DeploySuite[](total); + uint256 next = 0; + for (uint256 i = 0; i < groups.length; i++) { + for (uint256 j = 0; j < groups[i].length; j++) { + suites[next] = groups[i][j]; + next++; + } } } /// @inheritdoc RainDeploySuitesBase - /// @dev One entry per contract this repo deploys. A third deployed contract - /// is a third named candidate below, a third entry here, and a third entry - /// in `script/Build.sol`'s generated-contract list — nothing else. + /// @dev One entry per contract this repo deploys. A further deployed + /// contract is a further named candidate below, a further entry here, a + /// further group in `releasedSuites` above, and a further entry in + /// `script/Build.sol`'s generated-contract list — nothing else. function candidateSuites() internal pure override returns (DeployCandidate[] memory candidates) { - candidates = new DeployCandidate[](2); + candidates = new DeployCandidate[](3); candidates[0] = addressRegistryCandidate(); candidates[1] = migrationRegistryCandidate(); + candidates[2] = migrationRegistryV2Candidate(); } /// This repo's rolling `AddressRegistry` candidate. @@ -149,4 +170,38 @@ abstract contract RegistryDeploySuites is RainDeploySuitesBase { sourceCreationCode: type(MigrationRegistry).creationCode }); } + + /// This repo's rolling `MigrationRegistryV2` candidate. + /// + /// Everything said about the `AddressRegistry` candidate holds here + /// unchanged: the pins are aliased from the generated snapshot, the + /// creation and runtime code are recorded rather than derived, and the + /// source anchor is what says the record describes THIS contract. + /// + /// It is a separate suite from `migration-registry` rather than a newer + /// spelling of it. The creation code is what the Zoltu factory takes, so two + /// creation codes are two addresses and two deployments; both are broadcast, + /// both are verified against every chain, and a consumer pins whichever one + /// it reads. + /// + /// `MigrationRegistryV2` has no constructor argument, no compile-time + /// authority and no dependency to be on chain first — the namespace is + /// `msg.sender`, so there is nothing to configure and nothing to resolve. + /// That is also why it is deployable to a new network the day the network + /// is added, with no follow-up transaction to make it useful. + /// @return The candidate. + function migrationRegistryV2Candidate() internal pure returns (DeployCandidate memory) { + return DeployCandidate({ + snapshot: DeploySuite({ + suite: "migration-registry-v2", + creationCode: MIGRATION_REGISTRY_V2_CREATION_CODE_CANDIDATE, + storedDeployedAddress: LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, + storedBytecodeHash: LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + storedRuntimeCode: MIGRATION_REGISTRY_V2_RUNTIME_CODE_CANDIDATE, + artifactPath: "src/concrete/MigrationRegistryV2.sol:MigrationRegistryV2", + dependencies: new address[](0) + }), + sourceCreationCode: type(MigrationRegistryV2).creationCode + }); + } } diff --git a/src/concrete/MigrationRegistryV2.sol b/src/concrete/MigrationRegistryV2.sol new file mode 100644 index 0000000..83d2ba7 --- /dev/null +++ b/src/concrete/MigrationRegistryV2.sol @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../interface/IMigrationRegistryV2.sol"; + +/// @title MigrationRegistryV2 +/// @notice The whole of `IMigrationRegistryV2`: a writer applies one of its own +/// migrations onto the head it believes its namespace is at, at the moment it +/// says the migration ran, and anyone reads when a given writer applied a given +/// migration, or where that writer's namespace has got to. +/// +/// There is deliberately nothing else. No removal, no upgrade, no pause, and no +/// authority at all — which is the difference from `AddressRegistry`, and the +/// reason nothing here is CONFIGURED at compile time. `MIGRATION_HEAD_GENESIS` +/// is a compile-time constant, but it is the same value for every consumer on +/// every chain and names nobody, so it is part of what this contract IS rather +/// than a choice welded into it. +/// +/// `AddressRegistry` has a root, and a root has to be welded into the creation +/// code so it cannot be rotated, which puts it in the deterministic address. +/// That is workable there because there is one registry of names for the whole +/// organisation. It is not workable here: the account that applies a migration +/// is a different Safe, deployer or timelock for every consumer and every +/// chain, so a root would have to be all of them at once, and baking each +/// consumer's authority into creation code would give each of them a different +/// address for what is meant to be one shared registry. +/// +/// Keying by `msg.sender` removes the authority instead of choosing one. Anyone +/// may write, but only under themselves, so a reader asking about the namespace +/// of an authority it already trusts is reading something only that authority +/// could have written. Every other namespace holds unforgeable claims that no +/// reader asks about. With nothing to configure there is also no rollout state +/// in which this contract is inert: it does its whole job the moment it exists +/// on a chain. +/// +/// A record is append-only per writer, and a head only ever moves forward onto +/// something new. `applyMigration` refuses a migration the caller has already +/// applied, which is what makes re-running a migration fail rather than repeat, +/// and refuses one applied onto anything but the namespace's current head, which +/// is what makes a skipped or out-of-order migration fail rather than diverge. +/// There is no way to unrecord one, and no way to rewrite the moment recorded +/// against one — a record describes something that happened, and nothing that +/// happened stops having happened. +/// +/// The moment is the CALLER's, so a migration that ran before this contract +/// reached the chain is recordable with the time it actually ran rather than +/// with the time it was written down. The window it must fall in is what keeps +/// every other property of a record: nonzero, so `applied` never reads a record +/// as no record; not after this block, so a record never describes something +/// that has not happened; and not before the record of the head it is applied +/// onto, so a namespace's records read in head order never go backwards. +/// +/// Neither storage mapping is `public`. `applied` and `head` refuse the zero +/// writer, `applied` refuses the two ids a migration can never be, and a public +/// mapping's generated getter would answer all of them with zero — which for +/// `applied` is "not applied" and for `head` is a value no head can ever hold, +/// i.e. exactly the silent wrong-branch this contract reverts to prevent. +contract MigrationRegistryV2 is IMigrationRegistryV2 { + /// The moment recorded against each migration, namespaced by writer. Zero + /// means never. Not `public`: the only reader is `applied`, which refuses + /// the two inputs that can only be mistakes. + mapping(address writer => mapping(bytes32 migration => uint256 appliedAt)) internal sApplied; + + /// The most recent migration applied under each writer. Zero means the + /// namespace is empty, which reads out as `MIGRATION_HEAD_GENESIS` — the + /// only place that translation happens is `head`, so no reader and no + /// writer can disagree about where an empty namespace is. Not `public`, for + /// the same reason as the records: the untranslated zero is not a head. + mapping(address writer => bytes32 head) internal sHead; + + /// @inheritdoc IMigrationRegistryV2 + /// @dev The refusals run from the ones that describe the call alone, through + /// the ones that describe the namespace it arrives at, to the one that + /// describes the block it lands in — which is the order in which a caller + /// can do something about them. + function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + // Checked before everything else, so an uninitialised id is reported as + // the mistake it is rather than as a first record of zero. + if (migration == bytes32(0)) { + revert ZeroMigration(); + } + // Genesis is a head, not a migration. Applying it would leave `sHead` + // holding the value an empty namespace reads as, so a namespace that had + // applied something would be at a head indistinguishable from one that + // had applied nothing — and the next first-migration script would be + // accepted against it. + if (migration == MIGRATION_HEAD_GENESIS) { + revert GenesisMigration(); + } + // Beside the two id refusals because it is the same kind of mistake in + // the same kind of value: an uninitialised `uint256`, refused whatever + // namespace it arrives at and whatever block it lands in. Zero is the + // one moment a record cannot carry — `applied` would answer it as + // "never applied" while the head had moved and the migration could + // never be applied again. + if (appliedAt == 0) { + revert ZeroTimestamp(); + } + // There is deliberately no zero-writer case here. `msg.sender` cannot + // be the zero address, so the zero namespace is unreachable for writes + // and a guard on it would be unreachable code pretending to be a check. + // Nor is there a zero-head case: a head is either genesis or an applied + // id, both nonzero, so a zero `expectedHead` can never match and is + // already refused below, by an error that names the zero it was handed. + + // Checked before the head, because a migration that has already run has + // already run whatever the head is, and that is the more useful thing to + // say to a re-dispatched script. It is also not implied by the head + // check: re-applying a migration whose successor has landed presents a + // matching head, and would drag the head backwards and overwrite the + // original record. + if (sApplied[msg.sender][migration] != 0) { + revert MigrationAlreadyApplied(msg.sender, migration); + } + bytes32 actualHead = head(msg.sender); + if (expectedHead != actualHead) { + revert UnexpectedMigrationHead(msg.sender, expectedHead, actualHead); + } + // The head chain is the order the migrations ran in, so the record it is + // being appended to is the floor for this one. Read unconditionally + // rather than behind a genesis branch: genesis is refused as a migration + // so nothing is ever recorded against it, which makes an empty + // namespace's floor zero and every nonzero `appliedAt` above it. A + // branch here would be a second spelling of "an empty namespace has no + // previous record", and the two spellings are what drift. + // + // Comparing against the head's record alone is enough for the whole + // namespace: every record is at or above the one before it in the + // chain, so the head's is the largest. + uint256 headAppliedAt = sApplied[msg.sender][actualHead]; + if (appliedAt < headAppliedAt) { + revert TimestampBeforeHead(msg.sender, actualHead, appliedAt, headAppliedAt); + } + // Last, because it is the only refusal here that time itself resolves: + // every other one describes something wrong with the call or with where + // the namespace is, and this one describes a moment that has not + // arrived yet. + // + // The usual hazard behind a `block.timestamp` comparison — the one the + // static analysers flag here — is a validator nudging the clock across + // a threshold. The nudge is available and it is harmless: a validator + // that moves the clock forward admits a record a second earlier than it + // would otherwise have been admitted, which is a record of a migration + // that has run either way, and moving it backwards is not something a + // chain lets a proposer do. So the warnings are suppressed on this one + // comparison rather than turned off for the repo. + // + // Slither's is a start/end pair rather than a next-line because only one + // comment fits immediately above the `if`, `forge fmt` moves a trailing + // one inside the braces, and forge-lint has no pair form. + // slither-disable-start timestamp + // forge-lint: disable-next-line(block-timestamp) + if (appliedAt > block.timestamp) { + revert FutureTimestamp(appliedAt, block.timestamp); + } + // slither-disable-end timestamp + sApplied[msg.sender][migration] = appliedAt; + sHead[msg.sender] = migration; + emit Migrated(msg.sender, migration, appliedAt); + } + + /// @inheritdoc IMigrationRegistryV2 + /// @dev All three refusals are about a caller that has not supplied what it + /// thinks it has. None can ever be a real record: nothing originates from + /// the zero address, and `applyMigration` will write neither the zero id nor + /// the genesis one — so answering zero for any of them would be answering a + /// question the caller did not mean to ask, and answering it with the value + /// that sends it down its pre-migration branch. + function applied(address writer, bytes32 migration) external view returns (uint256) { + if (writer == address(0)) { + revert ZeroWriter(); + } + if (migration == bytes32(0)) { + revert ZeroMigration(); + } + if (migration == MIGRATION_HEAD_GENESIS) { + revert GenesisMigration(); + } + return sApplied[writer][migration]; + } + + /// @inheritdoc IMigrationRegistryV2 + /// @dev The zero namespace is refused rather than answered `genesis`: it is + /// provably empty forever, so "a namespace nothing has been applied to" is a + /// true statement about it and a false one about what the caller meant to + /// ask, which would send a first migration at it. + /// + /// The empty-namespace zero is translated to genesis here and nowhere else, + /// which is why this is one `public` function rather than a reader beside an + /// internal helper: `applyMigration` compares against exactly what a caller + /// reads, so the two cannot drift into different ideas of where a namespace + /// that has applied nothing is. + /// + /// `applyMigration` reaches it as `head(msg.sender)`, which can never be the + /// zero address, so the refusal is redundant on that path. It is one + /// function, so it is one refusal, and the reachable path is the one it is + /// there for. + function head(address writer) public view returns (bytes32) { + if (writer == address(0)) { + revert ZeroWriter(); + } + bytes32 storedHead = sHead[writer]; + return storedHead == bytes32(0) ? MIGRATION_HEAD_GENESIS : storedHead; + } +} diff --git a/src/generated/candidate/MigrationRegistryV2.sol b/src/generated/candidate/MigrationRegistryV2.sol new file mode 100644 index 0000000..c57f662 --- /dev/null +++ b/src/generated/candidate/MigrationRegistryV2.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. + +/// @dev Hash of the known bytecode. +bytes32 constant BYTECODE_HASH = bytes32(0xea9a3519ea559173dab4ad65a2305b3f1449b5b3b420b32b326757021e92f7f7); + +/// @dev The deterministic deploy address of the contract when deployed via +/// the Zoltu factory. +address constant DEPLOYED_ADDRESS = address(0x82C7793293d71057E87849493ADDc291fbfCC7c9); + +/// @dev The creation bytecode of the contract. +bytes constant CREATION_CODE = + hex"6080604052348015600e575f80fd5b506105648061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c80635965d2a614610043578063bda4fec514610058578063e29304161461007d575b5f80fd5b6100566100513660046104d2565b610090565b005b61006b610066366004610523565b610319565b60405190815260200160405180910390f35b61006b61008b36600461053c565b6103c1565b816100c7576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8203610120576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f03610159576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f90815260208181526040808320858452909152902054156101b6576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6101c033610319565b905080841461020b576040517facbe685200000000000000000000000000000000000000000000000000000000815233600482015260248101859052604481018290526064016101ad565b335f9081526020818152604080832084845290915290205480831015610274576040517f9bcec4350000000000000000000000000000000000000000000000000000000081523360048201526024810183905260448101849052606481018290526084016101ad565b428311156102b7576040517f8fdce34f000000000000000000000000000000000000000000000000000000008152600481018490524260248201526044016101ad565b335f818152602081815260408083208884528252808320879055838352600182529182902087905590518581528692917f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610367576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f90815260016020526040902054801561039857806103ba565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f73ffffffffffffffffffffffffffffffffffffffff831661040f576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b81610446576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f820361049f576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b5f805f606084860312156104e4575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461051e575f80fd5b919050565b5f60208284031215610533575f80fd5b6103ba826104fb565b5f806040838503121561054d575f80fd5b610556836104fb565b94602093909301359350505056"; + +/// @dev The runtime bytecode of the contract. +bytes constant RUNTIME_CODE = + hex"608060405234801561000f575f80fd5b506004361061003f575f3560e01c80635965d2a614610043578063bda4fec514610058578063e29304161461007d575b5f80fd5b6100566100513660046104d2565b610090565b005b61006b610066366004610523565b610319565b60405190815260200160405180910390f35b61006b61008b36600461053c565b6103c1565b816100c7576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8203610120576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f03610159576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f90815260208181526040808320858452909152902054156101b6576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6101c033610319565b905080841461020b576040517facbe685200000000000000000000000000000000000000000000000000000000815233600482015260248101859052604481018290526064016101ad565b335f9081526020818152604080832084845290915290205480831015610274576040517f9bcec4350000000000000000000000000000000000000000000000000000000081523360048201526024810183905260448101849052606481018290526084016101ad565b428311156102b7576040517f8fdce34f000000000000000000000000000000000000000000000000000000008152600481018490524260248201526044016101ad565b335f818152602081815260408083208884528252808320879055838352600182529182902087905590518581528692917f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610367576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f90815260016020526040902054801561039857806103ba565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f73ffffffffffffffffffffffffffffffffffffffff831661040f576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b81610446576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f820361049f576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b5f805f606084860312156104e4575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461051e575f80fd5b919050565b5f60208284031215610533575f80fd5b6103ba826104fb565b5f806040838503121561054d575f80fd5b610556836104fb565b94602093909301359350505056"; + +/// @dev The addresses that MUST already have code on a network before +/// this release can be broadcast there, `abi.encode`d as an `address[]` +/// because Solidity has no file-scope constant of dynamic array type. +bytes constant DEPENDENCIES = + hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; diff --git a/src/interface/IMigrationRegistryV2.sol b/src/interface/IMigrationRegistryV2.sol new file mode 100644 index 0000000..f420399 --- /dev/null +++ b/src/interface/IMigrationRegistryV2.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +import {MIGRATION_HEAD_GENESIS} from "./IMigrationRegistryV1.sol"; + +/// @title IMigrationRegistryV2 +/// @notice A per-writer record of which migrations have been applied and when, +/// with exactly three operations: a writer applies one of its own migrations +/// onto the head it believes its namespace is at, at the moment it says the +/// migration ran (`applyMigration`), anyone reads when a given writer applied a +/// given migration (`applied`), and anyone reads where a given writer's +/// namespace currently is (`head`). There is no removal, no upgrade and no +/// authority beyond the writer over its own namespace, and an implementation +/// MUST NOT add any. +/// +/// It exists so that a test can decide what to assert by reading what happened +/// on chain rather than by reading the clock. Without it, a test that spans a +/// migration accepts EITHER the pre-migration or the post-migration value until +/// a hardcoded deadline, which asserts nothing at all during the one window +/// where it matters most, and red-lines on a date rather than on a fact once +/// the deadline passes. With it, a test asserts EXACTLY the value implied by +/// the migrations that have run, in both branches. +/// +/// ## The caller says when, within a window the implementation enforces +/// +/// `appliedAt` is a parameter because the fact being recorded is that a +/// migration RAN, and the moment it ran is not in general the moment anybody +/// gets to write it down. A migration that ran before this registry reached the +/// chain, or before its writer started recording at all, has a real moment that +/// is already in the past by the time there is anywhere to put it. An +/// implementation that could only stamp its own block would offer such a writer +/// two options and no third: record a time that is false for every historical +/// migration, or record nothing — and recording nothing strands the namespace, +/// because `applyMigration` refuses anything not applied onto the current head, +/// so a writer that skipped its past migrations cannot record its next one +/// either. +/// +/// What a reader gives up is NOT authenticity. A record is namespaced by the +/// account that wrote it and no authority checks it, so every entry is already +/// exactly as trustworthy as the writer that wrote it and no more — a writer +/// free to invent a migration id was always free to invent the fact. What a +/// reader gives up is precisely this: `appliedAt` is no longer the block the +/// record landed in. +/// +/// Everything else a reader relied on is kept, by an implementation that MUST +/// refuse a supplied `appliedAt` outside the window a true record can occupy: +/// +/// - NEVER ZERO. Zero is what `applied` answers for a migration nobody applied, +/// so a record carrying it would read back as no record while the head had +/// moved and the migration could never be applied again. +/// - NEVER AFTER THE BLOCK IT IS WRITTEN IN. A migration that has run has run, +/// so a moment still in the future is not a late record of anything — it is a +/// claim that cannot be true when it is made. `applied` therefore never +/// answers a moment that had not arrived, and a consumer whose invariant is +/// an interval since the migration — a cliff, a grace period, a rate that +/// changes a week later — can subtract it from the current block without +/// underflowing. +/// - NEVER BEFORE THE RECORD IT IS APPLIED ONTO. The head chain is the order +/// the migrations ran in, so a namespace's records read in head order are +/// non-decreasing, and "this migration ran before that one" agrees with the +/// sequence rather than contradicting it. +/// +/// Equal is allowed at both ends. Two migrations applied in one transaction +/// share a block, and two backfilled migrations known only to the same day +/// share a moment; the head chain is what orders them, and forcing the +/// timestamps apart would make them carry an ordering they do not have. +/// +/// So a reader still has: nonzero means applied, the value never exceeds the +/// block that wrote it, and the values along a namespace's head chain never go +/// backwards. +/// +/// ## An index, not proof +/// +/// This registry says which invariant applies. It does NOT say that the +/// invariant holds. A multisig can act out of band — a beacon is upgraded by +/// hand and nothing here moves — and then a reader would confidently assert the +/// wrong state. +/// +/// So a consumer keeps both layers, with distinct jobs: this registry SELECTS +/// which invariant applies, and codehash or bytecode pins VERIFY that it +/// actually holds. Replacing the pins with this registry trades a clock-guess +/// for a bookkeeping-guess, which is not an improvement. An implementation MUST +/// NOT offer anything that invites it, and in particular MUST NOT record +/// anything about the state a migration produced — only that it was applied, +/// and when. +/// +/// `appliedAt` is a fact about the RECORD, not about the state: it says when a +/// migration ran and nothing whatsoever about what it did. Reading it back does +/// not become proof of anything, for the same reason reading the record back +/// does not. +/// +/// ## `applied` answers WHEN, and zero still means "not applied" +/// +/// `applied` is a timestamp rather than a flag because "which invariant applies" +/// is frequently "which invariant applies YET": a migration that starts a +/// vesting cliff, a rate change, a grace period. A flag forces a consumer that +/// needs the moment to go and find the log for it, or — worse — to go back to +/// the deadline constant this registry exists to delete. +/// +/// A migration nobody applied answers zero, and that is an ANSWER rather than a +/// revert: it is the ordinary state of every migration before it runs and of +/// every migration on a chain that never got it, and it is the branch a caller +/// asserts the pre-migration state in. Zero and nonzero are therefore the same +/// two distinct facts a flag carried, with the nonzero case saying more. +/// +/// That distinction is only sound while a real record can never BE zero, which +/// is what `ZeroTimestamp` is for. +/// +/// ## The head is what makes an ordered sequence ordered +/// +/// A namespace has a HEAD: the migration most recently applied under it, or +/// `MIGRATION_HEAD_GENESIS` if it has never applied one. `applyMigration` takes +/// the head the caller believes its namespace is at and refuses to write unless +/// that is where the namespace actually is; on success the applied migration +/// becomes the new head. +/// +/// This is what blocks a SKIPPED step. A migration script names its predecessor, +/// so a chain that never got the predecessor is a loud revert at the moment of +/// applying rather than a namespace that silently diverges from every other +/// chain's. It is equally what blocks two migrations dispatched concurrently +/// from landing in whichever order the mempool chose: the second one names a +/// head that has moved. +/// +/// It is also what carries the ORDER, which is why `appliedAt` is not asked to. +/// The times a writer supplies are bounded by the head chain rather than the +/// other way around: a record may not predate the one it is applied onto, so +/// the sequence the heads describe and the moments the records carry cannot +/// contradict each other. +/// +/// It does NOT block a DUPLICATE, and `MigrationAlreadyApplied` is not +/// redundant beside it. Re-applying a migration whose successor has since +/// landed presents a head that matches perfectly, and would move the head +/// BACKWARDS and overwrite the original timestamp — a record un-happening, which +/// is the one thing this registry promises cannot occur. The two refusals answer +/// two different questions: the head is about WHERE in the sequence a caller is, +/// and the already-applied refusal is about WHETHER this particular migration +/// has run at all. +/// +/// One namespace on one chain is therefore ONE linear sequence, and that is a +/// consequence to design around rather than an implementation detail. Two +/// unrelated sets of migrations applied from the same account on the same chain +/// interleave into one chain of heads, so each script's expected head is +/// whatever that account last applied rather than whatever that script's own +/// author had in mind. A consumer that wants two independent sequences applies +/// them from two accounts, which is the same lever that already decides who a +/// reader trusts. +/// +/// ## The namespace is the writer, and that is the whole access control +/// +/// A record is keyed by the account that wrote it. Anyone may write, but only +/// to their own namespace, so a reader that reads the namespace of an authority +/// it already trusts is reading something only that authority could have +/// written. Records under any other namespace are unforgeable garbage that no +/// reader asks for. +/// +/// This is deliberately not a root authority. The account that applies a +/// migration differs per consumer, per chain and per migration — a Safe +/// executing a bundle, a deployer EOA broadcasting a script, a timelock — so a +/// single root would have to be all of them at once. It is also what lets the +/// implementation be identical for every consumer, and therefore live at one +/// deterministic address on every chain: an authority baked into creation code +/// would give every consumer a different address, which is the property this +/// registry exists inside a deterministic-deploy library to keep. +/// +/// A compromised writer can therefore only lie about its own migrations, to +/// readers that have chosen to trust it. It cannot touch anybody else's record, +/// and it cannot unrecord its own. +/// +/// ## Identity is opaque +/// +/// A migration is an opaque 32-byte value. This interface says nothing about +/// how one is derived — hashed from a script path, a name, a counter — and an +/// implementation MUST NOT constrain it beyond the two values the head space +/// reserves: zero, which is what an empty namespace holds before it is read as +/// genesis, and `MIGRATION_HEAD_GENESIS`, which is what it reads as. Neither can +/// be a migration without a head losing the ability to say whether a namespace +/// has applied anything. Two callers agreeing on any other id is entirely their +/// business. +/// +/// The convention that suits scripts-as-migrations is the hash of the script's +/// identity, e.g. `keccak256("script/20260623-upgrade-receipt-vaults.s.sol")`. +/// A date alone is not enough: two migrations authored on one day collide, and +/// consumers do author two on one day. An id is fixed at the moment it is first +/// applied, so a script renamed afterwards keeps the id it was applied under +/// rather than acquiring a new one — which is why the id belongs in a named +/// constant beside the script, not derived from a path at the call site. +/// +/// A head is an id, so the same is true of the head a script names: it is the +/// predecessor's named constant, imported, not a second spelling of it. +/// +/// `MIGRATION_HEAD_GENESIS` is one value shared with `IMigrationRegistryV1` +/// rather than a second constant of its own. It is the value that means "this +/// namespace has applied nothing", it configures nothing and names nobody, and +/// two spellings of it would be two things to keep equal. +/// +/// ## Records live in the implementation that holds them +/// +/// An implementation of this interface answers about its own storage and +/// nothing else, so a writer's namespace under one deployment is a different +/// namespace from its namespace under any other, and both `applied` and `head` +/// answer per deployment. A consumer reads the registry it pins. +interface IMigrationRegistryV2 { + /// Thrown when `applyMigration` is called with the zero migration id, and + /// by `applied` when it is asked about one. The zero id is what an + /// uninitialised `bytes32` constant reads as, and an uninitialised id is + /// never a migration anybody meant to name. Rejected in both directions + /// because the read is the dangerous one: answering zero would silently + /// send a caller down its pre-migration branch. + /// + /// There is no matching refusal for a zero HEAD, and adding one would be a + /// guard on something already impossible: a head is either + /// `MIGRATION_HEAD_GENESIS` or an applied id, both nonzero, so a zero head + /// can never match and is already refused by `UnexpectedMigrationHead` — + /// which names the zero it was handed, so nothing about the mistake is lost. + error ZeroMigration(); + + /// Thrown when `applyMigration` is called with `MIGRATION_HEAD_GENESIS` as + /// the migration, and by `applied` when it is asked about it. Genesis is a + /// head, not a migration: applying it would leave a namespace that has + /// applied something at a head no different from one that has applied + /// nothing, and asking `applied` about it would answer zero forever for a + /// caller that has confused a head for a migration and will read that as + /// its pre-migration branch. + /// + /// This is the same refusal as `ZeroMigration` under a different diagnosis, + /// and they are separate errors because the mistakes are different: a zero + /// is a constant nobody set, and this is a constant set to the wrong one of + /// two that sit beside each other. + error GenesisMigration(); + + /// Thrown by `applied` and `head` when asked about the zero writer. No + /// transaction can originate from the zero address, so the zero namespace is + /// provably empty and the answer would always be "nothing applied, at + /// genesis" — an unresolved or unset writer constant would therefore read as + /// a pristine namespace rather than as the mistake it is. + /// + /// There is no matching case on `applyMigration`: `msg.sender` is never + /// zero, so the zero namespace cannot be written to in the first place. + error ZeroWriter(); + + /// Thrown when a writer applies a migration it has already applied. This + /// is what makes running a migration twice structurally impossible rather + /// than a warning in a workflow dropdown asking a human not to re-dispatch + /// it: a script consults `applied` before it acts, and this is the backstop + /// under that consultation. + /// + /// Checked BEFORE the head, because it is the more specific true statement + /// about the call and it is true whatever the head is. A re-dispatched + /// script is told the migration already ran, rather than told the namespace + /// has moved on and left to work out why. + /// @param writer The namespace, which is the caller. + /// @param migration The migration already applied under it. + error MigrationAlreadyApplied(address writer, bytes32 migration); + + /// Thrown when a writer applies onto a head its namespace is not at. Either + /// something the caller believed had been applied has not been, or something + /// it did not know about has been — a skipped predecessor, a concurrent + /// dispatch that landed first, or a chain that is simply further behind than + /// the script assumed. + /// @param writer The namespace, which is the caller. + /// @param expectedHead The head the caller said it was applying onto. + /// @param actualHead The head the namespace is actually at. + error UnexpectedMigrationHead(address writer, bytes32 expectedHead, bytes32 actualHead); + + /// Thrown when `applyMigration` is given a zero `appliedAt`. A record IS + /// its timestamp, so a zero one would read back through `applied` as no + /// record at all, while the head moved and the migration cannot be + /// re-applied — the worst of every branch at once. It is what an + /// uninitialised `uint256` holds, so it is refused for the same reason + /// `ZeroMigration` is, and refusing to write is the only outcome that + /// leaves the namespace describing something true. + error ZeroTimestamp(); + + /// Thrown when `applyMigration` is given an `appliedAt` earlier than the + /// record of the head it is applied onto. The head chain is the order the + /// migrations ran in, so a record that predates the one before it in that + /// chain contradicts the sequence it is being appended to — and a reader + /// comparing two of a namespace's records would get an answer that + /// disagrees with the heads. + /// @param writer The namespace, which is the caller. + /// @param head The head being applied onto, whose record is the floor. + /// @param appliedAt The moment supplied. + /// @param headAppliedAt The moment the head was applied at. + error TimestampBeforeHead(address writer, bytes32 head, uint256 appliedAt, uint256 headAppliedAt); + + /// Thrown when `applyMigration` is given an `appliedAt` after the timestamp + /// of the block it is called in. A record says a migration HAS run, so a + /// moment that has not arrived is not a record of anything — and a consumer + /// measuring an interval since the migration would be subtracting a future + /// moment from the present one. + /// @param appliedAt The moment supplied. + /// @param blockTimestamp The timestamp of the block the call landed in. + error FutureTimestamp(uint256 appliedAt, uint256 blockTimestamp); + + /// Emitted every time a migration is applied. A migration is applied at + /// most once per writer, so the log is the complete history of the registry + /// and the only way to discover a record without already knowing the id. + /// + /// It carries no head, because the log is ordered and one writer's entries + /// in order ARE that writer's chain of heads — each entry's migration is the + /// head the next one was applied onto, and the first was applied onto + /// `MIGRATION_HEAD_GENESIS`. + /// + /// It does carry `appliedAt`, because that is the one part of a record the + /// log does not otherwise hold: it is supplied by the caller rather than + /// taken from the block, so the block a log entry sits in says only when the + /// record was written and not when the migration ran. + /// @param writer The namespace, which is the caller. + /// @param migration The migration applied. + /// @param appliedAt The moment recorded against it. + event Migrated(address indexed writer, bytes32 indexed migration, uint256 appliedAt); + + /// Applies `migration` under the caller's namespace, onto `expectedHead`, + /// as having been applied at `appliedAt`. + /// + /// The implementation MUST revert `ZeroMigration` if `migration` is zero, + /// `GenesisMigration` if it is `MIGRATION_HEAD_GENESIS`, `ZeroTimestamp` if + /// `appliedAt` is zero, `MigrationAlreadyApplied` if the caller has already + /// applied it, `UnexpectedMigrationHead` if the caller's namespace is not at + /// `expectedHead`, `TimestampBeforeHead` if `appliedAt` is before the record + /// of `expectedHead`, and `FutureTimestamp` if `appliedAt` is after + /// `block.timestamp`. It MUST NOT provide any way to unrecord a migration, + /// to move a head backwards, or to move a record's timestamp once written. + /// On success it MUST record `appliedAt` against `migration`, make + /// `migration` the caller's new head, and emit `Migrated`. + /// + /// Nothing is returned: the new head is the `migration` just passed in and + /// the timestamp is the `appliedAt` just passed in, so both are already in + /// the caller's hand. + /// + /// There is exactly one way to write a record. A caller recording a + /// migration as it runs passes `block.timestamp`, which is the same + /// statement as any other `appliedAt` and gets the same three refusals; a + /// second entry point that supplied it would be a second way to write one + /// record, and the one thing it could express is what its argument already + /// spells. + /// + /// A caller SHOULD call this in the same atomic unit as the migration + /// itself where it can — a Safe appends this call to the bundle it is + /// already executing — so that the record and the change it describes + /// cannot land apart. Where they cannot be atomic, call it LAST: a record + /// that never landed leaves a reader asserting the pre-migration state, + /// which the verification layer then catches loudly, and leaves a re-run + /// possible. A record that landed for a migration that did not is the + /// harder state to get out of. + /// @param expectedHead The head the caller believes its namespace is at: + /// the migration it is applying onto, or `MIGRATION_HEAD_GENESIS` for the + /// first migration in a namespace. Never zero, which can never match. + /// @param migration The migration to apply. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @param appliedAt The moment `migration` was applied. Never zero, never + /// after this block, never before the record of `expectedHead`. + function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external; + + /// When `writer` applied `migration`, as the moment supplied with the + /// record. Zero if it never did. + /// + /// The implementation MUST revert `ZeroWriter`, `ZeroMigration` or + /// `GenesisMigration` rather than answering about any of them, and MUST + /// answer zero — not revert — for a real writer that has simply not applied + /// a real migration. + /// + /// That zero is the deliberate difference from a registry whose reads revert + /// on an unknown key. "This migration has not been applied here" is a + /// legitimate, expected answer that a caller branches on and asserts the + /// pre-migration state for; it is the ordinary state of every migration + /// before it runs, and of every migration on a chain that never got it. A + /// revert there would leave a caller with nothing to say about the state it + /// is actually looking at, which is the whole failure this registry removes. + /// + /// Zero is unambiguous because `applyMigration` refuses to write a zero + /// timestamp, so no applied migration can present as an unapplied one. + /// @param writer The namespace to read. Never the zero address. + /// @param migration The migration to ask about. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @return The moment `writer` applied `migration` at, or zero if it has + /// not. + function applied(address writer, bytes32 migration) external view returns (uint256); + + /// Where `writer`'s namespace currently is: the migration it applied most + /// recently, or `MIGRATION_HEAD_GENESIS` if it has never applied one. + /// + /// The implementation MUST revert `ZeroWriter` rather than answering about + /// the zero namespace, and MUST NEVER answer zero — an empty namespace is + /// genesis, and a nonempty one is a nonzero migration id, so a zero answer + /// could only mean the reader had reached something that is not this + /// registry. + /// + /// This is a read for authoring and for diagnosis: which migration a chain + /// is at, and therefore what the next script must name. It is NOT how a + /// script decides that its predecessor ran — that is `applied`, per + /// migration, because a head says only what was last, not what was ever. + /// @param writer The namespace to read. Never the zero address. + /// @return The head of `writer`'s namespace. Never zero. + function head(address writer) external view returns (bytes32); +} diff --git a/src/lib/LibMigrationRegistryV2.sol b/src/lib/LibMigrationRegistryV2.sol new file mode 100644 index 0000000..f07eb6b --- /dev/null +++ b/src/lib/LibMigrationRegistryV2.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +import {IMigrationRegistryV2} from "../interface/IMigrationRegistryV2.sol"; +import {LibMigrationRegistryV2Deploy} from "./LibMigrationRegistryV2Deploy.sol"; + +/// @title LibMigrationRegistryV2 +/// @notice Reads and writes the `MigrationRegistryV2` deployed at a single +/// deterministic address on every network, verifying the registry's code hash +/// first, exactly as `LibAddressRegistry` does for the address registry and +/// `LibRainDeploy` does for the Zoltu factory. An address alone says nothing on +/// a chain the caller has not audited; the address plus the code hash says the +/// caller is talking to the registry it compiled against. +/// +/// That is the whole library. It answers when a writer applied a migration and +/// where that writer's namespace has got to, and it applies one under the +/// caller. Which writer a test trusts, which invariant each answer selects, and +/// how an id is derived are entirely the consumer's business and none of this +/// library's. +/// +/// ## There is deliberately no broadcast runner here +/// +/// `LibRainDeploy` wraps broadcasting because a deploy is always a broadcast. +/// A migration is not: the dominant real shape is a Safe executing a bundle, +/// where the script emits transactions for the multisig to sign and never +/// broadcasts anything itself. Such a script appends `applyMigration` to the +/// bundle it is already emitting, which is what makes the record atomic with the +/// migration it describes — a property no runner in this library could offer, +/// and one a runner would quietly compete with. +/// +/// So `applyMigration` is an ordinary call. A broadcasting EOA script wraps it +/// in its own `vm.startBroadcast`, a Safe bundle appends it, and a test calls it +/// directly; none of those is privileged over the others here. +/// +/// ## Reading is what this is for +/// +/// A test asserts EXACTLY the value implied by the migrations that have run: +/// +/// ```solidity +/// if (LibMigrationRegistryV2.applied(SAFE, MIGRATION_V2) != 0) { +/// assertEq(vault.owner(), NEW_OWNER); +/// } else { +/// assertEq(vault.owner(), OLD_OWNER); +/// } +/// ``` +/// +/// Both branches assert. Neither reads the clock, neither skips, and the branch +/// is selected by what happened on chain rather than by a deadline somebody +/// guessed. `applied` answering zero is an ordinary, expected answer — it is +/// the state of every migration before it runs and of every migration on a +/// chain that never got it — which is why the registry answers it rather than +/// reverting. +/// +/// The nonzero answer is WHEN, which is what a test whose invariant is itself +/// time-shaped needs: a cliff that starts at the migration, a rate that changes +/// a week after it. That is still the clock being read, but it is the chain's +/// record of the migration being read, not a date somebody guessed in advance. +/// +/// ## Writing names the head it is applying onto, and the moment it ran +/// +/// `applyMigration` takes the migration the caller believes ran last in its +/// namespace, so a chain that never got that predecessor refuses the write +/// instead of silently skipping a step, and two migrations dispatched at once +/// cannot land in the wrong order. The first migration in a namespace names +/// `MIGRATION_HEAD_GENESIS`, imported from the interface — never a zero, which +/// is what an uninitialised constant would be and is refused everywhere. +/// +/// It also takes the moment the migration ran, which is what lets a script +/// record a migration that already happened with the time it happened rather +/// than the time it was written down. A script recording a migration it is +/// running right now passes `block.timestamp`; one backfilling a migration that +/// ran before the registry reached this chain passes the moment it ran. The +/// registry refuses a zero, one after the current block, and one before the +/// record of the head being applied onto. +/// +/// `head` reads the head back, which is how an author finds what a new script +/// must name and how an operator sees which migration a chain is at. It is not +/// how a script tests that its predecessor ran: a head says what was LAST, and +/// `applied` is what says whether a particular migration ever ran at all. +/// +/// The registry is an INDEX, not proof. It says which invariant applies; it does +/// not say the invariant holds. A multisig can act out of band and nothing here +/// moves. Codehash and bytecode pins are what verify the state itself, and this +/// library is not a substitute for them. +library LibMigrationRegistryV2 { + /// Thrown when the code at the registry address is not the registry this + /// library was compiled against. An address with no code hits this too: an + /// empty account's code hash is zero, never the expected value. + /// @param expectedCodeHash The code hash of the pinned registry. + /// @param actualCodeHash The code hash actually found at the address. + error UnexpectedMigrationRegistryV2CodeHash(bytes32 expectedCodeHash, bytes32 actualCodeHash); + + /// Reverts unless the pinned registry address holds the pinned code. + /// + /// Both entry points check, and they check the same way, because both are + /// worse than useless against unknown code: a read would branch a test on + /// whatever that code returned, and a write would record a migration + /// somewhere nothing will ever read it. The check is one function so the + /// two cannot drift into checking different things. + function checkCodeHash() internal view { + bytes32 actualCodeHash = LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash; + if (actualCodeHash != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH) { + revert UnexpectedMigrationRegistryV2CodeHash( + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, actualCodeHash + ); + } + } + + /// When `writer` applied `migration`, or zero if it never did. + /// + /// Verifies the registry's code hash before reading, so a chain where the + /// registry is absent, or where something else occupies its address, is a + /// loud revert rather than a call into unknown code. That distinction is + /// the whole point here: "no registry on this chain" and "this migration + /// has not been applied" are different facts, and silently collapsing the + /// first into the second would send a caller down its pre-migration branch + /// on every chain the registry was never deployed to. + /// + /// The registry itself refuses the zero writer, and refuses the two ids a + /// migration can never be, so those arrive as reverts from it rather than + /// as zero. + /// @param writer The namespace to read — the authority whose record the + /// caller trusts. Never the zero address. + /// @param migration The migration to ask about. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @return The moment `writer` applied `migration` at, or zero if it has + /// not. + function applied(address writer, bytes32 migration) internal view returns (uint256) { + checkCodeHash(); + return + IMigrationRegistryV2(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS) + .applied(writer, migration); + } + + /// The migration `writer` applied most recently, or `MIGRATION_HEAD_GENESIS` + /// if it has never applied one. + /// + /// Verifies the registry's code hash first for the same reason `applied` + /// does, and more sharply: a call into an empty account returns nothing, + /// which decodes as zero, and zero is the one value a head can never hold — + /// so an unverified read would hand back a head that is not a head at all, + /// on exactly the chains where nothing has been deployed. + /// @param writer The namespace to read. Never the zero address. + /// @return The head of `writer`'s namespace. Never zero. + function head(address writer) internal view returns (bytes32) { + checkCodeHash(); + return IMigrationRegistryV2(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS).head(writer); + } + + /// Applies `migration` under the CALLER's namespace, onto `expectedHead`, as + /// having been applied at `appliedAt`. + /// + /// The caller is whoever the resulting transaction is sent from — a Safe + /// executing a bundle, a broadcasting EOA, a timelock — and that account is + /// the namespace the record lands in. A reader has to ask about that same + /// account, so which account a migration is applied from is a decision + /// with a consequence rather than an implementation detail. It is also the + /// account whose head this moves, so two unrelated sequences applied from + /// one account interleave into one chain. + /// + /// Verifies the registry's code hash before writing, so a migration is + /// never "applied" into an empty address or into unknown code. A record + /// that went nowhere is worse than no record at all: the migration would + /// have run, and every reader would go on asserting the pre-migration + /// state. + /// + /// The registry refuses the zero id, refuses a migration this caller has + /// already applied, and refuses one applied onto anything but the + /// namespace's actual head — which between them make a re-dispatched, a + /// skipped and an out-of-order migration all fail rather than land. It + /// refuses the three moments a record cannot carry as well: zero, one after + /// the current block, and one before the record of `expectedHead`. + /// @param expectedHead The migration the caller believes it applied last, + /// or `MIGRATION_HEAD_GENESIS` for the first in this namespace. + /// @param migration The migration to apply. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @param appliedAt The moment `migration` was applied — `block.timestamp` + /// for a migration running now, the moment it ran for one being recorded + /// after the fact. + function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) internal { + checkCodeHash(); + IMigrationRegistryV2(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS) + .applyMigration(expectedHead, migration, appliedAt); + } +} diff --git a/src/lib/LibMigrationRegistryV2Deploy.sol b/src/lib/LibMigrationRegistryV2Deploy.sol new file mode 100644 index 0000000..f3317c5 --- /dev/null +++ b/src/lib/LibMigrationRegistryV2Deploy.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. + +import { + DEPLOYED_ADDRESS as MIGRATION_REGISTRY_V2_ADDR, + BYTECODE_HASH as MIGRATION_REGISTRY_V2_HASH +} from "../generated/candidate/MigrationRegistryV2.sol"; + +/// @title LibMigrationRegistryV2Deploy +/// @notice The deterministic Zoltu deploy address and code hash of +/// `MigrationRegistryV2`, aliased from its generated snapshot so that snapshot stays the +/// single source of truth. The import path never moves, so consumers are +/// unaffected by which snapshot it names. +library LibMigrationRegistryV2Deploy { + address constant MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS = MIGRATION_REGISTRY_V2_ADDR; + bytes32 constant MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH = MIGRATION_REGISTRY_V2_HASH; +} diff --git a/src/lib/LibMigrationRegistryV2Released.sol b/src/lib/LibMigrationRegistryV2Released.sol new file mode 100644 index 0000000..e06bdf1 --- /dev/null +++ b/src/lib/LibMigrationRegistryV2Released.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. + +import {DeploySuite} from "../abstract/RainDeploySuitesBase.sol"; + +/// @title LibMigrationRegistryV2Released +/// @notice Every frozen release of `MigrationRegistryV2`: one entry per file in +/// the append-only `src/generated//` record, in tag order. +/// +/// The deploy address, code hash, creation code, runtime code and dependency +/// list of each entry are aliased from that release's own frozen snapshot, so +/// what a release deployed, and what it required to already be on chain, are +/// read from the immutable file and from nowhere else. A dependency dropped +/// from current source stays required by the releases cut with it, and one +/// added is not imposed on releases cut without it. +/// +/// The key and the artifact path are explorer and ordering metadata +/// regenerated from the CURRENT declaration, and are not part of that +/// record. A moved source path retroactively updates every entry's artifact +/// path, which is intended: the alternative is parsing this generated file +/// back in to preserve what it last said. +library LibMigrationRegistryV2Released { + /// Every frozen release, in tag order. + /// @return suites The released suites. + function releasedSuites() internal pure returns (DeploySuite[] memory suites) { + suites = new DeploySuite[](0); + } +} diff --git a/test/concrete/MockMigrationApplierV2.sol b/test/concrete/MockMigrationApplierV2.sol new file mode 100644 index 0000000..ddf4abb --- /dev/null +++ b/test/concrete/MockMigrationApplierV2.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {LibMigrationRegistryV2} from "../../src/lib/LibMigrationRegistryV2.sol"; + +/// @title MockMigrationApplierV2 +/// @notice A consumer in the shape `LibMigrationRegistryV2.applyMigration` is +/// designed for: it calls the library and nothing else, so the record lands +/// under THIS contract's address. +/// +/// It exists so the namespace can be exercised as the property it is. The +/// library's functions are `internal` and inline into whatever executes them, +/// so `msg.sender` at the registry is the calling CONTRACT, not whoever +/// `vm.prank` last named — a test contract calling the library directly can +/// therefore only ever write one namespace. Two of these are two namespaces, +/// which is what makes "a record reaches nobody else" checkable rather than +/// asserted about a single account. +/// +/// Two of them are also two heads, which is what makes "one namespace is one +/// sequence" checkable at all: nothing about one applier's head can be shown +/// to leave the other's alone from inside a single namespace. +contract MockMigrationApplierV2 { + /// Applies `migration` under this contract, onto `expectedHead`, at + /// `appliedAt`. + /// @param expectedHead The head this contract believes it is at. + /// @param migration The migration to apply. + /// @param appliedAt The moment the migration was applied. + function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + LibMigrationRegistryV2.applyMigration(expectedHead, migration, appliedAt); + } + + /// When `writer` applied `migration`. + /// @param writer The namespace to read. + /// @param migration The migration to ask about. + /// @return The moment it was applied at, or zero. + function applied(address writer, bytes32 migration) external view returns (uint256) { + return LibMigrationRegistryV2.applied(writer, migration); + } + + /// The head of `writer`'s namespace. + /// @param writer The namespace to read. + /// @return The head. + function head(address writer) external view returns (bytes32) { + return LibMigrationRegistryV2.head(writer); + } +} diff --git a/test/src/concrete/MigrationRegistryV2Applied.t.sol b/test/src/concrete/MigrationRegistryV2Applied.t.sol new file mode 100644 index 0000000..47f7bda --- /dev/null +++ b/test/src/concrete/MigrationRegistryV2Applied.t.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV2.sol"; +import {MigrationRegistryV2} from "../../../src/concrete/MigrationRegistryV2.sol"; + +/// @title MigrationRegistryV2AppliedTest +/// @notice A test suite for `MigrationRegistryV2.applied`: it answers an applied +/// migration with the moment recorded against it, an unapplied one with zero, +/// refuses the three inputs that can only be mistakes, and is the only reader of +/// the records. +contract MigrationRegistryV2AppliedTest is Test { + /// The registry under test. Stateful, so a fresh one per test. + MigrationRegistryV2 internal sRegistry; + + function setUp() external { + sRegistry = new MigrationRegistryV2(); + } + + /// A migration id that is neither of the two values the head space reserves. + /// @param migration The fuzzed candidate. + function assumeMigration(bytes32 migration) internal pure { + vm.assume(migration != bytes32(0)); + vm.assume(migration != MIGRATION_HEAD_GENESIS); + } + + /// An unapplied migration answers zero rather than reverting. This is + /// the deliberate difference from a registry whose reads revert on an + /// unknown key: "not applied here" is the ordinary state of every migration + /// before it runs and of every migration on a chain that never got it, and + /// it is the answer a caller branches on to assert the pre-migration state + /// exactly. A revert would leave the caller with nothing to say about the + /// state it is actually looking at. + function testAppliedUnappliedIsZero(address writer, bytes32 migration) external view { + vm.assume(writer != address(0)); + assumeMigration(migration); + + assertEq(sRegistry.applied(writer, migration), 0); + } + + /// An applied migration answers the moment recorded against it, and keeps + /// answering it as time moves on. The value is when the migration was + /// applied, not when the record was written and not how long ago or how + /// recently anything was asked. + function testAppliedIsTheRecordedMoment( + address writer, + bytes32 migration, + uint32 appliedAt, + uint32 writtenAt, + uint32 readAt + ) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt >= appliedAt); + vm.assume(readAt >= writtenAt); + + vm.warp(writtenAt); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + vm.warp(readAt); + assertEq(sRegistry.applied(writer, migration), appliedAt); + } + + /// Reading does not consume or alter a record, so the same question asked + /// twice answers the same way. + function testAppliedIsIdempotent(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), block.timestamp); + assertEq(sRegistry.applied(writer, migration), block.timestamp); + } + + /// The zero writer is refused rather than answered. No transaction + /// originates from the zero address, so that namespace is provably empty + /// and zero would be the answer forever — an unresolved writer constant + /// would read as "nothing has been applied" instead of as the mistake it + /// is, and send its caller down the pre-migration branch on every chain. + function testAppliedZeroWriterReverts(bytes32 migration) external { + assumeMigration(migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); + sRegistry.applied(address(0), migration); + } + + /// The zero migration id is refused for the same reason in the other + /// direction: `applyMigration` will not write it, so it can never be a real + /// record. + function testAppliedZeroMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + sRegistry.applied(writer, bytes32(0)); + } + + /// The genesis head is refused as a migration for the same reason again: + /// `applyMigration` will not write it either, so asking about it would + /// answer zero forever to a caller that has confused a head for a migration + /// — and that caller reads zero as its pre-migration branch. + function testAppliedGenesisMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + sRegistry.applied(writer, MIGRATION_HEAD_GENESIS); + } + + /// The writer is checked before the migration, so a caller that has zeroed + /// both is told about the namespace first and gets one stable answer rather + /// than one that depends on which check happens to run. + function testAppliedZeroWriterCheckedFirst() external { + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); + sRegistry.applied(address(0), bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); + sRegistry.applied(address(0), MIGRATION_HEAD_GENESIS); + } + + /// A refusal is not a state change: the refused cases revert on a registry + /// that holds records exactly as they do on an empty one, and leave those + /// records intact. + function testAppliedRefusalLeavesRecordsIntact(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); + sRegistry.applied(address(0), migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + sRegistry.applied(writer, bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + sRegistry.applied(writer, MIGRATION_HEAD_GENESIS); + + assertEq(sRegistry.applied(writer, migration), block.timestamp); + assertEq(sRegistry.head(writer), migration); + } + + /// `applied` is the only reader of the records. The records mapping is not + /// `public`, so the getter a `public` mapping would generate — which answers + /// the zero writer and both refused ids with zero, the exact silent + /// wrong-branch these refusals exist to prevent — does not exist. + function testAppliedNoGeneratedMappingGetter(address writer, bytes32 migration) external { + (bool success,) = + address(sRegistry).call(abi.encodeWithSignature("sApplied(address,bytes32)", writer, migration)); + assertFalse(success); + } + + /// Nor for the heads, where a generated getter would be worse still: it + /// answers an empty namespace with zero, and zero is a value no head can + /// ever hold. + function testAppliedNoGeneratedHeadGetter(address writer) external { + (bool success,) = address(sRegistry).call(abi.encodeWithSignature("sHead(address)", writer)); + assertFalse(success); + } + + /// There is no other entry point at all: no fallback, no receive, and + /// nothing beyond the three `IMigrationRegistryV2` functions, so an unknown + /// selector reverts instead of being silently absorbed. The two-argument + /// `applyMigration` in particular is not here: there is exactly one way to + /// write a record, and it names the moment. + function testAppliedNoOtherEntryPoint(bytes4 selector, bytes32 migration) external { + vm.assume(selector != IMigrationRegistryV2.applied.selector); + vm.assume(selector != IMigrationRegistryV2.applyMigration.selector); + vm.assume(selector != IMigrationRegistryV2.head.selector); + + (bool success,) = address(sRegistry).call(abi.encodeWithSelector(selector, address(this), migration)); + assertFalse(success); + } +} diff --git a/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol b/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol new file mode 100644 index 0000000..6ffb081 --- /dev/null +++ b/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol @@ -0,0 +1,999 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; + +import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV2.sol"; +import {MigrationRegistryV2} from "../../../src/concrete/MigrationRegistryV2.sol"; + +/// @title MigrationRegistryV2ApplyMigrationTest +/// @notice A test suite for `MigrationRegistryV2.applyMigration`: who a record +/// belongs to, that a migration is applied at most once and only onto the head +/// its caller named, which moments a record may carry, what a record carries, +/// and what it may never become. +contract MigrationRegistryV2ApplyMigrationTest is Test { + /// The registry under test. Stateful, so a fresh one per test. + MigrationRegistryV2 internal sRegistry; + + function setUp() external { + sRegistry = new MigrationRegistryV2(); + } + + /// A migration id that is neither of the two values the head space reserves, + /// which is what every test that is not about those values wants. + /// @param migration The fuzzed candidate. + function assumeMigration(bytes32 migration) internal pure { + vm.assume(migration != bytes32(0)); + vm.assume(migration != MIGRATION_HEAD_GENESIS); + } + + /// Anyone may apply, and the record lands under the caller. There is no + /// authority to be refused by, which is the whole access-control design: + /// the namespace IS the caller. + function testApplyMigrationAnyCallerAppliesUnderItself(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), block.timestamp); + } + + /// A record carries the moment the CALLER supplied, which is what lets a + /// migration that already ran be recorded with the time it ran rather than + /// the time it was written down. The block the record lands in is not the + /// value, and a record written long after the fact says so. + function testApplyMigrationRecordsTheSuppliedMoment( + address writer, + bytes32 migration, + uint32 appliedAt, + uint32 writtenAt + ) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt > appliedAt); + vm.warp(writtenAt); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), appliedAt); + assertTrue(sRegistry.applied(writer, migration) != block.timestamp); + } + + /// The moment of the current block is an ordinary value for the parameter, + /// which is what a script recording a migration as it runs passes. There is + /// one way to write a record and this is it with today's argument. + function testApplyMigrationCurrentBlockIsAccepted(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), now_); + } + + /// A moment that has not arrived is refused. A record says a migration HAS + /// run, so a future one is not a late record of anything, and a consumer + /// measuring an interval since the migration would be subtracting a moment + /// later than the one it is measuring from. + function testApplyMigrationFutureTimestampReverts( + address writer, + bytes32 migration, + uint32 now_, + uint256 appliedAt + ) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.warp(now_); + appliedAt = bound(appliedAt, uint256(now_) + 1, type(uint256).max); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, appliedAt, uint256(now_)) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + } + + /// One second past the current block is refused, and the current block is + /// not: the boundary is the block's own timestamp, inclusive. + function testApplyMigrationFutureBoundary(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_) + ) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_)); + assertEq(sRegistry.applied(writer, migration), now_); + } + + /// A block whose timestamp is zero can hold no record at all: zero is + /// refused as a moment, and every other moment is still in the future. The + /// head does not move, so the namespace goes on describing something true + /// and the migration is still applicable once the clock has moved. + function testApplyMigrationZeroBlockRecordsNothing(address writer, bytes32 migration, uint256 appliedAt) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(appliedAt != 0); + vm.warp(0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, appliedAt, 0)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + + vm.warp(1); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); + assertEq(sRegistry.applied(writer, migration), 1); + } + + /// A record refuses to be written at all with a zero moment, rather than + /// write one that `applied` would read back as no record. The head does not + /// move and the migration can still be applied, which is the only outcome + /// that leaves the namespace describing something true. + function testApplyMigrationZeroTimestampReverts(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); + assertEq(sRegistry.applied(writer, migration), 1); + } + + /// A namespace that has applied nothing has no record to be after, so every + /// moment the block allows is accepted — the floor is not the current block, + /// which is the whole of what backfilling a first migration needs. + function testApplyMigrationEmptyNamespaceHasNoFloor( + address writer, + bytes32 migration, + uint32 now_, + uint256 appliedAt + ) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(now_ != 0); + vm.warp(now_); + appliedAt = bound(appliedAt, 1, uint256(now_)); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), appliedAt); + } + + /// A record may not predate the record of the head it is applied onto. The + /// head chain is the order the migrations ran in, so a moment behind the one + /// before it contradicts the sequence it is being appended to. + function testApplyMigrationBeforeHeadReverts( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 headAppliedAt, + uint256 appliedAt + ) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + vm.assume(headAppliedAt > 1); + vm.warp(headAppliedAt); + appliedAt = bound(appliedAt, 1, uint256(headAppliedAt) - 1); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, headAppliedAt); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.TimestampBeforeHead.selector, + writer, + migrationA, + appliedAt, + uint256(headAppliedAt) + ) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, appliedAt); + + assertEq(sRegistry.applied(writer, migrationB), 0); + assertEq(sRegistry.head(writer), migrationA); + } + + /// The floor is inclusive. Two migrations applied in one transaction share a + /// block, and two backfilled migrations known only to the same day share a + /// moment; the head chain is what orders them, so forcing the moments apart + /// would make them carry an ordering they do not have. + function testApplyMigrationEqualToHeadIsAccepted( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 appliedAt + ) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + vm.assume(appliedAt != 0); + vm.warp(appliedAt); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, appliedAt); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, appliedAt); + + assertEq(sRegistry.applied(writer, migrationA), appliedAt); + assertEq(sRegistry.applied(writer, migrationB), appliedAt); + } + + /// One second before the head's record is refused, and the head's own moment + /// is not: the floor is the head's record, inclusive. + function testApplyMigrationFloorBoundary(address writer, bytes32 migrationA, bytes32 migrationB, uint32 now_) + external + { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + vm.assume(now_ > 1); + vm.warp(now_); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(now_) - 1); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.TimestampBeforeHead.selector, + writer, + migrationA, + uint256(now_) - 2, + uint256(now_) - 1 + ) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, uint256(now_) - 2); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, uint256(now_) - 1); + assertEq(sRegistry.applied(writer, migrationB), uint256(now_) - 1); + } + + /// The floor follows the HEAD's record rather than staying at whatever the + /// first one was, so a sequence backfilled in head order keeps every moment + /// it was given and each step is measured against the step before it. + function testApplyMigrationFloorFollowsTheHead( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 migrationC + ) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + assumeMigration(migrationC); + vm.assume(migrationA != migrationB); + vm.assume(migrationB != migrationC); + vm.assume(migrationA != migrationC); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, 2000); + + // Above the first record and below the second, so a floor that had + // stayed at the first would accept this. + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.TimestampBeforeHead.selector, writer, migrationB, uint256(1500), uint256(2000) + ) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationB, migrationC, 1500); + + vm.prank(writer); + sRegistry.applyMigration(migrationB, migrationC, 3000); + + assertEq(sRegistry.applied(writer, migrationA), 1000); + assertEq(sRegistry.applied(writer, migrationB), 2000); + assertEq(sRegistry.applied(writer, migrationC), 3000); + } + + /// The floor belongs to one namespace. Another writer's records say nothing + /// about what this one may record, which is the same confinement the records + /// and the heads already have. + function testApplyMigrationFloorIsPerWriter( + address writer, + address other, + bytes32 migrationA, + bytes32 migrationB + ) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 5000); + + vm.prank(other); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB, 1000); + + assertEq(sRegistry.applied(other, migrationB), 1000); + } + + /// Two migrations applied at different moments carry those moments, and the + /// earlier one does not move when the later one lands. A record is of the + /// moment it happened, not of the last time anything happened. + function testApplyMigrationTimestampsAreIndependent(address writer, bytes32 migrationA, bytes32 migrationB) + external + { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.warp(1000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); + + vm.warp(2000); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, 2000); + + assertEq(sRegistry.applied(writer, migrationA), 1000); + assertEq(sRegistry.applied(writer, migrationB), 2000); + } + + /// The zero moment is refused BEFORE anything about the namespace is read, + /// so an uninitialised argument is reported as itself rather than as + /// whatever the namespace happens to make of it. Fuzzed over the head and + /// checked against a namespace that has moved on, because a head the + /// namespace happens to be at is accepted whichever check runs first. + function testApplyMigrationZeroTimestampCheckedBeforeTheNamespace( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 anyHead + ) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + // Already applied, and a zero moment: told about the moment. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 0); + + // A head that has moved on, and a zero moment: told about the moment. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, migrationB, 0); + } + + /// The two id refusals come before the moment, so a caller that has zeroed + /// both an id and a moment is told about the id: an id is what the record + /// is ABOUT, and a call with no subject has nothing to say a moment for. + function testApplyMigrationIdCheckedBeforeTimestamp(address writer, bytes32 anyHead) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, bytes32(0), 0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS, 0); + } + + /// The refusals that describe the NAMESPACE come before the two that + /// describe the moment against it, so a re-dispatched script is told its + /// migration already ran, and a script at the wrong point in the sequence is + /// told where the namespace is, rather than either of them being sent to + /// look at a clock. + function testApplyMigrationNamespaceCheckedBeforeTheWindow( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 skipped + ) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + assumeMigration(skipped); + vm.assume(migrationA != migrationB); + vm.assume(skipped != migrationA); + vm.assume(skipped != migrationB); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 5000); + + // Already applied, and backdated: told it already ran. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationA, 1000); + + // Already applied, and in the future: told it already ran. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationA, 9001); + + // The wrong head, and backdated: told where the namespace is. + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA + ) + ); + vm.prank(writer); + sRegistry.applyMigration(skipped, migrationB, 1000); + + // The wrong head, and in the future: told where the namespace is. + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA + ) + ); + vm.prank(writer); + sRegistry.applyMigration(skipped, migrationB, 9001); + } + + /// A record is confined to the caller's namespace. Applying under one + /// writer says nothing about any other, which is what makes a reader's + /// choice of namespace the whole of who it trusts — a hostile caller can + /// apply whatever it likes and reach nobody. + function testApplyMigrationDoesNotReachAnotherNamespace(address writer, address other, bytes32 migration) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), block.timestamp); + assertEq(sRegistry.applied(other, migration), 0); + } + + /// Two writers may apply the same migration id independently, and each + /// answers only for itself. Ids are opaque and namespaces are unrelated, so + /// a shared id is not a collision — including for the head, which each + /// writer advances from its own genesis. + function testApplyMigrationSameMigrationUnderTwoWriters(address writer, address other, bytes32 migration) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + vm.prank(other); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), block.timestamp); + assertEq(sRegistry.applied(other, migration), block.timestamp); + } + + /// Migrations are independent within one namespace: applying one says + /// nothing about any other. This is what a set buys over a high-water mark + /// — a reader asks about the migration its assertion actually depends on + /// rather than about a number that stands in for all of them. + function testApplyMigrationDistinctMigrations(address writer, bytes32 migrationA, bytes32 migrationB) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + assertEq(sRegistry.applied(writer, migrationA), block.timestamp); + assertEq(sRegistry.applied(writer, migrationB), 0); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, block.timestamp); + + assertEq(sRegistry.applied(writer, migrationA), block.timestamp); + assertEq(sRegistry.applied(writer, migrationB), block.timestamp); + } + + /// A successful application makes its migration the namespace's new head, + /// which is what the next one has to name. + function testApplyMigrationAdvancesTheHead(address writer, bytes32 migrationA, bytes32 migrationB) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + assertEq(sRegistry.head(writer), migrationA); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, block.timestamp); + assertEq(sRegistry.head(writer), migrationB); + } + + /// Applying onto a head the namespace is not at is refused. This is what + /// blocks a SKIPPED step: a script names its predecessor, so a chain that + /// never got that predecessor fails at the moment of applying rather than + /// diverging silently from every chain that did. + function testApplyMigrationSkippedPredecessorReverts( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 skipped + ) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + assumeMigration(skipped); + vm.assume(migrationA != migrationB); + vm.assume(skipped != migrationA); + vm.assume(skipped != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(skipped, migrationB, block.timestamp); + + assertEq(sRegistry.applied(writer, migrationB), 0); + assertEq(sRegistry.head(writer), migrationA); + } + + /// Genesis stops being an acceptable head the moment anything is applied, + /// so a first-migration script re-run against a namespace that has moved on + /// fails rather than restarting the sequence. + function testApplyMigrationOntoGenesisAfterFirstReverts(address writer, bytes32 migrationA, bytes32 migrationB) + external + { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, MIGRATION_HEAD_GENESIS, migrationA + ) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB, block.timestamp); + } + + /// A head belongs to one namespace. One writer advancing its head leaves + /// every other writer's exactly where it was, so a second consumer's + /// migrations are not blocked or unblocked by the first's. + function testApplyMigrationHeadIsPerWriter(address writer, address other, bytes32 migrationA, bytes32 migrationB) + external + { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + assertEq(sRegistry.head(other), MIGRATION_HEAD_GENESIS); + + // The other namespace is still at genesis, so `migrationA` is not the + // head there and naming it is refused. + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, other, migrationA, MIGRATION_HEAD_GENESIS + ) + ); + vm.prank(other); + sRegistry.applyMigration(migrationA, migrationB, block.timestamp); + + vm.prank(other); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB, block.timestamp); + assertEq(sRegistry.head(other), migrationB); + assertEq(sRegistry.head(writer), migrationA); + } + + /// A zero head never matches anything, including on a namespace that has + /// applied nothing — which is the whole reason genesis is not zero. An + /// uninitialised predecessor constant is a revert in every namespace state, + /// rather than a successful first application on every chain that happens + /// to be empty. + function testApplyMigrationZeroHeadRevertsOnEmptyNamespace(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, bytes32(0), MIGRATION_HEAD_GENESIS + ) + ); + vm.prank(writer); + sRegistry.applyMigration(bytes32(0), migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), 0); + } + + /// And on a namespace that has applied something. + function testApplyMigrationZeroHeadRevertsOnUsedNamespace(address writer, bytes32 migrationA, bytes32 migrationB) + external + { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, bytes32(0), migrationA + ) + ); + vm.prank(writer); + sRegistry.applyMigration(bytes32(0), migrationB, block.timestamp); + } + + /// Applying twice is refused. This is what makes running a migration twice + /// fail rather than repeat: a re-dispatched script cannot quietly apply + /// its way to looking like a first run. + function testApplyMigrationTwiceReverts(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migration) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), block.timestamp); + } + + /// The head does NOT subsume the already-applied refusal. Re-applying a + /// migration whose successor has since landed presents a head that matches + /// perfectly, and is still refused — otherwise the head would move BACKWARDS + /// and the original moment would be overwritten, which is a record + /// un-happening. + function testApplyMigrationAgainOnMatchingHeadReverts(address writer, bytes32 migrationA, bytes32 migrationB) + external + { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.warp(1000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, 1000); + + // The namespace really is at `migrationB`, so the head this names is + // correct and only the already-applied refusal can stop it. + assertEq(sRegistry.head(writer), migrationB); + vm.warp(2000); + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationB, migrationA, 2000); + + assertEq(sRegistry.head(writer), migrationB); + assertEq(sRegistry.applied(writer, migrationA), 1000); + } + + /// The already-applied refusal is checked BEFORE the head, so a + /// re-dispatched script — which names the same head it named the first time, + /// long since moved on — is told that its migration already ran rather than + /// told the namespace is somewhere else and left to work out why. + function testApplyMigrationAlreadyAppliedCheckedBeforeHead(address writer, bytes32 migrationA, bytes32 migrationB) + external + { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, block.timestamp); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + } + + /// A migration another writer has already applied is still a FIRST record + /// for this one. The refusal is per namespace, not global, or one consumer + /// choosing a common id would lock every other consumer out of it. + function testApplyMigrationTwiceIsPerWriter(address writer, address other, bytes32 migration) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + vm.prank(other); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(other, migration), block.timestamp); + } + + /// The zero migration id is refused. It is what an uninitialised `bytes32` + /// constant reads as, and there is deliberately no way to apply one, which + /// is what lets `applied` refuse it as a mistake rather than have to answer + /// about it. + function testApplyMigrationZeroMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, bytes32(0), block.timestamp); + } + + /// The zero id is refused BEFORE the already-applied read and before the + /// head, so it is always reported as `ZeroMigration` and never as anything + /// about where the namespace is. + /// + /// Fuzzed over the head against BOTH an empty namespace and one that has + /// moved on, for the same reason + /// `testApplyMigrationGenesisMigrationRevertsOnAnyHead` is: one namespace + /// state cannot tell the orderings apart, because a head the namespace + /// happens to be at is accepted whichever check runs first, and the two + /// states here have different heads so no fuzzed head matches both. + /// + /// The already-applied read can never answer anything but zero for this id + /// — this refusal is what keeps the zero id out of the records in the first + /// place — so what the second call pins is the reachable half of the same + /// claim: the refusal is a fact about the ID, not about the state of the + /// namespace it arrives at. + function testApplyMigrationZeroMigrationCheckedFirst(address writer, bytes32 anyHead, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, bytes32(0), block.timestamp); + + // A namespace that has moved on: the zero id is still reported as + // `ZeroMigration` rather than as anything about the head or about what + // has already been applied. + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, bytes32(0), block.timestamp); + } + + /// Genesis is a head, not a migration, and applying it is refused. It would + /// otherwise leave the namespace's head holding the exact value an empty + /// namespace reads as, so a namespace that had applied something would be + /// indistinguishable from one that had not — and the next first-migration + /// script would be accepted against it. + function testApplyMigrationGenesisMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_HEAD_GENESIS, block.timestamp); + + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + } + + /// Refused whatever head it is applied onto, so it is a fact about the id + /// rather than about where the namespace happens to be. That means a head + /// the namespace is NOT at as much as one it is: the refusal is checked + /// before the head, so a caller that has confused a head for a migration is + /// told which of the two it got wrong rather than sent to look at where the + /// namespace has got to. + /// + /// Fuzzed over the head for the same reason + /// `testApplyMigrationZeroMigrationCheckedFirst` is: a matching head alone + /// cannot tell the two orderings apart. + function testApplyMigrationGenesisMigrationRevertsOnAnyHead(address writer, bytes32 migration, bytes32 anyHead) + external + { + vm.assume(writer != address(0)); + assumeMigration(migration); + + // An empty namespace, whose head is genesis: still refused onto a head + // that does not match it. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS, block.timestamp); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + // A namespace that has moved: same refusal, onto the head it is at and + // onto any other. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(migration, MIGRATION_HEAD_GENESIS, block.timestamp); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS, block.timestamp); + + assertEq(sRegistry.head(writer), migration); + } + + /// Ids are opaque: nothing about a migration's bytes changes how it is + /// stored or read, including ids no hashing convention would produce. + function testApplyMigrationOpaqueMigrationIds(address writer) external { + vm.assume(writer != address(0)); + + bytes32[2] memory migrations = [bytes32(uint256(1)), bytes32(type(uint256).max)]; + for (uint256 i = 0; i < migrations.length; i++) { + MigrationRegistryV2 registry = new MigrationRegistryV2(); + vm.prank(writer); + registry.applyMigration(MIGRATION_HEAD_GENESIS, migrations[i], block.timestamp); + assertEq(registry.applied(writer, migrations[i]), block.timestamp); + assertEq(registry.head(writer), migrations[i]); + } + } + + /// `Migrated` is emitted with the writer and migration both indexed, so the + /// log can be filtered by either, and carries the moment as data. The log is + /// the only enumeration of the registry, so a record that does not emit is a + /// record nobody can find. + /// + /// It carries no head because the log already holds it: one writer's entries + /// in order ARE its chain of heads. It does carry the moment, which the + /// block a log entry sits in does not — that block says when the record was + /// written, and the moment says when the migration ran. + function testApplyMigrationEvent(address writer, bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt > appliedAt); + vm.warp(writtenAt); + + vm.recordLogs(); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].emitter, address(sRegistry)); + assertEq(entries[0].topics.length, 3); + assertEq(entries[0].topics[0], keccak256("Migrated(address,bytes32,uint256)")); + assertEq(entries[0].topics[1], bytes32(uint256(uint160(writer)))); + assertEq(entries[0].topics[2], migration); + assertEq(entries[0].data, abi.encode(uint256(appliedAt))); + } + + /// A refused `applyMigration` emits nothing, so a failed apply can never be + /// mistaken for a record by anything reading the logs — which for a + /// re-dispatched migration is exactly the mistake that matters. + function testApplyMigrationNoEventOnRevert(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 5000); + + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migration) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 5000); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(migration, bytes32(0), 5000); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(migration, MIGRATION_HEAD_GENESIS, 5000); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), 0); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, MIGRATION_HEAD_GENESIS, migration + ) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, keccak256(abi.encode(migration)), 5000); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.TimestampBeforeHead.selector, + writer, + migration, + uint256(4999), + uint256(5000) + ) + ); + vm.prank(writer); + sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), 4999); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, uint256(9001), uint256(9000)) + ); + vm.prank(writer); + sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), 9001); + assertEq(vm.getRecordedLogs().length, 0); + } +} diff --git a/test/src/concrete/MigrationRegistryV2Head.t.sol b/test/src/concrete/MigrationRegistryV2Head.t.sol new file mode 100644 index 0000000..a79b086 --- /dev/null +++ b/test/src/concrete/MigrationRegistryV2Head.t.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV2.sol"; +import {MigrationRegistryV2} from "../../../src/concrete/MigrationRegistryV2.sol"; + +/// @title MigrationRegistryV2HeadTest +/// @notice A test suite for `MigrationRegistryV2.head`: where a namespace is, +/// what an empty one answers, that the answer is never a value that is not a +/// head, and that it is the same answer `applyMigration` checks against. +contract MigrationRegistryV2HeadTest is Test { + /// The registry under test. Stateful, so a fresh one per test. + MigrationRegistryV2 internal sRegistry; + + function setUp() external { + sRegistry = new MigrationRegistryV2(); + } + + /// A migration id that is neither of the two values the head space reserves. + /// @param migration The fuzzed candidate. + function assumeMigration(bytes32 migration) internal pure { + vm.assume(migration != bytes32(0)); + vm.assume(migration != MIGRATION_HEAD_GENESIS); + } + + /// A namespace that has applied nothing is at genesis, which is an ANSWER + /// rather than a revert for the same reason an unapplied migration answers + /// zero: it is the ordinary state of every namespace before its first + /// migration, and of every namespace on a chain that never got one. + function testHeadEmptyNamespaceIsGenesis(address writer) external view { + vm.assume(writer != address(0)); + + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + } + + /// Genesis is deliberately not zero, so an uninitialised predecessor + /// constant can never be mistaken for "the start of the sequence" — which + /// is the mistake that would otherwise pass on every chain that has not been + /// migrated yet. + function testHeadGenesisIsNotZero() external pure { + assertTrue(MIGRATION_HEAD_GENESIS != bytes32(0)); + } + + /// Genesis holds no record, which is what makes an empty namespace's floor + /// zero: `applyMigration` reads the record of whatever head it is applying + /// onto, and for a namespace that has applied nothing that read is of a key + /// nothing can ever be written against. + function testHeadGenesisHoldsNoRecord(address writer, bytes32 migration, uint32 appliedAt) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + vm.assume(appliedAt != 0); + vm.warp(appliedAt); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + sRegistry.applied(writer, MIGRATION_HEAD_GENESIS); + } + + /// The head is the migration applied most recently, and it moves with each + /// one. + function testHeadFollowsTheRecords(address writer, bytes32 migrationA, bytes32 migrationB) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + assertEq(sRegistry.head(writer), migrationA); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, block.timestamp); + assertEq(sRegistry.head(writer), migrationB); + } + + /// Every writer has its own head, and one namespace's records leave every + /// other namespace exactly where it was. + function testHeadIsPerWriter(address writer, address other, bytes32 migration) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.head(writer), migration); + assertEq(sRegistry.head(other), MIGRATION_HEAD_GENESIS); + } + + /// The head `head` reports is exactly the head `applyMigration` demands: + /// whatever this answers is accepted, and it is the only value that is. The + /// two go through one translation of an empty namespace, so they cannot + /// disagree about where one is. + function testHeadIsWhatApplyMigrationAccepts(address writer, bytes32 migrationA, bytes32 migrationB) external { + vm.assume(writer != address(0)); + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + + // Hoisted, because `vm.prank` applies to the next call and reading the + // head is a call. + bytes32 headBeforeA = sRegistry.head(writer); + vm.prank(writer); + sRegistry.applyMigration(headBeforeA, migrationA, block.timestamp); + + bytes32 headBeforeB = sRegistry.head(writer); + vm.prank(writer); + sRegistry.applyMigration(headBeforeB, migrationB, block.timestamp); + + assertEq(sRegistry.head(writer), migrationB); + assertEq(sRegistry.applied(writer, migrationA), block.timestamp); + assertEq(sRegistry.applied(writer, migrationB), block.timestamp); + } + + /// The zero namespace is refused rather than answered genesis. It is + /// provably empty forever, so "nothing has been applied here" is true of it + /// and false of whatever the caller meant to ask about — and a caller that + /// believed it would send a first migration at it. + function testHeadZeroWriterReverts() external { + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); + sRegistry.head(address(0)); + } + + /// Refused on a registry holding records exactly as on an empty one, and + /// the refusal changes nothing. + function testHeadZeroWriterRevertsWithRecordsPresent(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); + sRegistry.head(address(0)); + + assertEq(sRegistry.head(writer), migration); + } + + /// A head is never zero, in any namespace state, which is what lets a + /// consumer treat a zero answer as "this is not the registry" rather than as + /// a namespace. + function testHeadIsNeverZero(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + + assertTrue(sRegistry.head(writer) != bytes32(0)); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertTrue(sRegistry.head(writer) != bytes32(0)); + } + + /// A refused apply leaves the head where it was. The head moves only for a + /// migration that was actually applied, so it can never describe a step + /// that did not happen. + function testHeadUnmovedByRefusedApplyMigration(address writer, bytes32 migration, bytes32 wrongHead) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + assumeMigration(wrongHead); + vm.assume(wrongHead != migration); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, wrongHead, MIGRATION_HEAD_GENESIS + ) + ); + vm.prank(writer); + sRegistry.applyMigration(wrongHead, migration, block.timestamp); + + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + } +} diff --git a/test/src/lib/LibMigrationRegistryV2.t.sol b/test/src/lib/LibMigrationRegistryV2.t.sol new file mode 100644 index 0000000..a18b5cc --- /dev/null +++ b/test/src/lib/LibMigrationRegistryV2.t.sol @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibMigrationRegistryDeploy} from "../../../src/lib/LibMigrationRegistryDeploy.sol"; +import {LibMigrationRegistryV2} from "../../../src/lib/LibMigrationRegistryV2.sol"; +import {LibMigrationRegistryV2Deploy} from "../../../src/lib/LibMigrationRegistryV2Deploy.sol"; +import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; +import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV2.sol"; +import {MigrationRegistryV2} from "../../../src/concrete/MigrationRegistryV2.sol"; +import {MockMigrationApplierV2} from "../../concrete/MockMigrationApplierV2.sol"; +import {DELEGATION_DESIGNATOR_LENGTH, LibAccountCode} from "../../lib/LibAccountCode.sol"; + +/// @title LibMigrationRegistryV2Test +/// Tests for `LibMigrationRegistryV2`. The registry is not mocked: the real +/// `MigrationRegistryV2` is deployed through the Zoltu factory, which is what +/// puts it at the pinned address with the pinned code hash, so every test runs +/// against the same bytecode a network would. +/// +/// External wrappers are used for the library functions so `vm.expectRevert` +/// lands at the correct call depth. +contract LibMigrationRegistryV2Test is Test { + /// Deploys `MigrationRegistryV2` through the Zoltu factory, which lands it + /// at the pinned address. + /// @return The deployed registry. + function deployRegistry() internal returns (IMigrationRegistryV2) { + LibRainDeploy.etchZoltuFactory(vm); + return IMigrationRegistryV2(LibRainDeploy.deployZoltu(type(MigrationRegistryV2).creationCode)); + } + + /// A migration id that is neither of the two values the head space reserves. + /// @param migration The fuzzed candidate. + function assumeMigration(bytes32 migration) internal pure { + vm.assume(migration != bytes32(0)); + vm.assume(migration != MIGRATION_HEAD_GENESIS); + } + + /// Occupant code that is ORDINARY contract code rather than a delegation + /// designator, which is the kind the three `WrongCode` cases fuzz. + /// + /// `LibAccountCode` is what says why the split exists. The designator kind + /// is covered on its own by the three `DelegatedCode` cases, because an + /// account holding one executes the delegate's code while carrying 23 bytes + /// of its own — a shape no amount of fuzzing `bytes` can construct, and one + /// `vm.etch` refuses at every length but that. + /// @param code The fuzzed candidate. + function assumeOrdinaryCode(bytes memory code) internal pure { + vm.assume(code.length > 0); + vm.assume(!LibAccountCode.hasDelegationPrefix(code)); + } + + /// The designator that delegates the registry address to `delegate`, + /// refusing the clearing form — a delegation to zero leaves the account + /// empty, which is what the `NoRegistry` cases already cover. + /// @param delegate The fuzzed delegate. + /// @return designator The 23-byte designator. + function assumedDesignator(address delegate) internal pure returns (bytes memory designator) { + vm.assume(delegate != address(0)); + designator = LibAccountCode.delegationDesignator(delegate); + } + + /// External wrapper for `applied` so that `vm.expectRevert` works at the + /// correct call depth. + /// @param writer The namespace to read. + /// @param migration The migration to ask about. + /// @return When `writer` applied `migration`, or zero. + function externalApplied(address writer, bytes32 migration) external view returns (uint256) { + return LibMigrationRegistryV2.applied(writer, migration); + } + + /// External wrapper for `head` so that `vm.expectRevert` works at the + /// correct call depth. + /// @param writer The namespace to read. + /// @return The head of that namespace. + function externalHead(address writer) external view returns (bytes32) { + return LibMigrationRegistryV2.head(writer); + } + + /// External wrapper for `applyMigration` so that `vm.expectRevert` works at + /// the correct call depth. + /// @param expectedHead The head this contract believes it is at. + /// @param migration The migration to apply. + /// @param appliedAt The moment the migration was applied. + function externalApplyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + LibMigrationRegistryV2.applyMigration(expectedHead, migration, appliedAt); + } + + /// The Zoltu deploy really does land the registry on its pinned address + /// with its pinned code hash. Every other test here depends on that, and a + /// pin that had gone stale would otherwise show up as an unrelated + /// code-hash revert in all of them. + function testDeployMatchesPins() external { + deployRegistry(); + + assertEq( + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH + ); + } + + /// This registry is a separate deployment from the one `LibMigrationRegistry` + /// reads, at its own address, so a consumer's namespace under one says + /// nothing about its namespace under the other. Two creation codes are two + /// addresses, which is what makes the two coexist on one chain. + function testDeployAddressIsItsOwn() external pure { + assertTrue( + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS + != LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS + ); + } + + /// An unapplied migration answers zero. This is the branch a caller + /// asserts the pre-migration state in, and it is the ordinary state of + /// every migration that has not run, so it is an answer rather than a + /// revert. + function testAppliedUnappliedIsZero(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + assumeMigration(migration); + deployRegistry(); + + assertEq(LibMigrationRegistryV2.applied(writer, migration), 0); + } + + /// An applied migration answers the moment it was given — read back through + /// the library, so what `applyMigration` writes is what `applied` finds, and + /// the moment survives the round trip rather than being replaced by the + /// block the write landed in. + function testApplyMigrationThenApplied(bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { + assumeMigration(migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt >= appliedAt); + deployRegistry(); + vm.warp(writtenAt); + + LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(LibMigrationRegistryV2.applied(address(this), migration), appliedAt); + } + + /// A namespace that has applied nothing reads back as genesis, and each + /// record moves the head to itself. This is the value the next migration + /// has to name, so it is read through the library rather than assumed. + function testHeadFollowsTheRecords(bytes32 migrationA, bytes32 migrationB) external { + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + deployRegistry(); + + assertEq(LibMigrationRegistryV2.head(address(this)), MIGRATION_HEAD_GENESIS); + + LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + assertEq(LibMigrationRegistryV2.head(address(this)), migrationA); + + LibMigrationRegistryV2.applyMigration(migrationA, migrationB, block.timestamp); + assertEq(LibMigrationRegistryV2.head(address(this)), migrationB); + } + + /// A migration applied onto a head this namespace is not at is refused, and + /// the registry's own revert arrives unmodified. This is a skipped step + /// failing at the moment of applying rather than a chain quietly diverging. + function testApplyMigrationSkippedPredecessorReverts(bytes32 migration, bytes32 skipped) external { + assumeMigration(migration); + assumeMigration(skipped); + deployRegistry(); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.UnexpectedMigrationHead.selector, address(this), skipped, MIGRATION_HEAD_GENESIS + ) + ); + this.externalApplyMigration(skipped, migration, block.timestamp); + + assertEq(LibMigrationRegistryV2.applied(address(this), migration), 0); + } + + /// The namespace is the CONTRACT that executes the library call. The + /// library's functions are `internal`, so they inline into their caller and + /// the registry sees that caller as `msg.sender` — which means a consumer + /// chooses its namespace by choosing what sends the transaction, and cannot + /// write anybody else's. + function testApplyMigrationLandsUnderTheCallingContract(bytes32 migration) external { + assumeMigration(migration); + deployRegistry(); + MockMigrationApplierV2 applier = new MockMigrationApplierV2(); + + applier.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(LibMigrationRegistryV2.applied(address(applier), migration), block.timestamp); + assertEq(LibMigrationRegistryV2.applied(address(this), migration), 0); + } + + /// One caller's record reaches no other namespace, and each answers only + /// for itself — heads included, so one consumer's sequence neither blocks + /// nor unblocks another's. This is the whole of the access control: a + /// reader's choice of writer is the whole of who it trusts. + function testApplyMigrationDoesNotReachAnotherNamespace(bytes32 migration) external { + assumeMigration(migration); + deployRegistry(); + MockMigrationApplierV2 applier = new MockMigrationApplierV2(); + MockMigrationApplierV2 other = new MockMigrationApplierV2(); + + applier.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(other.applied(address(applier), migration), block.timestamp); + assertEq(other.applied(address(other), migration), 0); + assertEq(other.head(address(applier)), migration); + assertEq(other.head(address(other)), MIGRATION_HEAD_GENESIS); + } + + /// Applying the same migration twice is refused, and the registry's own + /// revert arrives unmodified — the library adds no handling of its own, so + /// a re-dispatched migration fails naming the writer and the id. + function testApplyMigrationTwiceReverts(bytes32 migration) external { + assumeMigration(migration); + deployRegistry(); + + LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, address(this), migration) + ); + this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + } + + /// The registry's zero-id refusal arrives unmodified through + /// `applyMigration`. + function testApplyMigrationZeroMigrationReverts() external { + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + this.externalApplyMigration(MIGRATION_HEAD_GENESIS, bytes32(0), block.timestamp); + } + + /// The registry's genesis-id refusal arrives unmodified through + /// `applyMigration`. + function testApplyMigrationGenesisMigrationReverts() external { + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + this.externalApplyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_HEAD_GENESIS, block.timestamp); + } + + /// The registry's zero-moment refusal arrives unmodified through + /// `applyMigration`, so a consumer that left its `appliedAt` uninitialised + /// is told so rather than writing a record that reads back as none. + function testApplyMigrationZeroTimestampReverts(bytes32 migration) external { + assumeMigration(migration); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); + this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + } + + /// The registry's future-moment refusal arrives unmodified through + /// `applyMigration`. + function testApplyMigrationFutureTimestampReverts(bytes32 migration, uint32 now_) external { + assumeMigration(migration); + deployRegistry(); + vm.warp(now_); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_) + ) + ); + this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); + } + + /// The registry's before-the-head refusal arrives unmodified through + /// `applyMigration`. + function testApplyMigrationTimestampBeforeHeadReverts(bytes32 migrationA, bytes32 migrationB) external { + assumeMigration(migrationA); + assumeMigration(migrationB); + vm.assume(migrationA != migrationB); + deployRegistry(); + vm.warp(9000); + + LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 5000); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV2.TimestampBeforeHead.selector, + address(this), + migrationA, + uint256(4999), + uint256(5000) + ) + ); + this.externalApplyMigration(migrationA, migrationB, 4999); + } + + /// A namespace backfilled in head order keeps every moment it was given, so + /// a consumer whose migrations ran before this registry reached the chain + /// records what actually happened rather than the day it got round to + /// writing it down. + function testApplyMigrationBackfillsAHistoricalSequence( + bytes32 migrationA, + bytes32 migrationB, + bytes32 migrationC + ) external { + assumeMigration(migrationA); + assumeMigration(migrationB); + assumeMigration(migrationC); + vm.assume(migrationA != migrationB); + vm.assume(migrationB != migrationC); + vm.assume(migrationA != migrationC); + deployRegistry(); + vm.warp(9000); + + LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); + LibMigrationRegistryV2.applyMigration(migrationA, migrationB, 2000); + LibMigrationRegistryV2.applyMigration(migrationB, migrationC, 3000); + + assertEq(LibMigrationRegistryV2.applied(address(this), migrationA), 1000); + assertEq(LibMigrationRegistryV2.applied(address(this), migrationB), 2000); + assertEq(LibMigrationRegistryV2.applied(address(this), migrationC), 3000); + assertEq(LibMigrationRegistryV2.head(address(this)), migrationC); + } + + /// The registry's zero-writer refusal arrives unmodified through `applied`. + function testAppliedZeroWriterReverts(bytes32 migration) external { + assumeMigration(migration); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); + this.externalApplied(address(0), migration); + } + + /// The registry's zero-id refusal arrives unmodified through `applied`. + function testAppliedZeroMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); + this.externalApplied(writer, bytes32(0)); + } + + /// The registry's genesis-id refusal arrives unmodified through `applied`. + function testAppliedGenesisMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); + this.externalApplied(writer, MIGRATION_HEAD_GENESIS); + } + + /// The registry's zero-writer refusal arrives unmodified through `head`. + function testHeadZeroWriterReverts() external { + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); + this.externalHead(address(0)); + } + + /// A chain with no registry deployed reverts on the code hash rather than + /// calling into an empty account. That call would succeed and return + /// nothing, which `abi.decode` would read as zero — "this migration has + /// not been applied", on every chain the registry was never deployed to, + /// which is exactly the silent pre-migration branch this library exists to + /// make impossible. + function testAppliedNoRegistry(address writer, bytes32 migration) external { + assertEq(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.code.length, 0); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + bytes32(0) + ) + ); + this.externalApplied(writer, migration); + } + + /// Reading a head off a chain with no registry is refused for a sharper + /// version of the same reason: the empty-account read decodes as zero, and + /// zero is a value no head can ever hold, so an unverified read hands back + /// something that is not a head at all. + function testHeadNoRegistry(address writer) external { + assertEq(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.code.length, 0); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + bytes32(0) + ) + ); + this.externalHead(writer); + } + + /// Writing to a chain with no registry is refused for the mirror reason: an + /// `applyMigration` into an empty account is a migration that reports itself + /// applied and is not, which leaves every reader asserting the + /// pre-migration state forever. + function testApplyMigrationNoRegistry(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + assertEq(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.code.length, 0); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + bytes32(0) + ) + ); + this.externalApplyMigration(expectedHead, migration, appliedAt); + } + + /// A chain where ordinary code other than the pinned registry occupies the + /// address reverts on the code hash, so a migration is never read from code + /// the caller did not compile against. + function testAppliedWrongCode(address writer, bytes32 migration, bytes memory code) external { + assumeOrdinaryCode(code); + vm.assume(keccak256(code) != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH); + vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, code); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + keccak256(code) + ) + ); + this.externalApplied(writer, migration); + } + + /// Nor is a head. + function testHeadWrongCode(address writer, bytes memory code) external { + assumeOrdinaryCode(code); + vm.assume(keccak256(code) != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH); + vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, code); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + keccak256(code) + ) + ); + this.externalHead(writer); + } + + /// And never applied into it either. + function testApplyMigrationWrongCode( + bytes32 expectedHead, + bytes32 migration, + uint256 appliedAt, + bytes memory code + ) external { + assumeOrdinaryCode(code); + vm.assume(keccak256(code) != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH); + vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, code); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + keccak256(code) + ) + ); + this.externalApplyMigration(expectedHead, migration, appliedAt); + } + + /// A chain where an EOA has DELEGATED the registry address under EIP-7702 + /// is refused on the code hash exactly as ordinary wrong code is. This is + /// the other way an address gets occupied, and the worse one for a reader: + /// the account carries 23 bytes of designator while executing whatever the + /// delegate holds, so an address that looks like nothing at all can answer + /// `applied` with any timestamp it likes. + /// + /// The code hash refuses it without knowing anything about 7702 — a + /// delegated account hashes its designator and never the delegate's code, + /// so it can never present the pinned registry's hash. + /// @param writer The namespace a reader would ask about. + /// @param migration The migration a reader would ask about. + /// @param delegate The account the registry address is delegated to. + function testAppliedDelegatedCode(address writer, bytes32 migration, address delegate) external { + bytes memory designator = assumedDesignator(delegate); + assertEq(designator.length, DELEGATION_DESIGNATOR_LENGTH); + + vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, designator); + assertEq( + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash, keccak256(designator) + ); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + keccak256(designator) + ) + ); + this.externalApplied(writer, migration); + } + + /// Nor is a head read out of a delegated account. + /// @param writer The namespace a reader would ask about. + /// @param delegate The account the registry address is delegated to. + function testHeadDelegatedCode(address writer, address delegate) external { + bytes memory designator = assumedDesignator(delegate); + + vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, designator); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + keccak256(designator) + ) + ); + this.externalHead(writer); + } + + /// And a migration is never applied into one. This is the write, so the + /// delegate would otherwise be handed a record the writer believes is in + /// the registry and every later reader asserts against. + /// @param expectedHead The head the writer believes it is at. + /// @param migration The migration being applied. + /// @param appliedAt The moment being recorded. + /// @param delegate The account the registry address is delegated to. + function testApplyMigrationDelegatedCode( + bytes32 expectedHead, + bytes32 migration, + uint256 appliedAt, + address delegate + ) external { + bytes memory designator = assumedDesignator(delegate); + + vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, designator); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, + LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, + keccak256(designator) + ) + ); + this.externalApplyMigration(expectedHead, migration, appliedAt); + } +} From cd429f12a50f1920dc45a4ea1c826bdcf54a781b Mon Sep 17 00:00:00 2001 From: David Meister Date: Sun, 16 Aug 2026 13:09:39 +0000 Subject: [PATCH 2/8] Merge main, fix the floor-boundary domain, and document V2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The floor-boundary case fuzzed a head record of 1, which puts "one second below the floor" at zero — refused as a moment before any floor is read. Domain raised so the value below the floor is one a record could carry. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 86 +++++++++++++--- README.md | 99 ++++++++++++++----- src/lib/LibMigrationRegistryV2.sol | 15 +-- .../MigrationRegistryV2ApplyMigration.t.sol | 53 ++++------ test/src/lib/LibMigrationRegistryV2.t.sol | 25 ++--- 5 files changed, 185 insertions(+), 93 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c2b5c62..06ac1ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -196,12 +196,74 @@ already emitting, which is what makes the record atomic with the migration. **`src/lib/LibMigrationRegistryDeploy.sol`** — its pins, generated exactly as `LibAddressRegistryDeploy` is. +**`src/interface/IMigrationRegistryV2.sol`** — the same three operations, with +one difference: `applyMigration` takes a third argument, `uint256 appliedAt`, +instead of stamping the block. The fact recorded is that a migration RAN, and +the moment it ran is not in general the moment anybody gets to write it down — a +migration that ran before the registry reached the chain has a real moment +already in the past. Stamping the block offers such a writer two options and no +third: record a time that is false, or record nothing, and recording nothing +strands the namespace because `applyMigration` refuses anything not applied onto +the current head. + +What a reader gives up is NOT authenticity. The namespace is `msg.sender` with +no authority, so every entry was always exactly as trustworthy as its writer. +What it gives up is only that `appliedAt` is the block the record landed in. + +Everything else is kept by a window on the supplied value, checked in this +order: `ZeroMigration` → `GenesisMigration` → `ZeroTimestamp` → +`MigrationAlreadyApplied` → `UnexpectedMigrationHead` → `TimestampBeforeHead` → +`FutureTimestamp`. The pure input refusals come first, then the ones about the +namespace the call arrives at, then the one about the block it lands in — which +is the order in which a caller can do something about them. `FutureTimestamp` is +last because it is the only one time itself resolves. + +`ZeroTimestamp` keeps its name and changes character: it guards ordinary caller +input rather than a zero-timestamp block. Zero is still the one moment a record +cannot carry, because `applied` reads it as "never applied" while the head has +moved and the migration can never be applied again. + +`TimestampBeforeHead` refuses only `<`, so equal is allowed: two migrations in +one transaction share a block, and two backfilled to the same day share a +moment. The head chain carries the order and the timestamps are not forced to. +The floor is read unconditionally as `sApplied[msg.sender][actualHead]` — +genesis can never be applied, so an empty namespace's record is always zero and +its floor is zero with no branch. + +The order of `TimestampBeforeHead` against `FutureTimestamp` is UNOBSERVABLE: +`headAppliedAt <= block.timestamp` always holds, so no input reaches both. No +test claims to pin it. + +`MIGRATION_HEAD_GENESIS` is imported from `IMigrationRegistryV1.sol` rather than +redeclared — one value, shared, configuring nothing and naming nobody. + +`Migrated(address indexed writer, bytes32 indexed migration, uint256 appliedAt)` +carries the moment as data, because it is supplied by the caller rather than +taken from the block: the block a log entry sits in says when the record was +written, not when the migration ran. + +There is exactly one entry point. "Apply now" is `block.timestamp` passed as the +argument, so a second entry point would be a second way to write one record and +could express nothing the argument does not. + +**`src/concrete/MigrationRegistryV2.sol`** — the implementation, at its own +deterministic address. Two creation codes are two addresses and two deployments, +so it stands alongside `MigrationRegistry` rather than replacing it, and a +writer's namespace under one is a different namespace from its namespace under +the other. The `appliedAt > block.timestamp` comparison carries slither's +`timestamp` start/end pair and forge-lint's `block-timestamp` next-line +suppression, on that one comparison rather than for the repo. + +**`src/lib/LibMigrationRegistryV2.sol`** and +**`src/lib/LibMigrationRegistryV2Deploy.sol`** — the consumer surface and the +pins, exactly as the V1 pair are. + ### Generated snapshots, and the assertions that specify their shape Every deploy snapshot in this repo is GENERATED and committed. There is no hand-maintained hex anywhere: `src/generated/candidate/` holds one deploy record -per deployed contract — `AddressRegistry.sol` and `MigrationRegistry.sol` — from -`forge script script/Build.sol`. +per deployed contract — `AddressRegistry.sol`, `MigrationRegistry.sol` and +`MigrationRegistryV2.sol` — from `forge script script/Build.sol`. `script/Build.sol` declares those contracts ONCE, in `generatedContracts()`, and the regeneration, both lib writers and the freeze all read that list. A contract @@ -282,9 +344,10 @@ abstracts live under `test/`. The exception is earned by what this repo IS, and a repo that merely USES this machinery has not earned it. The exception is scoped to the deploy/verify abstracts and the suite -declaration. `src/concrete/AddressRegistry.sol` and -`src/concrete/MigrationRegistry.sol` are ordinary deployed contracts, tested -from `test/src/concrete/` exactly as the convention requires. +declaration. `src/concrete/AddressRegistry.sol`, +`src/concrete/MigrationRegistry.sol` and `src/concrete/MigrationRegistryV2.sol` +are ordinary deployed contracts, tested from `test/src/concrete/` exactly as the +convention requires. **`src/abstract/RainDeploySuitesBase.sol`** — the ONE declaration of what a repo deploys: per suite, a key, the creation code, the recorded address/code @@ -417,12 +480,13 @@ expected addresses, expected code hashes, and dependency lists. things. `script/Deploy.sol` broadcasts the suite `DEPLOYMENT_SUITE` names to every network in `supportedNetworks()`, dispatched by hand through `.github/workflows/manual-sol-artifacts.yaml`, whose `suite` input is a choice - over the declared keys. One suite per dispatch, so this repo's two registries - are two dispatches. Only then is there a deployment for `rainix-tag-release` - to verify pins against — it verifies and publishes, it never broadcasts. - Broadcasting is key custody and real money, so it is `workflow_dispatch` and - nothing else. Deploying is idempotent: a network that already has the code is - skipped, so a partial run is fixed by running it again. + over the declared keys. One suite per dispatch, so this repo's three + registries are three dispatches. Only then is there a deployment for + `rainix-tag-release` to verify pins against — it verifies and publishes, it + never broadcasts. Broadcasting is key custody and real money, so it is + `workflow_dispatch` and nothing else. Deploying is idempotent: a network that + already has the code is skipped, so a partial run is fixed by running it + again. `RegistryDeployChainTest` is what verifies it, and it checks the RELEASED suites. Nothing is released yet, so it has nothing to check and forks nothing. diff --git a/README.md b/README.md index 0cbce0a..c6dd31b 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ It answers: - Is every version I have ever released still live, with the code I compiled, on every network I support? - Which operational migrations have actually been applied on this chain, and - when, so a test can assert the state they imply instead of guessing from a - date? + when they ran — including the ones that ran before there was anywhere to write + them down — so a test can assert the state they imply instead of guessing from + a date? - Can a migration be skipped, repeated or applied out of order on one chain and not another? @@ -38,8 +39,8 @@ Approach: - An address registry, read at run time rather than compiled into creation code, and a post-deploy check that every target network's deployment took the address it was supposed to. -- A migration registry, so operational scripts record what they applied and - when, each onto the head it is applying to, and tests assert the state that +- A migration registry, so operational scripts record what they applied and when + it ran, each onto the head it is applying to, and tests assert the state that implies rather than branching on a deadline. - One inherited deploy-pin verification, parameterized over versions, rather than assertions hand-enumerated per version and per chain in every deploy @@ -184,11 +185,20 @@ library supplies the fork loop and the comparison. ## Migration registry -`MigrationRegistry` records that a migration has been applied, and when: a -writer applies one of its own onto the migration it believes ran last -(`applyMigration`), anyone reads when a given writer applied a given one -(`applied`), and anyone reads where a given writer's sequence has got to -(`head`). There is no removal and no upgrade. +`MigrationRegistry` and `MigrationRegistryV2` each record that a migration has +been applied, and when: a writer applies one of its own onto the migration it +believes ran last (`applyMigration`), anyone reads when a given writer applied a +given one (`applied`), and anyone reads where a given writer's sequence has got +to (`head`). There is no removal and no upgrade. + +They differ in exactly one thing, and everything below holds for both except +where it says otherwise: where the recorded moment comes from. +`MigrationRegistry` stamps the block the record lands in, so it can only say a +migration ran now. `MigrationRegistryV2` takes the moment as an argument, so a +migration that ran before the registry reached the chain is recordable with the +time it actually ran. Two creation codes are two addresses and two deployments; +a consumer pins whichever one it reads, and a writer's namespace under one is a +different namespace from its namespace under the other. It exists because prod-state tests otherwise decide what to assert by reading the **clock**. The pattern that emerges without it is a dual-state invariant — @@ -216,9 +226,47 @@ frequently "which invariant applies _yet_": a cliff that starts at the migration, a rate that changes a week after it. A flag sends a consumer that needs the moment back to a hardcoded date, which is the thing this registry exists to delete. Zero and nonzero carry the same two distinct facts a flag did, -with the nonzero case saying more — and zero stays unambiguous because a record -is refused outright in a block whose timestamp is zero rather than written as -one that reads back as no record. +with the nonzero case saying more — and zero stays unambiguous because a zero +moment is refused outright rather than written as a record that reads back as no +record. + +**The moment is the caller's, inside a window `MigrationRegistryV2` enforces.** +The fact being recorded is that a migration RAN, and the moment it ran is not in +general the moment anybody gets to write it down. A registry that could only +stamp its own block offers a writer with history two options and no third: +record a time that is false for every past migration, or record nothing — and +recording nothing strands the namespace, because `applyMigration` refuses +anything not applied onto the current head, so a writer that skipped its past +migrations cannot record its next one either. + +What a reader gives up is **not** authenticity. A record is namespaced by the +account that wrote it and no authority checks it, so every entry is already +exactly as trustworthy as its writer and no more; a writer free to invent an id +was always free to invent the fact. What a reader gives up is precisely that +`appliedAt` is the block the record landed in. Everything else is kept, by three +refusals: + +- **Never zero** (`ZeroTimestamp`), or the record would read back through + `applied` as no record while the head had moved and the migration could never + be applied again. +- **Never after the block it is written in** (`FutureTimestamp`). A migration + that has run has run, so a moment still to come is not a late record of + anything, and a consumer measuring an interval since the migration — a cliff, + a grace period, a rate that changes a week later — can subtract it from the + current block without underflowing. +- **Never before the record of the head it is applied onto** + (`TimestampBeforeHead`), so a namespace's records read in head order never go + backwards. + +Equal is allowed at both ends. Two migrations applied in one transaction share a +block, and two backfilled to the same day share a moment; the head chain is what +orders them, so forcing the moments apart would make them carry an ordering they +do not have. + +There is one way to write a record. A script recording a migration as it runs +passes `block.timestamp`, which is the same statement as any other `appliedAt` +and gets the same three refusals — so a second entry point could express nothing +the argument does not. **A set of applied migrations, not a high-water mark.** A mark needs a total order consumers do not have: two migrations authored on one day collide, and one @@ -237,9 +285,11 @@ wrong order. ```solidity // The first migration in a namespace. -LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_V1); +LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_V1, block.timestamp); // Every later one names its predecessor. -LibMigrationRegistry.applyMigration(MIGRATION_V1, MIGRATION_V2); +LibMigrationRegistryV2.applyMigration(MIGRATION_V1, MIGRATION_V2, block.timestamp); +// One that ran before the registry reached this chain names the moment it ran. +LibMigrationRegistryV2.applyMigration(MIGRATION_V2, MIGRATION_V3, 1750000000); ``` Genesis is deliberately **not zero**. Zero is what an uninitialised `bytes32` @@ -276,11 +326,13 @@ say the invariant holds — a multisig can act out of band and nothing here move Keep both layers: this selects, codehash and bytecode pins verify. Replacing the pins with it trades a clock-guess for a bookkeeping-guess. -`LibMigrationRegistry` is the surface — `applied`, `head` and `applyMigration`, -all verifying the registry's code hash first. There is deliberately **no -broadcast runner**: the dominant real shape is a Safe executing a bundle that -never broadcasts, and such a script appends `applyMigration` to the bundle it is -already emitting, which makes the record atomic with the migration it describes. +`LibMigrationRegistry` and `LibMigrationRegistryV2` are the surfaces — +`applied`, `head` and `applyMigration`, each verifying its own registry's code +hash first, and each reading only the deployment it pins. There is deliberately +**no broadcast runner**: the dominant real shape is a Safe executing a bundle +that never broadcasts, and such a script appends `applyMigration` to the bundle +it is already emitting, which makes the record atomic with the migration it +describes. ## Deploying, and then releasing @@ -290,10 +342,11 @@ Three separate steps, in this order. Nothing automatic ever broadcasts. [`Manual sol artifacts`](.github/workflows/manual-sol-artifacts.yaml) workflow, choosing a `suite`. It runs `script/Deploy.sol` and broadcasts that suite to every network in `supportedNetworks()`. One suite per dispatch, so - this repo's two registries are two dispatches. `workflow_dispatch` only: this - is key custody and real money, and no merge or tag should be able to trigger - it. It is idempotent — a network that already has the code is skipped — so a - partial run is fixed by running it again rather than by unpicking anything. + this repo's three registries are three dispatches. `workflow_dispatch` only: + this is key custody and real money, and no merge or tag should be able to + trigger it. It is idempotent — a network that already has the code is skipped + — so a partial run is fixed by running it again rather than by unpicking + anything. 2. **Verify.** `RegistryDeployChainTest` passes only once every **released** suite is live on every supported network, with the code that release froze. This repo has released none, so today it has nothing to check and passes; it diff --git a/src/lib/LibMigrationRegistryV2.sol b/src/lib/LibMigrationRegistryV2.sol index f07eb6b..e4c3366 100644 --- a/src/lib/LibMigrationRegistryV2.sol +++ b/src/lib/LibMigrationRegistryV2.sol @@ -93,11 +93,13 @@ library LibMigrationRegistryV2 { /// Reverts unless the pinned registry address holds the pinned code. /// - /// Both entry points check, and they check the same way, because both are - /// worse than useless against unknown code: a read would branch a test on - /// whatever that code returned, and a write would record a migration + /// All three entry points check, and they check the same way, because each + /// is worse than useless against unknown code: `applied` would branch a + /// test on whatever timestamp that code returned, `head` would hand back a + /// value that is not a head, and `applyMigration` would record a migration /// somewhere nothing will ever read it. The check is one function so the - /// two cannot drift into checking different things. + /// three cannot drift into checking different things, and an entry point + /// added later has one place to call rather than a rule to remember. function checkCodeHash() internal view { bytes32 actualCodeHash = LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash; if (actualCodeHash != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH) { @@ -128,9 +130,8 @@ library LibMigrationRegistryV2 { /// not. function applied(address writer, bytes32 migration) internal view returns (uint256) { checkCodeHash(); - return - IMigrationRegistryV2(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS) - .applied(writer, migration); + return IMigrationRegistryV2(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS) + .applied(writer, migration); } /// The migration `writer` applied most recently, or `MIGRATION_HEAD_GENESIS` diff --git a/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol b/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol index 6ffb081..467fc9e 100644 --- a/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol +++ b/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol @@ -83,20 +83,15 @@ contract MigrationRegistryV2ApplyMigrationTest is Test { /// run, so a future one is not a late record of anything, and a consumer /// measuring an interval since the migration would be subtracting a moment /// later than the one it is measuring from. - function testApplyMigrationFutureTimestampReverts( - address writer, - bytes32 migration, - uint32 now_, - uint256 appliedAt - ) external { + function testApplyMigrationFutureTimestampReverts(address writer, bytes32 migration, uint32 now_, uint256 appliedAt) + external + { vm.assume(writer != address(0)); assumeMigration(migration); vm.warp(now_); appliedAt = bound(appliedAt, uint256(now_) + 1, type(uint256).max); - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, appliedAt, uint256(now_)) - ); + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, appliedAt, uint256(now_))); vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); @@ -113,9 +108,7 @@ contract MigrationRegistryV2ApplyMigrationTest is Test { vm.warp(now_); vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_) - ) + abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) ); vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); @@ -218,11 +211,7 @@ contract MigrationRegistryV2ApplyMigrationTest is Test { vm.expectRevert( abi.encodeWithSelector( - IMigrationRegistryV2.TimestampBeforeHead.selector, - writer, - migrationA, - appliedAt, - uint256(headAppliedAt) + IMigrationRegistryV2.TimestampBeforeHead.selector, writer, migrationA, appliedAt, uint256(headAppliedAt) ) ); vm.prank(writer); @@ -260,6 +249,11 @@ contract MigrationRegistryV2ApplyMigrationTest is Test { /// One second before the head's record is refused, and the head's own moment /// is not: the floor is the head's record, inclusive. + /// + /// The floor is at least 2, so that one second below it is a moment a record + /// could otherwise carry. A floor of 1 puts that second at zero, which is + /// refused as a moment before any floor is consulted — a true refusal of a + /// different rule, and one that says nothing about where this boundary sits. function testApplyMigrationFloorBoundary(address writer, bytes32 migrationA, bytes32 migrationB, uint32 now_) external { @@ -267,7 +261,7 @@ contract MigrationRegistryV2ApplyMigrationTest is Test { assumeMigration(migrationA); assumeMigration(migrationB); vm.assume(migrationA != migrationB); - vm.assume(now_ > 1); + vm.assume(now_ > 2); vm.warp(now_); vm.prank(writer); @@ -334,12 +328,9 @@ contract MigrationRegistryV2ApplyMigrationTest is Test { /// The floor belongs to one namespace. Another writer's records say nothing /// about what this one may record, which is the same confinement the records /// and the heads already have. - function testApplyMigrationFloorIsPerWriter( - address writer, - address other, - bytes32 migrationA, - bytes32 migrationB - ) external { + function testApplyMigrationFloorIsPerWriter(address writer, address other, bytes32 migrationA, bytes32 migrationB) + external + { vm.assume(writer != address(0)); vm.assume(other != address(0)); vm.assume(writer != other); @@ -464,18 +455,14 @@ contract MigrationRegistryV2ApplyMigrationTest is Test { // The wrong head, and backdated: told where the namespace is. vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA - ) + abi.encodeWithSelector(IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA) ); vm.prank(writer); sRegistry.applyMigration(skipped, migrationB, 1000); // The wrong head, and in the future: told where the namespace is. vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA - ) + abi.encodeWithSelector(IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA) ); vm.prank(writer); sRegistry.applyMigration(skipped, migrationB, 9001); @@ -977,11 +964,7 @@ contract MigrationRegistryV2ApplyMigrationTest is Test { vm.recordLogs(); vm.expectRevert( abi.encodeWithSelector( - IMigrationRegistryV2.TimestampBeforeHead.selector, - writer, - migration, - uint256(4999), - uint256(5000) + IMigrationRegistryV2.TimestampBeforeHead.selector, writer, migration, uint256(4999), uint256(5000) ) ); vm.prank(writer); diff --git a/test/src/lib/LibMigrationRegistryV2.t.sol b/test/src/lib/LibMigrationRegistryV2.t.sol index a18b5cc..8eddb3a 100644 --- a/test/src/lib/LibMigrationRegistryV2.t.sol +++ b/test/src/lib/LibMigrationRegistryV2.t.sol @@ -260,9 +260,7 @@ contract LibMigrationRegistryV2Test is Test { vm.warp(now_); vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_) - ) + abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) ); this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); } @@ -294,11 +292,9 @@ contract LibMigrationRegistryV2Test is Test { /// a consumer whose migrations ran before this registry reached the chain /// records what actually happened rather than the day it got round to /// writing it down. - function testApplyMigrationBackfillsAHistoricalSequence( - bytes32 migrationA, - bytes32 migrationB, - bytes32 migrationC - ) external { + function testApplyMigrationBackfillsAHistoricalSequence(bytes32 migrationA, bytes32 migrationB, bytes32 migrationC) + external + { assumeMigration(migrationA); assumeMigration(migrationB); assumeMigration(migrationC); @@ -441,12 +437,9 @@ contract LibMigrationRegistryV2Test is Test { } /// And never applied into it either. - function testApplyMigrationWrongCode( - bytes32 expectedHead, - bytes32 migration, - uint256 appliedAt, - bytes memory code - ) external { + function testApplyMigrationWrongCode(bytes32 expectedHead, bytes32 migration, uint256 appliedAt, bytes memory code) + external + { assumeOrdinaryCode(code); vm.assume(keccak256(code) != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH); vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, code); @@ -479,9 +472,7 @@ contract LibMigrationRegistryV2Test is Test { assertEq(designator.length, DELEGATION_DESIGNATOR_LENGTH); vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, designator); - assertEq( - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash, keccak256(designator) - ); + assertEq(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash, keccak256(designator)); vm.expectRevert( abi.encodeWithSelector( From 168196f3e9247ae52cfa90388edece9b2319c5f6 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Sun, 16 Aug 2026 17:22:06 +0000 Subject: [PATCH 3/8] Carry main's corrected codehash-check natspec into the V2 lib main corrected LibMigrationRegistry's account of why checkCodeHash matters: an absent registry reverts unguarded (solc rejects returndata too short to decode), so what the check actually forbids is the call that SUCCEEDS into code that is not the registry, EIP-7702 delegation included. This branch's LibMigrationRegistryV2 was written against the superseded wording and still claimed an empty-account call decodes as zero, which is not true. Comments only: the creation code and every generated snapshot are unchanged. Co-Authored-By: Claude Opus 5 --- src/lib/LibMigrationRegistryV2.sol | 31 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/lib/LibMigrationRegistryV2.sol b/src/lib/LibMigrationRegistryV2.sol index e4c3366..6d17302 100644 --- a/src/lib/LibMigrationRegistryV2.sol +++ b/src/lib/LibMigrationRegistryV2.sol @@ -113,11 +113,13 @@ library LibMigrationRegistryV2 { /// /// Verifies the registry's code hash before reading, so a chain where the /// registry is absent, or where something else occupies its address, is a - /// loud revert rather than a call into unknown code. That distinction is - /// the whole point here: "no registry on this chain" and "this migration - /// has not been applied" are different facts, and silently collapsing the - /// first into the second would send a caller down its pre-migration branch - /// on every chain the registry was never deployed to. + /// NAMED revert rather than a call into unknown code. An absent registry + /// reverts either way — solc reverts a high-level call whose returndata is + /// too short to decode — but anonymously, saying nothing about which of the + /// two it was. What the check actually forbids is the case that does NOT + /// revert: code at the address that is not this registry, an EIP-7702 + /// delegation included, is free to answer zero to every migration and send + /// every caller down its pre-migration branch. /// /// The registry itself refuses the zero writer, and refuses the two ids a /// migration can never be, so those arrive as reverts from it rather than @@ -138,10 +140,11 @@ library LibMigrationRegistryV2 { /// if it has never applied one. /// /// Verifies the registry's code hash first for the same reason `applied` - /// does, and more sharply: a call into an empty account returns nothing, - /// which decodes as zero, and zero is the one value a head can never hold — - /// so an unverified read would hand back a head that is not a head at all, - /// on exactly the chains where nothing has been deployed. + /// does, and more sharply: occupying code is free to answer any head it + /// likes, including the zero no head can ever hold, so an unverified read + /// can hand back something that is not a head at all. An empty address is + /// not that case — there is no returndata for a `bytes32` to decode from, + /// so it reverts unguarded — and the check is what gives it a name. /// @param writer The namespace to read. Never the zero address. /// @return The head of `writer`'s namespace. Never zero. function head(address writer) internal view returns (bytes32) { @@ -161,10 +164,12 @@ library LibMigrationRegistryV2 { /// one account interleave into one chain. /// /// Verifies the registry's code hash before writing, so a migration is - /// never "applied" into an empty address or into unknown code. A record - /// that went nowhere is worse than no record at all: the migration would - /// have run, and every reader would go on asserting the pre-migration - /// state. + /// never "applied" into unknown code. A write into an EMPTY address fails + /// unguarded — solc checks the callee exists when no return data is + /// expected — so what this stops is the write that SUCCEEDS into something + /// that is not the registry: a record that went nowhere is worse than no + /// record at all, because the migration ran and every reader goes on + /// asserting the pre-migration state. /// /// The registry refuses the zero id, refuses a migration this caller has /// already applied, and refuses one applied onto anything but the From 081b04ff80a7a7a36d668a72979306f582c52f57 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 10:39:15 +0000 Subject: [PATCH 4/8] WIP: collapse MigrationRegistryV2 into MigrationRegistry Incomplete. src/ compiles; tests have never been compiled or run, README still describes V2, and the candidate snapshot is stale. Committed as a recovery point rather than left uncommitted on disk. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/manual-sol-artifacts.yaml | 15 +- script/Build.sol | 7 +- src/abstract/RegistryDeploySuites.sol | 79 +- src/concrete/MigrationRegistry.sol | 152 ++- src/concrete/MigrationRegistryV2.sol | 206 ---- .../candidate/MigrationRegistryV2.sol | 26 - src/interface/IMigrationRegistryV1.sol | 190 +++- src/interface/IMigrationRegistryV2.sol | 397 ------- src/lib/LibMigrationRegistry.sol | 75 +- src/lib/LibMigrationRegistryV2.sol | 192 ---- src/lib/LibMigrationRegistryV2Deploy.sol | 20 - src/lib/LibMigrationRegistryV2Released.sol | 31 - test/concrete/MockMigrationApplier.sol | 19 +- test/concrete/MockMigrationApplierV2.sol | 47 - .../concrete/MigrationRegistryApplied.t.sol | 74 +- .../MigrationRegistryAppliedOnto.t.sol | 208 ++++ .../MigrationRegistryApplyMigration.t.sol | 517 ++++++++- .../concrete/MigrationRegistryV2Applied.t.sol | 180 ---- .../MigrationRegistryV2ApplyMigration.t.sol | 982 ------------------ .../concrete/MigrationRegistryV2Head.t.sol | 179 ---- test/src/lib/LibMigrationRegistry.t.sol | 266 +++++ test/src/lib/LibMigrationRegistryV2.t.sol | 531 ---------- 22 files changed, 1359 insertions(+), 3034 deletions(-) delete mode 100644 src/concrete/MigrationRegistryV2.sol delete mode 100644 src/generated/candidate/MigrationRegistryV2.sol delete mode 100644 src/interface/IMigrationRegistryV2.sol delete mode 100644 src/lib/LibMigrationRegistryV2.sol delete mode 100644 src/lib/LibMigrationRegistryV2Deploy.sol delete mode 100644 src/lib/LibMigrationRegistryV2Released.sol delete mode 100644 test/concrete/MockMigrationApplierV2.sol create mode 100644 test/src/concrete/MigrationRegistryAppliedOnto.t.sol delete mode 100644 test/src/concrete/MigrationRegistryV2Applied.t.sol delete mode 100644 test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol delete mode 100644 test/src/concrete/MigrationRegistryV2Head.t.sol delete mode 100644 test/src/lib/LibMigrationRegistryV2.t.sol diff --git a/.github/workflows/manual-sol-artifacts.yaml b/.github/workflows/manual-sol-artifacts.yaml index 2761fa3..a6f6248 100644 --- a/.github/workflows/manual-sol-artifacts.yaml +++ b/.github/workflows/manual-sol-artifacts.yaml @@ -1,10 +1,10 @@ name: Manual sol artifacts # The on-chain deploy, run by hand. This repo carries deployed concretes -# (`AddressRegistry`, `MigrationRegistry`, `MigrationRegistryV2`) whose address -# + codehash consumers pin, and `package-release.yaml` cuts a release for a -# deployment that ALREADY exists — rainix-tag-release verifies the live chains -# against freshly generated pins and never broadcasts. So the deploy has to -# happen first, and separately, which is this. +# (`AddressRegistry`, `MigrationRegistry`) whose address + codehash consumers +# pin, and `package-release.yaml` cuts a release for a deployment that ALREADY +# exists — rainix-tag-release verifies the live chains against freshly generated +# pins and never broadcasts. So the deploy has to happen first, and separately, +# which is this. # # Order is: dispatch this once per suite, confirm `RegistryDeployChainTest` # passes on every supported network, then push the `sol-v*` tag. @@ -19,15 +19,14 @@ on: required: true description: | Which declared suite to broadcast. One dispatch deploys one suite, - because `DEPLOYMENT_SUITE` selects one — a repo with three deployed - contracts is three dispatches. Offered as a choice rather than typed, + because `DEPLOYMENT_SUITE` selects one — a repo with two deployed + contracts is two dispatches. Offered as a choice rather than typed, so a key that no suite declares cannot be dispatched at all; the script still refuses one, naming the valid keys, if this list falls behind the declaration. options: - address-registry - migration-registry - - migration-registry-v2 jobs: deploy: uses: rainlanguage/rainix/.github/workflows/rainix-manual-sol-artifacts.yaml@main diff --git a/script/Build.sol b/script/Build.sol index 71a3d2f..25997f0 100644 --- a/script/Build.sol +++ b/script/Build.sol @@ -41,7 +41,7 @@ contract Build is Script, RegistryDeploySuites { /// Every contract this repo generates deploy pins for. /// @return contracts The generated contracts. function generatedContracts() internal pure returns (GeneratedContract[] memory contracts) { - contracts = new GeneratedContract[](3); + contracts = new GeneratedContract[](2); contracts[0] = GeneratedContract({ contractName: "AddressRegistry", constantPrefix: "ADDRESS_REGISTRY", candidate: addressRegistryCandidate() }); @@ -50,11 +50,6 @@ contract Build is Script, RegistryDeploySuites { constantPrefix: "MIGRATION_REGISTRY", candidate: migrationRegistryCandidate() }); - contracts[2] = GeneratedContract({ - contractName: "MigrationRegistryV2", - constantPrefix: "MIGRATION_REGISTRY_V2", - candidate: migrationRegistryV2Candidate() - }); } /// @notice Regenerate the rolling snapshots, their alias libs and the diff --git a/src/abstract/RegistryDeploySuites.sol b/src/abstract/RegistryDeploySuites.sol index 65bd9f5..85f0e46 100644 --- a/src/abstract/RegistryDeploySuites.sol +++ b/src/abstract/RegistryDeploySuites.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.25; import {DeployCandidate, DeploySuite, RainDeploySuitesBase} from "./RainDeploySuitesBase.sol"; import {AddressRegistry} from "../concrete/AddressRegistry.sol"; import {MigrationRegistry} from "../concrete/MigrationRegistry.sol"; -import {MigrationRegistryV2} from "../concrete/MigrationRegistryV2.sol"; import { CREATION_CODE as ADDRESS_REGISTRY_CREATION_CODE_CANDIDATE, RUNTIME_CODE as ADDRESS_REGISTRY_RUNTIME_CODE_CANDIDATE @@ -14,16 +13,10 @@ import { CREATION_CODE as MIGRATION_REGISTRY_CREATION_CODE_CANDIDATE, RUNTIME_CODE as MIGRATION_REGISTRY_RUNTIME_CODE_CANDIDATE } from "../generated/candidate/MigrationRegistry.sol"; -import { - CREATION_CODE as MIGRATION_REGISTRY_V2_CREATION_CODE_CANDIDATE, - RUNTIME_CODE as MIGRATION_REGISTRY_V2_RUNTIME_CODE_CANDIDATE -} from "../generated/candidate/MigrationRegistryV2.sol"; import {LibAddressRegistryDeploy} from "../lib/LibAddressRegistryDeploy.sol"; import {LibAddressRegistryReleased} from "../lib/LibAddressRegistryReleased.sol"; import {LibMigrationRegistryDeploy} from "../lib/LibMigrationRegistryDeploy.sol"; import {LibMigrationRegistryReleased} from "../lib/LibMigrationRegistryReleased.sol"; -import {LibMigrationRegistryV2Deploy} from "../lib/LibMigrationRegistryV2Deploy.sol"; -import {LibMigrationRegistryV2Released} from "../lib/LibMigrationRegistryV2Released.sol"; /// @title RegistryDeploySuites /// @notice Everything this repo deploys, declared ONCE. @@ -72,43 +65,29 @@ abstract contract RegistryDeploySuites is RainDeploySuitesBase { /// that took the record whole would give another contract's snapshot this /// contract's suite key and collide with its own entry for that tag. /// - /// Flattened by a loop over the libs rather than by a run of index - /// arithmetic per lib, so a contract is added by adding one entry to the - /// group list and nothing else here moves. - /// - /// All are empty until the first release is cut. The rolling `candidate/` + /// Both are empty until the first release is cut. The rolling `candidate/` /// snapshots are not releases and do exist. function releasedSuites() internal pure override returns (DeploySuite[] memory suites) { - DeploySuite[][] memory groups = new DeploySuite[][](3); - groups[0] = LibAddressRegistryReleased.releasedSuites(); - groups[1] = LibMigrationRegistryReleased.releasedSuites(); - groups[2] = LibMigrationRegistryV2Released.releasedSuites(); + DeploySuite[] memory addressRegistry = LibAddressRegistryReleased.releasedSuites(); + DeploySuite[] memory migrationRegistry = LibMigrationRegistryReleased.releasedSuites(); - uint256 total = 0; - for (uint256 i = 0; i < groups.length; i++) { - total += groups[i].length; + suites = new DeploySuite[](addressRegistry.length + migrationRegistry.length); + for (uint256 i = 0; i < addressRegistry.length; i++) { + suites[i] = addressRegistry[i]; } - - suites = new DeploySuite[](total); - uint256 next = 0; - for (uint256 i = 0; i < groups.length; i++) { - for (uint256 j = 0; j < groups[i].length; j++) { - suites[next] = groups[i][j]; - next++; - } + for (uint256 i = 0; i < migrationRegistry.length; i++) { + suites[addressRegistry.length + i] = migrationRegistry[i]; } } /// @inheritdoc RainDeploySuitesBase - /// @dev One entry per contract this repo deploys. A further deployed - /// contract is a further named candidate below, a further entry here, a - /// further group in `releasedSuites` above, and a further entry in - /// `script/Build.sol`'s generated-contract list — nothing else. + /// @dev One entry per contract this repo deploys. A third deployed contract + /// is a third named candidate below, a third entry here, and a third entry + /// in `script/Build.sol`'s generated-contract list — nothing else. function candidateSuites() internal pure override returns (DeployCandidate[] memory candidates) { - candidates = new DeployCandidate[](3); + candidates = new DeployCandidate[](2); candidates[0] = addressRegistryCandidate(); candidates[1] = migrationRegistryCandidate(); - candidates[2] = migrationRegistryV2Candidate(); } /// This repo's rolling `AddressRegistry` candidate. @@ -176,38 +155,4 @@ abstract contract RegistryDeploySuites is RainDeploySuitesBase { sourceCreationCode: type(MigrationRegistry).creationCode }); } - - /// This repo's rolling `MigrationRegistryV2` candidate. - /// - /// Everything said about the `AddressRegistry` candidate holds here - /// unchanged: the pins are aliased from the generated snapshot, the - /// creation and runtime code are recorded rather than derived, and the - /// source anchor is what says the record describes THIS contract. - /// - /// It is a separate suite from `migration-registry` rather than a newer - /// spelling of it. The creation code is what the Zoltu factory takes, so two - /// creation codes are two addresses and two deployments; both are broadcast, - /// both are verified against every chain, and a consumer pins whichever one - /// it reads. - /// - /// `MigrationRegistryV2` has no constructor argument, no compile-time - /// authority and no dependency to be on chain first — the namespace is - /// `msg.sender`, so there is nothing to configure and nothing to resolve. - /// That is also why it is deployable to a new network the day the network - /// is added, with no follow-up transaction to make it useful. - /// @return The candidate. - function migrationRegistryV2Candidate() internal pure returns (DeployCandidate memory) { - return DeployCandidate({ - snapshot: DeploySuite({ - suite: "migration-registry-v2", - creationCode: MIGRATION_REGISTRY_V2_CREATION_CODE_CANDIDATE, - storedDeployedAddress: LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, - storedBytecodeHash: LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - storedRuntimeCode: MIGRATION_REGISTRY_V2_RUNTIME_CODE_CANDIDATE, - artifactPath: "src/concrete/MigrationRegistryV2.sol:MigrationRegistryV2", - dependencies: new address[](0) - }), - sourceCreationCode: type(MigrationRegistryV2).creationCode - }); - } } diff --git a/src/concrete/MigrationRegistry.sol b/src/concrete/MigrationRegistry.sol index 7effdee..f572d4e 100644 --- a/src/concrete/MigrationRegistry.sol +++ b/src/concrete/MigrationRegistry.sol @@ -4,11 +4,23 @@ pragma solidity =0.8.25; import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../interface/IMigrationRegistryV1.sol"; +/// @dev One writer's record of one migration. Written whole, so a record can +/// never hold one half of itself. +struct MigrationRecord { + /// The moment recorded against the migration. Zero means never applied, + /// which `applyMigration` refuses to write. + uint256 appliedAt; + /// The head the namespace was at when the record was written. Zero means + /// never applied: a head is genesis or an applied id, both nonzero. + bytes32 appliedOnto; +} + /// @title MigrationRegistry /// @notice The whole of `IMigrationRegistryV1`: a writer applies one of its own -/// migrations onto the head it believes its namespace is at, and anyone reads -/// when a given writer applied a given migration, or where that writer's -/// namespace has got to. +/// migrations onto the head it believes its namespace is at, at the moment it +/// says the migration ran, and anyone reads when a given writer applied a given +/// migration, what it applied it onto, or where that writer's namespace has got +/// to. /// /// There is deliberately nothing else. No removal, no upgrade, no pause, and no /// authority at all — which is the difference from `AddressRegistry`, and the @@ -39,19 +51,27 @@ import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../interface/IMigrat /// applied, which is what makes re-running a migration fail rather than repeat, /// and refuses one applied onto anything but the namespace's current head, which /// is what makes a skipped or out-of-order migration fail rather than diverge. -/// There is no way to unrecord one — a record describes something that happened, -/// and nothing that happened stops having happened. +/// There is no way to unrecord one, and no way to rewrite one — a record +/// describes something that happened, and nothing that happened stops having +/// happened. +/// +/// The moment is the CALLER's on the three-argument form, so a migration that +/// ran before this contract reached the chain is recordable with the time it +/// actually ran. The order is not the caller's: each record keeps the head it +/// was applied onto, so a namespace's records are a chain from `head` back to +/// `MIGRATION_HEAD_GENESIS` whatever moments they carry. /// -/// Neither storage mapping is `public`. `applied` and `head` refuse the zero -/// writer, `applied` refuses the two ids a migration can never be, and a public -/// mapping's generated getter would answer all of them with zero — which for -/// `applied` is "not applied" and for `head` is a value no head can ever hold, -/// i.e. exactly the silent wrong-branch this contract reverts to prevent. +/// Neither storage mapping is `public`. `applied`, `appliedOnto` and `head` +/// refuse the zero writer, the two record readers refuse the two ids a +/// migration can never be, and a public mapping's generated getter would answer +/// all of them with zero — which for a record is "not applied" and for `head` +/// is a value no head can ever hold, i.e. exactly the silent wrong-branch this +/// contract reverts to prevent. contract MigrationRegistry is IMigrationRegistryV1 { - /// When each record landed, namespaced by writer. Zero means never. Not - /// `public`: the only reader is `applied`, which refuses the two inputs that - /// can only be mistakes. - mapping(address writer => mapping(bytes32 migration => uint256 appliedAt)) internal sApplied; + /// Every record, namespaced by writer. A zero `appliedAt` means never + /// applied. Not `public`: the only readers are `applied` and `appliedOnto`, + /// which refuse the inputs that can only be mistakes. + mapping(address writer => mapping(bytes32 migration => MigrationRecord record)) internal sRecords; /// The most recent migration applied under each writer. Zero means the /// namespace is empty, which reads out as `MIGRATION_HEAD_GENESIS` — the @@ -61,10 +81,30 @@ contract MigrationRegistry is IMigrationRegistryV1 { mapping(address writer => bytes32 head) internal sHead; /// @inheritdoc IMigrationRegistryV1 - /// @dev The refusals run caller-input first and environment last: the two - /// that describe a mistake in the call are true whatever block this lands - /// in, so they are what a caller is told about first. + /// @dev The block is the moment, so a caller that has nothing to say about + /// when its migration ran does not have to say it. + // slither-disable-next-line timestamp + // forge-lint: disable-next-line(block-timestamp) function applyMigration(bytes32 expectedHead, bytes32 migration) external { + applyMigrationRecord(expectedHead, migration, block.timestamp); + } + + /// @inheritdoc IMigrationRegistryV1 + function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + applyMigrationRecord(expectedHead, migration, appliedAt); + } + + /// Both entry points, so there is one record and one set of refusals + /// whichever of them supplied the moment. + /// + /// The refusals run from the ones that describe the call alone, through the + /// ones that describe the namespace it arrives at, to the one that + /// describes the block it lands in — which is the order in which a caller + /// can do something about them. + /// @param expectedHead The head the caller believes its namespace is at. + /// @param migration The migration to apply. + /// @param appliedAt The moment to record against it. + function applyMigrationRecord(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) internal { // Checked before everything else, so an uninitialised id is reported as // the mistake it is rather than as a first record of zero. if (migration == bytes32(0)) { @@ -78,6 +118,17 @@ contract MigrationRegistry is IMigrationRegistryV1 { if (migration == MIGRATION_HEAD_GENESIS) { revert GenesisMigration(); } + // Beside the two id refusals because it is the same kind of mistake in + // the same kind of value: an uninitialised `uint256`, refused whatever + // namespace it arrives at and whatever block it lands in. Zero is the + // one moment a record cannot carry — `applied` would answer it as + // "never applied" while the head had moved and the migration could + // never be applied again. Reached by the two-argument form as well, in + // a block whose timestamp is zero: a test can warp to zero and a chain + // can be configured from a zero genesis. + if (appliedAt == 0) { + revert ZeroTimestamp(); + } // There is deliberately no zero-writer case here. `msg.sender` cannot // be the zero address, so the zero namespace is unreachable for writes // and a guard on it would be unreachable code pretending to be a check. @@ -90,40 +141,41 @@ contract MigrationRegistry is IMigrationRegistryV1 { // say to a re-dispatched script. It is also not implied by the head // check: re-applying a migration whose successor has landed presents a // matching head, and would drag the head backwards and overwrite the - // original timestamp. - if (sApplied[msg.sender][migration] != 0) { + // original record. + if (sRecords[msg.sender][migration].appliedAt != 0) { revert MigrationAlreadyApplied(msg.sender, migration); } bytes32 actualHead = head(msg.sender); if (expectedHead != actualHead) { revert UnexpectedMigrationHead(msg.sender, expectedHead, actualHead); } - // A zero timestamp is the one value a record cannot carry: `applied` - // would answer it as "never applied" while the head had moved and the - // migration could never be applied again. Not unreachable — a test can - // warp to zero and a chain can be configured from a zero genesis — so - // this is a real check rather than a decorative one. + // Last, because it is the only refusal here that time itself resolves: + // every other one describes something wrong with the call or with where + // the namespace is, and this one describes a moment that has not + // arrived yet. // - // The usual hazard behind a `block.timestamp` comparison — the one both - // the static analysers flag here — is a validator nudging the clock - // across a threshold. There is no threshold here and no nudge - // available: zero is not a value a validator on a live chain can - // produce at all, which is why this is an equality against it rather - // than a window around it, and why all three warnings are suppressed on - // this one comparison rather than turned off for the repo. + // The usual hazard behind a `block.timestamp` comparison — the one the + // static analysers flag here — is a validator nudging the clock across a + // threshold. The nudge is available and it is harmless: a validator that + // moves the clock forward admits a record a second earlier than it would + // otherwise have been admitted, of a migration that has run either way, + // and moving it backwards is not something a chain lets a proposer do. + // So the warnings are suppressed on this one comparison rather than + // turned off for the repo. // - // Slither's two are a start/end pair rather than a next-line because - // only one comment fits immediately above the `if`, `forge fmt` moves a - // trailing one inside the braces, and forge-lint has no pair form. - // slither-disable-start incorrect-equality,timestamp + // Slither's is a start/end pair rather than a next-line because only one + // comment fits immediately above the `if`, `forge fmt` moves a trailing + // one inside the braces, and forge-lint has no pair form. + // slither-disable-start timestamp // forge-lint: disable-next-line(block-timestamp) - if (block.timestamp == 0) { - revert ZeroTimestamp(); + if (appliedAt > block.timestamp) { + revert FutureTimestamp(appliedAt, block.timestamp); } - // slither-disable-end incorrect-equality,timestamp - sApplied[msg.sender][migration] = block.timestamp; + // slither-disable-end timestamp + // Written whole, so the moment and the predecessor cannot land apart. + sRecords[msg.sender][migration] = MigrationRecord({appliedAt: appliedAt, appliedOnto: actualHead}); sHead[msg.sender] = migration; - emit Migrated(msg.sender, migration); + emit Migrated(msg.sender, migration, appliedAt); } /// @inheritdoc IMigrationRegistryV1 @@ -134,6 +186,25 @@ contract MigrationRegistry is IMigrationRegistryV1 { /// question the caller did not mean to ask, and answering it with the value /// that sends it down its pre-migration branch. function applied(address writer, bytes32 migration) external view returns (uint256) { + checkRecordKey(writer, migration); + return sRecords[writer][migration].appliedAt; + } + + /// @inheritdoc IMigrationRegistryV1 + /// @dev The same three refusals as `applied`, for the same reason and on + /// the same key: a zero answer here reads as "never applied" exactly as a + /// zero moment does. + function appliedOnto(address writer, bytes32 migration) external view returns (bytes32) { + checkRecordKey(writer, migration); + return sRecords[writer][migration].appliedOnto; + } + + /// Refuses the three inputs that can only be a mistake in the caller rather + /// than a record to read. One function, so the two readers of a record + /// cannot drift into refusing different things. + /// @param writer The namespace being read. + /// @param migration The migration being asked about. + function checkRecordKey(address writer, bytes32 migration) internal pure { if (writer == address(0)) { revert ZeroWriter(); } @@ -143,7 +214,6 @@ contract MigrationRegistry is IMigrationRegistryV1 { if (migration == MIGRATION_HEAD_GENESIS) { revert GenesisMigration(); } - return sApplied[writer][migration]; } /// @inheritdoc IMigrationRegistryV1 diff --git a/src/concrete/MigrationRegistryV2.sol b/src/concrete/MigrationRegistryV2.sol deleted file mode 100644 index 83d2ba7..0000000 --- a/src/concrete/MigrationRegistryV2.sol +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../interface/IMigrationRegistryV2.sol"; - -/// @title MigrationRegistryV2 -/// @notice The whole of `IMigrationRegistryV2`: a writer applies one of its own -/// migrations onto the head it believes its namespace is at, at the moment it -/// says the migration ran, and anyone reads when a given writer applied a given -/// migration, or where that writer's namespace has got to. -/// -/// There is deliberately nothing else. No removal, no upgrade, no pause, and no -/// authority at all — which is the difference from `AddressRegistry`, and the -/// reason nothing here is CONFIGURED at compile time. `MIGRATION_HEAD_GENESIS` -/// is a compile-time constant, but it is the same value for every consumer on -/// every chain and names nobody, so it is part of what this contract IS rather -/// than a choice welded into it. -/// -/// `AddressRegistry` has a root, and a root has to be welded into the creation -/// code so it cannot be rotated, which puts it in the deterministic address. -/// That is workable there because there is one registry of names for the whole -/// organisation. It is not workable here: the account that applies a migration -/// is a different Safe, deployer or timelock for every consumer and every -/// chain, so a root would have to be all of them at once, and baking each -/// consumer's authority into creation code would give each of them a different -/// address for what is meant to be one shared registry. -/// -/// Keying by `msg.sender` removes the authority instead of choosing one. Anyone -/// may write, but only under themselves, so a reader asking about the namespace -/// of an authority it already trusts is reading something only that authority -/// could have written. Every other namespace holds unforgeable claims that no -/// reader asks about. With nothing to configure there is also no rollout state -/// in which this contract is inert: it does its whole job the moment it exists -/// on a chain. -/// -/// A record is append-only per writer, and a head only ever moves forward onto -/// something new. `applyMigration` refuses a migration the caller has already -/// applied, which is what makes re-running a migration fail rather than repeat, -/// and refuses one applied onto anything but the namespace's current head, which -/// is what makes a skipped or out-of-order migration fail rather than diverge. -/// There is no way to unrecord one, and no way to rewrite the moment recorded -/// against one — a record describes something that happened, and nothing that -/// happened stops having happened. -/// -/// The moment is the CALLER's, so a migration that ran before this contract -/// reached the chain is recordable with the time it actually ran rather than -/// with the time it was written down. The window it must fall in is what keeps -/// every other property of a record: nonzero, so `applied` never reads a record -/// as no record; not after this block, so a record never describes something -/// that has not happened; and not before the record of the head it is applied -/// onto, so a namespace's records read in head order never go backwards. -/// -/// Neither storage mapping is `public`. `applied` and `head` refuse the zero -/// writer, `applied` refuses the two ids a migration can never be, and a public -/// mapping's generated getter would answer all of them with zero — which for -/// `applied` is "not applied" and for `head` is a value no head can ever hold, -/// i.e. exactly the silent wrong-branch this contract reverts to prevent. -contract MigrationRegistryV2 is IMigrationRegistryV2 { - /// The moment recorded against each migration, namespaced by writer. Zero - /// means never. Not `public`: the only reader is `applied`, which refuses - /// the two inputs that can only be mistakes. - mapping(address writer => mapping(bytes32 migration => uint256 appliedAt)) internal sApplied; - - /// The most recent migration applied under each writer. Zero means the - /// namespace is empty, which reads out as `MIGRATION_HEAD_GENESIS` — the - /// only place that translation happens is `head`, so no reader and no - /// writer can disagree about where an empty namespace is. Not `public`, for - /// the same reason as the records: the untranslated zero is not a head. - mapping(address writer => bytes32 head) internal sHead; - - /// @inheritdoc IMigrationRegistryV2 - /// @dev The refusals run from the ones that describe the call alone, through - /// the ones that describe the namespace it arrives at, to the one that - /// describes the block it lands in — which is the order in which a caller - /// can do something about them. - function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { - // Checked before everything else, so an uninitialised id is reported as - // the mistake it is rather than as a first record of zero. - if (migration == bytes32(0)) { - revert ZeroMigration(); - } - // Genesis is a head, not a migration. Applying it would leave `sHead` - // holding the value an empty namespace reads as, so a namespace that had - // applied something would be at a head indistinguishable from one that - // had applied nothing — and the next first-migration script would be - // accepted against it. - if (migration == MIGRATION_HEAD_GENESIS) { - revert GenesisMigration(); - } - // Beside the two id refusals because it is the same kind of mistake in - // the same kind of value: an uninitialised `uint256`, refused whatever - // namespace it arrives at and whatever block it lands in. Zero is the - // one moment a record cannot carry — `applied` would answer it as - // "never applied" while the head had moved and the migration could - // never be applied again. - if (appliedAt == 0) { - revert ZeroTimestamp(); - } - // There is deliberately no zero-writer case here. `msg.sender` cannot - // be the zero address, so the zero namespace is unreachable for writes - // and a guard on it would be unreachable code pretending to be a check. - // Nor is there a zero-head case: a head is either genesis or an applied - // id, both nonzero, so a zero `expectedHead` can never match and is - // already refused below, by an error that names the zero it was handed. - - // Checked before the head, because a migration that has already run has - // already run whatever the head is, and that is the more useful thing to - // say to a re-dispatched script. It is also not implied by the head - // check: re-applying a migration whose successor has landed presents a - // matching head, and would drag the head backwards and overwrite the - // original record. - if (sApplied[msg.sender][migration] != 0) { - revert MigrationAlreadyApplied(msg.sender, migration); - } - bytes32 actualHead = head(msg.sender); - if (expectedHead != actualHead) { - revert UnexpectedMigrationHead(msg.sender, expectedHead, actualHead); - } - // The head chain is the order the migrations ran in, so the record it is - // being appended to is the floor for this one. Read unconditionally - // rather than behind a genesis branch: genesis is refused as a migration - // so nothing is ever recorded against it, which makes an empty - // namespace's floor zero and every nonzero `appliedAt` above it. A - // branch here would be a second spelling of "an empty namespace has no - // previous record", and the two spellings are what drift. - // - // Comparing against the head's record alone is enough for the whole - // namespace: every record is at or above the one before it in the - // chain, so the head's is the largest. - uint256 headAppliedAt = sApplied[msg.sender][actualHead]; - if (appliedAt < headAppliedAt) { - revert TimestampBeforeHead(msg.sender, actualHead, appliedAt, headAppliedAt); - } - // Last, because it is the only refusal here that time itself resolves: - // every other one describes something wrong with the call or with where - // the namespace is, and this one describes a moment that has not - // arrived yet. - // - // The usual hazard behind a `block.timestamp` comparison — the one the - // static analysers flag here — is a validator nudging the clock across - // a threshold. The nudge is available and it is harmless: a validator - // that moves the clock forward admits a record a second earlier than it - // would otherwise have been admitted, which is a record of a migration - // that has run either way, and moving it backwards is not something a - // chain lets a proposer do. So the warnings are suppressed on this one - // comparison rather than turned off for the repo. - // - // Slither's is a start/end pair rather than a next-line because only one - // comment fits immediately above the `if`, `forge fmt` moves a trailing - // one inside the braces, and forge-lint has no pair form. - // slither-disable-start timestamp - // forge-lint: disable-next-line(block-timestamp) - if (appliedAt > block.timestamp) { - revert FutureTimestamp(appliedAt, block.timestamp); - } - // slither-disable-end timestamp - sApplied[msg.sender][migration] = appliedAt; - sHead[msg.sender] = migration; - emit Migrated(msg.sender, migration, appliedAt); - } - - /// @inheritdoc IMigrationRegistryV2 - /// @dev All three refusals are about a caller that has not supplied what it - /// thinks it has. None can ever be a real record: nothing originates from - /// the zero address, and `applyMigration` will write neither the zero id nor - /// the genesis one — so answering zero for any of them would be answering a - /// question the caller did not mean to ask, and answering it with the value - /// that sends it down its pre-migration branch. - function applied(address writer, bytes32 migration) external view returns (uint256) { - if (writer == address(0)) { - revert ZeroWriter(); - } - if (migration == bytes32(0)) { - revert ZeroMigration(); - } - if (migration == MIGRATION_HEAD_GENESIS) { - revert GenesisMigration(); - } - return sApplied[writer][migration]; - } - - /// @inheritdoc IMigrationRegistryV2 - /// @dev The zero namespace is refused rather than answered `genesis`: it is - /// provably empty forever, so "a namespace nothing has been applied to" is a - /// true statement about it and a false one about what the caller meant to - /// ask, which would send a first migration at it. - /// - /// The empty-namespace zero is translated to genesis here and nowhere else, - /// which is why this is one `public` function rather than a reader beside an - /// internal helper: `applyMigration` compares against exactly what a caller - /// reads, so the two cannot drift into different ideas of where a namespace - /// that has applied nothing is. - /// - /// `applyMigration` reaches it as `head(msg.sender)`, which can never be the - /// zero address, so the refusal is redundant on that path. It is one - /// function, so it is one refusal, and the reachable path is the one it is - /// there for. - function head(address writer) public view returns (bytes32) { - if (writer == address(0)) { - revert ZeroWriter(); - } - bytes32 storedHead = sHead[writer]; - return storedHead == bytes32(0) ? MIGRATION_HEAD_GENESIS : storedHead; - } -} diff --git a/src/generated/candidate/MigrationRegistryV2.sol b/src/generated/candidate/MigrationRegistryV2.sol deleted file mode 100644 index c57f662..0000000 --- a/src/generated/candidate/MigrationRegistryV2.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.25; - -// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. - -/// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0xea9a3519ea559173dab4ad65a2305b3f1449b5b3b420b32b326757021e92f7f7); - -/// @dev The deterministic deploy address of the contract when deployed via -/// the Zoltu factory. -address constant DEPLOYED_ADDRESS = address(0x82C7793293d71057E87849493ADDc291fbfCC7c9); - -/// @dev The creation bytecode of the contract. -bytes constant CREATION_CODE = - hex"6080604052348015600e575f80fd5b506105648061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c80635965d2a614610043578063bda4fec514610058578063e29304161461007d575b5f80fd5b6100566100513660046104d2565b610090565b005b61006b610066366004610523565b610319565b60405190815260200160405180910390f35b61006b61008b36600461053c565b6103c1565b816100c7576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8203610120576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f03610159576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f90815260208181526040808320858452909152902054156101b6576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6101c033610319565b905080841461020b576040517facbe685200000000000000000000000000000000000000000000000000000000815233600482015260248101859052604481018290526064016101ad565b335f9081526020818152604080832084845290915290205480831015610274576040517f9bcec4350000000000000000000000000000000000000000000000000000000081523360048201526024810183905260448101849052606481018290526084016101ad565b428311156102b7576040517f8fdce34f000000000000000000000000000000000000000000000000000000008152600481018490524260248201526044016101ad565b335f818152602081815260408083208884528252808320879055838352600182529182902087905590518581528692917f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610367576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f90815260016020526040902054801561039857806103ba565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f73ffffffffffffffffffffffffffffffffffffffff831661040f576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b81610446576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f820361049f576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b5f805f606084860312156104e4575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461051e575f80fd5b919050565b5f60208284031215610533575f80fd5b6103ba826104fb565b5f806040838503121561054d575f80fd5b610556836104fb565b94602093909301359350505056"; - -/// @dev The runtime bytecode of the contract. -bytes constant RUNTIME_CODE = - hex"608060405234801561000f575f80fd5b506004361061003f575f3560e01c80635965d2a614610043578063bda4fec514610058578063e29304161461007d575b5f80fd5b6100566100513660046104d2565b610090565b005b61006b610066366004610523565b610319565b60405190815260200160405180910390f35b61006b61008b36600461053c565b6103c1565b816100c7576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8203610120576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f03610159576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f90815260208181526040808320858452909152902054156101b6576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6101c033610319565b905080841461020b576040517facbe685200000000000000000000000000000000000000000000000000000000815233600482015260248101859052604481018290526064016101ad565b335f9081526020818152604080832084845290915290205480831015610274576040517f9bcec4350000000000000000000000000000000000000000000000000000000081523360048201526024810183905260448101849052606481018290526084016101ad565b428311156102b7576040517f8fdce34f000000000000000000000000000000000000000000000000000000008152600481018490524260248201526044016101ad565b335f818152602081815260408083208884528252808320879055838352600182529182902087905590518581528692917f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610367576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f90815260016020526040902054801561039857806103ba565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f73ffffffffffffffffffffffffffffffffffffffff831661040f576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b81610446576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f820361049f576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b5f805f606084860312156104e4575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461051e575f80fd5b919050565b5f60208284031215610533575f80fd5b6103ba826104fb565b5f806040838503121561054d575f80fd5b610556836104fb565b94602093909301359350505056"; - -/// @dev The addresses that MUST already have code on a network before -/// this release can be broadcast there, `abi.encode`d as an `address[]` -/// because Solidity has no file-scope constant of dynamic array type. -bytes constant DEPENDENCIES = - hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; diff --git a/src/interface/IMigrationRegistryV1.sol b/src/interface/IMigrationRegistryV1.sol index 143c9ba..9824e46 100644 --- a/src/interface/IMigrationRegistryV1.sol +++ b/src/interface/IMigrationRegistryV1.sol @@ -30,13 +30,21 @@ pragma solidity ^0.8.25; bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.head.genesis"); /// @title IMigrationRegistryV1 -/// @notice A per-writer record of which migrations have been applied and when, -/// with exactly three operations: a writer applies one of its own migrations -/// onto the head it believes its namespace is at (`applyMigration`), anyone -/// reads when a given writer applied a given migration (`applied`), and anyone -/// reads where a given writer's namespace currently is (`head`). There is no -/// removal, no upgrade and no authority beyond the writer over its own -/// namespace, and an implementation MUST NOT add any. +/// @notice A per-writer record of which migrations have been applied, when, and +/// onto what, with exactly four operations: a writer applies one of its own +/// migrations onto the head it believes its namespace is at (`applyMigration`), +/// anyone reads when a given writer applied a given migration (`applied`), what +/// that writer applied it onto (`appliedOnto`), and where that writer's +/// namespace currently is (`head`). There is no removal, no upgrade and no +/// authority beyond the writer over its own namespace, and an implementation +/// MUST NOT add any. +/// +/// `applyMigration` has two forms, differing only in where the recorded moment +/// comes from. The two-argument form records the block it lands in, for a +/// script applying its own migration in the same atomic unit as the migration. +/// The three-argument form takes the moment as an argument, for a migration +/// that already ran — one that ran before this registry reached the chain, or +/// before its writer started recording at all. /// /// It exists so that a test can decide what to assert by reading what happened /// on chain rather than by reading the clock. Without it, a test that spans a @@ -61,11 +69,32 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// anything about the state a migration produced — only that it was applied, /// and when. /// -/// The timestamp is a fact about the RECORD, not about the state: it is the -/// block the record landed in, which the record's own log already carries, and -/// it says nothing whatsoever about what the migration did. Reading it back does -/// not become proof of anything, for the same reason reading the record back -/// does not. +/// The timestamp is a fact about the RECORD, not about the state: it says when +/// a migration ran and nothing whatsoever about what it did. Reading it back +/// does not become proof of anything, for the same reason reading the record +/// back does not. +/// +/// ## A moment is data, bounded at both ends +/// +/// A moment supplied by the caller is not authenticated, and nothing else in a +/// record is either. A record is namespaced by the account that wrote it and no +/// authority checks it, so every entry is exactly as trustworthy as the writer +/// that wrote it: a writer free to invent a migration id is free to invent the +/// moment. An implementation MUST bound it anyway, however it arrived: +/// +/// - NEVER ZERO (`ZeroTimestamp`). Zero is what `applied` answers for a +/// migration nobody applied, so a record carrying it would read back as no +/// record while the head had moved and the migration could never be applied +/// again. +/// - NEVER AFTER THE BLOCK IT IS WRITTEN IN (`FutureTimestamp`). A record says +/// a migration HAS run. `applied` therefore never answers a moment later than +/// the block asking, and a consumer whose invariant is an interval since the +/// migration — a cliff, a grace period, a rate that changes a week later — +/// subtracts it from the current block without underflowing. +/// +/// Nothing else constrains it. Two records may carry the same moment, and a +/// record may carry a moment earlier than the record before it in the chain. +/// The order the migrations ran in is the chain, not the moments. /// /// ## `applied` answers WHEN, and zero still means "not applied" /// @@ -81,11 +110,11 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// asserts the pre-migration state in. Zero and nonzero are therefore the same /// two distinct facts a flag carried, with the nonzero case saying more. /// -/// That distinction is only sound while a real record can never BE zero, so an -/// implementation MUST refuse to write a record in a block whose timestamp is -/// zero rather than write one that reads back as no record at all. That is not a -/// hypothetical branch: a test can `vm.warp(0)`, and a chain can be configured -/// from a zero genesis. +/// That distinction is only sound while a real record can never BE zero, which +/// is what `ZeroTimestamp` is for. It refuses a caller that supplies zero, and +/// it refuses the two-argument form in a block whose timestamp is zero — +/// neither hypothetical, because a test can `vm.warp(0)` and a chain can be +/// configured from a zero genesis. /// /// ## The head is what makes an ordered sequence ordered /// @@ -95,6 +124,12 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// that is where the namespace actually is; on success the applied migration /// becomes the new head. /// +/// Each record keeps the head it was applied onto, which `appliedOnto` reads +/// back. A namespace's records are therefore a chain in storage: from `head`, +/// each `appliedOnto` names the record before it, down to +/// `MIGRATION_HEAD_GENESIS`. That chain IS the order the migrations ran in, and +/// it is exact whatever moments the records carry. +/// /// This is what blocks a SKIPPED step. A migration script names its predecessor, /// so a chain that never got the predecessor is a loud revert at the moment of /// applying rather than a namespace that silently diverges from every other @@ -225,49 +260,57 @@ interface IMigrationRegistryV1 { /// @param actualHead The head the namespace is actually at. error UnexpectedMigrationHead(address writer, bytes32 expectedHead, bytes32 actualHead); - /// Thrown when `applyMigration` is called in a block whose timestamp is - /// zero. A record IS its timestamp, so a zero one would read back through + /// Thrown when `applyMigration` would record a zero moment: a caller that + /// supplied zero, or the two-argument form in a block whose timestamp is + /// zero. A record IS its moment, so a zero one would read back through /// `applied` as no record at all, while the head moved and the migration - /// cannot be re-applied — the worst of every branch at once. Refusing to - /// write is the only outcome that leaves the namespace describing something - /// true. + /// cannot be re-applied — the worst of every branch at once. Zero is also + /// what an uninitialised `uint256` holds, so it is refused for the same + /// reason `ZeroMigration` is. error ZeroTimestamp(); + /// Thrown when `applyMigration` is given an `appliedAt` after the timestamp + /// of the block it is called in. A record says a migration HAS run, so a + /// moment that has not arrived is not a record of anything — and a consumer + /// measuring an interval since the migration would be subtracting a future + /// moment from the present one. + /// @param appliedAt The moment supplied. + /// @param blockTimestamp The timestamp of the block the call landed in. + error FutureTimestamp(uint256 appliedAt, uint256 blockTimestamp); + /// Emitted every time a migration is applied. A migration is applied at /// most once per writer, so the log is the complete history of the registry /// and the only way to discover a record without already knowing the id. /// - /// It carries neither the head nor the timestamp because both are already - /// there: the log is ordered, and one writer's entries in order ARE that - /// writer's chain of heads — each entry's migration is the head the next one - /// was applied onto, and the first was applied onto - /// `MIGRATION_HEAD_GENESIS`. The timestamp is the block's. + /// It carries no head, because the log is ordered and one writer's entries + /// in order ARE that writer's chain of heads — each entry's migration is the + /// head the next one was applied onto, and the first was applied onto + /// `MIGRATION_HEAD_GENESIS`. + /// + /// It does carry `appliedAt`, which the log does not otherwise hold: the + /// block a log entry sits in says when the record was written, and + /// `appliedAt` says when the migration ran. /// @param writer The namespace, which is the caller. /// @param migration The migration applied. - event Migrated(address indexed writer, bytes32 indexed migration); + /// @param appliedAt The moment recorded against it. + event Migrated(address indexed writer, bytes32 indexed migration, uint256 appliedAt); - /// Applies `migration` under the caller's namespace, onto `expectedHead`. + /// Applies `migration` under the caller's namespace, onto `expectedHead`, + /// as having been applied in the block this call lands in. /// - /// The implementation MUST revert `ZeroMigration` if `migration` is zero, - /// `GenesisMigration` if it is `MIGRATION_HEAD_GENESIS`, - /// `MigrationAlreadyApplied` if the caller has already applied it, - /// `UnexpectedMigrationHead` if the caller's namespace is not at - /// `expectedHead`, and `ZeroTimestamp` if `block.timestamp` is zero. It MUST - /// NOT provide any way to unrecord a migration or to move a head backwards. - /// On success it MUST record the current block timestamp against - /// `migration`, make `migration` the caller's new head, and emit `Migrated`. - /// - /// Nothing is returned: the new head is the `migration` just passed in and - /// the timestamp is the block's, so both are already in the caller's hand. - /// - /// A caller SHOULD call this in the same atomic unit as the migration - /// itself where it can — a Safe appends this call to the bundle it is - /// already executing — so that the record and the change it describes + /// This is the form for a script applying its own migration, so the record + /// lands in the same atomic unit as the change it describes — a Safe + /// appends this call to the bundle it is already executing — and the two /// cannot land apart. Where they cannot be atomic, call it LAST: a record /// that never landed leaves a reader asserting the pre-migration state, /// which the verification layer then catches loudly, and leaves a re-run /// possible. A record that landed for a migration that did not is the /// harder state to get out of. + /// + /// It records the same record as the three-argument form and makes the same + /// refusals, against `block.timestamp` as the moment — so a block whose + /// timestamp is zero is `ZeroTimestamp`, and the moment can never be in the + /// future. /// @param expectedHead The head the caller believes its namespace is at: /// the migration it is applying onto, or `MIGRATION_HEAD_GENESIS` for the /// first migration in a namespace. Never zero, which can never match. @@ -275,8 +318,35 @@ interface IMigrationRegistryV1 { /// `MIGRATION_HEAD_GENESIS`. function applyMigration(bytes32 expectedHead, bytes32 migration) external; - /// When `writer` applied `migration`, as the timestamp of the block the - /// record landed in. Zero if it never did. + /// Applies `migration` under the caller's namespace, onto `expectedHead`, as + /// having been applied at `appliedAt`. + /// + /// This is the form for a migration that ALREADY ran, which records the + /// moment it ran rather than the moment it was written down. + /// + /// The implementation MUST revert `ZeroMigration` if `migration` is zero, + /// `GenesisMigration` if it is `MIGRATION_HEAD_GENESIS`, `ZeroTimestamp` if + /// `appliedAt` is zero, `MigrationAlreadyApplied` if the caller has already + /// applied it, `UnexpectedMigrationHead` if the caller's namespace is not at + /// `expectedHead`, and `FutureTimestamp` if `appliedAt` is after + /// `block.timestamp`. It MUST NOT provide any way to unrecord a migration, + /// to move a head backwards, or to move a record once written. On success it + /// MUST record `appliedAt` and `expectedHead` against `migration`, make + /// `migration` the caller's new head, and emit `Migrated`. + /// + /// Nothing is returned: every part of the record is an argument the caller + /// just handed in. + /// @param expectedHead The head the caller believes its namespace is at: + /// the migration it is applying onto, or `MIGRATION_HEAD_GENESIS` for the + /// first migration in a namespace. Never zero, which can never match. + /// @param migration The migration to apply. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @param appliedAt The moment `migration` was applied. Never zero, never + /// after the block this call lands in. + function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external; + + /// When `writer` applied `migration`, as the moment recorded with the + /// record. Zero if it never did. /// /// The implementation MUST revert `ZeroWriter`, `ZeroMigration` or /// `GenesisMigration` rather than answering about any of them, and MUST @@ -292,14 +362,36 @@ interface IMigrationRegistryV1 { /// is actually looking at, which is the whole failure this registry removes. /// /// Zero is unambiguous because `applyMigration` refuses to write a zero - /// timestamp, so no applied migration can present as an unapplied one. + /// moment, so no applied migration can present as an unapplied one. /// @param writer The namespace to read. Never the zero address. /// @param migration The migration to ask about. Never zero, never /// `MIGRATION_HEAD_GENESIS`. - /// @return The block timestamp `writer` applied `migration` at, or zero if - /// it has not. + /// @return The moment `writer` applied `migration` at, or zero if it has + /// not. function applied(address writer, bytes32 migration) external view returns (uint256); + /// What `writer` applied `migration` ONTO: the head that namespace was at + /// when the record was written. Zero if `writer` never applied `migration`. + /// + /// The implementation MUST revert `ZeroWriter`, `ZeroMigration` or + /// `GenesisMigration` rather than answering about any of them, and MUST + /// answer zero — not revert — for a real writer that has simply not applied + /// a real migration. + /// + /// Zero is unambiguous: a head is either `MIGRATION_HEAD_GENESIS` or an + /// applied id, both nonzero, so zero is never something a record holds. + /// + /// This is what makes a namespace's records a chain rather than a set. + /// Walked from `head` back, each answer names the record before it and the + /// walk ends at `MIGRATION_HEAD_GENESIS`, which is the order the migrations + /// ran in whatever moments they carry. + /// @param writer The namespace to read. Never the zero address. + /// @param migration The migration to ask about. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @return The head `writer` applied `migration` onto, or zero if it has + /// not applied it. + function appliedOnto(address writer, bytes32 migration) external view returns (bytes32); + /// Where `writer`'s namespace currently is: the migration it applied most /// recently, or `MIGRATION_HEAD_GENESIS` if it has never applied one. /// diff --git a/src/interface/IMigrationRegistryV2.sol b/src/interface/IMigrationRegistryV2.sol deleted file mode 100644 index f420399..0000000 --- a/src/interface/IMigrationRegistryV2.sol +++ /dev/null @@ -1,397 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.25; - -import {MIGRATION_HEAD_GENESIS} from "./IMigrationRegistryV1.sol"; - -/// @title IMigrationRegistryV2 -/// @notice A per-writer record of which migrations have been applied and when, -/// with exactly three operations: a writer applies one of its own migrations -/// onto the head it believes its namespace is at, at the moment it says the -/// migration ran (`applyMigration`), anyone reads when a given writer applied a -/// given migration (`applied`), and anyone reads where a given writer's -/// namespace currently is (`head`). There is no removal, no upgrade and no -/// authority beyond the writer over its own namespace, and an implementation -/// MUST NOT add any. -/// -/// It exists so that a test can decide what to assert by reading what happened -/// on chain rather than by reading the clock. Without it, a test that spans a -/// migration accepts EITHER the pre-migration or the post-migration value until -/// a hardcoded deadline, which asserts nothing at all during the one window -/// where it matters most, and red-lines on a date rather than on a fact once -/// the deadline passes. With it, a test asserts EXACTLY the value implied by -/// the migrations that have run, in both branches. -/// -/// ## The caller says when, within a window the implementation enforces -/// -/// `appliedAt` is a parameter because the fact being recorded is that a -/// migration RAN, and the moment it ran is not in general the moment anybody -/// gets to write it down. A migration that ran before this registry reached the -/// chain, or before its writer started recording at all, has a real moment that -/// is already in the past by the time there is anywhere to put it. An -/// implementation that could only stamp its own block would offer such a writer -/// two options and no third: record a time that is false for every historical -/// migration, or record nothing — and recording nothing strands the namespace, -/// because `applyMigration` refuses anything not applied onto the current head, -/// so a writer that skipped its past migrations cannot record its next one -/// either. -/// -/// What a reader gives up is NOT authenticity. A record is namespaced by the -/// account that wrote it and no authority checks it, so every entry is already -/// exactly as trustworthy as the writer that wrote it and no more — a writer -/// free to invent a migration id was always free to invent the fact. What a -/// reader gives up is precisely this: `appliedAt` is no longer the block the -/// record landed in. -/// -/// Everything else a reader relied on is kept, by an implementation that MUST -/// refuse a supplied `appliedAt` outside the window a true record can occupy: -/// -/// - NEVER ZERO. Zero is what `applied` answers for a migration nobody applied, -/// so a record carrying it would read back as no record while the head had -/// moved and the migration could never be applied again. -/// - NEVER AFTER THE BLOCK IT IS WRITTEN IN. A migration that has run has run, -/// so a moment still in the future is not a late record of anything — it is a -/// claim that cannot be true when it is made. `applied` therefore never -/// answers a moment that had not arrived, and a consumer whose invariant is -/// an interval since the migration — a cliff, a grace period, a rate that -/// changes a week later — can subtract it from the current block without -/// underflowing. -/// - NEVER BEFORE THE RECORD IT IS APPLIED ONTO. The head chain is the order -/// the migrations ran in, so a namespace's records read in head order are -/// non-decreasing, and "this migration ran before that one" agrees with the -/// sequence rather than contradicting it. -/// -/// Equal is allowed at both ends. Two migrations applied in one transaction -/// share a block, and two backfilled migrations known only to the same day -/// share a moment; the head chain is what orders them, and forcing the -/// timestamps apart would make them carry an ordering they do not have. -/// -/// So a reader still has: nonzero means applied, the value never exceeds the -/// block that wrote it, and the values along a namespace's head chain never go -/// backwards. -/// -/// ## An index, not proof -/// -/// This registry says which invariant applies. It does NOT say that the -/// invariant holds. A multisig can act out of band — a beacon is upgraded by -/// hand and nothing here moves — and then a reader would confidently assert the -/// wrong state. -/// -/// So a consumer keeps both layers, with distinct jobs: this registry SELECTS -/// which invariant applies, and codehash or bytecode pins VERIFY that it -/// actually holds. Replacing the pins with this registry trades a clock-guess -/// for a bookkeeping-guess, which is not an improvement. An implementation MUST -/// NOT offer anything that invites it, and in particular MUST NOT record -/// anything about the state a migration produced — only that it was applied, -/// and when. -/// -/// `appliedAt` is a fact about the RECORD, not about the state: it says when a -/// migration ran and nothing whatsoever about what it did. Reading it back does -/// not become proof of anything, for the same reason reading the record back -/// does not. -/// -/// ## `applied` answers WHEN, and zero still means "not applied" -/// -/// `applied` is a timestamp rather than a flag because "which invariant applies" -/// is frequently "which invariant applies YET": a migration that starts a -/// vesting cliff, a rate change, a grace period. A flag forces a consumer that -/// needs the moment to go and find the log for it, or — worse — to go back to -/// the deadline constant this registry exists to delete. -/// -/// A migration nobody applied answers zero, and that is an ANSWER rather than a -/// revert: it is the ordinary state of every migration before it runs and of -/// every migration on a chain that never got it, and it is the branch a caller -/// asserts the pre-migration state in. Zero and nonzero are therefore the same -/// two distinct facts a flag carried, with the nonzero case saying more. -/// -/// That distinction is only sound while a real record can never BE zero, which -/// is what `ZeroTimestamp` is for. -/// -/// ## The head is what makes an ordered sequence ordered -/// -/// A namespace has a HEAD: the migration most recently applied under it, or -/// `MIGRATION_HEAD_GENESIS` if it has never applied one. `applyMigration` takes -/// the head the caller believes its namespace is at and refuses to write unless -/// that is where the namespace actually is; on success the applied migration -/// becomes the new head. -/// -/// This is what blocks a SKIPPED step. A migration script names its predecessor, -/// so a chain that never got the predecessor is a loud revert at the moment of -/// applying rather than a namespace that silently diverges from every other -/// chain's. It is equally what blocks two migrations dispatched concurrently -/// from landing in whichever order the mempool chose: the second one names a -/// head that has moved. -/// -/// It is also what carries the ORDER, which is why `appliedAt` is not asked to. -/// The times a writer supplies are bounded by the head chain rather than the -/// other way around: a record may not predate the one it is applied onto, so -/// the sequence the heads describe and the moments the records carry cannot -/// contradict each other. -/// -/// It does NOT block a DUPLICATE, and `MigrationAlreadyApplied` is not -/// redundant beside it. Re-applying a migration whose successor has since -/// landed presents a head that matches perfectly, and would move the head -/// BACKWARDS and overwrite the original timestamp — a record un-happening, which -/// is the one thing this registry promises cannot occur. The two refusals answer -/// two different questions: the head is about WHERE in the sequence a caller is, -/// and the already-applied refusal is about WHETHER this particular migration -/// has run at all. -/// -/// One namespace on one chain is therefore ONE linear sequence, and that is a -/// consequence to design around rather than an implementation detail. Two -/// unrelated sets of migrations applied from the same account on the same chain -/// interleave into one chain of heads, so each script's expected head is -/// whatever that account last applied rather than whatever that script's own -/// author had in mind. A consumer that wants two independent sequences applies -/// them from two accounts, which is the same lever that already decides who a -/// reader trusts. -/// -/// ## The namespace is the writer, and that is the whole access control -/// -/// A record is keyed by the account that wrote it. Anyone may write, but only -/// to their own namespace, so a reader that reads the namespace of an authority -/// it already trusts is reading something only that authority could have -/// written. Records under any other namespace are unforgeable garbage that no -/// reader asks for. -/// -/// This is deliberately not a root authority. The account that applies a -/// migration differs per consumer, per chain and per migration — a Safe -/// executing a bundle, a deployer EOA broadcasting a script, a timelock — so a -/// single root would have to be all of them at once. It is also what lets the -/// implementation be identical for every consumer, and therefore live at one -/// deterministic address on every chain: an authority baked into creation code -/// would give every consumer a different address, which is the property this -/// registry exists inside a deterministic-deploy library to keep. -/// -/// A compromised writer can therefore only lie about its own migrations, to -/// readers that have chosen to trust it. It cannot touch anybody else's record, -/// and it cannot unrecord its own. -/// -/// ## Identity is opaque -/// -/// A migration is an opaque 32-byte value. This interface says nothing about -/// how one is derived — hashed from a script path, a name, a counter — and an -/// implementation MUST NOT constrain it beyond the two values the head space -/// reserves: zero, which is what an empty namespace holds before it is read as -/// genesis, and `MIGRATION_HEAD_GENESIS`, which is what it reads as. Neither can -/// be a migration without a head losing the ability to say whether a namespace -/// has applied anything. Two callers agreeing on any other id is entirely their -/// business. -/// -/// The convention that suits scripts-as-migrations is the hash of the script's -/// identity, e.g. `keccak256("script/20260623-upgrade-receipt-vaults.s.sol")`. -/// A date alone is not enough: two migrations authored on one day collide, and -/// consumers do author two on one day. An id is fixed at the moment it is first -/// applied, so a script renamed afterwards keeps the id it was applied under -/// rather than acquiring a new one — which is why the id belongs in a named -/// constant beside the script, not derived from a path at the call site. -/// -/// A head is an id, so the same is true of the head a script names: it is the -/// predecessor's named constant, imported, not a second spelling of it. -/// -/// `MIGRATION_HEAD_GENESIS` is one value shared with `IMigrationRegistryV1` -/// rather than a second constant of its own. It is the value that means "this -/// namespace has applied nothing", it configures nothing and names nobody, and -/// two spellings of it would be two things to keep equal. -/// -/// ## Records live in the implementation that holds them -/// -/// An implementation of this interface answers about its own storage and -/// nothing else, so a writer's namespace under one deployment is a different -/// namespace from its namespace under any other, and both `applied` and `head` -/// answer per deployment. A consumer reads the registry it pins. -interface IMigrationRegistryV2 { - /// Thrown when `applyMigration` is called with the zero migration id, and - /// by `applied` when it is asked about one. The zero id is what an - /// uninitialised `bytes32` constant reads as, and an uninitialised id is - /// never a migration anybody meant to name. Rejected in both directions - /// because the read is the dangerous one: answering zero would silently - /// send a caller down its pre-migration branch. - /// - /// There is no matching refusal for a zero HEAD, and adding one would be a - /// guard on something already impossible: a head is either - /// `MIGRATION_HEAD_GENESIS` or an applied id, both nonzero, so a zero head - /// can never match and is already refused by `UnexpectedMigrationHead` — - /// which names the zero it was handed, so nothing about the mistake is lost. - error ZeroMigration(); - - /// Thrown when `applyMigration` is called with `MIGRATION_HEAD_GENESIS` as - /// the migration, and by `applied` when it is asked about it. Genesis is a - /// head, not a migration: applying it would leave a namespace that has - /// applied something at a head no different from one that has applied - /// nothing, and asking `applied` about it would answer zero forever for a - /// caller that has confused a head for a migration and will read that as - /// its pre-migration branch. - /// - /// This is the same refusal as `ZeroMigration` under a different diagnosis, - /// and they are separate errors because the mistakes are different: a zero - /// is a constant nobody set, and this is a constant set to the wrong one of - /// two that sit beside each other. - error GenesisMigration(); - - /// Thrown by `applied` and `head` when asked about the zero writer. No - /// transaction can originate from the zero address, so the zero namespace is - /// provably empty and the answer would always be "nothing applied, at - /// genesis" — an unresolved or unset writer constant would therefore read as - /// a pristine namespace rather than as the mistake it is. - /// - /// There is no matching case on `applyMigration`: `msg.sender` is never - /// zero, so the zero namespace cannot be written to in the first place. - error ZeroWriter(); - - /// Thrown when a writer applies a migration it has already applied. This - /// is what makes running a migration twice structurally impossible rather - /// than a warning in a workflow dropdown asking a human not to re-dispatch - /// it: a script consults `applied` before it acts, and this is the backstop - /// under that consultation. - /// - /// Checked BEFORE the head, because it is the more specific true statement - /// about the call and it is true whatever the head is. A re-dispatched - /// script is told the migration already ran, rather than told the namespace - /// has moved on and left to work out why. - /// @param writer The namespace, which is the caller. - /// @param migration The migration already applied under it. - error MigrationAlreadyApplied(address writer, bytes32 migration); - - /// Thrown when a writer applies onto a head its namespace is not at. Either - /// something the caller believed had been applied has not been, or something - /// it did not know about has been — a skipped predecessor, a concurrent - /// dispatch that landed first, or a chain that is simply further behind than - /// the script assumed. - /// @param writer The namespace, which is the caller. - /// @param expectedHead The head the caller said it was applying onto. - /// @param actualHead The head the namespace is actually at. - error UnexpectedMigrationHead(address writer, bytes32 expectedHead, bytes32 actualHead); - - /// Thrown when `applyMigration` is given a zero `appliedAt`. A record IS - /// its timestamp, so a zero one would read back through `applied` as no - /// record at all, while the head moved and the migration cannot be - /// re-applied — the worst of every branch at once. It is what an - /// uninitialised `uint256` holds, so it is refused for the same reason - /// `ZeroMigration` is, and refusing to write is the only outcome that - /// leaves the namespace describing something true. - error ZeroTimestamp(); - - /// Thrown when `applyMigration` is given an `appliedAt` earlier than the - /// record of the head it is applied onto. The head chain is the order the - /// migrations ran in, so a record that predates the one before it in that - /// chain contradicts the sequence it is being appended to — and a reader - /// comparing two of a namespace's records would get an answer that - /// disagrees with the heads. - /// @param writer The namespace, which is the caller. - /// @param head The head being applied onto, whose record is the floor. - /// @param appliedAt The moment supplied. - /// @param headAppliedAt The moment the head was applied at. - error TimestampBeforeHead(address writer, bytes32 head, uint256 appliedAt, uint256 headAppliedAt); - - /// Thrown when `applyMigration` is given an `appliedAt` after the timestamp - /// of the block it is called in. A record says a migration HAS run, so a - /// moment that has not arrived is not a record of anything — and a consumer - /// measuring an interval since the migration would be subtracting a future - /// moment from the present one. - /// @param appliedAt The moment supplied. - /// @param blockTimestamp The timestamp of the block the call landed in. - error FutureTimestamp(uint256 appliedAt, uint256 blockTimestamp); - - /// Emitted every time a migration is applied. A migration is applied at - /// most once per writer, so the log is the complete history of the registry - /// and the only way to discover a record without already knowing the id. - /// - /// It carries no head, because the log is ordered and one writer's entries - /// in order ARE that writer's chain of heads — each entry's migration is the - /// head the next one was applied onto, and the first was applied onto - /// `MIGRATION_HEAD_GENESIS`. - /// - /// It does carry `appliedAt`, because that is the one part of a record the - /// log does not otherwise hold: it is supplied by the caller rather than - /// taken from the block, so the block a log entry sits in says only when the - /// record was written and not when the migration ran. - /// @param writer The namespace, which is the caller. - /// @param migration The migration applied. - /// @param appliedAt The moment recorded against it. - event Migrated(address indexed writer, bytes32 indexed migration, uint256 appliedAt); - - /// Applies `migration` under the caller's namespace, onto `expectedHead`, - /// as having been applied at `appliedAt`. - /// - /// The implementation MUST revert `ZeroMigration` if `migration` is zero, - /// `GenesisMigration` if it is `MIGRATION_HEAD_GENESIS`, `ZeroTimestamp` if - /// `appliedAt` is zero, `MigrationAlreadyApplied` if the caller has already - /// applied it, `UnexpectedMigrationHead` if the caller's namespace is not at - /// `expectedHead`, `TimestampBeforeHead` if `appliedAt` is before the record - /// of `expectedHead`, and `FutureTimestamp` if `appliedAt` is after - /// `block.timestamp`. It MUST NOT provide any way to unrecord a migration, - /// to move a head backwards, or to move a record's timestamp once written. - /// On success it MUST record `appliedAt` against `migration`, make - /// `migration` the caller's new head, and emit `Migrated`. - /// - /// Nothing is returned: the new head is the `migration` just passed in and - /// the timestamp is the `appliedAt` just passed in, so both are already in - /// the caller's hand. - /// - /// There is exactly one way to write a record. A caller recording a - /// migration as it runs passes `block.timestamp`, which is the same - /// statement as any other `appliedAt` and gets the same three refusals; a - /// second entry point that supplied it would be a second way to write one - /// record, and the one thing it could express is what its argument already - /// spells. - /// - /// A caller SHOULD call this in the same atomic unit as the migration - /// itself where it can — a Safe appends this call to the bundle it is - /// already executing — so that the record and the change it describes - /// cannot land apart. Where they cannot be atomic, call it LAST: a record - /// that never landed leaves a reader asserting the pre-migration state, - /// which the verification layer then catches loudly, and leaves a re-run - /// possible. A record that landed for a migration that did not is the - /// harder state to get out of. - /// @param expectedHead The head the caller believes its namespace is at: - /// the migration it is applying onto, or `MIGRATION_HEAD_GENESIS` for the - /// first migration in a namespace. Never zero, which can never match. - /// @param migration The migration to apply. Never zero, never - /// `MIGRATION_HEAD_GENESIS`. - /// @param appliedAt The moment `migration` was applied. Never zero, never - /// after this block, never before the record of `expectedHead`. - function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external; - - /// When `writer` applied `migration`, as the moment supplied with the - /// record. Zero if it never did. - /// - /// The implementation MUST revert `ZeroWriter`, `ZeroMigration` or - /// `GenesisMigration` rather than answering about any of them, and MUST - /// answer zero — not revert — for a real writer that has simply not applied - /// a real migration. - /// - /// That zero is the deliberate difference from a registry whose reads revert - /// on an unknown key. "This migration has not been applied here" is a - /// legitimate, expected answer that a caller branches on and asserts the - /// pre-migration state for; it is the ordinary state of every migration - /// before it runs, and of every migration on a chain that never got it. A - /// revert there would leave a caller with nothing to say about the state it - /// is actually looking at, which is the whole failure this registry removes. - /// - /// Zero is unambiguous because `applyMigration` refuses to write a zero - /// timestamp, so no applied migration can present as an unapplied one. - /// @param writer The namespace to read. Never the zero address. - /// @param migration The migration to ask about. Never zero, never - /// `MIGRATION_HEAD_GENESIS`. - /// @return The moment `writer` applied `migration` at, or zero if it has - /// not. - function applied(address writer, bytes32 migration) external view returns (uint256); - - /// Where `writer`'s namespace currently is: the migration it applied most - /// recently, or `MIGRATION_HEAD_GENESIS` if it has never applied one. - /// - /// The implementation MUST revert `ZeroWriter` rather than answering about - /// the zero namespace, and MUST NEVER answer zero — an empty namespace is - /// genesis, and a nonempty one is a nonzero migration id, so a zero answer - /// could only mean the reader had reached something that is not this - /// registry. - /// - /// This is a read for authoring and for diagnosis: which migration a chain - /// is at, and therefore what the next script must name. It is NOT how a - /// script decides that its predecessor ran — that is `applied`, per - /// migration, because a head says only what was last, not what was ever. - /// @param writer The namespace to read. Never the zero address. - /// @return The head of `writer`'s namespace. Never zero. - function head(address writer) external view returns (bytes32); -} diff --git a/src/lib/LibMigrationRegistry.sol b/src/lib/LibMigrationRegistry.sol index 9c17e84..b040924 100644 --- a/src/lib/LibMigrationRegistry.sol +++ b/src/lib/LibMigrationRegistry.sol @@ -13,11 +13,11 @@ import {LibMigrationRegistryDeploy} from "./LibMigrationRegistryDeploy.sol"; /// a chain the caller has not audited; the address plus the code hash says the /// caller is talking to the registry it compiled against. /// -/// That is the whole library. It answers when a writer applied a migration and -/// where that writer's namespace has got to, and it applies one under the -/// caller. Which writer a test trusts, which invariant each answer selects, and -/// how an id is derived are entirely the consumer's business and none of this -/// library's. +/// That is the whole library. It answers when a writer applied a migration, +/// what that writer applied it onto and where that writer's namespace has got +/// to, and it applies one under the caller. Which writer a test trusts, which +/// invariant each answer selects, and how an id is derived are entirely the +/// consumer's business and none of this library's. /// /// ## There is deliberately no broadcast runner here /// @@ -71,6 +71,11 @@ import {LibMigrationRegistryDeploy} from "./LibMigrationRegistryDeploy.sol"; /// how a script tests that its predecessor ran: a head says what was LAST, and /// `applied` is what says whether a particular migration ever ran at all. /// +/// `appliedOnto` reads back the head a record was applied onto, so a namespace +/// walked from `head` back is the order its migrations ran in — which is a +/// stronger statement than the moments make, because a moment is whatever the +/// writer supplied and the chain is what the registry enforced. +/// /// The registry is an INDEX, not proof. It says which invariant applies; it does /// not say the invariant holds. A multisig can act out of band and nothing here /// moves. Codehash and bytecode pins are what verify the state itself, and this @@ -85,12 +90,12 @@ library LibMigrationRegistry { /// Reverts unless the pinned registry address holds the pinned code. /// - /// All three entry points check, and they check the same way, because each - /// is worse than useless against unknown code: `applied` would branch a - /// test on whatever timestamp that code returned, `head` would hand back a - /// value that is not a head, and `applyMigration` would record a migration - /// somewhere nothing will ever read it. The check is one function so the - /// three cannot drift into checking different things, and an entry point + /// Every entry point checks, and they check the same way, because each is + /// worse than useless against unknown code: `applied` would branch a test on + /// whatever timestamp that code returned, `appliedOnto` and `head` would + /// hand back values that are not heads, and `applyMigration` would record a + /// migration somewhere nothing will ever read it. The check is one function + /// so they cannot drift into checking different things, and an entry point /// added later has one place to call rather than a rule to remember. function checkCodeHash() internal view { bytes32 actualCodeHash = LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS.codehash; @@ -129,6 +134,26 @@ library LibMigrationRegistry { .applied(writer, migration); } + /// What `writer` applied `migration` onto, or zero if it never applied it. + /// + /// Verifies the registry's code hash before reading, for the same reason + /// `applied` does: occupying code is free to answer zero to every migration, + /// which here reads as "never applied" exactly as a zero moment does. + /// + /// This is the step that walks a namespace. From `head`, each answer names + /// the record before it, ending at `MIGRATION_HEAD_GENESIS`. + /// @param writer The namespace to read. Never the zero address. + /// @param migration The migration to ask about. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @return The head `writer` applied `migration` onto, or zero if it has + /// not applied it. + function appliedOnto(address writer, bytes32 migration) internal view returns (bytes32) { + checkCodeHash(); + return IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS).appliedOnto( + writer, migration + ); + } + /// The migration `writer` applied most recently, or `MIGRATION_HEAD_GENESIS` /// if it has never applied one. /// @@ -166,7 +191,9 @@ library LibMigrationRegistry { /// The registry refuses the zero id, refuses a migration this caller has /// already applied, and refuses one applied onto anything but the /// namespace's actual head — which between them make a re-dispatched, a - /// skipped and an out-of-order migration all fail rather than land. + /// skipped and an out-of-order migration all fail rather than land. The + /// moment is the block this lands in, which the registry refuses if it is + /// zero. /// @param expectedHead The migration the caller believes it applied last, /// or `MIGRATION_HEAD_GENESIS` for the first in this namespace. /// @param migration The migration to apply. Never zero, never @@ -176,4 +203,28 @@ library LibMigrationRegistry { IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS) .applyMigration(expectedHead, migration); } + + /// Applies `migration` under the CALLER's namespace, onto `expectedHead`, as + /// having been applied at `appliedAt`. + /// + /// This is the form for a migration that already ran — one that ran before + /// this registry reached the chain, or before its writer started recording + /// at all — so the record carries the moment it ran rather than the moment + /// it was written down. + /// + /// Everything the two-argument form says about the namespace, the code-hash + /// check and the registry's refusals holds here unchanged. The registry + /// refuses the two moments a record cannot carry as well: zero, and one + /// after the block this lands in. + /// @param expectedHead The migration the caller believes it applied last, + /// or `MIGRATION_HEAD_GENESIS` for the first in this namespace. + /// @param migration The migration to apply. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @param appliedAt The moment `migration` was applied. Never zero, never + /// after the block this lands in. + function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) internal { + checkCodeHash(); + IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS) + .applyMigration(expectedHead, migration, appliedAt); + } } diff --git a/src/lib/LibMigrationRegistryV2.sol b/src/lib/LibMigrationRegistryV2.sol deleted file mode 100644 index 6d17302..0000000 --- a/src/lib/LibMigrationRegistryV2.sol +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.25; - -import {IMigrationRegistryV2} from "../interface/IMigrationRegistryV2.sol"; -import {LibMigrationRegistryV2Deploy} from "./LibMigrationRegistryV2Deploy.sol"; - -/// @title LibMigrationRegistryV2 -/// @notice Reads and writes the `MigrationRegistryV2` deployed at a single -/// deterministic address on every network, verifying the registry's code hash -/// first, exactly as `LibAddressRegistry` does for the address registry and -/// `LibRainDeploy` does for the Zoltu factory. An address alone says nothing on -/// a chain the caller has not audited; the address plus the code hash says the -/// caller is talking to the registry it compiled against. -/// -/// That is the whole library. It answers when a writer applied a migration and -/// where that writer's namespace has got to, and it applies one under the -/// caller. Which writer a test trusts, which invariant each answer selects, and -/// how an id is derived are entirely the consumer's business and none of this -/// library's. -/// -/// ## There is deliberately no broadcast runner here -/// -/// `LibRainDeploy` wraps broadcasting because a deploy is always a broadcast. -/// A migration is not: the dominant real shape is a Safe executing a bundle, -/// where the script emits transactions for the multisig to sign and never -/// broadcasts anything itself. Such a script appends `applyMigration` to the -/// bundle it is already emitting, which is what makes the record atomic with the -/// migration it describes — a property no runner in this library could offer, -/// and one a runner would quietly compete with. -/// -/// So `applyMigration` is an ordinary call. A broadcasting EOA script wraps it -/// in its own `vm.startBroadcast`, a Safe bundle appends it, and a test calls it -/// directly; none of those is privileged over the others here. -/// -/// ## Reading is what this is for -/// -/// A test asserts EXACTLY the value implied by the migrations that have run: -/// -/// ```solidity -/// if (LibMigrationRegistryV2.applied(SAFE, MIGRATION_V2) != 0) { -/// assertEq(vault.owner(), NEW_OWNER); -/// } else { -/// assertEq(vault.owner(), OLD_OWNER); -/// } -/// ``` -/// -/// Both branches assert. Neither reads the clock, neither skips, and the branch -/// is selected by what happened on chain rather than by a deadline somebody -/// guessed. `applied` answering zero is an ordinary, expected answer — it is -/// the state of every migration before it runs and of every migration on a -/// chain that never got it — which is why the registry answers it rather than -/// reverting. -/// -/// The nonzero answer is WHEN, which is what a test whose invariant is itself -/// time-shaped needs: a cliff that starts at the migration, a rate that changes -/// a week after it. That is still the clock being read, but it is the chain's -/// record of the migration being read, not a date somebody guessed in advance. -/// -/// ## Writing names the head it is applying onto, and the moment it ran -/// -/// `applyMigration` takes the migration the caller believes ran last in its -/// namespace, so a chain that never got that predecessor refuses the write -/// instead of silently skipping a step, and two migrations dispatched at once -/// cannot land in the wrong order. The first migration in a namespace names -/// `MIGRATION_HEAD_GENESIS`, imported from the interface — never a zero, which -/// is what an uninitialised constant would be and is refused everywhere. -/// -/// It also takes the moment the migration ran, which is what lets a script -/// record a migration that already happened with the time it happened rather -/// than the time it was written down. A script recording a migration it is -/// running right now passes `block.timestamp`; one backfilling a migration that -/// ran before the registry reached this chain passes the moment it ran. The -/// registry refuses a zero, one after the current block, and one before the -/// record of the head being applied onto. -/// -/// `head` reads the head back, which is how an author finds what a new script -/// must name and how an operator sees which migration a chain is at. It is not -/// how a script tests that its predecessor ran: a head says what was LAST, and -/// `applied` is what says whether a particular migration ever ran at all. -/// -/// The registry is an INDEX, not proof. It says which invariant applies; it does -/// not say the invariant holds. A multisig can act out of band and nothing here -/// moves. Codehash and bytecode pins are what verify the state itself, and this -/// library is not a substitute for them. -library LibMigrationRegistryV2 { - /// Thrown when the code at the registry address is not the registry this - /// library was compiled against. An address with no code hits this too: an - /// empty account's code hash is zero, never the expected value. - /// @param expectedCodeHash The code hash of the pinned registry. - /// @param actualCodeHash The code hash actually found at the address. - error UnexpectedMigrationRegistryV2CodeHash(bytes32 expectedCodeHash, bytes32 actualCodeHash); - - /// Reverts unless the pinned registry address holds the pinned code. - /// - /// All three entry points check, and they check the same way, because each - /// is worse than useless against unknown code: `applied` would branch a - /// test on whatever timestamp that code returned, `head` would hand back a - /// value that is not a head, and `applyMigration` would record a migration - /// somewhere nothing will ever read it. The check is one function so the - /// three cannot drift into checking different things, and an entry point - /// added later has one place to call rather than a rule to remember. - function checkCodeHash() internal view { - bytes32 actualCodeHash = LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash; - if (actualCodeHash != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH) { - revert UnexpectedMigrationRegistryV2CodeHash( - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, actualCodeHash - ); - } - } - - /// When `writer` applied `migration`, or zero if it never did. - /// - /// Verifies the registry's code hash before reading, so a chain where the - /// registry is absent, or where something else occupies its address, is a - /// NAMED revert rather than a call into unknown code. An absent registry - /// reverts either way — solc reverts a high-level call whose returndata is - /// too short to decode — but anonymously, saying nothing about which of the - /// two it was. What the check actually forbids is the case that does NOT - /// revert: code at the address that is not this registry, an EIP-7702 - /// delegation included, is free to answer zero to every migration and send - /// every caller down its pre-migration branch. - /// - /// The registry itself refuses the zero writer, and refuses the two ids a - /// migration can never be, so those arrive as reverts from it rather than - /// as zero. - /// @param writer The namespace to read — the authority whose record the - /// caller trusts. Never the zero address. - /// @param migration The migration to ask about. Never zero, never - /// `MIGRATION_HEAD_GENESIS`. - /// @return The moment `writer` applied `migration` at, or zero if it has - /// not. - function applied(address writer, bytes32 migration) internal view returns (uint256) { - checkCodeHash(); - return IMigrationRegistryV2(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS) - .applied(writer, migration); - } - - /// The migration `writer` applied most recently, or `MIGRATION_HEAD_GENESIS` - /// if it has never applied one. - /// - /// Verifies the registry's code hash first for the same reason `applied` - /// does, and more sharply: occupying code is free to answer any head it - /// likes, including the zero no head can ever hold, so an unverified read - /// can hand back something that is not a head at all. An empty address is - /// not that case — there is no returndata for a `bytes32` to decode from, - /// so it reverts unguarded — and the check is what gives it a name. - /// @param writer The namespace to read. Never the zero address. - /// @return The head of `writer`'s namespace. Never zero. - function head(address writer) internal view returns (bytes32) { - checkCodeHash(); - return IMigrationRegistryV2(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS).head(writer); - } - - /// Applies `migration` under the CALLER's namespace, onto `expectedHead`, as - /// having been applied at `appliedAt`. - /// - /// The caller is whoever the resulting transaction is sent from — a Safe - /// executing a bundle, a broadcasting EOA, a timelock — and that account is - /// the namespace the record lands in. A reader has to ask about that same - /// account, so which account a migration is applied from is a decision - /// with a consequence rather than an implementation detail. It is also the - /// account whose head this moves, so two unrelated sequences applied from - /// one account interleave into one chain. - /// - /// Verifies the registry's code hash before writing, so a migration is - /// never "applied" into unknown code. A write into an EMPTY address fails - /// unguarded — solc checks the callee exists when no return data is - /// expected — so what this stops is the write that SUCCEEDS into something - /// that is not the registry: a record that went nowhere is worse than no - /// record at all, because the migration ran and every reader goes on - /// asserting the pre-migration state. - /// - /// The registry refuses the zero id, refuses a migration this caller has - /// already applied, and refuses one applied onto anything but the - /// namespace's actual head — which between them make a re-dispatched, a - /// skipped and an out-of-order migration all fail rather than land. It - /// refuses the three moments a record cannot carry as well: zero, one after - /// the current block, and one before the record of `expectedHead`. - /// @param expectedHead The migration the caller believes it applied last, - /// or `MIGRATION_HEAD_GENESIS` for the first in this namespace. - /// @param migration The migration to apply. Never zero, never - /// `MIGRATION_HEAD_GENESIS`. - /// @param appliedAt The moment `migration` was applied — `block.timestamp` - /// for a migration running now, the moment it ran for one being recorded - /// after the fact. - function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) internal { - checkCodeHash(); - IMigrationRegistryV2(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS) - .applyMigration(expectedHead, migration, appliedAt); - } -} diff --git a/src/lib/LibMigrationRegistryV2Deploy.sol b/src/lib/LibMigrationRegistryV2Deploy.sol deleted file mode 100644 index f3317c5..0000000 --- a/src/lib/LibMigrationRegistryV2Deploy.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.25; - -// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. - -import { - DEPLOYED_ADDRESS as MIGRATION_REGISTRY_V2_ADDR, - BYTECODE_HASH as MIGRATION_REGISTRY_V2_HASH -} from "../generated/candidate/MigrationRegistryV2.sol"; - -/// @title LibMigrationRegistryV2Deploy -/// @notice The deterministic Zoltu deploy address and code hash of -/// `MigrationRegistryV2`, aliased from its generated snapshot so that snapshot stays the -/// single source of truth. The import path never moves, so consumers are -/// unaffected by which snapshot it names. -library LibMigrationRegistryV2Deploy { - address constant MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS = MIGRATION_REGISTRY_V2_ADDR; - bytes32 constant MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH = MIGRATION_REGISTRY_V2_HASH; -} diff --git a/src/lib/LibMigrationRegistryV2Released.sol b/src/lib/LibMigrationRegistryV2Released.sol deleted file mode 100644 index e06bdf1..0000000 --- a/src/lib/LibMigrationRegistryV2Released.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.25; - -// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. - -import {DeploySuite} from "../abstract/RainDeploySuitesBase.sol"; - -/// @title LibMigrationRegistryV2Released -/// @notice Every frozen release of `MigrationRegistryV2`: one entry per file in -/// the append-only `src/generated//` record, in tag order. -/// -/// The deploy address, code hash, creation code, runtime code and dependency -/// list of each entry are aliased from that release's own frozen snapshot, so -/// what a release deployed, and what it required to already be on chain, are -/// read from the immutable file and from nowhere else. A dependency dropped -/// from current source stays required by the releases cut with it, and one -/// added is not imposed on releases cut without it. -/// -/// The key and the artifact path are explorer and ordering metadata -/// regenerated from the CURRENT declaration, and are not part of that -/// record. A moved source path retroactively updates every entry's artifact -/// path, which is intended: the alternative is parsing this generated file -/// back in to preserve what it last said. -library LibMigrationRegistryV2Released { - /// Every frozen release, in tag order. - /// @return suites The released suites. - function releasedSuites() internal pure returns (DeploySuite[] memory suites) { - suites = new DeploySuite[](0); - } -} diff --git a/test/concrete/MockMigrationApplier.sol b/test/concrete/MockMigrationApplier.sol index 0db70b0..8fbb53d 100644 --- a/test/concrete/MockMigrationApplier.sol +++ b/test/concrete/MockMigrationApplier.sol @@ -28,14 +28,31 @@ contract MockMigrationApplier { LibMigrationRegistry.applyMigration(expectedHead, migration); } + /// Applies `migration` under this contract, onto `expectedHead`, at + /// `appliedAt`. + /// @param expectedHead The head this contract believes it is at. + /// @param migration The migration to apply. + /// @param appliedAt The moment the migration was applied. + function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + LibMigrationRegistry.applyMigration(expectedHead, migration, appliedAt); + } + /// When `writer` applied `migration`. /// @param writer The namespace to read. /// @param migration The migration to ask about. - /// @return The timestamp it was applied at, or zero. + /// @return The moment it was applied at, or zero. function applied(address writer, bytes32 migration) external view returns (uint256) { return LibMigrationRegistry.applied(writer, migration); } + /// What `writer` applied `migration` onto. + /// @param writer The namespace to read. + /// @param migration The migration to ask about. + /// @return The head it was applied onto, or zero. + function appliedOnto(address writer, bytes32 migration) external view returns (bytes32) { + return LibMigrationRegistry.appliedOnto(writer, migration); + } + /// The head of `writer`'s namespace. /// @param writer The namespace to read. /// @return The head. diff --git a/test/concrete/MockMigrationApplierV2.sol b/test/concrete/MockMigrationApplierV2.sol deleted file mode 100644 index ddf4abb..0000000 --- a/test/concrete/MockMigrationApplierV2.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {LibMigrationRegistryV2} from "../../src/lib/LibMigrationRegistryV2.sol"; - -/// @title MockMigrationApplierV2 -/// @notice A consumer in the shape `LibMigrationRegistryV2.applyMigration` is -/// designed for: it calls the library and nothing else, so the record lands -/// under THIS contract's address. -/// -/// It exists so the namespace can be exercised as the property it is. The -/// library's functions are `internal` and inline into whatever executes them, -/// so `msg.sender` at the registry is the calling CONTRACT, not whoever -/// `vm.prank` last named — a test contract calling the library directly can -/// therefore only ever write one namespace. Two of these are two namespaces, -/// which is what makes "a record reaches nobody else" checkable rather than -/// asserted about a single account. -/// -/// Two of them are also two heads, which is what makes "one namespace is one -/// sequence" checkable at all: nothing about one applier's head can be shown -/// to leave the other's alone from inside a single namespace. -contract MockMigrationApplierV2 { - /// Applies `migration` under this contract, onto `expectedHead`, at - /// `appliedAt`. - /// @param expectedHead The head this contract believes it is at. - /// @param migration The migration to apply. - /// @param appliedAt The moment the migration was applied. - function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { - LibMigrationRegistryV2.applyMigration(expectedHead, migration, appliedAt); - } - - /// When `writer` applied `migration`. - /// @param writer The namespace to read. - /// @param migration The migration to ask about. - /// @return The moment it was applied at, or zero. - function applied(address writer, bytes32 migration) external view returns (uint256) { - return LibMigrationRegistryV2.applied(writer, migration); - } - - /// The head of `writer`'s namespace. - /// @param writer The namespace to read. - /// @return The head. - function head(address writer) external view returns (bytes32) { - return LibMigrationRegistryV2.head(writer); - } -} diff --git a/test/src/concrete/MigrationRegistryApplied.t.sol b/test/src/concrete/MigrationRegistryApplied.t.sol index 2e1771b..b559a2f 100644 --- a/test/src/concrete/MigrationRegistryApplied.t.sol +++ b/test/src/concrete/MigrationRegistryApplied.t.sol @@ -10,9 +10,9 @@ import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; /// @title MigrationRegistryAppliedTest /// @notice A test suite for `MigrationRegistry.applied`: it answers an applied -/// migration with the moment it was applied, an unapplied one with zero, +/// migration with the moment recorded against it, an unapplied one with zero, /// refuses the three inputs that can only be mistakes, and is the only reader of -/// the records. +/// a record's moment. contract MigrationRegistryAppliedTest is Test { /// The registry under test. Stateful, so a fresh one per test. MigrationRegistry internal sRegistry; @@ -35,26 +35,56 @@ contract MigrationRegistryAppliedTest is Test { assertEq(sRegistry.applied(writer, migration), 0); } - /// An applied migration answers the timestamp of the block it was applied - /// in, and keeps answering it as time moves on. The value is when the - /// migration was applied, not how long ago or how recently anything was - /// asked. - function testAppliedIsTheApplicationTimestamp(address writer, bytes32 migration, uint32 appliedAt, uint32 readAt) - external - { + /// An applied migration answers the moment recorded against it, and keeps + /// answering it as time moves on. The value is when the migration was + /// applied, not when the record was written and not how long ago or how + /// recently anything was asked. + function testAppliedIsTheRecordedMoment( + address writer, + bytes32 migration, + uint32 appliedAt, + uint32 writtenAt, + uint32 readAt + ) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); - vm.assume(readAt >= appliedAt); + vm.assume(writtenAt >= appliedAt); + vm.assume(readAt >= writtenAt); - vm.warp(appliedAt); + vm.warp(writtenAt); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); vm.warp(readAt); assertEq(sRegistry.applied(writer, migration), appliedAt); } + /// The moment `applied` answers never exceeds the block that asks, whichever + /// form wrote it. That is what lets a consumer measuring an interval since a + /// migration subtract the answer from the current block without + /// underflowing. + function testAppliedNeverExceedsTheReadingBlock( + address writer, + bytes32 migration, + uint32 appliedAt, + uint32 writtenAt, + uint32 readAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt >= appliedAt); + vm.assume(readAt >= writtenAt); + + vm.warp(writtenAt); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + vm.warp(readAt); + assertLe(sRegistry.applied(writer, migration), block.timestamp); + } + /// Reading does not consume or alter a record, so the same question asked /// twice answers the same way. function testAppliedIsIdempotent(address writer, bytes32 migration) external { @@ -135,13 +165,14 @@ contract MigrationRegistryAppliedTest is Test { assertEq(sRegistry.head(writer), migration); } - /// `applied` is the only reader of the records. The records mapping is not - /// `public`, so the getter a `public` mapping would generate — which answers - /// the zero writer and both refused ids with zero, the exact silent - /// wrong-branch these refusals exist to prevent — does not exist. + /// `applied` and `appliedOnto` are the only readers of the records. The + /// records mapping is not `public`, so the getter a `public` mapping would + /// generate — which answers the zero writer and both refused ids with zero, + /// the exact silent wrong-branch these refusals exist to prevent — does not + /// exist. function testAppliedNoGeneratedMappingGetter(address writer, bytes32 migration) external { (bool success,) = - address(sRegistry).call(abi.encodeWithSignature("sApplied(address,bytes32)", writer, migration)); + address(sRegistry).call(abi.encodeWithSignature("sRecords(address,bytes32)", writer, migration)); assertFalse(success); } @@ -154,11 +185,16 @@ contract MigrationRegistryAppliedTest is Test { } /// There is no other entry point at all: no fallback, no receive, and - /// nothing beyond the three `IMigrationRegistryV1` functions, so an unknown + /// nothing beyond the `IMigrationRegistryV1` functions, so an unknown /// selector reverts instead of being silently absorbed. + /// + /// The two `applyMigration` selectors are spelled from their signatures + /// because `.selector` has no single answer for an overloaded name. function testAppliedNoOtherEntryPoint(bytes4 selector, bytes32 migration) external { vm.assume(selector != IMigrationRegistryV1.applied.selector); - vm.assume(selector != IMigrationRegistryV1.applyMigration.selector); + vm.assume(selector != IMigrationRegistryV1.appliedOnto.selector); + vm.assume(selector != bytes4(keccak256("applyMigration(bytes32,bytes32)"))); + vm.assume(selector != bytes4(keccak256("applyMigration(bytes32,bytes32,uint256)"))); vm.assume(selector != IMigrationRegistryV1.head.selector); (bool success,) = address(sRegistry).call(abi.encodeWithSelector(selector, address(this), migration)); diff --git a/test/src/concrete/MigrationRegistryAppliedOnto.t.sol b/test/src/concrete/MigrationRegistryAppliedOnto.t.sol new file mode 100644 index 0000000..e808ace --- /dev/null +++ b/test/src/concrete/MigrationRegistryAppliedOnto.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.2/src/Test.sol"; + +import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV1.sol"; +import {MigrationRegistry} from "../../../src/concrete/MigrationRegistry.sol"; +import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; + +/// @title MigrationRegistryAppliedOntoTest +/// @notice A test suite for `MigrationRegistry.appliedOnto`: it answers an +/// applied migration with the head it was applied onto, an unapplied one with +/// zero, refuses the three inputs that can only be mistakes, and is the step +/// that walks a namespace back to genesis. +contract MigrationRegistryAppliedOntoTest is Test { + /// The registry under test. Stateful, so a fresh one per test. + MigrationRegistry internal sRegistry; + + function setUp() external { + sRegistry = new MigrationRegistry(); + } + + /// An unapplied migration answers zero rather than reverting, exactly as + /// `applied` does. Zero is not a head — a head is genesis or an applied id, + /// both nonzero — so it says "no record" and nothing else. + function testAppliedOntoUnappliedIsZero(address writer, bytes32 migration) external view { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + + assertEq(sRegistry.appliedOnto(writer, migration), bytes32(0)); + } + + /// The first migration in a namespace answers `MIGRATION_HEAD_GENESIS`, so + /// the walk back has a terminator that is not the "no record" zero. + function testAppliedOntoFirstRecordIsGenesis(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + } + + /// A later migration answers the migration before it, which is the head the + /// registry itself checked rather than a value the caller was free to + /// choose: a caller that names anything else is refused, so the record can + /// only ever hold where the namespace actually was. + function testAppliedOntoIsTheCheckedHead(address writer, bytes32 migrationA, bytes32 migrationB, bytes32 wrongHead) + external + { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, wrongHead); + vm.assume(migrationA != migrationB); + vm.assume(wrongHead != migrationA); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, wrongHead, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(wrongHead, migrationB); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB); + + assertEq(sRegistry.appliedOnto(writer, migrationB), migrationA); + } + + /// A record never moves. The answer for an earlier migration is the same + /// after a later one lands, so a chain read at any moment describes the same + /// history. + function testAppliedOntoRecordsAreImmutable(address writer, bytes32 migrationA, bytes32 migrationB) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB); + + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(writer, migrationB), migrationA); + } + + /// Reading twice answers the same way. + function testAppliedOntoIsIdempotent(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + } + + /// A record is confined to the caller's namespace here as everywhere else: + /// one writer's chain says nothing about another's. + function testAppliedOntoIsPerWriter(address writer, address other, bytes32 migration) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(other, migration), bytes32(0)); + } + + /// The zero writer is refused rather than answered, for the reason `applied` + /// refuses it: the zero namespace is provably empty, so an unresolved writer + /// constant would read as "nothing has been applied" rather than as the + /// mistake it is. + function testAppliedOntoZeroWriterReverts(bytes32 migration) external { + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + sRegistry.appliedOnto(address(0), migration); + } + + /// The zero migration id is refused: `applyMigration` will not write it, so + /// it can never be a real record. + function testAppliedOntoZeroMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); + sRegistry.appliedOnto(writer, bytes32(0)); + } + + /// The genesis head is refused as a migration. It is where a walk ENDS, so a + /// caller that carried on asking about it has confused the terminator for a + /// record and would read zero as one. + function testAppliedOntoGenesisMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + sRegistry.appliedOnto(writer, MIGRATION_HEAD_GENESIS); + } + + /// The writer is checked before the migration, so a caller that has zeroed + /// both gets one stable answer rather than one that depends on which check + /// happens to run — the same order `applied` uses, from the same check. + function testAppliedOntoZeroWriterCheckedFirst() external { + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + sRegistry.appliedOnto(address(0), bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + sRegistry.appliedOnto(address(0), MIGRATION_HEAD_GENESIS); + } + + /// A refusal is not a state change: the refused cases revert on a registry + /// that holds records exactly as they do on an empty one, and leave those + /// records intact. + function testAppliedOntoRefusalLeavesRecordsIntact(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + sRegistry.appliedOnto(address(0), migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); + sRegistry.appliedOnto(writer, bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + sRegistry.appliedOnto(writer, MIGRATION_HEAD_GENESIS); + + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.head(writer), migration); + } + + /// The two readers of a record agree about whether it exists. `applied` + /// answering zero and `appliedOnto` answering zero are the same fact, and a + /// nonzero answer from either comes with a nonzero answer from the other — + /// which is what a single whole-struct write buys. + function testAppliedOntoAgreesWithApplied(address writer, bytes32 migrationA, bytes32 migrationB) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + assertEq(sRegistry.applied(writer, migrationA), 0); + assertEq(sRegistry.appliedOnto(writer, migrationA), bytes32(0)); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + + assertTrue(sRegistry.applied(writer, migrationA) != 0); + assertTrue(sRegistry.appliedOnto(writer, migrationA) != bytes32(0)); + + assertEq(sRegistry.applied(writer, migrationB), 0); + assertEq(sRegistry.appliedOnto(writer, migrationB), bytes32(0)); + } +} diff --git a/test/src/concrete/MigrationRegistryApplyMigration.t.sol b/test/src/concrete/MigrationRegistryApplyMigration.t.sol index d6ce0ac..d52d3f1 100644 --- a/test/src/concrete/MigrationRegistryApplyMigration.t.sol +++ b/test/src/concrete/MigrationRegistryApplyMigration.t.sol @@ -9,9 +9,10 @@ import {MigrationRegistry} from "../../../src/concrete/MigrationRegistry.sol"; import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; /// @title MigrationRegistryApplyMigrationTest -/// @notice A test suite for `MigrationRegistry.applyMigration`: who a record -/// belongs to, that a migration is applied at most once and only onto the head -/// its caller named, what a record carries, and what it may never become. +/// @notice A test suite for both forms of `MigrationRegistry.applyMigration`: +/// who a record belongs to, that a migration is applied at most once and only +/// onto the head its caller named, which moments a record may carry, what a +/// record carries, and what it may never become. contract MigrationRegistryApplyMigrationTest is Test { /// The registry under test. Stateful, so a fresh one per test. MigrationRegistry internal sRegistry; @@ -33,7 +34,7 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.applied(writer, migration), block.timestamp); } - /// A record IS the block timestamp it landed in, which is the whole + /// The two-argument form records the block it landed in, which is the whole /// difference from a flag: a consumer whose invariant starts AT the /// migration — a cliff, a rate change, a grace period — reads the moment /// from the chain rather than from a constant somebody guessed. @@ -72,11 +73,12 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.applied(writer, migrationB), 2000); } - /// A record refuses to be written at all in a block whose timestamp is zero, - /// rather than write one that `applied` would read back as no record. The - /// head does not move and the migration can still be applied, which is the - /// only outcome that leaves the namespace describing something true. - function testApplyMigrationZeroTimestampReverts(address writer, bytes32 migration) external { + /// The two-argument form refuses to write at all in a block whose timestamp + /// is zero, rather than write a record that `applied` would read back as no + /// record. The head does not move and the migration can still be applied, + /// which is the only outcome that leaves the namespace describing something + /// true. + function testApplyMigrationZeroBlockReverts(address writer, bytes32 migration) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.warp(0); @@ -94,13 +96,12 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.applied(writer, migration), 1); } - /// The zero timestamp is checked LAST, after every refusal that describes a - /// mistake in the call. Those are true whatever block the call lands in, so - /// a caller in a zero-timestamp block is told which of its arguments is - /// wrong rather than told to come back later — and only a caller whose - /// arguments are all right is told about the block, which is the one - /// refusal that goes away on its own. - function testApplyMigrationZeroTimestampCheckedLast( + /// The zero moment is checked after the two id refusals and before anything + /// about the namespace, on the two-argument form as on the other. An id is + /// what a record is ABOUT, so a call with no subject has nothing to say a + /// moment for; everything after describes a namespace no writable record + /// will reach. + function testApplyMigrationZeroBlockCheckedAfterIdsAndBeforeTheNamespace( address writer, bytes32 migrationA, bytes32 migrationB, @@ -125,26 +126,18 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.prank(writer); sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS); - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV1.MigrationAlreadyApplied.selector, writer, migrationA) - ); + // Already applied, in a block that can hold no record: told about the + // moment, because the record could not be written whatever namespace it + // arrived at. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, MIGRATION_HEAD_GENESIS, migrationA - ) - ); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB); - - // With nothing left to say about the call, the block. This is what - // makes the four refusals above statements about the ORDER rather than - // about a check that was not live in this block at all. + // A head the namespace has moved on from, in the same block: told about + // the moment. vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB); } /// A record is confined to the caller's namespace. Applying under one @@ -564,28 +557,53 @@ contract MigrationRegistryApplyMigrationTest is Test { } /// `Migrated` is emitted with the writer and migration both indexed, so the - /// log can be filtered by either. The log is the only enumeration of the - /// registry, so a record that does not emit is a record nobody can find. + /// log can be filtered by either, and carries the moment as data. The log is + /// the only enumeration of the registry, so a record that does not emit is a + /// record nobody can find. /// - /// It carries no head and no timestamp because the log already holds both: - /// one writer's entries in order ARE its chain of heads, and the timestamp - /// is the block's. - function testApplyMigrationEvent(address writer, bytes32 migration) external { + /// It carries no head because the log already holds it: one writer's entries + /// in order ARE its chain of heads. It does carry the moment, which the + /// block a log entry sits in does not — that block says when the record was + /// written, and the moment says when the migration ran. + function testApplyMigrationEvent(address writer, bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt > appliedAt); + vm.warp(writtenAt); vm.recordLogs(); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); assertEq(entries[0].emitter, address(sRegistry)); assertEq(entries[0].topics.length, 3); - assertEq(entries[0].topics[0], keccak256("Migrated(address,bytes32)")); + assertEq(entries[0].topics[0], keccak256("Migrated(address,bytes32,uint256)")); assertEq(entries[0].topics[1], bytes32(uint256(uint160(writer)))); assertEq(entries[0].topics[2], migration); - assertEq(entries[0].data.length, 0); + assertEq(entries[0].data, abi.encode(uint256(appliedAt))); + } + + /// The two-argument form emits the same event, carrying the block it stamped + /// — so a reader of the log never has to know which form wrote a record. + function testApplyMigrationEventFromTheBlockStampingForm(address writer, bytes32 migration, uint32 now_) + external + { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.recordLogs(); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].topics[0], keccak256("Migrated(address,bytes32,uint256)")); + assertEq(entries[0].data, abi.encode(uint256(now_))); } /// A refused `applyMigration` emits nothing, so a failed apply can never be @@ -627,5 +645,424 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, keccak256(abi.encode(migration))); assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), 0); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV1.FutureTimestamp.selector, block.timestamp + 1, block.timestamp + ) + ); + vm.prank(writer); + sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), block.timestamp + 1); + assertEq(vm.getRecordedLogs().length, 0); + } + + /// The three-argument form records the moment the CALLER supplied, which is + /// what lets a migration that already ran be recorded with the time it ran + /// rather than the time it was written down. The block the record lands in + /// is not the value, and a record written long after the fact says so. + function testApplyMigrationRecordsTheSuppliedMoment( + address writer, + bytes32 migration, + uint32 appliedAt, + uint32 writtenAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt > appliedAt); + vm.warp(writtenAt); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), appliedAt); + assertTrue(sRegistry.applied(writer, migration) != block.timestamp); + } + + /// The moment of the current block is an ordinary value for the parameter, + /// which is what a caller reaching for the three-argument form to record a + /// migration running now passes. + function testApplyMigrationCurrentBlockIsAccepted(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), now_); + } + + /// The two forms write the SAME record when the moment is this block, down + /// to the head each was applied onto — which is what makes the two-argument + /// form the other one with today's argument rather than a second way to + /// write a record. + function testApplyMigrationBothFormsWriteTheSameRecord( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 now_ + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(now_ != 0); + vm.warp(now_); + + MigrationRegistry stamping = new MigrationRegistry(); + MigrationRegistry supplied = new MigrationRegistry(); + + vm.prank(writer); + stamping.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + vm.prank(writer); + supplied.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + vm.prank(writer); + stamping.applyMigration(migrationA, migrationB); + vm.prank(writer); + supplied.applyMigration(migrationA, migrationB, block.timestamp); + + assertEq(stamping.applied(writer, migrationB), supplied.applied(writer, migrationB)); + assertEq(stamping.appliedOnto(writer, migrationB), supplied.appliedOnto(writer, migrationB)); + assertEq(stamping.head(writer), supplied.head(writer)); + } + + /// A moment that has not arrived is refused. A record says a migration HAS + /// run, so a future one is not a late record of anything, and a consumer + /// measuring an interval since the migration would be subtracting a moment + /// later than the one it is measuring from. + function testApplyMigrationFutureTimestampReverts(address writer, bytes32 migration, uint32 now_, uint256 appliedAt) + external + { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.warp(now_); + appliedAt = bound(appliedAt, uint256(now_) + 1, type(uint256).max); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, appliedAt, uint256(now_)) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.appliedOnto(writer, migration), bytes32(0)); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + } + + /// One second past the current block is refused, and the current block is + /// not: the boundary is the block's own timestamp, inclusive. + function testApplyMigrationFutureBoundary(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) + ); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_)); + assertEq(sRegistry.applied(writer, migration), now_); + } + + /// A supplied zero is refused, rather than written as a record that + /// `applied` would read back as no record. The head does not move and the + /// migration can still be applied. + function testApplyMigrationZeroTimestampReverts(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); + assertEq(sRegistry.applied(writer, migration), 1); + } + + /// A block whose timestamp is zero can hold no record at all: zero is + /// refused as a moment, and every other moment is still in the future. The + /// head does not move, so the namespace goes on describing something true + /// and the migration is still applicable once the clock has moved. + function testApplyMigrationZeroBlockRecordsNothing(address writer, bytes32 migration, uint256 appliedAt) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.warp(0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, appliedAt, 0)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + + vm.warp(1); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); + assertEq(sRegistry.applied(writer, migration), 1); + } + + /// The zero moment is refused BEFORE anything about the namespace is read, + /// so an uninitialised argument is reported as itself rather than as + /// whatever the namespace happens to make of it. Fuzzed over the head and + /// checked against a namespace that has moved on, because a head the + /// namespace happens to be at is accepted whichever check runs first. + function testApplyMigrationZeroTimestampCheckedBeforeTheNamespace( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 anyHead + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + // Already applied, and a zero moment: told about the moment. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 0); + + // A head that has moved on, and a zero moment: told about the moment. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, migrationB, 0); + } + + /// The two id refusals come before the moment, so a caller that has zeroed + /// both an id and a moment is told about the id: an id is what the record is + /// ABOUT, and a call with no subject has nothing to say a moment for. + function testApplyMigrationIdCheckedBeforeTimestamp(address writer, bytes32 anyHead) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, bytes32(0), 0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS, 0); + } + + /// The refusals that describe the NAMESPACE come before the future-moment + /// one, so a re-dispatched script is told its migration already ran, and a + /// script at the wrong point in the sequence is told where the namespace is, + /// rather than either of them being sent to look at a clock. + function testApplyMigrationNamespaceCheckedBeforeTheFuture( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 skipped + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, skipped); + vm.assume(migrationA != migrationB); + vm.assume(skipped != migrationA); + vm.assume(skipped != migrationB); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 5000); + + // Already applied, and in the future: told it already ran. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.MigrationAlreadyApplied.selector, writer, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationA, 9001); + + // The wrong head, and in the future: told where the namespace is. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, skipped, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(skipped, migrationB, 9001); + } + + /// A record may carry a moment EARLIER than the record before it in the + /// chain. Nothing orders the moments, because the chain does: a namespace + /// backfilled out of order, or one whose writer learned of an older + /// migration late, records what it knows rather than being refused for + /// contradicting a sequence it is not the source of. + function testApplyMigrationMomentsMayGoBackwards( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 now_, + uint256 earlier + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(now_ > 1); + vm.warp(now_); + earlier = bound(earlier, 1, uint256(now_) - 1); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(now_)); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, earlier); + + assertEq(sRegistry.applied(writer, migrationA), uint256(now_)); + assertEq(sRegistry.applied(writer, migrationB), earlier); + assertEq(sRegistry.head(writer), migrationB); + } + + /// Two records may carry the SAME moment. Two migrations applied in one + /// transaction share a block, and two backfilled migrations known only to + /// the same day share a moment; the chain is what orders them, so the + /// moments are not asked to. + function testApplyMigrationMomentsMayBeEqual( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 appliedAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(appliedAt != 0); + vm.warp(appliedAt); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, appliedAt); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, appliedAt); + + assertEq(sRegistry.applied(writer, migrationA), appliedAt); + assertEq(sRegistry.applied(writer, migrationB), appliedAt); + } + + /// A record keeps the head it was applied onto, which is what makes the + /// order structural. The first record in a namespace holds + /// `MIGRATION_HEAD_GENESIS`, and each later one holds the migration before + /// it — the value the caller named and the registry checked, not one the + /// caller could have chosen freely. + function testApplyMigrationRecordsTheHeadItWasAppliedOnto( + address writer, + bytes32 migrationA, + bytes32 migrationB + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + assertEq(sRegistry.appliedOnto(writer, migrationA), bytes32(0)); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB); + assertEq(sRegistry.appliedOnto(writer, migrationB), migrationA); + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + } + + /// The chain is the order the migrations ran in, and it says so while the + /// moments say the opposite. Three records written newest-moment-first walk + /// back from the head in the order they were APPLIED, ending at genesis. + function testApplyMigrationChainIsTheOrderWhateverTheMoments( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 migrationC + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, migrationC); + vm.assume(migrationA != migrationB); + vm.assume(migrationB != migrationC); + vm.assume(migrationA != migrationC); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 3000); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, 2000); + vm.prank(writer); + sRegistry.applyMigration(migrationB, migrationC, 1000); + + // Every moment is below the one before it, so nothing about the order + // can be read out of them. + assertEq(sRegistry.applied(writer, migrationA), 3000); + assertEq(sRegistry.applied(writer, migrationB), 2000); + assertEq(sRegistry.applied(writer, migrationC), 1000); + + // The chain still says exactly what happened. + bytes32 cursor = sRegistry.head(writer); + assertEq(cursor, migrationC); + cursor = sRegistry.appliedOnto(writer, cursor); + assertEq(cursor, migrationB); + cursor = sRegistry.appliedOnto(writer, cursor); + assertEq(cursor, migrationA); + cursor = sRegistry.appliedOnto(writer, cursor); + assertEq(cursor, MIGRATION_HEAD_GENESIS); + } + + /// A chain belongs to one namespace. Another writer applying the same + /// migrations builds its own chain, and neither reaches the other. + function testApplyMigrationChainIsPerWriter( + address writer, + address other, + bytes32 migrationA, + bytes32 migrationB + ) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB); + + // The other namespace applies them in the opposite order, so a chain + // that leaked would be visibly the first one's. + vm.prank(other); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB); + vm.prank(other); + sRegistry.applyMigration(migrationB, migrationA); + + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(writer, migrationB), migrationA); + assertEq(sRegistry.appliedOnto(other, migrationB), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(other, migrationA), migrationB); } } diff --git a/test/src/concrete/MigrationRegistryV2Applied.t.sol b/test/src/concrete/MigrationRegistryV2Applied.t.sol deleted file mode 100644 index 47f7bda..0000000 --- a/test/src/concrete/MigrationRegistryV2Applied.t.sol +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {Test} from "forge-std-1.16.1/src/Test.sol"; - -import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV2.sol"; -import {MigrationRegistryV2} from "../../../src/concrete/MigrationRegistryV2.sol"; - -/// @title MigrationRegistryV2AppliedTest -/// @notice A test suite for `MigrationRegistryV2.applied`: it answers an applied -/// migration with the moment recorded against it, an unapplied one with zero, -/// refuses the three inputs that can only be mistakes, and is the only reader of -/// the records. -contract MigrationRegistryV2AppliedTest is Test { - /// The registry under test. Stateful, so a fresh one per test. - MigrationRegistryV2 internal sRegistry; - - function setUp() external { - sRegistry = new MigrationRegistryV2(); - } - - /// A migration id that is neither of the two values the head space reserves. - /// @param migration The fuzzed candidate. - function assumeMigration(bytes32 migration) internal pure { - vm.assume(migration != bytes32(0)); - vm.assume(migration != MIGRATION_HEAD_GENESIS); - } - - /// An unapplied migration answers zero rather than reverting. This is - /// the deliberate difference from a registry whose reads revert on an - /// unknown key: "not applied here" is the ordinary state of every migration - /// before it runs and of every migration on a chain that never got it, and - /// it is the answer a caller branches on to assert the pre-migration state - /// exactly. A revert would leave the caller with nothing to say about the - /// state it is actually looking at. - function testAppliedUnappliedIsZero(address writer, bytes32 migration) external view { - vm.assume(writer != address(0)); - assumeMigration(migration); - - assertEq(sRegistry.applied(writer, migration), 0); - } - - /// An applied migration answers the moment recorded against it, and keeps - /// answering it as time moves on. The value is when the migration was - /// applied, not when the record was written and not how long ago or how - /// recently anything was asked. - function testAppliedIsTheRecordedMoment( - address writer, - bytes32 migration, - uint32 appliedAt, - uint32 writtenAt, - uint32 readAt - ) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(appliedAt != 0); - vm.assume(writtenAt >= appliedAt); - vm.assume(readAt >= writtenAt); - - vm.warp(writtenAt); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); - - vm.warp(readAt); - assertEq(sRegistry.applied(writer, migration), appliedAt); - } - - /// Reading does not consume or alter a record, so the same question asked - /// twice answers the same way. - function testAppliedIsIdempotent(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(sRegistry.applied(writer, migration), block.timestamp); - assertEq(sRegistry.applied(writer, migration), block.timestamp); - } - - /// The zero writer is refused rather than answered. No transaction - /// originates from the zero address, so that namespace is provably empty - /// and zero would be the answer forever — an unresolved writer constant - /// would read as "nothing has been applied" instead of as the mistake it - /// is, and send its caller down the pre-migration branch on every chain. - function testAppliedZeroWriterReverts(bytes32 migration) external { - assumeMigration(migration); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); - sRegistry.applied(address(0), migration); - } - - /// The zero migration id is refused for the same reason in the other - /// direction: `applyMigration` will not write it, so it can never be a real - /// record. - function testAppliedZeroMigrationReverts(address writer) external { - vm.assume(writer != address(0)); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - sRegistry.applied(writer, bytes32(0)); - } - - /// The genesis head is refused as a migration for the same reason again: - /// `applyMigration` will not write it either, so asking about it would - /// answer zero forever to a caller that has confused a head for a migration - /// — and that caller reads zero as its pre-migration branch. - function testAppliedGenesisMigrationReverts(address writer) external { - vm.assume(writer != address(0)); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - sRegistry.applied(writer, MIGRATION_HEAD_GENESIS); - } - - /// The writer is checked before the migration, so a caller that has zeroed - /// both is told about the namespace first and gets one stable answer rather - /// than one that depends on which check happens to run. - function testAppliedZeroWriterCheckedFirst() external { - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); - sRegistry.applied(address(0), bytes32(0)); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); - sRegistry.applied(address(0), MIGRATION_HEAD_GENESIS); - } - - /// A refusal is not a state change: the refused cases revert on a registry - /// that holds records exactly as they do on an empty one, and leave those - /// records intact. - function testAppliedRefusalLeavesRecordsIntact(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); - sRegistry.applied(address(0), migration); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - sRegistry.applied(writer, bytes32(0)); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - sRegistry.applied(writer, MIGRATION_HEAD_GENESIS); - - assertEq(sRegistry.applied(writer, migration), block.timestamp); - assertEq(sRegistry.head(writer), migration); - } - - /// `applied` is the only reader of the records. The records mapping is not - /// `public`, so the getter a `public` mapping would generate — which answers - /// the zero writer and both refused ids with zero, the exact silent - /// wrong-branch these refusals exist to prevent — does not exist. - function testAppliedNoGeneratedMappingGetter(address writer, bytes32 migration) external { - (bool success,) = - address(sRegistry).call(abi.encodeWithSignature("sApplied(address,bytes32)", writer, migration)); - assertFalse(success); - } - - /// Nor for the heads, where a generated getter would be worse still: it - /// answers an empty namespace with zero, and zero is a value no head can - /// ever hold. - function testAppliedNoGeneratedHeadGetter(address writer) external { - (bool success,) = address(sRegistry).call(abi.encodeWithSignature("sHead(address)", writer)); - assertFalse(success); - } - - /// There is no other entry point at all: no fallback, no receive, and - /// nothing beyond the three `IMigrationRegistryV2` functions, so an unknown - /// selector reverts instead of being silently absorbed. The two-argument - /// `applyMigration` in particular is not here: there is exactly one way to - /// write a record, and it names the moment. - function testAppliedNoOtherEntryPoint(bytes4 selector, bytes32 migration) external { - vm.assume(selector != IMigrationRegistryV2.applied.selector); - vm.assume(selector != IMigrationRegistryV2.applyMigration.selector); - vm.assume(selector != IMigrationRegistryV2.head.selector); - - (bool success,) = address(sRegistry).call(abi.encodeWithSelector(selector, address(this), migration)); - assertFalse(success); - } -} diff --git a/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol b/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol deleted file mode 100644 index 467fc9e..0000000 --- a/test/src/concrete/MigrationRegistryV2ApplyMigration.t.sol +++ /dev/null @@ -1,982 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; - -import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV2.sol"; -import {MigrationRegistryV2} from "../../../src/concrete/MigrationRegistryV2.sol"; - -/// @title MigrationRegistryV2ApplyMigrationTest -/// @notice A test suite for `MigrationRegistryV2.applyMigration`: who a record -/// belongs to, that a migration is applied at most once and only onto the head -/// its caller named, which moments a record may carry, what a record carries, -/// and what it may never become. -contract MigrationRegistryV2ApplyMigrationTest is Test { - /// The registry under test. Stateful, so a fresh one per test. - MigrationRegistryV2 internal sRegistry; - - function setUp() external { - sRegistry = new MigrationRegistryV2(); - } - - /// A migration id that is neither of the two values the head space reserves, - /// which is what every test that is not about those values wants. - /// @param migration The fuzzed candidate. - function assumeMigration(bytes32 migration) internal pure { - vm.assume(migration != bytes32(0)); - vm.assume(migration != MIGRATION_HEAD_GENESIS); - } - - /// Anyone may apply, and the record lands under the caller. There is no - /// authority to be refused by, which is the whole access-control design: - /// the namespace IS the caller. - function testApplyMigrationAnyCallerAppliesUnderItself(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(sRegistry.applied(writer, migration), block.timestamp); - } - - /// A record carries the moment the CALLER supplied, which is what lets a - /// migration that already ran be recorded with the time it ran rather than - /// the time it was written down. The block the record lands in is not the - /// value, and a record written long after the fact says so. - function testApplyMigrationRecordsTheSuppliedMoment( - address writer, - bytes32 migration, - uint32 appliedAt, - uint32 writtenAt - ) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(appliedAt != 0); - vm.assume(writtenAt > appliedAt); - vm.warp(writtenAt); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); - - assertEq(sRegistry.applied(writer, migration), appliedAt); - assertTrue(sRegistry.applied(writer, migration) != block.timestamp); - } - - /// The moment of the current block is an ordinary value for the parameter, - /// which is what a script recording a migration as it runs passes. There is - /// one way to write a record and this is it with today's argument. - function testApplyMigrationCurrentBlockIsAccepted(address writer, bytes32 migration, uint32 now_) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(now_ != 0); - vm.warp(now_); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(sRegistry.applied(writer, migration), now_); - } - - /// A moment that has not arrived is refused. A record says a migration HAS - /// run, so a future one is not a late record of anything, and a consumer - /// measuring an interval since the migration would be subtracting a moment - /// later than the one it is measuring from. - function testApplyMigrationFutureTimestampReverts(address writer, bytes32 migration, uint32 now_, uint256 appliedAt) - external - { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.warp(now_); - appliedAt = bound(appliedAt, uint256(now_) + 1, type(uint256).max); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, appliedAt, uint256(now_))); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); - - assertEq(sRegistry.applied(writer, migration), 0); - assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); - } - - /// One second past the current block is refused, and the current block is - /// not: the boundary is the block's own timestamp, inclusive. - function testApplyMigrationFutureBoundary(address writer, bytes32 migration, uint32 now_) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(now_ != 0); - vm.warp(now_); - - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) - ); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_)); - assertEq(sRegistry.applied(writer, migration), now_); - } - - /// A block whose timestamp is zero can hold no record at all: zero is - /// refused as a moment, and every other moment is still in the future. The - /// head does not move, so the namespace goes on describing something true - /// and the migration is still applicable once the clock has moved. - function testApplyMigrationZeroBlockRecordsNothing(address writer, bytes32 migration, uint256 appliedAt) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(appliedAt != 0); - vm.warp(0); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 0); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, appliedAt, 0)); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); - - assertEq(sRegistry.applied(writer, migration), 0); - assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); - - vm.warp(1); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); - assertEq(sRegistry.applied(writer, migration), 1); - } - - /// A record refuses to be written at all with a zero moment, rather than - /// write one that `applied` would read back as no record. The head does not - /// move and the migration can still be applied, which is the only outcome - /// that leaves the namespace describing something true. - function testApplyMigrationZeroTimestampReverts(address writer, bytes32 migration, uint32 now_) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(now_ != 0); - vm.warp(now_); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 0); - - assertEq(sRegistry.applied(writer, migration), 0); - assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); - assertEq(sRegistry.applied(writer, migration), 1); - } - - /// A namespace that has applied nothing has no record to be after, so every - /// moment the block allows is accepted — the floor is not the current block, - /// which is the whole of what backfilling a first migration needs. - function testApplyMigrationEmptyNamespaceHasNoFloor( - address writer, - bytes32 migration, - uint32 now_, - uint256 appliedAt - ) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(now_ != 0); - vm.warp(now_); - appliedAt = bound(appliedAt, 1, uint256(now_)); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); - - assertEq(sRegistry.applied(writer, migration), appliedAt); - } - - /// A record may not predate the record of the head it is applied onto. The - /// head chain is the order the migrations ran in, so a moment behind the one - /// before it contradicts the sequence it is being appended to. - function testApplyMigrationBeforeHeadReverts( - address writer, - bytes32 migrationA, - bytes32 migrationB, - uint32 headAppliedAt, - uint256 appliedAt - ) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - vm.assume(headAppliedAt > 1); - vm.warp(headAppliedAt); - appliedAt = bound(appliedAt, 1, uint256(headAppliedAt) - 1); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, headAppliedAt); - - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.TimestampBeforeHead.selector, writer, migrationA, appliedAt, uint256(headAppliedAt) - ) - ); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, appliedAt); - - assertEq(sRegistry.applied(writer, migrationB), 0); - assertEq(sRegistry.head(writer), migrationA); - } - - /// The floor is inclusive. Two migrations applied in one transaction share a - /// block, and two backfilled migrations known only to the same day share a - /// moment; the head chain is what orders them, so forcing the moments apart - /// would make them carry an ordering they do not have. - function testApplyMigrationEqualToHeadIsAccepted( - address writer, - bytes32 migrationA, - bytes32 migrationB, - uint32 appliedAt - ) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - vm.assume(appliedAt != 0); - vm.warp(appliedAt); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, appliedAt); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, appliedAt); - - assertEq(sRegistry.applied(writer, migrationA), appliedAt); - assertEq(sRegistry.applied(writer, migrationB), appliedAt); - } - - /// One second before the head's record is refused, and the head's own moment - /// is not: the floor is the head's record, inclusive. - /// - /// The floor is at least 2, so that one second below it is a moment a record - /// could otherwise carry. A floor of 1 puts that second at zero, which is - /// refused as a moment before any floor is consulted — a true refusal of a - /// different rule, and one that says nothing about where this boundary sits. - function testApplyMigrationFloorBoundary(address writer, bytes32 migrationA, bytes32 migrationB, uint32 now_) - external - { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - vm.assume(now_ > 2); - vm.warp(now_); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(now_) - 1); - - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.TimestampBeforeHead.selector, - writer, - migrationA, - uint256(now_) - 2, - uint256(now_) - 1 - ) - ); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, uint256(now_) - 2); - - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, uint256(now_) - 1); - assertEq(sRegistry.applied(writer, migrationB), uint256(now_) - 1); - } - - /// The floor follows the HEAD's record rather than staying at whatever the - /// first one was, so a sequence backfilled in head order keeps every moment - /// it was given and each step is measured against the step before it. - function testApplyMigrationFloorFollowsTheHead( - address writer, - bytes32 migrationA, - bytes32 migrationB, - bytes32 migrationC - ) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - assumeMigration(migrationC); - vm.assume(migrationA != migrationB); - vm.assume(migrationB != migrationC); - vm.assume(migrationA != migrationC); - - vm.warp(9000); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, 2000); - - // Above the first record and below the second, so a floor that had - // stayed at the first would accept this. - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.TimestampBeforeHead.selector, writer, migrationB, uint256(1500), uint256(2000) - ) - ); - vm.prank(writer); - sRegistry.applyMigration(migrationB, migrationC, 1500); - - vm.prank(writer); - sRegistry.applyMigration(migrationB, migrationC, 3000); - - assertEq(sRegistry.applied(writer, migrationA), 1000); - assertEq(sRegistry.applied(writer, migrationB), 2000); - assertEq(sRegistry.applied(writer, migrationC), 3000); - } - - /// The floor belongs to one namespace. Another writer's records say nothing - /// about what this one may record, which is the same confinement the records - /// and the heads already have. - function testApplyMigrationFloorIsPerWriter(address writer, address other, bytes32 migrationA, bytes32 migrationB) - external - { - vm.assume(writer != address(0)); - vm.assume(other != address(0)); - vm.assume(writer != other); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.warp(9000); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 5000); - - vm.prank(other); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB, 1000); - - assertEq(sRegistry.applied(other, migrationB), 1000); - } - - /// Two migrations applied at different moments carry those moments, and the - /// earlier one does not move when the later one lands. A record is of the - /// moment it happened, not of the last time anything happened. - function testApplyMigrationTimestampsAreIndependent(address writer, bytes32 migrationA, bytes32 migrationB) - external - { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.warp(1000); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); - - vm.warp(2000); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, 2000); - - assertEq(sRegistry.applied(writer, migrationA), 1000); - assertEq(sRegistry.applied(writer, migrationB), 2000); - } - - /// The zero moment is refused BEFORE anything about the namespace is read, - /// so an uninitialised argument is reported as itself rather than as - /// whatever the namespace happens to make of it. Fuzzed over the head and - /// checked against a namespace that has moved on, because a head the - /// namespace happens to be at is accepted whichever check runs first. - function testApplyMigrationZeroTimestampCheckedBeforeTheNamespace( - address writer, - bytes32 migrationA, - bytes32 migrationB, - bytes32 anyHead - ) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - - // Already applied, and a zero moment: told about the moment. - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 0); - - // A head that has moved on, and a zero moment: told about the moment. - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); - vm.prank(writer); - sRegistry.applyMigration(anyHead, migrationB, 0); - } - - /// The two id refusals come before the moment, so a caller that has zeroed - /// both an id and a moment is told about the id: an id is what the record - /// is ABOUT, and a call with no subject has nothing to say a moment for. - function testApplyMigrationIdCheckedBeforeTimestamp(address writer, bytes32 anyHead) external { - vm.assume(writer != address(0)); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(anyHead, bytes32(0), 0); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS, 0); - } - - /// The refusals that describe the NAMESPACE come before the two that - /// describe the moment against it, so a re-dispatched script is told its - /// migration already ran, and a script at the wrong point in the sequence is - /// told where the namespace is, rather than either of them being sent to - /// look at a clock. - function testApplyMigrationNamespaceCheckedBeforeTheWindow( - address writer, - bytes32 migrationA, - bytes32 migrationB, - bytes32 skipped - ) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - assumeMigration(skipped); - vm.assume(migrationA != migrationB); - vm.assume(skipped != migrationA); - vm.assume(skipped != migrationB); - - vm.warp(9000); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 5000); - - // Already applied, and backdated: told it already ran. - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migrationA) - ); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationA, 1000); - - // Already applied, and in the future: told it already ran. - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migrationA) - ); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationA, 9001); - - // The wrong head, and backdated: told where the namespace is. - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA) - ); - vm.prank(writer); - sRegistry.applyMigration(skipped, migrationB, 1000); - - // The wrong head, and in the future: told where the namespace is. - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA) - ); - vm.prank(writer); - sRegistry.applyMigration(skipped, migrationB, 9001); - } - - /// A record is confined to the caller's namespace. Applying under one - /// writer says nothing about any other, which is what makes a reader's - /// choice of namespace the whole of who it trusts — a hostile caller can - /// apply whatever it likes and reach nobody. - function testApplyMigrationDoesNotReachAnotherNamespace(address writer, address other, bytes32 migration) external { - vm.assume(writer != address(0)); - vm.assume(other != address(0)); - vm.assume(writer != other); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(sRegistry.applied(writer, migration), block.timestamp); - assertEq(sRegistry.applied(other, migration), 0); - } - - /// Two writers may apply the same migration id independently, and each - /// answers only for itself. Ids are opaque and namespaces are unrelated, so - /// a shared id is not a collision — including for the head, which each - /// writer advances from its own genesis. - function testApplyMigrationSameMigrationUnderTwoWriters(address writer, address other, bytes32 migration) external { - vm.assume(writer != address(0)); - vm.assume(other != address(0)); - vm.assume(writer != other); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - vm.prank(other); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(sRegistry.applied(writer, migration), block.timestamp); - assertEq(sRegistry.applied(other, migration), block.timestamp); - } - - /// Migrations are independent within one namespace: applying one says - /// nothing about any other. This is what a set buys over a high-water mark - /// — a reader asks about the migration its assertion actually depends on - /// rather than about a number that stands in for all of them. - function testApplyMigrationDistinctMigrations(address writer, bytes32 migrationA, bytes32 migrationB) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - - assertEq(sRegistry.applied(writer, migrationA), block.timestamp); - assertEq(sRegistry.applied(writer, migrationB), 0); - - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, block.timestamp); - - assertEq(sRegistry.applied(writer, migrationA), block.timestamp); - assertEq(sRegistry.applied(writer, migrationB), block.timestamp); - } - - /// A successful application makes its migration the namespace's new head, - /// which is what the next one has to name. - function testApplyMigrationAdvancesTheHead(address writer, bytes32 migrationA, bytes32 migrationB) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - assertEq(sRegistry.head(writer), migrationA); - - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, block.timestamp); - assertEq(sRegistry.head(writer), migrationB); - } - - /// Applying onto a head the namespace is not at is refused. This is what - /// blocks a SKIPPED step: a script names its predecessor, so a chain that - /// never got that predecessor fails at the moment of applying rather than - /// diverging silently from every chain that did. - function testApplyMigrationSkippedPredecessorReverts( - address writer, - bytes32 migrationA, - bytes32 migrationB, - bytes32 skipped - ) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - assumeMigration(skipped); - vm.assume(migrationA != migrationB); - vm.assume(skipped != migrationA); - vm.assume(skipped != migrationB); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, skipped, migrationA) - ); - vm.prank(writer); - sRegistry.applyMigration(skipped, migrationB, block.timestamp); - - assertEq(sRegistry.applied(writer, migrationB), 0); - assertEq(sRegistry.head(writer), migrationA); - } - - /// Genesis stops being an acceptable head the moment anything is applied, - /// so a first-migration script re-run against a namespace that has moved on - /// fails rather than restarting the sequence. - function testApplyMigrationOntoGenesisAfterFirstReverts(address writer, bytes32 migrationA, bytes32 migrationB) - external - { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, MIGRATION_HEAD_GENESIS, migrationA - ) - ); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB, block.timestamp); - } - - /// A head belongs to one namespace. One writer advancing its head leaves - /// every other writer's exactly where it was, so a second consumer's - /// migrations are not blocked or unblocked by the first's. - function testApplyMigrationHeadIsPerWriter(address writer, address other, bytes32 migrationA, bytes32 migrationB) - external - { - vm.assume(writer != address(0)); - vm.assume(other != address(0)); - vm.assume(writer != other); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - - assertEq(sRegistry.head(other), MIGRATION_HEAD_GENESIS); - - // The other namespace is still at genesis, so `migrationA` is not the - // head there and naming it is refused. - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, other, migrationA, MIGRATION_HEAD_GENESIS - ) - ); - vm.prank(other); - sRegistry.applyMigration(migrationA, migrationB, block.timestamp); - - vm.prank(other); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB, block.timestamp); - assertEq(sRegistry.head(other), migrationB); - assertEq(sRegistry.head(writer), migrationA); - } - - /// A zero head never matches anything, including on a namespace that has - /// applied nothing — which is the whole reason genesis is not zero. An - /// uninitialised predecessor constant is a revert in every namespace state, - /// rather than a successful first application on every chain that happens - /// to be empty. - function testApplyMigrationZeroHeadRevertsOnEmptyNamespace(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, bytes32(0), MIGRATION_HEAD_GENESIS - ) - ); - vm.prank(writer); - sRegistry.applyMigration(bytes32(0), migration, block.timestamp); - - assertEq(sRegistry.applied(writer, migration), 0); - } - - /// And on a namespace that has applied something. - function testApplyMigrationZeroHeadRevertsOnUsedNamespace(address writer, bytes32 migrationA, bytes32 migrationB) - external - { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, bytes32(0), migrationA - ) - ); - vm.prank(writer); - sRegistry.applyMigration(bytes32(0), migrationB, block.timestamp); - } - - /// Applying twice is refused. This is what makes running a migration twice - /// fail rather than repeat: a re-dispatched script cannot quietly apply - /// its way to looking like a first run. - function testApplyMigrationTwiceReverts(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migration) - ); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(sRegistry.applied(writer, migration), block.timestamp); - } - - /// The head does NOT subsume the already-applied refusal. Re-applying a - /// migration whose successor has since landed presents a head that matches - /// perfectly, and is still refused — otherwise the head would move BACKWARDS - /// and the original moment would be overwritten, which is a record - /// un-happening. - function testApplyMigrationAgainOnMatchingHeadReverts(address writer, bytes32 migrationA, bytes32 migrationB) - external - { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.warp(1000); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, 1000); - - // The namespace really is at `migrationB`, so the head this names is - // correct and only the already-applied refusal can stop it. - assertEq(sRegistry.head(writer), migrationB); - vm.warp(2000); - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migrationA) - ); - vm.prank(writer); - sRegistry.applyMigration(migrationB, migrationA, 2000); - - assertEq(sRegistry.head(writer), migrationB); - assertEq(sRegistry.applied(writer, migrationA), 1000); - } - - /// The already-applied refusal is checked BEFORE the head, so a - /// re-dispatched script — which names the same head it named the first time, - /// long since moved on — is told that its migration already ran rather than - /// told the namespace is somewhere else and left to work out why. - function testApplyMigrationAlreadyAppliedCheckedBeforeHead(address writer, bytes32 migrationA, bytes32 migrationB) - external - { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, block.timestamp); - - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migrationA) - ); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - } - - /// A migration another writer has already applied is still a FIRST record - /// for this one. The refusal is per namespace, not global, or one consumer - /// choosing a common id would lock every other consumer out of it. - function testApplyMigrationTwiceIsPerWriter(address writer, address other, bytes32 migration) external { - vm.assume(writer != address(0)); - vm.assume(other != address(0)); - vm.assume(writer != other); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - vm.prank(other); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(sRegistry.applied(other, migration), block.timestamp); - } - - /// The zero migration id is refused. It is what an uninitialised `bytes32` - /// constant reads as, and there is deliberately no way to apply one, which - /// is what lets `applied` refuse it as a mistake rather than have to answer - /// about it. - function testApplyMigrationZeroMigrationReverts(address writer) external { - vm.assume(writer != address(0)); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, bytes32(0), block.timestamp); - } - - /// The zero id is refused BEFORE the already-applied read and before the - /// head, so it is always reported as `ZeroMigration` and never as anything - /// about where the namespace is. - /// - /// Fuzzed over the head against BOTH an empty namespace and one that has - /// moved on, for the same reason - /// `testApplyMigrationGenesisMigrationRevertsOnAnyHead` is: one namespace - /// state cannot tell the orderings apart, because a head the namespace - /// happens to be at is accepted whichever check runs first, and the two - /// states here have different heads so no fuzzed head matches both. - /// - /// The already-applied read can never answer anything but zero for this id - /// — this refusal is what keeps the zero id out of the records in the first - /// place — so what the second call pins is the reachable half of the same - /// claim: the refusal is a fact about the ID, not about the state of the - /// namespace it arrives at. - function testApplyMigrationZeroMigrationCheckedFirst(address writer, bytes32 anyHead, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(anyHead, bytes32(0), block.timestamp); - - // A namespace that has moved on: the zero id is still reported as - // `ZeroMigration` rather than as anything about the head or about what - // has already been applied. - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(anyHead, bytes32(0), block.timestamp); - } - - /// Genesis is a head, not a migration, and applying it is refused. It would - /// otherwise leave the namespace's head holding the exact value an empty - /// namespace reads as, so a namespace that had applied something would be - /// indistinguishable from one that had not — and the next first-migration - /// script would be accepted against it. - function testApplyMigrationGenesisMigrationReverts(address writer) external { - vm.assume(writer != address(0)); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_HEAD_GENESIS, block.timestamp); - - assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); - } - - /// Refused whatever head it is applied onto, so it is a fact about the id - /// rather than about where the namespace happens to be. That means a head - /// the namespace is NOT at as much as one it is: the refusal is checked - /// before the head, so a caller that has confused a head for a migration is - /// told which of the two it got wrong rather than sent to look at where the - /// namespace has got to. - /// - /// Fuzzed over the head for the same reason - /// `testApplyMigrationZeroMigrationCheckedFirst` is: a matching head alone - /// cannot tell the two orderings apart. - function testApplyMigrationGenesisMigrationRevertsOnAnyHead(address writer, bytes32 migration, bytes32 anyHead) - external - { - vm.assume(writer != address(0)); - assumeMigration(migration); - - // An empty namespace, whose head is genesis: still refused onto a head - // that does not match it. - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS, block.timestamp); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - // A namespace that has moved: same refusal, onto the head it is at and - // onto any other. - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(migration, MIGRATION_HEAD_GENESIS, block.timestamp); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS, block.timestamp); - - assertEq(sRegistry.head(writer), migration); - } - - /// Ids are opaque: nothing about a migration's bytes changes how it is - /// stored or read, including ids no hashing convention would produce. - function testApplyMigrationOpaqueMigrationIds(address writer) external { - vm.assume(writer != address(0)); - - bytes32[2] memory migrations = [bytes32(uint256(1)), bytes32(type(uint256).max)]; - for (uint256 i = 0; i < migrations.length; i++) { - MigrationRegistryV2 registry = new MigrationRegistryV2(); - vm.prank(writer); - registry.applyMigration(MIGRATION_HEAD_GENESIS, migrations[i], block.timestamp); - assertEq(registry.applied(writer, migrations[i]), block.timestamp); - assertEq(registry.head(writer), migrations[i]); - } - } - - /// `Migrated` is emitted with the writer and migration both indexed, so the - /// log can be filtered by either, and carries the moment as data. The log is - /// the only enumeration of the registry, so a record that does not emit is a - /// record nobody can find. - /// - /// It carries no head because the log already holds it: one writer's entries - /// in order ARE its chain of heads. It does carry the moment, which the - /// block a log entry sits in does not — that block says when the record was - /// written, and the moment says when the migration ran. - function testApplyMigrationEvent(address writer, bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(appliedAt != 0); - vm.assume(writtenAt > appliedAt); - vm.warp(writtenAt); - - vm.recordLogs(); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); - Vm.Log[] memory entries = vm.getRecordedLogs(); - - assertEq(entries.length, 1); - assertEq(entries[0].emitter, address(sRegistry)); - assertEq(entries[0].topics.length, 3); - assertEq(entries[0].topics[0], keccak256("Migrated(address,bytes32,uint256)")); - assertEq(entries[0].topics[1], bytes32(uint256(uint160(writer)))); - assertEq(entries[0].topics[2], migration); - assertEq(entries[0].data, abi.encode(uint256(appliedAt))); - } - - /// A refused `applyMigration` emits nothing, so a failed apply can never be - /// mistaken for a record by anything reading the logs — which for a - /// re-dispatched migration is exactly the mistake that matters. - function testApplyMigrationNoEventOnRevert(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - vm.warp(9000); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 5000); - - vm.recordLogs(); - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, writer, migration) - ); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 5000); - assertEq(vm.getRecordedLogs().length, 0); - - vm.recordLogs(); - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(migration, bytes32(0), 5000); - assertEq(vm.getRecordedLogs().length, 0); - - vm.recordLogs(); - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - vm.prank(writer); - sRegistry.applyMigration(migration, MIGRATION_HEAD_GENESIS, 5000); - assertEq(vm.getRecordedLogs().length, 0); - - vm.recordLogs(); - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); - vm.prank(writer); - sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), 0); - assertEq(vm.getRecordedLogs().length, 0); - - vm.recordLogs(); - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, MIGRATION_HEAD_GENESIS, migration - ) - ); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, keccak256(abi.encode(migration)), 5000); - assertEq(vm.getRecordedLogs().length, 0); - - vm.recordLogs(); - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.TimestampBeforeHead.selector, writer, migration, uint256(4999), uint256(5000) - ) - ); - vm.prank(writer); - sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), 4999); - assertEq(vm.getRecordedLogs().length, 0); - - vm.recordLogs(); - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, uint256(9001), uint256(9000)) - ); - vm.prank(writer); - sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), 9001); - assertEq(vm.getRecordedLogs().length, 0); - } -} diff --git a/test/src/concrete/MigrationRegistryV2Head.t.sol b/test/src/concrete/MigrationRegistryV2Head.t.sol deleted file mode 100644 index a79b086..0000000 --- a/test/src/concrete/MigrationRegistryV2Head.t.sol +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {Test} from "forge-std-1.16.1/src/Test.sol"; - -import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV2.sol"; -import {MigrationRegistryV2} from "../../../src/concrete/MigrationRegistryV2.sol"; - -/// @title MigrationRegistryV2HeadTest -/// @notice A test suite for `MigrationRegistryV2.head`: where a namespace is, -/// what an empty one answers, that the answer is never a value that is not a -/// head, and that it is the same answer `applyMigration` checks against. -contract MigrationRegistryV2HeadTest is Test { - /// The registry under test. Stateful, so a fresh one per test. - MigrationRegistryV2 internal sRegistry; - - function setUp() external { - sRegistry = new MigrationRegistryV2(); - } - - /// A migration id that is neither of the two values the head space reserves. - /// @param migration The fuzzed candidate. - function assumeMigration(bytes32 migration) internal pure { - vm.assume(migration != bytes32(0)); - vm.assume(migration != MIGRATION_HEAD_GENESIS); - } - - /// A namespace that has applied nothing is at genesis, which is an ANSWER - /// rather than a revert for the same reason an unapplied migration answers - /// zero: it is the ordinary state of every namespace before its first - /// migration, and of every namespace on a chain that never got one. - function testHeadEmptyNamespaceIsGenesis(address writer) external view { - vm.assume(writer != address(0)); - - assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); - } - - /// Genesis is deliberately not zero, so an uninitialised predecessor - /// constant can never be mistaken for "the start of the sequence" — which - /// is the mistake that would otherwise pass on every chain that has not been - /// migrated yet. - function testHeadGenesisIsNotZero() external pure { - assertTrue(MIGRATION_HEAD_GENESIS != bytes32(0)); - } - - /// Genesis holds no record, which is what makes an empty namespace's floor - /// zero: `applyMigration` reads the record of whatever head it is applying - /// onto, and for a namespace that has applied nothing that read is of a key - /// nothing can ever be written against. - function testHeadGenesisHoldsNoRecord(address writer, bytes32 migration, uint32 appliedAt) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - vm.assume(appliedAt != 0); - vm.warp(appliedAt); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - sRegistry.applied(writer, MIGRATION_HEAD_GENESIS); - } - - /// The head is the migration applied most recently, and it moves with each - /// one. - function testHeadFollowsTheRecords(address writer, bytes32 migrationA, bytes32 migrationB) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - assertEq(sRegistry.head(writer), migrationA); - - vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, block.timestamp); - assertEq(sRegistry.head(writer), migrationB); - } - - /// Every writer has its own head, and one namespace's records leave every - /// other namespace exactly where it was. - function testHeadIsPerWriter(address writer, address other, bytes32 migration) external { - vm.assume(writer != address(0)); - vm.assume(other != address(0)); - vm.assume(writer != other); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(sRegistry.head(writer), migration); - assertEq(sRegistry.head(other), MIGRATION_HEAD_GENESIS); - } - - /// The head `head` reports is exactly the head `applyMigration` demands: - /// whatever this answers is accepted, and it is the only value that is. The - /// two go through one translation of an empty namespace, so they cannot - /// disagree about where one is. - function testHeadIsWhatApplyMigrationAccepts(address writer, bytes32 migrationA, bytes32 migrationB) external { - vm.assume(writer != address(0)); - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - - // Hoisted, because `vm.prank` applies to the next call and reading the - // head is a call. - bytes32 headBeforeA = sRegistry.head(writer); - vm.prank(writer); - sRegistry.applyMigration(headBeforeA, migrationA, block.timestamp); - - bytes32 headBeforeB = sRegistry.head(writer); - vm.prank(writer); - sRegistry.applyMigration(headBeforeB, migrationB, block.timestamp); - - assertEq(sRegistry.head(writer), migrationB); - assertEq(sRegistry.applied(writer, migrationA), block.timestamp); - assertEq(sRegistry.applied(writer, migrationB), block.timestamp); - } - - /// The zero namespace is refused rather than answered genesis. It is - /// provably empty forever, so "nothing has been applied here" is true of it - /// and false of whatever the caller meant to ask about — and a caller that - /// believed it would send a first migration at it. - function testHeadZeroWriterReverts() external { - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); - sRegistry.head(address(0)); - } - - /// Refused on a registry holding records exactly as on an empty one, and - /// the refusal changes nothing. - function testHeadZeroWriterRevertsWithRecordsPresent(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); - sRegistry.head(address(0)); - - assertEq(sRegistry.head(writer), migration); - } - - /// A head is never zero, in any namespace state, which is what lets a - /// consumer treat a zero answer as "this is not the registry" rather than as - /// a namespace. - function testHeadIsNeverZero(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - - assertTrue(sRegistry.head(writer) != bytes32(0)); - - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertTrue(sRegistry.head(writer) != bytes32(0)); - } - - /// A refused apply leaves the head where it was. The head moves only for a - /// migration that was actually applied, so it can never describe a step - /// that did not happen. - function testHeadUnmovedByRefusedApplyMigration(address writer, bytes32 migration, bytes32 wrongHead) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - assumeMigration(wrongHead); - vm.assume(wrongHead != migration); - - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, writer, wrongHead, MIGRATION_HEAD_GENESIS - ) - ); - vm.prank(writer); - sRegistry.applyMigration(wrongHead, migration, block.timestamp); - - assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); - } -} diff --git a/test/src/lib/LibMigrationRegistry.t.sol b/test/src/lib/LibMigrationRegistry.t.sol index 41bb030..40060db 100644 --- a/test/src/lib/LibMigrationRegistry.t.sol +++ b/test/src/lib/LibMigrationRegistry.t.sol @@ -62,6 +62,15 @@ contract LibMigrationRegistryTest is Test { return LibMigrationRegistry.applied(writer, migration); } + /// External wrapper for `appliedOnto` so that `vm.expectRevert` works at + /// the correct call depth. + /// @param writer The namespace to read. + /// @param migration The migration to ask about. + /// @return What `writer` applied `migration` onto, or zero. + function externalAppliedOnto(address writer, bytes32 migration) external view returns (bytes32) { + return LibMigrationRegistry.appliedOnto(writer, migration); + } + /// External wrapper for `head` so that `vm.expectRevert` works at the /// correct call depth. /// @param writer The namespace to read. @@ -78,6 +87,15 @@ contract LibMigrationRegistryTest is Test { LibMigrationRegistry.applyMigration(expectedHead, migration); } + /// External wrapper for the `appliedAt` form of `applyMigration` so that + /// `vm.expectRevert` works at the correct call depth. + /// @param expectedHead The head this contract believes it is at. + /// @param migration The migration to apply. + /// @param appliedAt The moment to record against it. + function externalApplyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + LibMigrationRegistry.applyMigration(expectedHead, migration, appliedAt); + } + /// The Zoltu deploy really does land the registry on its pinned address /// with its pinned code hash. Every other test here depends on that, and a /// pin that had gone stale would otherwise show up as an unrelated @@ -431,4 +449,252 @@ contract LibMigrationRegistryTest is Test { ); this.externalApplyMigration(expectedHead, migration); } + + /// A migration recorded with a supplied moment reads back as that moment + /// through the library, so what the `appliedAt` form writes is what + /// `applied` finds — and it is not the block the write landed in. + function testApplyMigrationWithAppliedAtThenApplied(bytes32 migration, uint32 appliedAt, uint32 writtenAt) + external + { + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt > appliedAt); + deployRegistry(); + vm.warp(writtenAt); + + LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(LibMigrationRegistry.applied(address(this), migration), appliedAt); + } + + /// A namespace backfilled in head order keeps every moment it was given, and + /// the chain reads back as the order it was applied in. This is a consumer + /// whose migrations ran before this registry reached the chain recording + /// what actually happened rather than the day it got round to writing it + /// down. + function testApplyMigrationBackfillsAHistoricalSequence(bytes32 migrationA, bytes32 migrationB, bytes32 migrationC) + external + { + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, migrationC); + vm.assume(migrationA != migrationB); + vm.assume(migrationB != migrationC); + vm.assume(migrationA != migrationC); + deployRegistry(); + vm.warp(9000); + + LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); + LibMigrationRegistry.applyMigration(migrationA, migrationB, 2000); + LibMigrationRegistry.applyMigration(migrationB, migrationC, 3000); + + assertEq(LibMigrationRegistry.applied(address(this), migrationA), 1000); + assertEq(LibMigrationRegistry.applied(address(this), migrationB), 2000); + assertEq(LibMigrationRegistry.applied(address(this), migrationC), 3000); + assertEq(LibMigrationRegistry.head(address(this)), migrationC); + + bytes32 cursor = LibMigrationRegistry.head(address(this)); + cursor = LibMigrationRegistry.appliedOnto(address(this), cursor); + assertEq(cursor, migrationB); + cursor = LibMigrationRegistry.appliedOnto(address(this), cursor); + assertEq(cursor, migrationA); + cursor = LibMigrationRegistry.appliedOnto(address(this), cursor); + assertEq(cursor, MIGRATION_HEAD_GENESIS); + } + + /// The registry's zero-moment refusal arrives unmodified through + /// `applyMigration`, so a consumer that left its `appliedAt` uninitialised + /// is told so rather than writing a record that reads back as none. + function testApplyMigrationZeroTimestampReverts(bytes32 migration) external { + LibMigrationFuzz.assumeMigration(vm, migration); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + } + + /// The registry's future-moment refusal arrives unmodified through + /// `applyMigration`. + function testApplyMigrationFutureTimestampReverts(bytes32 migration, uint32 now_) external { + LibMigrationFuzz.assumeMigration(vm, migration); + deployRegistry(); + vm.warp(now_); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) + ); + this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); + } + + /// The namespace of the `appliedAt` form is the calling CONTRACT too, so a + /// consumer backfilling its history writes its own namespace and nobody + /// else's. + function testApplyMigrationWithAppliedAtLandsUnderTheCallingContract(bytes32 migration, uint32 appliedAt) + external + { + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + deployRegistry(); + vm.warp(appliedAt); + MockMigrationApplier applier = new MockMigrationApplier(); + + applier.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(LibMigrationRegistry.applied(address(applier), migration), appliedAt); + assertEq(LibMigrationRegistry.appliedOnto(address(applier), migration), MIGRATION_HEAD_GENESIS); + assertEq(LibMigrationRegistry.applied(address(this), migration), 0); + } + + /// An unapplied migration answers a zero head, which is the same "no record" + /// answer `applied` gives as a zero moment. + function testAppliedOntoUnappliedIsZero(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + deployRegistry(); + + assertEq(LibMigrationRegistry.appliedOnto(writer, migration), bytes32(0)); + } + + /// The registry's zero-writer refusal arrives unmodified through + /// `appliedOnto`. + function testAppliedOntoZeroWriterReverts(bytes32 migration) external { + LibMigrationFuzz.assumeMigration(vm, migration); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + this.externalAppliedOnto(address(0), migration); + } + + /// The registry's zero-id refusal arrives unmodified through `appliedOnto`. + function testAppliedOntoZeroMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); + this.externalAppliedOnto(writer, bytes32(0)); + } + + /// The registry's genesis-id refusal arrives unmodified through + /// `appliedOnto`. + function testAppliedOntoGenesisMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + this.externalAppliedOnto(writer, MIGRATION_HEAD_GENESIS); + } + + /// Reading a chain step off a chain with no registry is refused by the code + /// hash, with the same named error as every other read. + function testAppliedOntoNoRegistry(address writer, bytes32 migration) external { + assertEq(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS.code.length, 0); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + bytes32(0) + ) + ); + this.externalAppliedOnto(writer, migration); + } + + /// Nor is a chain step read out of ordinary occupying code, which is free to + /// answer a head that was never applied and send a walk anywhere it likes. + function testAppliedOntoWrongCode(address writer, bytes32 migration, bytes memory code) external { + assumeOrdinaryCode(code); + vm.assume(keccak256(code) != LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH); + vm.etch(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS, code); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + keccak256(code) + ) + ); + this.externalAppliedOnto(writer, migration); + } + + /// Nor out of a delegated account. + /// @param writer The namespace a reader would ask about. + /// @param migration The migration a reader would ask about. + /// @param delegate The account the registry address is delegated to. + function testAppliedOntoDelegatedCode(address writer, bytes32 migration, address delegate) external { + bytes memory designator = assumedDesignator(delegate); + + vm.etch(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS, designator); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + keccak256(designator) + ) + ); + this.externalAppliedOnto(writer, migration); + } + + /// The `appliedAt` form checks the code hash too, so a backfill is never + /// written to a chain with no registry. + function testApplyMigrationWithAppliedAtNoRegistry(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) + external + { + assertEq(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS.code.length, 0); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + bytes32(0) + ) + ); + this.externalApplyMigration(expectedHead, migration, appliedAt); + } + + /// Nor into ordinary occupying code. + function testApplyMigrationWithAppliedAtWrongCode( + bytes32 expectedHead, + bytes32 migration, + uint256 appliedAt, + bytes memory code + ) external { + assumeOrdinaryCode(code); + vm.assume(keccak256(code) != LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH); + vm.etch(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS, code); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + keccak256(code) + ) + ); + this.externalApplyMigration(expectedHead, migration, appliedAt); + } + + /// Nor into a delegated account. + /// @param expectedHead The head the writer believes it is at. + /// @param migration The migration being applied. + /// @param appliedAt The moment being recorded. + /// @param delegate The account the registry address is delegated to. + function testApplyMigrationWithAppliedAtDelegatedCode( + bytes32 expectedHead, + bytes32 migration, + uint256 appliedAt, + address delegate + ) external { + bytes memory designator = assumedDesignator(delegate); + + vm.etch(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS, designator); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + keccak256(designator) + ) + ); + this.externalApplyMigration(expectedHead, migration, appliedAt); + } } diff --git a/test/src/lib/LibMigrationRegistryV2.t.sol b/test/src/lib/LibMigrationRegistryV2.t.sol deleted file mode 100644 index 8eddb3a..0000000 --- a/test/src/lib/LibMigrationRegistryV2.t.sol +++ /dev/null @@ -1,531 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibMigrationRegistryDeploy} from "../../../src/lib/LibMigrationRegistryDeploy.sol"; -import {LibMigrationRegistryV2} from "../../../src/lib/LibMigrationRegistryV2.sol"; -import {LibMigrationRegistryV2Deploy} from "../../../src/lib/LibMigrationRegistryV2Deploy.sol"; -import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; -import {IMigrationRegistryV2, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV2.sol"; -import {MigrationRegistryV2} from "../../../src/concrete/MigrationRegistryV2.sol"; -import {MockMigrationApplierV2} from "../../concrete/MockMigrationApplierV2.sol"; -import {DELEGATION_DESIGNATOR_LENGTH, LibAccountCode} from "../../lib/LibAccountCode.sol"; - -/// @title LibMigrationRegistryV2Test -/// Tests for `LibMigrationRegistryV2`. The registry is not mocked: the real -/// `MigrationRegistryV2` is deployed through the Zoltu factory, which is what -/// puts it at the pinned address with the pinned code hash, so every test runs -/// against the same bytecode a network would. -/// -/// External wrappers are used for the library functions so `vm.expectRevert` -/// lands at the correct call depth. -contract LibMigrationRegistryV2Test is Test { - /// Deploys `MigrationRegistryV2` through the Zoltu factory, which lands it - /// at the pinned address. - /// @return The deployed registry. - function deployRegistry() internal returns (IMigrationRegistryV2) { - LibRainDeploy.etchZoltuFactory(vm); - return IMigrationRegistryV2(LibRainDeploy.deployZoltu(type(MigrationRegistryV2).creationCode)); - } - - /// A migration id that is neither of the two values the head space reserves. - /// @param migration The fuzzed candidate. - function assumeMigration(bytes32 migration) internal pure { - vm.assume(migration != bytes32(0)); - vm.assume(migration != MIGRATION_HEAD_GENESIS); - } - - /// Occupant code that is ORDINARY contract code rather than a delegation - /// designator, which is the kind the three `WrongCode` cases fuzz. - /// - /// `LibAccountCode` is what says why the split exists. The designator kind - /// is covered on its own by the three `DelegatedCode` cases, because an - /// account holding one executes the delegate's code while carrying 23 bytes - /// of its own — a shape no amount of fuzzing `bytes` can construct, and one - /// `vm.etch` refuses at every length but that. - /// @param code The fuzzed candidate. - function assumeOrdinaryCode(bytes memory code) internal pure { - vm.assume(code.length > 0); - vm.assume(!LibAccountCode.hasDelegationPrefix(code)); - } - - /// The designator that delegates the registry address to `delegate`, - /// refusing the clearing form — a delegation to zero leaves the account - /// empty, which is what the `NoRegistry` cases already cover. - /// @param delegate The fuzzed delegate. - /// @return designator The 23-byte designator. - function assumedDesignator(address delegate) internal pure returns (bytes memory designator) { - vm.assume(delegate != address(0)); - designator = LibAccountCode.delegationDesignator(delegate); - } - - /// External wrapper for `applied` so that `vm.expectRevert` works at the - /// correct call depth. - /// @param writer The namespace to read. - /// @param migration The migration to ask about. - /// @return When `writer` applied `migration`, or zero. - function externalApplied(address writer, bytes32 migration) external view returns (uint256) { - return LibMigrationRegistryV2.applied(writer, migration); - } - - /// External wrapper for `head` so that `vm.expectRevert` works at the - /// correct call depth. - /// @param writer The namespace to read. - /// @return The head of that namespace. - function externalHead(address writer) external view returns (bytes32) { - return LibMigrationRegistryV2.head(writer); - } - - /// External wrapper for `applyMigration` so that `vm.expectRevert` works at - /// the correct call depth. - /// @param expectedHead The head this contract believes it is at. - /// @param migration The migration to apply. - /// @param appliedAt The moment the migration was applied. - function externalApplyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { - LibMigrationRegistryV2.applyMigration(expectedHead, migration, appliedAt); - } - - /// The Zoltu deploy really does land the registry on its pinned address - /// with its pinned code hash. Every other test here depends on that, and a - /// pin that had gone stale would otherwise show up as an unrelated - /// code-hash revert in all of them. - function testDeployMatchesPins() external { - deployRegistry(); - - assertEq( - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH - ); - } - - /// This registry is a separate deployment from the one `LibMigrationRegistry` - /// reads, at its own address, so a consumer's namespace under one says - /// nothing about its namespace under the other. Two creation codes are two - /// addresses, which is what makes the two coexist on one chain. - function testDeployAddressIsItsOwn() external pure { - assertTrue( - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS - != LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS - ); - } - - /// An unapplied migration answers zero. This is the branch a caller - /// asserts the pre-migration state in, and it is the ordinary state of - /// every migration that has not run, so it is an answer rather than a - /// revert. - function testAppliedUnappliedIsZero(address writer, bytes32 migration) external { - vm.assume(writer != address(0)); - assumeMigration(migration); - deployRegistry(); - - assertEq(LibMigrationRegistryV2.applied(writer, migration), 0); - } - - /// An applied migration answers the moment it was given — read back through - /// the library, so what `applyMigration` writes is what `applied` finds, and - /// the moment survives the round trip rather than being replaced by the - /// block the write landed in. - function testApplyMigrationThenApplied(bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { - assumeMigration(migration); - vm.assume(appliedAt != 0); - vm.assume(writtenAt >= appliedAt); - deployRegistry(); - vm.warp(writtenAt); - - LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); - - assertEq(LibMigrationRegistryV2.applied(address(this), migration), appliedAt); - } - - /// A namespace that has applied nothing reads back as genesis, and each - /// record moves the head to itself. This is the value the next migration - /// has to name, so it is read through the library rather than assumed. - function testHeadFollowsTheRecords(bytes32 migrationA, bytes32 migrationB) external { - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - deployRegistry(); - - assertEq(LibMigrationRegistryV2.head(address(this)), MIGRATION_HEAD_GENESIS); - - LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); - assertEq(LibMigrationRegistryV2.head(address(this)), migrationA); - - LibMigrationRegistryV2.applyMigration(migrationA, migrationB, block.timestamp); - assertEq(LibMigrationRegistryV2.head(address(this)), migrationB); - } - - /// A migration applied onto a head this namespace is not at is refused, and - /// the registry's own revert arrives unmodified. This is a skipped step - /// failing at the moment of applying rather than a chain quietly diverging. - function testApplyMigrationSkippedPredecessorReverts(bytes32 migration, bytes32 skipped) external { - assumeMigration(migration); - assumeMigration(skipped); - deployRegistry(); - - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.UnexpectedMigrationHead.selector, address(this), skipped, MIGRATION_HEAD_GENESIS - ) - ); - this.externalApplyMigration(skipped, migration, block.timestamp); - - assertEq(LibMigrationRegistryV2.applied(address(this), migration), 0); - } - - /// The namespace is the CONTRACT that executes the library call. The - /// library's functions are `internal`, so they inline into their caller and - /// the registry sees that caller as `msg.sender` — which means a consumer - /// chooses its namespace by choosing what sends the transaction, and cannot - /// write anybody else's. - function testApplyMigrationLandsUnderTheCallingContract(bytes32 migration) external { - assumeMigration(migration); - deployRegistry(); - MockMigrationApplierV2 applier = new MockMigrationApplierV2(); - - applier.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(LibMigrationRegistryV2.applied(address(applier), migration), block.timestamp); - assertEq(LibMigrationRegistryV2.applied(address(this), migration), 0); - } - - /// One caller's record reaches no other namespace, and each answers only - /// for itself — heads included, so one consumer's sequence neither blocks - /// nor unblocks another's. This is the whole of the access control: a - /// reader's choice of writer is the whole of who it trusts. - function testApplyMigrationDoesNotReachAnotherNamespace(bytes32 migration) external { - assumeMigration(migration); - deployRegistry(); - MockMigrationApplierV2 applier = new MockMigrationApplierV2(); - MockMigrationApplierV2 other = new MockMigrationApplierV2(); - - applier.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - assertEq(other.applied(address(applier), migration), block.timestamp); - assertEq(other.applied(address(other), migration), 0); - assertEq(other.head(address(applier)), migration); - assertEq(other.head(address(other)), MIGRATION_HEAD_GENESIS); - } - - /// Applying the same migration twice is refused, and the registry's own - /// revert arrives unmodified — the library adds no handling of its own, so - /// a re-dispatched migration fails naming the writer and the id. - function testApplyMigrationTwiceReverts(bytes32 migration) external { - assumeMigration(migration); - deployRegistry(); - - LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.MigrationAlreadyApplied.selector, address(this), migration) - ); - this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); - } - - /// The registry's zero-id refusal arrives unmodified through - /// `applyMigration`. - function testApplyMigrationZeroMigrationReverts() external { - deployRegistry(); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - this.externalApplyMigration(MIGRATION_HEAD_GENESIS, bytes32(0), block.timestamp); - } - - /// The registry's genesis-id refusal arrives unmodified through - /// `applyMigration`. - function testApplyMigrationGenesisMigrationReverts() external { - deployRegistry(); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - this.externalApplyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_HEAD_GENESIS, block.timestamp); - } - - /// The registry's zero-moment refusal arrives unmodified through - /// `applyMigration`, so a consumer that left its `appliedAt` uninitialised - /// is told so rather than writing a record that reads back as none. - function testApplyMigrationZeroTimestampReverts(bytes32 migration) external { - assumeMigration(migration); - deployRegistry(); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroTimestamp.selector)); - this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, 0); - } - - /// The registry's future-moment refusal arrives unmodified through - /// `applyMigration`. - function testApplyMigrationFutureTimestampReverts(bytes32 migration, uint32 now_) external { - assumeMigration(migration); - deployRegistry(); - vm.warp(now_); - - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV2.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) - ); - this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); - } - - /// The registry's before-the-head refusal arrives unmodified through - /// `applyMigration`. - function testApplyMigrationTimestampBeforeHeadReverts(bytes32 migrationA, bytes32 migrationB) external { - assumeMigration(migrationA); - assumeMigration(migrationB); - vm.assume(migrationA != migrationB); - deployRegistry(); - vm.warp(9000); - - LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 5000); - - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV2.TimestampBeforeHead.selector, - address(this), - migrationA, - uint256(4999), - uint256(5000) - ) - ); - this.externalApplyMigration(migrationA, migrationB, 4999); - } - - /// A namespace backfilled in head order keeps every moment it was given, so - /// a consumer whose migrations ran before this registry reached the chain - /// records what actually happened rather than the day it got round to - /// writing it down. - function testApplyMigrationBackfillsAHistoricalSequence(bytes32 migrationA, bytes32 migrationB, bytes32 migrationC) - external - { - assumeMigration(migrationA); - assumeMigration(migrationB); - assumeMigration(migrationC); - vm.assume(migrationA != migrationB); - vm.assume(migrationB != migrationC); - vm.assume(migrationA != migrationC); - deployRegistry(); - vm.warp(9000); - - LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); - LibMigrationRegistryV2.applyMigration(migrationA, migrationB, 2000); - LibMigrationRegistryV2.applyMigration(migrationB, migrationC, 3000); - - assertEq(LibMigrationRegistryV2.applied(address(this), migrationA), 1000); - assertEq(LibMigrationRegistryV2.applied(address(this), migrationB), 2000); - assertEq(LibMigrationRegistryV2.applied(address(this), migrationC), 3000); - assertEq(LibMigrationRegistryV2.head(address(this)), migrationC); - } - - /// The registry's zero-writer refusal arrives unmodified through `applied`. - function testAppliedZeroWriterReverts(bytes32 migration) external { - assumeMigration(migration); - deployRegistry(); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); - this.externalApplied(address(0), migration); - } - - /// The registry's zero-id refusal arrives unmodified through `applied`. - function testAppliedZeroMigrationReverts(address writer) external { - vm.assume(writer != address(0)); - deployRegistry(); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroMigration.selector)); - this.externalApplied(writer, bytes32(0)); - } - - /// The registry's genesis-id refusal arrives unmodified through `applied`. - function testAppliedGenesisMigrationReverts(address writer) external { - vm.assume(writer != address(0)); - deployRegistry(); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.GenesisMigration.selector)); - this.externalApplied(writer, MIGRATION_HEAD_GENESIS); - } - - /// The registry's zero-writer refusal arrives unmodified through `head`. - function testHeadZeroWriterReverts() external { - deployRegistry(); - - vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV2.ZeroWriter.selector)); - this.externalHead(address(0)); - } - - /// A chain with no registry deployed reverts on the code hash rather than - /// calling into an empty account. That call would succeed and return - /// nothing, which `abi.decode` would read as zero — "this migration has - /// not been applied", on every chain the registry was never deployed to, - /// which is exactly the silent pre-migration branch this library exists to - /// make impossible. - function testAppliedNoRegistry(address writer, bytes32 migration) external { - assertEq(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.code.length, 0); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - bytes32(0) - ) - ); - this.externalApplied(writer, migration); - } - - /// Reading a head off a chain with no registry is refused for a sharper - /// version of the same reason: the empty-account read decodes as zero, and - /// zero is a value no head can ever hold, so an unverified read hands back - /// something that is not a head at all. - function testHeadNoRegistry(address writer) external { - assertEq(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.code.length, 0); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - bytes32(0) - ) - ); - this.externalHead(writer); - } - - /// Writing to a chain with no registry is refused for the mirror reason: an - /// `applyMigration` into an empty account is a migration that reports itself - /// applied and is not, which leaves every reader asserting the - /// pre-migration state forever. - function testApplyMigrationNoRegistry(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { - assertEq(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.code.length, 0); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - bytes32(0) - ) - ); - this.externalApplyMigration(expectedHead, migration, appliedAt); - } - - /// A chain where ordinary code other than the pinned registry occupies the - /// address reverts on the code hash, so a migration is never read from code - /// the caller did not compile against. - function testAppliedWrongCode(address writer, bytes32 migration, bytes memory code) external { - assumeOrdinaryCode(code); - vm.assume(keccak256(code) != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH); - vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, code); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - keccak256(code) - ) - ); - this.externalApplied(writer, migration); - } - - /// Nor is a head. - function testHeadWrongCode(address writer, bytes memory code) external { - assumeOrdinaryCode(code); - vm.assume(keccak256(code) != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH); - vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, code); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - keccak256(code) - ) - ); - this.externalHead(writer); - } - - /// And never applied into it either. - function testApplyMigrationWrongCode(bytes32 expectedHead, bytes32 migration, uint256 appliedAt, bytes memory code) - external - { - assumeOrdinaryCode(code); - vm.assume(keccak256(code) != LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH); - vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, code); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - keccak256(code) - ) - ); - this.externalApplyMigration(expectedHead, migration, appliedAt); - } - - /// A chain where an EOA has DELEGATED the registry address under EIP-7702 - /// is refused on the code hash exactly as ordinary wrong code is. This is - /// the other way an address gets occupied, and the worse one for a reader: - /// the account carries 23 bytes of designator while executing whatever the - /// delegate holds, so an address that looks like nothing at all can answer - /// `applied` with any timestamp it likes. - /// - /// The code hash refuses it without knowing anything about 7702 — a - /// delegated account hashes its designator and never the delegate's code, - /// so it can never present the pinned registry's hash. - /// @param writer The namespace a reader would ask about. - /// @param migration The migration a reader would ask about. - /// @param delegate The account the registry address is delegated to. - function testAppliedDelegatedCode(address writer, bytes32 migration, address delegate) external { - bytes memory designator = assumedDesignator(delegate); - assertEq(designator.length, DELEGATION_DESIGNATOR_LENGTH); - - vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, designator); - assertEq(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS.codehash, keccak256(designator)); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - keccak256(designator) - ) - ); - this.externalApplied(writer, migration); - } - - /// Nor is a head read out of a delegated account. - /// @param writer The namespace a reader would ask about. - /// @param delegate The account the registry address is delegated to. - function testHeadDelegatedCode(address writer, address delegate) external { - bytes memory designator = assumedDesignator(delegate); - - vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, designator); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - keccak256(designator) - ) - ); - this.externalHead(writer); - } - - /// And a migration is never applied into one. This is the write, so the - /// delegate would otherwise be handed a record the writer believes is in - /// the registry and every later reader asserts against. - /// @param expectedHead The head the writer believes it is at. - /// @param migration The migration being applied. - /// @param appliedAt The moment being recorded. - /// @param delegate The account the registry address is delegated to. - function testApplyMigrationDelegatedCode( - bytes32 expectedHead, - bytes32 migration, - uint256 appliedAt, - address delegate - ) external { - bytes memory designator = assumedDesignator(delegate); - - vm.etch(LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_ADDRESS, designator); - - vm.expectRevert( - abi.encodeWithSelector( - LibMigrationRegistryV2.UnexpectedMigrationRegistryV2CodeHash.selector, - LibMigrationRegistryV2Deploy.MIGRATION_REGISTRY_V2_DEPLOYED_CODEHASH, - keccak256(designator) - ) - ); - this.externalApplyMigration(expectedHead, migration, appliedAt); - } -} From 0c83b476fb11c7b835f4db8eb841ae75c8717b43 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 10:58:17 +0000 Subject: [PATCH 5/8] Let a writer record the moment a migration actually ran MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `applyMigration` gains a three-argument form taking `appliedAt`, for a migration that ran before this registry reached the chain. The two-argument form stays and passes `block.timestamp`. Both delegate to one internal function, so there is one record and one set of refusals. A record is now a `MigrationRecord` — the moment, and the head it was applied onto — written whole. `appliedOnto` reads the head back, so a namespace's records are a chain in storage from `head` down to `MIGRATION_HEAD_GENESIS`, and that chain is the order the migrations ran in. Two new refusals bound the moment beyond the existing `ZeroTimestamp`: `FutureTimestamp` for one after the block it is written in, and `TimestampBeforeHead` for one before the record it is chained onto. Equal is accepted at both. `Migrated` carries `appliedAt`. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 107 ++++---- src/concrete/MigrationRegistry.sol | 19 +- src/generated/candidate/MigrationRegistry.sol | 8 +- src/interface/IMigrationRegistryV1.sol | 47 +++- src/lib/LibMigrationRegistry.sol | 10 +- .../MigrationRegistryApplyMigration.t.sol | 238 +++++++++++++++--- test/src/lib/LibMigrationRegistry.t.sol | 20 +- 7 files changed, 343 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 5bdf5d8..7395a29 100644 --- a/README.md +++ b/README.md @@ -206,20 +206,19 @@ library supplies the fork loop and the comparison. ## Migration registry -`MigrationRegistry` and `MigrationRegistryV2` each record that a migration has -been applied, and when: a writer applies one of its own onto the migration it -believes ran last (`applyMigration`), anyone reads when a given writer applied a -given one (`applied`), and anyone reads where a given writer's sequence has got -to (`head`). There is no removal and no upgrade. - -They differ in exactly one thing, and everything below holds for both except -where it says otherwise: where the recorded moment comes from. -`MigrationRegistry` stamps the block the record lands in, so it can only say a -migration ran now. `MigrationRegistryV2` takes the moment as an argument, so a +`MigrationRegistry` records that a migration has been applied, when, and onto +what: a writer applies one of its own onto the migration it believes ran last +(`applyMigration`), anyone reads when a given writer applied a given one +(`applied`), what that writer applied it onto (`appliedOnto`), and where a given +writer's sequence has got to (`head`). There is no removal and no upgrade. + +`applyMigration` has two forms, differing in exactly one thing: where the +recorded moment comes from. The two-argument form stamps the block the record +lands in, for a script applying its own migration in the same atomic unit as the +migration itself. The three-argument form takes the moment as an argument, so a migration that ran before the registry reached the chain is recordable with the -time it actually ran. Two creation codes are two addresses and two deployments; -a consumer pins whichever one it reads, and a writer's namespace under one is a -different namespace from its namespace under the other. +time it actually ran. They write the same record, into the same namespace, and +make the same refusals. It exists because prod-state tests otherwise decide what to assert by reading the **clock**. The pattern that emerges without it is a dual-state invariant — @@ -251,14 +250,14 @@ with the nonzero case saying more — and zero stays unambiguous because a zero moment is refused outright rather than written as a record that reads back as no record. -**The moment is the caller's, inside a window `MigrationRegistryV2` enforces.** -The fact being recorded is that a migration RAN, and the moment it ran is not in -general the moment anybody gets to write it down. A registry that could only -stamp its own block offers a writer with history two options and no third: -record a time that is false for every past migration, or record nothing — and -recording nothing strands the namespace, because `applyMigration` refuses -anything not applied onto the current head, so a writer that skipped its past -migrations cannot record its next one either. +**The moment is the caller's, inside a window the registry enforces.** The fact +being recorded is that a migration RAN, and the moment it ran is not in general +the moment anybody gets to write it down. A registry that could only stamp its +own block offers a writer with history two options and no third: record a time +that is false for every past migration, or record nothing — and recording +nothing strands the namespace, because `applyMigration` refuses anything not +applied onto the current head, so a writer that skipped its past migrations +cannot record its next one either. What a reader gives up is **not** authenticity. A record is namespaced by the account that wrote it and no authority checks it, so every entry is already @@ -275,19 +274,23 @@ refusals: anything, and a consumer measuring an interval since the migration — a cliff, a grace period, a rate that changes a week later — can subtract it from the current block without underflowing. -- **Never before the record of the head it is applied onto** - (`TimestampBeforeHead`), so a namespace's records read in head order never go - backwards. - -Equal is allowed at both ends. Two migrations applied in one transaction share a -block, and two backfilled to the same day share a moment; the head chain is what -orders them, so forcing the moments apart would make them carry an ordering they -do not have. - -There is one way to write a record. A script recording a migration as it runs -passes `block.timestamp`, which is the same statement as any other `appliedAt` -and gets the same three refusals — so a second entry point could express nothing -the argument does not. +- **Never before the record it is applied onto** (`TimestampBeforeHead`), so a + namespace's moments never go backwards along its chain and the gap between two + of its migrations subtracts in chain order without underflowing either. The + first migration in a namespace is applied onto `MIGRATION_HEAD_GENESIS`, which + holds no record and so bounds nothing. + +Equal is allowed wherever there is a neighbour. A moment may be exactly the +block it is written in, and two records may carry the same moment: two +migrations applied in one transaction share a block, and two backfilled to the +same day share a moment, so forcing them apart would demand a precision the +moments do not have. Which of them ran first is the chain, not the moments — +total order comes from `appliedOnto`, and the bound above only stops a record +claiming to predate the one it is chained onto. + +Both entry points get all three, `block.timestamp` included: a block whose +timestamp is zero is `ZeroTimestamp` on the two-argument form, which a test that +warps to zero and a chain configured from a zero genesis both reach. **A set of applied migrations, not a high-water mark.** A mark needs a total order consumers do not have: two migrations authored on one day collide, and one @@ -305,14 +308,20 @@ diverging silently, and two migrations dispatched at once cannot land in the wrong order. ```solidity -// The first migration in a namespace. -LibMigrationRegistryV2.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_V1, block.timestamp); +// The first migration in a namespace, applied in this transaction. +LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_V1); // Every later one names its predecessor. -LibMigrationRegistryV2.applyMigration(MIGRATION_V1, MIGRATION_V2, block.timestamp); +LibMigrationRegistry.applyMigration(MIGRATION_V1, MIGRATION_V2); // One that ran before the registry reached this chain names the moment it ran. -LibMigrationRegistryV2.applyMigration(MIGRATION_V2, MIGRATION_V3, 1750000000); +LibMigrationRegistry.applyMigration(MIGRATION_V2, MIGRATION_V3, 1750000000); ``` +Each record also keeps the head it was applied onto, which `appliedOnto` reads +back, so a namespace is a chain in storage rather than a set of moments to sort: +from `head`, each answer names the record before it, down to +`MIGRATION_HEAD_GENESIS`. That chain is the order the migrations ran in whatever +moments the records carry. + Genesis is deliberately **not zero**. Zero is what an uninitialised `bytes32` constant reads as, and a zero genesis would make a mis-set predecessor constant a _successful_ first application on any namespace that happens to be empty — the @@ -347,13 +356,12 @@ say the invariant holds — a multisig can act out of band and nothing here move Keep both layers: this selects, codehash and bytecode pins verify. Replacing the pins with it trades a clock-guess for a bookkeeping-guess. -`LibMigrationRegistry` and `LibMigrationRegistryV2` are the surfaces — -`applied`, `head` and `applyMigration`, each verifying its own registry's code -hash first, and each reading only the deployment it pins. There is deliberately -**no broadcast runner**: the dominant real shape is a Safe executing a bundle -that never broadcasts, and such a script appends `applyMigration` to the bundle -it is already emitting, which makes the record atomic with the migration it -describes. +`LibMigrationRegistry` is the surface — `applied`, `appliedOnto`, `head` and +both forms of `applyMigration`, each verifying the registry's code hash before +it reads or writes. There is deliberately **no broadcast runner**: the dominant +real shape is a Safe executing a bundle that never broadcasts, and such a script +appends `applyMigration` to the bundle it is already emitting, which makes the +record atomic with the migration it describes. ## Deploying, and then releasing @@ -363,11 +371,10 @@ Three separate steps, in this order. Nothing automatic ever broadcasts. [`Manual sol artifacts`](.github/workflows/manual-sol-artifacts.yaml) workflow, choosing a `suite`. It runs `script/Deploy.sol` and broadcasts that suite to every network in `supportedNetworks()`. One suite per dispatch, so - this repo's three registries are three dispatches. `workflow_dispatch` only: - this is key custody and real money, and no merge or tag should be able to - trigger it. It is idempotent — a network that already has the code is skipped - — so a partial run is fixed by running it again rather than by unpicking - anything. + this repo's two registries are two dispatches. `workflow_dispatch` only: this + is key custody and real money, and no merge or tag should be able to trigger + it. It is idempotent — a network that already has the code is skipped — so a + partial run is fixed by running it again rather than by unpicking anything. 2. **Verify.** `RegistryDeployChainTest` passes only once every **released** suite is live on every supported network, with the code that release froze. This repo has released none, so today it has nothing to check and passes; it diff --git a/src/concrete/MigrationRegistry.sol b/src/concrete/MigrationRegistry.sol index f572d4e..5081460 100644 --- a/src/concrete/MigrationRegistry.sol +++ b/src/concrete/MigrationRegistry.sol @@ -59,7 +59,9 @@ struct MigrationRecord { /// ran before this contract reached the chain is recordable with the time it /// actually ran. The order is not the caller's: each record keeps the head it /// was applied onto, so a namespace's records are a chain from `head` back to -/// `MIGRATION_HEAD_GENESIS` whatever moments they carry. +/// `MIGRATION_HEAD_GENESIS` whatever moments they carry. The moments run with +/// that chain rather than against it — a record is never earlier than the one +/// it was applied onto, though it may be equal to it. /// /// Neither storage mapping is `public`. `applied`, `appliedOnto` and `head` /// refuse the zero writer, the two record readers refuse the two ids a @@ -149,6 +151,21 @@ contract MigrationRegistry is IMigrationRegistryV1 { if (expectedHead != actualHead) { revert UnexpectedMigrationHead(msg.sender, expectedHead, actualHead); } + // Last of the refusals about the namespace, because it is the only one + // that reads a RECORD rather than a key, and the record it reads is the + // one at the head the check above has just confirmed. + // + // At genesis there is nothing to be before. Genesis can never be + // applied, so the record at it is empty in every namespace forever and + // this comparison against zero can only pass — which is the same + // statement a genesis branch would make, made by the value itself. + // + // Neither the head nor its own moment is restated in the error: the + // caller named the head, and was told above if it named the wrong one. + uint256 headAppliedAt = sRecords[msg.sender][actualHead].appliedAt; + if (appliedAt < headAppliedAt) { + revert TimestampBeforeHead(appliedAt, headAppliedAt); + } // Last, because it is the only refusal here that time itself resolves: // every other one describes something wrong with the call or with where // the namespace is, and this one describes a moment that has not diff --git a/src/generated/candidate/MigrationRegistry.sol b/src/generated/candidate/MigrationRegistry.sol index 2e4f696..9b44db5 100644 --- a/src/generated/candidate/MigrationRegistry.sol +++ b/src/generated/candidate/MigrationRegistry.sol @@ -5,19 +5,19 @@ pragma solidity ^0.8.25; // THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x10624d7ac73d3b4e379fc0af77347144e32f78808fb51795ac7b2613d2f4df53); +bytes32 constant BYTECODE_HASH = bytes32(0xaa8bc7e0eab188014af2732a60e9109f17183b2623604c56557b7b8b65f9b0e4); /// @dev The deterministic deploy address of the contract when deployed via /// the Zoltu factory. -address constant DEPLOYED_ADDRESS = address(0x6E3aE74aDCd6CF28A1b2F685D5E709ffE44D429D); +address constant DEPLOYED_ADDRESS = address(0x0253603e9b8b2BDCd57d8A7D96b6c989C736D321); /// @dev The creation bytecode of the contract. bytes constant CREATION_CODE = - hex"6080604052348015600e575f80fd5b506104a18061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c8063a56d39e014610043578063bda4fec514610058578063e29304161461007d575b5f80fd5b610056610051366004610418565b610090565b005b61006b610066366004610460565b61025f565b60405190815260200160405180910390f35b61006b61008b366004610479565b610307565b806100c7576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610120576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f908152602081815260408083208484529091529020541561017d576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018290526044015b60405180910390fd5b5f6101873361025f565b90508083146101d2576040517facbe68520000000000000000000000000000000000000000000000000000000081523360048201526024810184905260448101829052606401610174565b425f0361020b576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f8181526020818152604080832086845282528083204290558383526001909152808220859055518492917f4b783664c1bcc23d06e2e5633252a3fce6e0d115df1940913aecf8082b893de191a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff82166102ad576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156102de5780610300565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f73ffffffffffffffffffffffffffffffffffffffff8316610355576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8161038c576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036103e5576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b5f8060408385031215610429575f80fd5b50508035926020909101359150565b803573ffffffffffffffffffffffffffffffffffffffff8116811461045b575f80fd5b919050565b5f60208284031215610470575f80fd5b61030082610438565b5f806040838503121561048a575f80fd5b61049383610438565b94602093909301359350505056"; + hex"6080604052348015600e575f80fd5b506106428061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610064575f3560e01c8063a56d39e01161004d578063a56d39e0146100a2578063bda4fec5146100b5578063e2930416146100c8575f80fd5b80635965d2a6146100685780638c3f2ba51461007d575b5f80fd5b61007b610076366004610590565b6100db565b005b61009061008b3660046105e1565b6100eb565b60405190815260200160405180910390f35b61007b6100b0366004610609565b61012c565b6100906100c3366004610629565b61013b565b6100906100d63660046105e1565b6101e3565b6100e6838383610221565b505050565b5f6100f683836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052206001015490565b610137828242610221565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610189576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156101ba57806101dc565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f6101ee83836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b81610258576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036102b1576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f036102ea576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f9081526020818152604080832085845290915290205415610347576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6103513361013b565b905080841461039c576040517facbe6852000000000000000000000000000000000000000000000000000000008152336004820152602481018590526044810182905260640161033e565b335f90815260208181526040808320848452909152902054808310156103f8576040517f29d1b347000000000000000000000000000000000000000000000000000000008152600481018490526024810182905260440161033e565b4283111561043b576040517f8fdce34f0000000000000000000000000000000000000000000000000000000081526004810184905242602482015260440161033e565b6040805180820182528481526020808201858152335f8181528084528581208a82528452858120945185559151600194850155808252928252839020879055915185815286927f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b73ffffffffffffffffffffffffffffffffffffffff8216610500576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80610537576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610137576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805f606084860312156105a2575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff811681146105dc575f80fd5b919050565b5f80604083850312156105f2575f80fd5b6105fb836105b9565b946020939093013593505050565b5f806040838503121561061a575f80fd5b50508035926020909101359150565b5f60208284031215610639575f80fd5b6101dc826105b956"; /// @dev The runtime bytecode of the contract. bytes constant RUNTIME_CODE = - hex"608060405234801561000f575f80fd5b506004361061003f575f3560e01c8063a56d39e014610043578063bda4fec514610058578063e29304161461007d575b5f80fd5b610056610051366004610418565b610090565b005b61006b610066366004610460565b61025f565b60405190815260200160405180910390f35b61006b61008b366004610479565b610307565b806100c7576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610120576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f908152602081815260408083208484529091529020541561017d576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018290526044015b60405180910390fd5b5f6101873361025f565b90508083146101d2576040517facbe68520000000000000000000000000000000000000000000000000000000081523360048201526024810184905260448101829052606401610174565b425f0361020b576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f8181526020818152604080832086845282528083204290558383526001909152808220859055518492917f4b783664c1bcc23d06e2e5633252a3fce6e0d115df1940913aecf8082b893de191a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff82166102ad576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156102de5780610300565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f73ffffffffffffffffffffffffffffffffffffffff8316610355576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8161038c576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036103e5576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b5f8060408385031215610429575f80fd5b50508035926020909101359150565b803573ffffffffffffffffffffffffffffffffffffffff8116811461045b575f80fd5b919050565b5f60208284031215610470575f80fd5b61030082610438565b5f806040838503121561048a575f80fd5b61049383610438565b94602093909301359350505056"; + hex"608060405234801561000f575f80fd5b5060043610610064575f3560e01c8063a56d39e01161004d578063a56d39e0146100a2578063bda4fec5146100b5578063e2930416146100c8575f80fd5b80635965d2a6146100685780638c3f2ba51461007d575b5f80fd5b61007b610076366004610590565b6100db565b005b61009061008b3660046105e1565b6100eb565b60405190815260200160405180910390f35b61007b6100b0366004610609565b61012c565b6100906100c3366004610629565b61013b565b6100906100d63660046105e1565b6101e3565b6100e6838383610221565b505050565b5f6100f683836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052206001015490565b610137828242610221565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610189576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156101ba57806101dc565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f6101ee83836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b81610258576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036102b1576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f036102ea576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f9081526020818152604080832085845290915290205415610347576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6103513361013b565b905080841461039c576040517facbe6852000000000000000000000000000000000000000000000000000000008152336004820152602481018590526044810182905260640161033e565b335f90815260208181526040808320848452909152902054808310156103f8576040517f29d1b347000000000000000000000000000000000000000000000000000000008152600481018490526024810182905260440161033e565b4283111561043b576040517f8fdce34f0000000000000000000000000000000000000000000000000000000081526004810184905242602482015260440161033e565b6040805180820182528481526020808201858152335f8181528084528581208a82528452858120945185559151600194850155808252928252839020879055915185815286927f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b73ffffffffffffffffffffffffffffffffffffffff8216610500576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80610537576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610137576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805f606084860312156105a2575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff811681146105dc575f80fd5b919050565b5f80604083850312156105f2575f80fd5b6105fb836105b9565b946020939093013593505050565b5f806040838503121561061a575f80fd5b50508035926020909101359150565b5f60208284031215610639575f80fd5b6101dc826105b956"; /// @dev The addresses that MUST already have code on a network before /// this release can be broadcast there, `abi.encode`d as an `address[]` diff --git a/src/interface/IMigrationRegistryV1.sol b/src/interface/IMigrationRegistryV1.sol index 9824e46..60ebcb8 100644 --- a/src/interface/IMigrationRegistryV1.sol +++ b/src/interface/IMigrationRegistryV1.sol @@ -74,7 +74,7 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// does not become proof of anything, for the same reason reading the record /// back does not. /// -/// ## A moment is data, bounded at both ends +/// ## A moment is data, and it is bounded /// /// A moment supplied by the caller is not authenticated, and nothing else in a /// record is either. A record is namespaced by the account that wrote it and no @@ -91,10 +91,19 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// the block asking, and a consumer whose invariant is an interval since the /// migration — a cliff, a grace period, a rate that changes a week later — /// subtracts it from the current block without underflowing. +/// - NEVER BEFORE THE RECORD IT IS APPLIED ONTO (`TimestampBeforeHead`). A +/// namespace's moments therefore never go backwards along its chain, so a +/// consumer measuring the gap between two of its migrations subtracts them +/// in chain order without underflowing either. The first migration in a +/// namespace is applied onto `MIGRATION_HEAD_GENESIS`, which holds no record +/// and so bounds nothing. /// -/// Nothing else constrains it. Two records may carry the same moment, and a -/// record may carry a moment earlier than the record before it in the chain. -/// The order the migrations ran in is the chain, not the moments. +/// Nothing else constrains it, and EQUAL is accepted at both of the bounds that +/// have a neighbour: a moment may be exactly the block it is written in, and +/// two records may carry the same moment. Two migrations applied in one +/// transaction share a block and two backfilled to the same day share a +/// moment, so forcing them apart would demand a precision the moments do not +/// have. Which of them ran first is the chain, not the moments. /// /// ## `applied` answers WHEN, and zero still means "not applied" /// @@ -278,6 +287,30 @@ interface IMigrationRegistryV1 { /// @param blockTimestamp The timestamp of the block the call landed in. error FutureTimestamp(uint256 appliedAt, uint256 blockTimestamp); + /// Thrown when `applyMigration` is given an `appliedAt` before the moment + /// recorded against the head it is being applied onto. A record says its + /// migration ran after the one before it in the chain, so an earlier moment + /// contradicts the sequence the same call just named, and a consumer + /// measuring the gap between two migrations would be subtracting the later + /// moment from the earlier one. + /// + /// Equal is accepted. Two migrations applied in one transaction share a + /// block, and two backfilled migrations known only to the same day share a + /// moment; the chain is what tells those apart, so refusing them would + /// demand a precision the moments do not have. + /// + /// The first migration in a namespace is never refused this way: it is + /// applied onto `MIGRATION_HEAD_GENESIS`, which is a head rather than a + /// migration and so holds no record and no moment to be before. + /// + /// Neither the head nor the caller is named, because both are values the + /// caller handed in and was told about first: `msg.sender` is the + /// namespace, and a wrong head is `UnexpectedMigrationHead`. + /// @param appliedAt The moment supplied. + /// @param headAppliedAt The moment recorded against the head it is being + /// applied onto. + error TimestampBeforeHead(uint256 appliedAt, uint256 headAppliedAt); + /// Emitted every time a migration is applied. A migration is applied at /// most once per writer, so the log is the complete history of the registry /// and the only way to discover a record without already knowing the id. @@ -328,8 +361,10 @@ interface IMigrationRegistryV1 { /// `GenesisMigration` if it is `MIGRATION_HEAD_GENESIS`, `ZeroTimestamp` if /// `appliedAt` is zero, `MigrationAlreadyApplied` if the caller has already /// applied it, `UnexpectedMigrationHead` if the caller's namespace is not at - /// `expectedHead`, and `FutureTimestamp` if `appliedAt` is after - /// `block.timestamp`. It MUST NOT provide any way to unrecord a migration, + /// `expectedHead`, `TimestampBeforeHead` if `appliedAt` is before the moment + /// recorded against `expectedHead`, and `FutureTimestamp` if `appliedAt` is + /// after `block.timestamp`. It MUST NOT provide any way to unrecord a + /// migration, /// to move a head backwards, or to move a record once written. On success it /// MUST record `appliedAt` and `expectedHead` against `migration`, make /// `migration` the caller's new head, and emit `Migrated`. diff --git a/src/lib/LibMigrationRegistry.sol b/src/lib/LibMigrationRegistry.sol index b040924..a8ec3d8 100644 --- a/src/lib/LibMigrationRegistry.sol +++ b/src/lib/LibMigrationRegistry.sol @@ -149,9 +149,8 @@ library LibMigrationRegistry { /// not applied it. function appliedOnto(address writer, bytes32 migration) internal view returns (bytes32) { checkCodeHash(); - return IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS).appliedOnto( - writer, migration - ); + return IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS) + .appliedOnto(writer, migration); } /// The migration `writer` applied most recently, or `MIGRATION_HEAD_GENESIS` @@ -214,8 +213,9 @@ library LibMigrationRegistry { /// /// Everything the two-argument form says about the namespace, the code-hash /// check and the registry's refusals holds here unchanged. The registry - /// refuses the two moments a record cannot carry as well: zero, and one - /// after the block this lands in. + /// refuses the three moments a record cannot carry as well: zero, one after + /// the block this lands in, and one before the record at the head it is + /// applied onto. /// @param expectedHead The migration the caller believes it applied last, /// or `MIGRATION_HEAD_GENESIS` for the first in this namespace. /// @param migration The migration to apply. Never zero, never diff --git a/test/src/concrete/MigrationRegistryApplyMigration.t.sol b/test/src/concrete/MigrationRegistryApplyMigration.t.sol index d52d3f1..0c8bbc7 100644 --- a/test/src/concrete/MigrationRegistryApplyMigration.t.sol +++ b/test/src/concrete/MigrationRegistryApplyMigration.t.sol @@ -588,9 +588,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// The two-argument form emits the same event, carrying the block it stamped /// — so a reader of the log never has to know which form wrote a record. - function testApplyMigrationEventFromTheBlockStampingForm(address writer, bytes32 migration, uint32 now_) - external - { + function testApplyMigrationEventFromTheBlockStampingForm(address writer, bytes32 migration, uint32 now_) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(now_ != 0); @@ -612,6 +610,7 @@ contract MigrationRegistryApplyMigrationTest is Test { function testApplyMigrationNoEventOnRevert(address writer, bytes32 migration) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); + vm.warp(1000); vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); @@ -654,13 +653,19 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.recordLogs(); vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV1.FutureTimestamp.selector, block.timestamp + 1, block.timestamp - ) + abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, block.timestamp + 1, block.timestamp) ); vm.prank(writer); sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), block.timestamp + 1); assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, block.timestamp - 1, 1000) + ); + vm.prank(writer); + sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), block.timestamp - 1); + assertEq(vm.getRecordedLogs().length, 0); } /// The three-argument form records the moment the CALLER supplied, which is @@ -748,9 +753,7 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(now_); appliedAt = bound(appliedAt, uint256(now_) + 1, type(uint256).max); - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, appliedAt, uint256(now_)) - ); + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, appliedAt, uint256(now_))); vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); @@ -908,12 +911,10 @@ contract MigrationRegistryApplyMigrationTest is Test { sRegistry.applyMigration(skipped, migrationB, 9001); } - /// A record may carry a moment EARLIER than the record before it in the - /// chain. Nothing orders the moments, because the chain does: a namespace - /// backfilled out of order, or one whose writer learned of an older - /// migration late, records what it knows rather than being refused for - /// contradicting a sequence it is not the source of. - function testApplyMigrationMomentsMayGoBackwards( + /// A record may NOT carry a moment earlier than the record it is applied + /// onto. Nothing is written and the head does not move, so the migration is + /// still applicable with a moment the chain admits. + function testApplyMigrationMomentBeforeHeadReverts( address writer, bytes32 migrationA, bytes32 migrationB, @@ -930,14 +931,182 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(now_)); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, earlier, uint256(now_)) + ); vm.prank(writer); sRegistry.applyMigration(migrationA, migrationB, earlier); - assertEq(sRegistry.applied(writer, migrationA), uint256(now_)); - assertEq(sRegistry.applied(writer, migrationB), earlier); + assertEq(sRegistry.applied(writer, migrationB), 0); + assertEq(sRegistry.appliedOnto(writer, migrationB), bytes32(0)); + assertEq(sRegistry.head(writer), migrationA); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, uint256(now_)); + assertEq(sRegistry.applied(writer, migrationB), uint256(now_)); + assertEq(sRegistry.head(writer), migrationB); + } + + /// The boundary is the head's own moment, inclusive: exactly it is + /// accepted, and one second below it is refused. + function testApplyMigrationBeforeHeadBoundary( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 headAppliedAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(headAppliedAt > 1); + vm.warp(headAppliedAt); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(headAppliedAt)); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV1.TimestampBeforeHead.selector, uint256(headAppliedAt) - 1, uint256(headAppliedAt) + ) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, uint256(headAppliedAt) - 1); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, uint256(headAppliedAt)); + assertEq(sRegistry.applied(writer, migrationB), uint256(headAppliedAt)); + } + + /// A moment AFTER the head's is the ordinary case, and it is the moment the + /// record carries rather than anything derived from the one before it. + function testApplyMigrationMomentAfterHeadIsAccepted( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 first, + uint32 second + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(first != 0); + vm.assume(second > first); + vm.warp(second); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(first)); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, uint256(second)); + + assertEq(sRegistry.applied(writer, migrationA), uint256(first)); + assertEq(sRegistry.applied(writer, migrationB), uint256(second)); assertEq(sRegistry.head(writer), migrationB); } + /// The first migration in a namespace is compared to nothing. It is applied + /// onto `MIGRATION_HEAD_GENESIS`, which is refused as a migration and so + /// holds no record in any namespace ever — which is why the smallest moment + /// a record may carry is accepted at genesis in the latest block. + function testApplyMigrationNoMomentBoundAtGenesis(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + // Nothing can put a record at genesis to be bounded by. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_HEAD_GENESIS, uint256(now_)); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); + + assertEq(sRegistry.applied(writer, migration), 1); + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + } + + /// The refusals that read the namespace's KEYS come before the one that + /// reads its RECORD, so a re-dispatched script is told its migration already + /// ran and a script at the wrong point is told where the namespace is, + /// rather than either being told about the moment of a record it was never + /// going to be chained onto. The zero moment still comes before all of them. + function testApplyMigrationNamespaceCheckedBeforeTheHeadsMoment( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 skipped + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, skipped); + vm.assume(migrationA != migrationB); + vm.assume(skipped != migrationA); + vm.assume(skipped != migrationB); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 9000); + + // Already applied, and before the head's moment: told it already ran. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.MigrationAlreadyApplied.selector, writer, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationA, 1); + + // The wrong head, and before the head's moment: told where the + // namespace is. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, skipped, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(skipped, migrationB, 1); + + // A zero moment, which is also before the head's: told about the zero. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, 0); + + // With nothing else wrong, the head's moment is what refuses it. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1, 9000)); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, 1); + } + + /// A head's moment bounds only its own namespace. Another writer at genesis + /// is bounded by nothing, whatever moment the first namespace recorded. + function testApplyMigrationHeadMomentIsPerWriter( + address writer, + address other, + bytes32 migrationA, + bytes32 migrationB + ) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 9000); + + vm.prank(other); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB, 1); + + assertEq(sRegistry.applied(other, migrationB), 1); + + // And the other namespace's own head bounds it from there. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1, 9000)); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB, 1); + } + /// Two records may carry the SAME moment. Two migrations applied in one /// transaction share a block, and two backfilled migrations known only to /// the same day share a moment; the chain is what orders them, so the @@ -969,11 +1138,9 @@ contract MigrationRegistryApplyMigrationTest is Test { /// `MIGRATION_HEAD_GENESIS`, and each later one holds the migration before /// it — the value the caller named and the registry checked, not one the /// caller could have chosen freely. - function testApplyMigrationRecordsTheHeadItWasAppliedOnto( - address writer, - bytes32 migrationA, - bytes32 migrationB - ) external { + function testApplyMigrationRecordsTheHeadItWasAppliedOnto(address writer, bytes32 migrationA, bytes32 migrationB) + external + { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migrationA); LibMigrationFuzz.assumeMigration(vm, migrationB); @@ -991,9 +1158,9 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); } - /// The chain is the order the migrations ran in, and it says so while the - /// moments say the opposite. Three records written newest-moment-first walk - /// back from the head in the order they were APPLIED, ending at genesis. + /// The chain is the order the migrations ran in, and it says so where the + /// moments cannot. Three records carrying one moment walk back from the head + /// in the order they were APPLIED, ending at genesis. function testApplyMigrationChainIsTheOrderWhateverTheMoments( address writer, bytes32 migrationA, @@ -1012,15 +1179,15 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 3000); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, 2000); + sRegistry.applyMigration(migrationA, migrationB, 3000); vm.prank(writer); - sRegistry.applyMigration(migrationB, migrationC, 1000); + sRegistry.applyMigration(migrationB, migrationC, 3000); - // Every moment is below the one before it, so nothing about the order - // can be read out of them. + // Every moment is the same as the one before it, so nothing about the + // order can be read out of them. assertEq(sRegistry.applied(writer, migrationA), 3000); - assertEq(sRegistry.applied(writer, migrationB), 2000); - assertEq(sRegistry.applied(writer, migrationC), 1000); + assertEq(sRegistry.applied(writer, migrationB), 3000); + assertEq(sRegistry.applied(writer, migrationC), 3000); // The chain still says exactly what happened. bytes32 cursor = sRegistry.head(writer); @@ -1035,12 +1202,9 @@ contract MigrationRegistryApplyMigrationTest is Test { /// A chain belongs to one namespace. Another writer applying the same /// migrations builds its own chain, and neither reaches the other. - function testApplyMigrationChainIsPerWriter( - address writer, - address other, - bytes32 migrationA, - bytes32 migrationB - ) external { + function testApplyMigrationChainIsPerWriter(address writer, address other, bytes32 migrationA, bytes32 migrationB) + external + { vm.assume(writer != address(0)); vm.assume(other != address(0)); vm.assume(writer != other); diff --git a/test/src/lib/LibMigrationRegistry.t.sol b/test/src/lib/LibMigrationRegistry.t.sol index 40060db..63d7721 100644 --- a/test/src/lib/LibMigrationRegistry.t.sol +++ b/test/src/lib/LibMigrationRegistry.t.sol @@ -526,12 +526,26 @@ contract LibMigrationRegistryTest is Test { this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); } + /// The registry's before-the-head refusal arrives unmodified through + /// `applyMigration`, so a consumer backfilling its history out of order is + /// told which moment it contradicted. + function testApplyMigrationTimestampBeforeHeadReverts(bytes32 migrationA, bytes32 migrationB) external { + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + deployRegistry(); + vm.warp(9000); + + LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 2000); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1999, 2000)); + this.externalApplyMigration(migrationA, migrationB, 1999); + } + /// The namespace of the `appliedAt` form is the calling CONTRACT too, so a /// consumer backfilling its history writes its own namespace and nobody /// else's. - function testApplyMigrationWithAppliedAtLandsUnderTheCallingContract(bytes32 migration, uint32 appliedAt) - external - { + function testApplyMigrationWithAppliedAtLandsUnderTheCallingContract(bytes32 migration, uint32 appliedAt) external { LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); deployRegistry(); From 75f33ac53402c10b09d78b309afd6857e1d3d9c0 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 11:04:13 +0000 Subject: [PATCH 6/8] Suppress slither's strict-equality detector on the zero-moment refusal `appliedAt` reaches the comparison from `block.timestamp` on the two-argument form, so slither reports `incorrect-equality` for it. The same suppression was on the same refusal before the moment became an argument. Co-Authored-By: Claude Opus 5 (1M context) --- src/concrete/MigrationRegistry.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/concrete/MigrationRegistry.sol b/src/concrete/MigrationRegistry.sol index 5081460..8b63404 100644 --- a/src/concrete/MigrationRegistry.sol +++ b/src/concrete/MigrationRegistry.sol @@ -128,6 +128,13 @@ contract MigrationRegistry is IMigrationRegistryV1 { // never be applied again. Reached by the two-argument form as well, in // a block whose timestamp is zero: a test can warp to zero and a chain // can be configured from a zero genesis. + // + // Slither flags a strict equality on anything reaching it from + // `block.timestamp`, which the two-argument form does. Zero is the only + // value this refuses and the only one it can refuse, so there is no + // window for a validator to nudge the clock across. Suppressed on this + // comparison rather than turned off for the repo. + // slither-disable-next-line incorrect-equality if (appliedAt == 0) { revert ZeroTimestamp(); } From 01fd2e8a1dee02d3cd9fd18c9e2191db587b4829 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 11:26:30 +0000 Subject: [PATCH 7/8] Name the historical entry point applyMigrationHistory The three-argument form, which takes a caller-supplied moment for a migration that already ran, is now applyMigrationHistory. The two-argument atomic form keeps applyMigration. They stop being two arities of one name. Snapshot regeneration, fmt, suite and slither not yet run. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 56 +++--- src/concrete/MigrationRegistry.sol | 38 ++-- src/interface/IMigrationRegistryV1.sol | 102 +++++------ src/lib/LibMigrationRegistry.sol | 27 +-- test/concrete/MockMigrationApplier.sol | 10 +- .../concrete/MigrationRegistryApplied.t.sol | 16 +- .../MigrationRegistryAppliedOnto.t.sol | 4 +- .../MigrationRegistryApplyMigration.t.sol | 167 +++++++++--------- test/src/concrete/MigrationRegistryHead.t.sol | 6 +- test/src/lib/LibMigrationRegistry.t.sol | 69 ++++---- 10 files changed, 248 insertions(+), 247 deletions(-) diff --git a/README.md b/README.md index 7395a29..63d08f8 100644 --- a/README.md +++ b/README.md @@ -208,17 +208,18 @@ library supplies the fork loop and the comparison. `MigrationRegistry` records that a migration has been applied, when, and onto what: a writer applies one of its own onto the migration it believes ran last -(`applyMigration`), anyone reads when a given writer applied a given one -(`applied`), what that writer applied it onto (`appliedOnto`), and where a given -writer's sequence has got to (`head`). There is no removal and no upgrade. - -`applyMigration` has two forms, differing in exactly one thing: where the -recorded moment comes from. The two-argument form stamps the block the record -lands in, for a script applying its own migration in the same atomic unit as the -migration itself. The three-argument form takes the moment as an argument, so a -migration that ran before the registry reached the chain is recordable with the -time it actually ran. They write the same record, into the same namespace, and -make the same refusals. +(`applyMigration`, or `applyMigrationHistory` for one that already ran), anyone +reads when a given writer applied a given one (`applied`), what that writer +applied it onto (`appliedOnto`), and where a given writer's sequence has got to +(`head`). There is no removal and no upgrade. + +The two writes differ in exactly one thing: where the recorded moment comes +from. `applyMigration` stamps the block the record lands in, for a script +applying its own migration in the same atomic unit as the migration itself. +`applyMigrationHistory` takes the moment as an argument, so a migration that ran +before the registry reached the chain is recordable with the time it actually +ran. They write the same record, into the same namespace, and make the same +refusals. It exists because prod-state tests otherwise decide what to assert by reading the **clock**. The pattern that emerges without it is a dual-state invariant — @@ -255,9 +256,9 @@ being recorded is that a migration RAN, and the moment it ran is not in general the moment anybody gets to write it down. A registry that could only stamp its own block offers a writer with history two options and no third: record a time that is false for every past migration, or record nothing — and recording -nothing strands the namespace, because `applyMigration` refuses anything not -applied onto the current head, so a writer that skipped its past migrations -cannot record its next one either. +nothing strands the namespace, because the registry refuses anything not applied +onto the current head, so a writer that skipped its past migrations cannot +record its next one either. What a reader gives up is **not** authenticity. A record is namespaced by the account that wrote it and no authority checks it, so every entry is already @@ -288,9 +289,9 @@ moments do not have. Which of them ran first is the chain, not the moments — total order comes from `appliedOnto`, and the bound above only stops a record claiming to predate the one it is chained onto. -Both entry points get all three, `block.timestamp` included: a block whose -timestamp is zero is `ZeroTimestamp` on the two-argument form, which a test that -warps to zero and a chain configured from a zero genesis both reach. +Both writes get all three, `block.timestamp` included: a block whose timestamp +is zero is `ZeroTimestamp` on `applyMigration`, which a test that warps to zero +and a chain configured from a zero genesis both reach. **A set of applied migrations, not a high-water mark.** A mark needs a total order consumers do not have: two migrations authored on one day collide, and one @@ -302,10 +303,9 @@ dependency actually lives. **A head, so a step cannot be skipped or repeated.** A namespace has a head: the migration it applied most recently, or `MIGRATION_HEAD_GENESIS` if it has -applied none. `applyMigration` names the head it is applying onto, so a chain -that never got the predecessor fails at the moment of applying rather than -diverging silently, and two migrations dispatched at once cannot land in the -wrong order. +applied none. Both writes name the head they are applying onto, so a chain that +never got the predecessor fails at the moment of applying rather than diverging +silently, and two migrations dispatched at once cannot land in the wrong order. ```solidity // The first migration in a namespace, applied in this transaction. @@ -313,7 +313,7 @@ LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_V1); // Every later one names its predecessor. LibMigrationRegistry.applyMigration(MIGRATION_V1, MIGRATION_V2); // One that ran before the registry reached this chain names the moment it ran. -LibMigrationRegistry.applyMigration(MIGRATION_V2, MIGRATION_V3, 1750000000); +LibMigrationRegistry.applyMigrationHistory(MIGRATION_V2, MIGRATION_V3, 1750000000); ``` Each record also keeps the head it was applied onto, which `appliedOnto` reads @@ -356,12 +356,12 @@ say the invariant holds — a multisig can act out of band and nothing here move Keep both layers: this selects, codehash and bytecode pins verify. Replacing the pins with it trades a clock-guess for a bookkeeping-guess. -`LibMigrationRegistry` is the surface — `applied`, `appliedOnto`, `head` and -both forms of `applyMigration`, each verifying the registry's code hash before -it reads or writes. There is deliberately **no broadcast runner**: the dominant -real shape is a Safe executing a bundle that never broadcasts, and such a script -appends `applyMigration` to the bundle it is already emitting, which makes the -record atomic with the migration it describes. +`LibMigrationRegistry` is the surface — `applied`, `appliedOnto`, `head`, +`applyMigration` and `applyMigrationHistory`, each verifying the registry's code +hash before it reads or writes. There is deliberately **no broadcast runner**: +the dominant real shape is a Safe executing a bundle that never broadcasts, and +such a script appends `applyMigration` to the bundle it is already emitting, +which makes the record atomic with the migration it describes. ## Deploying, and then releasing diff --git a/src/concrete/MigrationRegistry.sol b/src/concrete/MigrationRegistry.sol index 8b63404..c99355d 100644 --- a/src/concrete/MigrationRegistry.sol +++ b/src/concrete/MigrationRegistry.sol @@ -8,7 +8,7 @@ import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../interface/IMigrat /// never hold one half of itself. struct MigrationRecord { /// The moment recorded against the migration. Zero means never applied, - /// which `applyMigration` refuses to write. + /// which neither write records. uint256 appliedAt; /// The head the namespace was at when the record was written. Zero means /// never applied: a head is genesis or an applied id, both nonzero. @@ -47,15 +47,15 @@ struct MigrationRecord { /// on a chain. /// /// A record is append-only per writer, and a head only ever moves forward onto -/// something new. `applyMigration` refuses a migration the caller has already -/// applied, which is what makes re-running a migration fail rather than repeat, -/// and refuses one applied onto anything but the namespace's current head, which -/// is what makes a skipped or out-of-order migration fail rather than diverge. +/// something new. Both writes refuse a migration the caller has already applied, +/// which is what makes re-running a migration fail rather than repeat, and +/// refuse one applied onto anything but the namespace's current head, which is +/// what makes a skipped or out-of-order migration fail rather than diverge. /// There is no way to unrecord one, and no way to rewrite one — a record /// describes something that happened, and nothing that happened stops having /// happened. /// -/// The moment is the CALLER's on the three-argument form, so a migration that +/// The moment is the CALLER's on `applyMigrationHistory`, so a migration that /// ran before this contract reached the chain is recordable with the time it /// actually ran. The order is not the caller's: each record keeps the head it /// was applied onto, so a namespace's records are a chain from `head` back to @@ -92,12 +92,12 @@ contract MigrationRegistry is IMigrationRegistryV1 { } /// @inheritdoc IMigrationRegistryV1 - function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + function applyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { applyMigrationRecord(expectedHead, migration, appliedAt); } - /// Both entry points, so there is one record and one set of refusals - /// whichever of them supplied the moment. + /// Reached by `applyMigration` and by `applyMigrationHistory`, so there is + /// one record and one set of refusals whichever of them supplied the moment. /// /// The refusals run from the ones that describe the call alone, through the /// ones that describe the namespace it arrives at, to the one that @@ -125,12 +125,12 @@ contract MigrationRegistry is IMigrationRegistryV1 { // namespace it arrives at and whatever block it lands in. Zero is the // one moment a record cannot carry — `applied` would answer it as // "never applied" while the head had moved and the migration could - // never be applied again. Reached by the two-argument form as well, in - // a block whose timestamp is zero: a test can warp to zero and a chain + // never be applied again. Reached by `applyMigration` as well, in a + // block whose timestamp is zero: a test can warp to zero and a chain // can be configured from a zero genesis. // // Slither flags a strict equality on anything reaching it from - // `block.timestamp`, which the two-argument form does. Zero is the only + // `block.timestamp`, which `applyMigration` does. Zero is the only // value this refuses and the only one it can refuse, so there is no // window for a validator to nudge the clock across. Suppressed on this // comparison rather than turned off for the repo. @@ -205,8 +205,8 @@ contract MigrationRegistry is IMigrationRegistryV1 { /// @inheritdoc IMigrationRegistryV1 /// @dev All three refusals are about a caller that has not supplied what it /// thinks it has. None can ever be a real record: nothing originates from - /// the zero address, and `applyMigration` will write neither the zero id nor - /// the genesis one — so answering zero for any of them would be answering a + /// the zero address, and neither write records the zero id or the genesis + /// one — so answering zero for any of them would be answering a /// question the caller did not mean to ask, and answering it with the value /// that sends it down its pre-migration branch. function applied(address writer, bytes32 migration) external view returns (uint256) { @@ -248,12 +248,12 @@ contract MigrationRegistry is IMigrationRegistryV1 { /// /// The empty-namespace zero is translated to genesis here and nowhere else, /// which is why this is one `public` function rather than a reader beside an - /// internal helper: `applyMigration` compares against exactly what a caller - /// reads, so the two cannot drift into different ideas of where a namespace - /// that has applied nothing is. + /// internal helper: a write compares against exactly what a caller reads, so + /// the two cannot drift into different ideas of where a namespace that has + /// applied nothing is. /// - /// `applyMigration` reaches it as `head(msg.sender)`, which can never be the - /// zero address, so the refusal is redundant on that path. It is one + /// A write reaches it as `head(msg.sender)`, which can never be the zero + /// address, so the refusal is redundant on that path. It is one /// function, so it is one refusal, and the reachable path is the one it is /// there for. function head(address writer) public view returns (bytes32) { diff --git a/src/interface/IMigrationRegistryV1.sol b/src/interface/IMigrationRegistryV1.sol index 60ebcb8..733938c 100644 --- a/src/interface/IMigrationRegistryV1.sol +++ b/src/interface/IMigrationRegistryV1.sol @@ -2,9 +2,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; -/// @dev The head of a namespace that has never applied a migration. A writer's -/// first `applyMigration` names this, and every later one names the migration -/// before it. +/// @dev The head of a namespace that has never applied a migration. The first +/// migration a writer applies names this, and every later one names the +/// migration before it. /// /// It is deliberately NOT zero. Zero is what an uninitialised `bytes32` constant /// reads as, and a genesis of zero would make an uninitialised predecessor @@ -31,18 +31,19 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// @title IMigrationRegistryV1 /// @notice A per-writer record of which migrations have been applied, when, and -/// onto what, with exactly four operations: a writer applies one of its own -/// migrations onto the head it believes its namespace is at (`applyMigration`), -/// anyone reads when a given writer applied a given migration (`applied`), what -/// that writer applied it onto (`appliedOnto`), and where that writer's -/// namespace currently is (`head`). There is no removal, no upgrade and no -/// authority beyond the writer over its own namespace, and an implementation -/// MUST NOT add any. +/// onto what, with exactly five operations: a writer applies one of its own +/// migrations onto the head it believes its namespace is at, either as applied +/// now (`applyMigration`) or as already applied at a moment it supplies +/// (`applyMigrationHistory`), anyone reads when a given writer applied a given +/// migration (`applied`), what that writer applied it onto (`appliedOnto`), and +/// where that writer's namespace currently is (`head`). There is no removal, no +/// upgrade and no authority beyond the writer over its own namespace, and an +/// implementation MUST NOT add any. /// -/// `applyMigration` has two forms, differing only in where the recorded moment -/// comes from. The two-argument form records the block it lands in, for a -/// script applying its own migration in the same atomic unit as the migration. -/// The three-argument form takes the moment as an argument, for a migration +/// The two writes differ only in where the recorded moment comes from. +/// `applyMigration` records the block it lands in, for a script applying its +/// own migration in the same atomic unit as the migration. +/// `applyMigrationHistory` takes the moment as an argument, for a migration /// that already ran — one that ran before this registry reached the chain, or /// before its writer started recording at all. /// @@ -121,17 +122,17 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// /// That distinction is only sound while a real record can never BE zero, which /// is what `ZeroTimestamp` is for. It refuses a caller that supplies zero, and -/// it refuses the two-argument form in a block whose timestamp is zero — -/// neither hypothetical, because a test can `vm.warp(0)` and a chain can be -/// configured from a zero genesis. +/// it refuses `applyMigration` in a block whose timestamp is zero — neither +/// hypothetical, because a test can `vm.warp(0)` and a chain can be configured +/// from a zero genesis. /// /// ## The head is what makes an ordered sequence ordered /// /// A namespace has a HEAD: the migration most recently applied under it, or -/// `MIGRATION_HEAD_GENESIS` if it has never applied one. `applyMigration` takes -/// the head the caller believes its namespace is at and refuses to write unless -/// that is where the namespace actually is; on success the applied migration -/// becomes the new head. +/// `MIGRATION_HEAD_GENESIS` if it has never applied one. Both writes take the +/// head the caller believes its namespace is at and refuse to write unless that +/// is where the namespace actually is; on success the applied migration becomes +/// the new head. /// /// Each record keeps the head it was applied onto, which `appliedOnto` reads /// back. A namespace's records are therefore a chain in storage: from `head`, @@ -207,8 +208,8 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// A head is an id, so the same is true of the head a script names: it is the /// predecessor's named constant, imported, not a second spelling of it. interface IMigrationRegistryV1 { - /// Thrown when `applyMigration` is called with the zero migration id, and - /// by `applied` when it is asked about one. The zero id is what an + /// Thrown when either write is called with the zero migration id, and by + /// `applied` when it is asked about one. The zero id is what an /// uninitialised `bytes32` constant reads as, and an uninitialised id is /// never a migration anybody meant to name. Rejected in both directions /// because the read is the dangerous one: answering zero would silently @@ -221,8 +222,8 @@ interface IMigrationRegistryV1 { /// which names the zero it was handed, so nothing about the mistake is lost. error ZeroMigration(); - /// Thrown when `applyMigration` is called with `MIGRATION_HEAD_GENESIS` as - /// the migration, and by `applied` when it is asked about it. Genesis is a + /// Thrown when either write is called with `MIGRATION_HEAD_GENESIS` as the + /// migration, and by `applied` when it is asked about it. Genesis is a /// head, not a migration: applying it would leave a namespace that has /// applied something at a head no different from one that has applied /// nothing, and asking `applied` about it would answer zero forever for a @@ -241,8 +242,8 @@ interface IMigrationRegistryV1 { /// genesis" — an unresolved or unset writer constant would therefore read as /// a pristine namespace rather than as the mistake it is. /// - /// There is no matching case on `applyMigration`: `msg.sender` is never - /// zero, so the zero namespace cannot be written to in the first place. + /// There is no matching case on either write: `msg.sender` is never zero, + /// so the zero namespace cannot be written to in the first place. error ZeroWriter(); /// Thrown when a writer applies a migration it has already applied. This @@ -269,28 +270,28 @@ interface IMigrationRegistryV1 { /// @param actualHead The head the namespace is actually at. error UnexpectedMigrationHead(address writer, bytes32 expectedHead, bytes32 actualHead); - /// Thrown when `applyMigration` would record a zero moment: a caller that - /// supplied zero, or the two-argument form in a block whose timestamp is - /// zero. A record IS its moment, so a zero one would read back through + /// Thrown when a write would record a zero moment: `applyMigrationHistory` + /// given zero, or `applyMigration` in a block whose timestamp is zero. A + /// record IS its moment, so a zero one would read back through /// `applied` as no record at all, while the head moved and the migration /// cannot be re-applied — the worst of every branch at once. Zero is also /// what an uninitialised `uint256` holds, so it is refused for the same /// reason `ZeroMigration` is. error ZeroTimestamp(); - /// Thrown when `applyMigration` is given an `appliedAt` after the timestamp - /// of the block it is called in. A record says a migration HAS run, so a - /// moment that has not arrived is not a record of anything — and a consumer - /// measuring an interval since the migration would be subtracting a future - /// moment from the present one. + /// Thrown when `applyMigrationHistory` is given an `appliedAt` after the + /// timestamp of the block it is called in. A record says a migration HAS + /// run, so a moment that has not arrived is not a record of anything — and a + /// consumer measuring an interval since the migration would be subtracting a + /// future moment from the present one. /// @param appliedAt The moment supplied. /// @param blockTimestamp The timestamp of the block the call landed in. error FutureTimestamp(uint256 appliedAt, uint256 blockTimestamp); - /// Thrown when `applyMigration` is given an `appliedAt` before the moment - /// recorded against the head it is being applied onto. A record says its - /// migration ran after the one before it in the chain, so an earlier moment - /// contradicts the sequence the same call just named, and a consumer + /// Thrown when `applyMigrationHistory` is given an `appliedAt` before the + /// moment recorded against the head it is being applied onto. A record says + /// its migration ran after the one before it in the chain, so an earlier + /// moment contradicts the sequence the same call just named, and a consumer /// measuring the gap between two migrations would be subtracting the later /// moment from the earlier one. /// @@ -331,8 +332,8 @@ interface IMigrationRegistryV1 { /// Applies `migration` under the caller's namespace, onto `expectedHead`, /// as having been applied in the block this call lands in. /// - /// This is the form for a script applying its own migration, so the record - /// lands in the same atomic unit as the change it describes — a Safe + /// This is for a script applying its own migration, so the record lands in + /// the same atomic unit as the change it describes — a Safe /// appends this call to the bundle it is already executing — and the two /// cannot land apart. Where they cannot be atomic, call it LAST: a record /// that never landed leaves a reader asserting the pre-migration state, @@ -340,7 +341,7 @@ interface IMigrationRegistryV1 { /// possible. A record that landed for a migration that did not is the /// harder state to get out of. /// - /// It records the same record as the three-argument form and makes the same + /// It records the same record as `applyMigrationHistory` and makes the same /// refusals, against `block.timestamp` as the moment — so a block whose /// timestamp is zero is `ZeroTimestamp`, and the moment can never be in the /// future. @@ -354,8 +355,8 @@ interface IMigrationRegistryV1 { /// Applies `migration` under the caller's namespace, onto `expectedHead`, as /// having been applied at `appliedAt`. /// - /// This is the form for a migration that ALREADY ran, which records the - /// moment it ran rather than the moment it was written down. + /// This is for a migration that ALREADY ran, which records the moment it ran + /// rather than the moment it was written down. /// /// The implementation MUST revert `ZeroMigration` if `migration` is zero, /// `GenesisMigration` if it is `MIGRATION_HEAD_GENESIS`, `ZeroTimestamp` if @@ -364,10 +365,9 @@ interface IMigrationRegistryV1 { /// `expectedHead`, `TimestampBeforeHead` if `appliedAt` is before the moment /// recorded against `expectedHead`, and `FutureTimestamp` if `appliedAt` is /// after `block.timestamp`. It MUST NOT provide any way to unrecord a - /// migration, - /// to move a head backwards, or to move a record once written. On success it - /// MUST record `appliedAt` and `expectedHead` against `migration`, make - /// `migration` the caller's new head, and emit `Migrated`. + /// migration, to move a head backwards, or to move a record once written. On + /// success it MUST record `appliedAt` and `expectedHead` against + /// `migration`, make `migration` the caller's new head, and emit `Migrated`. /// /// Nothing is returned: every part of the record is an argument the caller /// just handed in. @@ -378,7 +378,7 @@ interface IMigrationRegistryV1 { /// `MIGRATION_HEAD_GENESIS`. /// @param appliedAt The moment `migration` was applied. Never zero, never /// after the block this call lands in. - function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external; + function applyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external; /// When `writer` applied `migration`, as the moment recorded with the /// record. Zero if it never did. @@ -396,8 +396,8 @@ interface IMigrationRegistryV1 { /// revert there would leave a caller with nothing to say about the state it /// is actually looking at, which is the whole failure this registry removes. /// - /// Zero is unambiguous because `applyMigration` refuses to write a zero - /// moment, so no applied migration can present as an unapplied one. + /// Zero is unambiguous because neither write records a zero moment, so no + /// applied migration can present as an unapplied one. /// @param writer The namespace to read. Never the zero address. /// @param migration The migration to ask about. Never zero, never /// `MIGRATION_HEAD_GENESIS`. diff --git a/src/lib/LibMigrationRegistry.sol b/src/lib/LibMigrationRegistry.sol index a8ec3d8..5e909a3 100644 --- a/src/lib/LibMigrationRegistry.sol +++ b/src/lib/LibMigrationRegistry.sol @@ -59,10 +59,13 @@ import {LibMigrationRegistryDeploy} from "./LibMigrationRegistryDeploy.sol"; /// /// ## Writing names the head it is applying onto /// -/// `applyMigration` takes the migration the caller believes ran last in its -/// namespace, so a chain that never got that predecessor refuses the write -/// instead of silently skipping a step, and two migrations dispatched at once -/// cannot land in the wrong order. The first migration in a namespace names +/// `applyMigration` and `applyMigrationHistory` both take the migration the +/// caller believes ran last in its namespace, so a chain that never got that +/// predecessor refuses the write instead of silently skipping a step, and two +/// migrations dispatched at once cannot land in the wrong order. They differ +/// only in where the recorded moment comes from: the block this lands in, or +/// the moment the caller supplies for a migration that already ran. The first +/// migration in a namespace names /// `MIGRATION_HEAD_GENESIS`, imported from the interface — never a zero, which /// is what an uninitialised constant would be and is refused everywhere. /// @@ -93,7 +96,7 @@ library LibMigrationRegistry { /// Every entry point checks, and they check the same way, because each is /// worse than useless against unknown code: `applied` would branch a test on /// whatever timestamp that code returned, `appliedOnto` and `head` would - /// hand back values that are not heads, and `applyMigration` would record a + /// hand back values that are not heads, and either write would record a /// migration somewhere nothing will ever read it. The check is one function /// so they cannot drift into checking different things, and an entry point /// added later has one place to call rather than a rule to remember. @@ -206,12 +209,12 @@ library LibMigrationRegistry { /// Applies `migration` under the CALLER's namespace, onto `expectedHead`, as /// having been applied at `appliedAt`. /// - /// This is the form for a migration that already ran — one that ran before - /// this registry reached the chain, or before its writer started recording - /// at all — so the record carries the moment it ran rather than the moment - /// it was written down. + /// This is for a migration that already ran — one that ran before this + /// registry reached the chain, or before its writer started recording at + /// all — so the record carries the moment it ran rather than the moment it + /// was written down. /// - /// Everything the two-argument form says about the namespace, the code-hash + /// Everything `applyMigration` says about the namespace, the code-hash /// check and the registry's refusals holds here unchanged. The registry /// refuses the three moments a record cannot carry as well: zero, one after /// the block this lands in, and one before the record at the head it is @@ -222,9 +225,9 @@ library LibMigrationRegistry { /// `MIGRATION_HEAD_GENESIS`. /// @param appliedAt The moment `migration` was applied. Never zero, never /// after the block this lands in. - function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) internal { + function applyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) internal { checkCodeHash(); IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS) - .applyMigration(expectedHead, migration, appliedAt); + .applyMigrationHistory(expectedHead, migration, appliedAt); } } diff --git a/test/concrete/MockMigrationApplier.sol b/test/concrete/MockMigrationApplier.sol index 8fbb53d..e8ee312 100644 --- a/test/concrete/MockMigrationApplier.sol +++ b/test/concrete/MockMigrationApplier.sol @@ -5,9 +5,9 @@ pragma solidity =0.8.25; import {LibMigrationRegistry} from "../../src/lib/LibMigrationRegistry.sol"; /// @title MockMigrationApplier -/// @notice A consumer in the shape `LibMigrationRegistry.applyMigration` is -/// designed for: it calls the library and nothing else, so the record lands -/// under THIS contract's address. +/// @notice A consumer in the shape `LibMigrationRegistry`'s writes are designed +/// for: it calls the library and nothing else, so the record lands under THIS +/// contract's address. /// /// It exists so the namespace can be exercised as the property it is. The /// library's functions are `internal` and inline into whatever executes them, @@ -33,8 +33,8 @@ contract MockMigrationApplier { /// @param expectedHead The head this contract believes it is at. /// @param migration The migration to apply. /// @param appliedAt The moment the migration was applied. - function applyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { - LibMigrationRegistry.applyMigration(expectedHead, migration, appliedAt); + function applyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + LibMigrationRegistry.applyMigrationHistory(expectedHead, migration, appliedAt); } /// When `writer` applied `migration`. diff --git a/test/src/concrete/MigrationRegistryApplied.t.sol b/test/src/concrete/MigrationRegistryApplied.t.sol index b559a2f..e049fbf 100644 --- a/test/src/concrete/MigrationRegistryApplied.t.sol +++ b/test/src/concrete/MigrationRegistryApplied.t.sol @@ -54,7 +54,7 @@ contract MigrationRegistryAppliedTest is Test { vm.warp(writtenAt); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); vm.warp(readAt); assertEq(sRegistry.applied(writer, migration), appliedAt); @@ -79,7 +79,7 @@ contract MigrationRegistryAppliedTest is Test { vm.warp(writtenAt); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); vm.warp(readAt); assertLe(sRegistry.applied(writer, migration), block.timestamp); @@ -111,8 +111,7 @@ contract MigrationRegistryAppliedTest is Test { } /// The zero migration id is refused for the same reason in the other - /// direction: `applyMigration` will not write it, so it can never be a real - /// record. + /// direction: neither write records it, so it can never be a real record. function testAppliedZeroMigrationReverts(address writer) external { vm.assume(writer != address(0)); @@ -121,7 +120,7 @@ contract MigrationRegistryAppliedTest is Test { } /// The genesis head is refused as a migration for the same reason again: - /// `applyMigration` will not write it either, so asking about it would + /// neither write records it either, so asking about it would /// answer zero forever to a caller that has confused a head for a migration /// — and that caller reads zero as its pre-migration branch. function testAppliedGenesisMigrationReverts(address writer) external { @@ -187,14 +186,11 @@ contract MigrationRegistryAppliedTest is Test { /// There is no other entry point at all: no fallback, no receive, and /// nothing beyond the `IMigrationRegistryV1` functions, so an unknown /// selector reverts instead of being silently absorbed. - /// - /// The two `applyMigration` selectors are spelled from their signatures - /// because `.selector` has no single answer for an overloaded name. function testAppliedNoOtherEntryPoint(bytes4 selector, bytes32 migration) external { vm.assume(selector != IMigrationRegistryV1.applied.selector); vm.assume(selector != IMigrationRegistryV1.appliedOnto.selector); - vm.assume(selector != bytes4(keccak256("applyMigration(bytes32,bytes32)"))); - vm.assume(selector != bytes4(keccak256("applyMigration(bytes32,bytes32,uint256)"))); + vm.assume(selector != IMigrationRegistryV1.applyMigration.selector); + vm.assume(selector != IMigrationRegistryV1.applyMigrationHistory.selector); vm.assume(selector != IMigrationRegistryV1.head.selector); (bool success,) = address(sRegistry).call(abi.encodeWithSelector(selector, address(this), migration)); diff --git a/test/src/concrete/MigrationRegistryAppliedOnto.t.sol b/test/src/concrete/MigrationRegistryAppliedOnto.t.sol index e808ace..65f31f6 100644 --- a/test/src/concrete/MigrationRegistryAppliedOnto.t.sol +++ b/test/src/concrete/MigrationRegistryAppliedOnto.t.sol @@ -130,8 +130,8 @@ contract MigrationRegistryAppliedOntoTest is Test { sRegistry.appliedOnto(address(0), migration); } - /// The zero migration id is refused: `applyMigration` will not write it, so - /// it can never be a real record. + /// The zero migration id is refused: neither write records it, so it can + /// never be a real record. function testAppliedOntoZeroMigrationReverts(address writer) external { vm.assume(writer != address(0)); diff --git a/test/src/concrete/MigrationRegistryApplyMigration.t.sol b/test/src/concrete/MigrationRegistryApplyMigration.t.sol index 0c8bbc7..d3f1aeb 100644 --- a/test/src/concrete/MigrationRegistryApplyMigration.t.sol +++ b/test/src/concrete/MigrationRegistryApplyMigration.t.sol @@ -9,10 +9,11 @@ import {MigrationRegistry} from "../../../src/concrete/MigrationRegistry.sol"; import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; /// @title MigrationRegistryApplyMigrationTest -/// @notice A test suite for both forms of `MigrationRegistry.applyMigration`: -/// who a record belongs to, that a migration is applied at most once and only -/// onto the head its caller named, which moments a record may carry, what a -/// record carries, and what it may never become. +/// @notice A test suite for `MigrationRegistry.applyMigration` and +/// `MigrationRegistry.applyMigrationHistory`: who a record belongs to, that a +/// migration is applied at most once and only onto the head its caller named, +/// which moments a record may carry, what a record carries, and what it may +/// never become. contract MigrationRegistryApplyMigrationTest is Test { /// The registry under test. Stateful, so a fresh one per test. MigrationRegistry internal sRegistry; @@ -34,7 +35,7 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.applied(writer, migration), block.timestamp); } - /// The two-argument form records the block it landed in, which is the whole + /// `applyMigration` records the block it landed in, which is the whole /// difference from a flag: a consumer whose invariant starts AT the /// migration — a cliff, a rate change, a grace period — reads the moment /// from the chain rather than from a constant somebody guessed. @@ -73,8 +74,8 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.applied(writer, migrationB), 2000); } - /// The two-argument form refuses to write at all in a block whose timestamp - /// is zero, rather than write a record that `applied` would read back as no + /// `applyMigration` refuses to write at all in a block whose timestamp is + /// zero, rather than write a record that `applied` would read back as no /// record. The head does not move and the migration can still be applied, /// which is the only outcome that leaves the namespace describing something /// true. @@ -97,7 +98,7 @@ contract MigrationRegistryApplyMigrationTest is Test { } /// The zero moment is checked after the two id refusals and before anything - /// about the namespace, on the two-argument form as on the other. An id is + /// about the namespace, on `applyMigration` as on `applyMigrationHistory`. An id is /// what a record is ABOUT, so a call with no subject has nothing to say a /// moment for; everything after describes a namespace no writable record /// will reach. @@ -565,7 +566,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// in order ARE its chain of heads. It does carry the moment, which the /// block a log entry sits in does not — that block says when the record was /// written, and the moment says when the migration ran. - function testApplyMigrationEvent(address writer, bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { + function testApplyMigrationHistoryEvent(address writer, bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); @@ -574,7 +575,7 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.recordLogs(); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); @@ -586,9 +587,9 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(entries[0].data, abi.encode(uint256(appliedAt))); } - /// The two-argument form emits the same event, carrying the block it stamped - /// — so a reader of the log never has to know which form wrote a record. - function testApplyMigrationEventFromTheBlockStampingForm(address writer, bytes32 migration, uint32 now_) external { + /// `applyMigration` emits the same event, carrying the block it stamped — so + /// a reader of the log never has to know which of the two wrote a record. + function testApplyMigrationEvent(address writer, bytes32 migration, uint32 now_) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(now_ != 0); @@ -604,7 +605,7 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(entries[0].data, abi.encode(uint256(now_))); } - /// A refused `applyMigration` emits nothing, so a failed apply can never be + /// A refused write emits nothing, so a failed apply can never be /// mistaken for a record by anything reading the logs — which for a /// re-dispatched migration is exactly the mistake that matters. function testApplyMigrationNoEventOnRevert(address writer, bytes32 migration) external { @@ -648,7 +649,7 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.recordLogs(); vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); - sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), 0); + sRegistry.applyMigrationHistory(migration, keccak256(abi.encode(migration)), 0); assertEq(vm.getRecordedLogs().length, 0); vm.recordLogs(); @@ -656,7 +657,7 @@ contract MigrationRegistryApplyMigrationTest is Test { abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, block.timestamp + 1, block.timestamp) ); vm.prank(writer); - sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), block.timestamp + 1); + sRegistry.applyMigrationHistory(migration, keccak256(abi.encode(migration)), block.timestamp + 1); assertEq(vm.getRecordedLogs().length, 0); vm.recordLogs(); @@ -664,15 +665,15 @@ contract MigrationRegistryApplyMigrationTest is Test { abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, block.timestamp - 1, 1000) ); vm.prank(writer); - sRegistry.applyMigration(migration, keccak256(abi.encode(migration)), block.timestamp - 1); + sRegistry.applyMigrationHistory(migration, keccak256(abi.encode(migration)), block.timestamp - 1); assertEq(vm.getRecordedLogs().length, 0); } - /// The three-argument form records the moment the CALLER supplied, which is + /// `applyMigrationHistory` records the moment the CALLER supplied, which is /// what lets a migration that already ran be recorded with the time it ran /// rather than the time it was written down. The block the record lands in /// is not the value, and a record written long after the fact says so. - function testApplyMigrationRecordsTheSuppliedMoment( + function testApplyMigrationHistoryRecordsTheSuppliedMoment( address writer, bytes32 migration, uint32 appliedAt, @@ -685,32 +686,32 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(writtenAt); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); assertEq(sRegistry.applied(writer, migration), appliedAt); assertTrue(sRegistry.applied(writer, migration) != block.timestamp); } /// The moment of the current block is an ordinary value for the parameter, - /// which is what a caller reaching for the three-argument form to record a + /// which is what a caller reaching for `applyMigrationHistory` to record a /// migration running now passes. - function testApplyMigrationCurrentBlockIsAccepted(address writer, bytes32 migration, uint32 now_) external { + function testApplyMigrationHistoryCurrentBlockIsAccepted(address writer, bytes32 migration, uint32 now_) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(now_ != 0); vm.warp(now_); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, block.timestamp); assertEq(sRegistry.applied(writer, migration), now_); } - /// The two forms write the SAME record when the moment is this block, down - /// to the head each was applied onto — which is what makes the two-argument - /// form the other one with today's argument rather than a second way to + /// The two writes make the SAME record when the moment is this block, down + /// to the head each was applied onto — which is what makes `applyMigration` + /// `applyMigrationHistory` with today's moment rather than a second way to /// write a record. - function testApplyMigrationBothFormsWriteTheSameRecord( + function testApplyMigrationAndHistoryWriteTheSameRecord( address writer, bytes32 migrationA, bytes32 migrationB, @@ -729,12 +730,12 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.prank(writer); stamping.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); vm.prank(writer); - supplied.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + supplied.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); vm.prank(writer); stamping.applyMigration(migrationA, migrationB); vm.prank(writer); - supplied.applyMigration(migrationA, migrationB, block.timestamp); + supplied.applyMigrationHistory(migrationA, migrationB, block.timestamp); assertEq(stamping.applied(writer, migrationB), supplied.applied(writer, migrationB)); assertEq(stamping.appliedOnto(writer, migrationB), supplied.appliedOnto(writer, migrationB)); @@ -745,7 +746,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// run, so a future one is not a late record of anything, and a consumer /// measuring an interval since the migration would be subtracting a moment /// later than the one it is measuring from. - function testApplyMigrationFutureTimestampReverts(address writer, bytes32 migration, uint32 now_, uint256 appliedAt) + function testApplyMigrationHistoryFutureTimestampReverts(address writer, bytes32 migration, uint32 now_, uint256 appliedAt) external { vm.assume(writer != address(0)); @@ -755,7 +756,7 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, appliedAt, uint256(now_))); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); assertEq(sRegistry.applied(writer, migration), 0); assertEq(sRegistry.appliedOnto(writer, migration), bytes32(0)); @@ -764,7 +765,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// One second past the current block is refused, and the current block is /// not: the boundary is the block's own timestamp, inclusive. - function testApplyMigrationFutureBoundary(address writer, bytes32 migration, uint32 now_) external { + function testApplyMigrationHistoryFutureBoundary(address writer, bytes32 migration, uint32 now_) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(now_ != 0); @@ -774,17 +775,17 @@ contract MigrationRegistryApplyMigrationTest is Test { abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) ); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_)); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, uint256(now_)); assertEq(sRegistry.applied(writer, migration), now_); } /// A supplied zero is refused, rather than written as a record that /// `applied` would read back as no record. The head does not move and the /// migration can still be applied. - function testApplyMigrationZeroTimestampReverts(address writer, bytes32 migration, uint32 now_) external { + function testApplyMigrationHistoryZeroTimestampReverts(address writer, bytes32 migration, uint32 now_) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(now_ != 0); @@ -792,13 +793,13 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 0); assertEq(sRegistry.applied(writer, migration), 0); assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 1); assertEq(sRegistry.applied(writer, migration), 1); } @@ -806,7 +807,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// refused as a moment, and every other moment is still in the future. The /// head does not move, so the namespace goes on describing something true /// and the migration is still applicable once the clock has moved. - function testApplyMigrationZeroBlockRecordsNothing(address writer, bytes32 migration, uint256 appliedAt) external { + function testApplyMigrationHistoryZeroBlockRecordsNothing(address writer, bytes32 migration, uint256 appliedAt) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); @@ -814,18 +815,18 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 0); vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, appliedAt, 0)); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); assertEq(sRegistry.applied(writer, migration), 0); assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); vm.warp(1); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 1); assertEq(sRegistry.applied(writer, migration), 1); } @@ -834,7 +835,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// whatever the namespace happens to make of it. Fuzzed over the head and /// checked against a namespace that has moved on, because a head the /// namespace happens to be at is accepted whichever check runs first. - function testApplyMigrationZeroTimestampCheckedBeforeTheNamespace( + function testApplyMigrationHistoryZeroTimestampCheckedBeforeTheNamespace( address writer, bytes32 migrationA, bytes32 migrationB, @@ -846,39 +847,39 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.assume(migrationA != migrationB); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); // Already applied, and a zero moment: told about the moment. vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 0); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 0); // A head that has moved on, and a zero moment: told about the moment. vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); - sRegistry.applyMigration(anyHead, migrationB, 0); + sRegistry.applyMigrationHistory(anyHead, migrationB, 0); } /// The two id refusals come before the moment, so a caller that has zeroed /// both an id and a moment is told about the id: an id is what the record is /// ABOUT, and a call with no subject has nothing to say a moment for. - function testApplyMigrationIdCheckedBeforeTimestamp(address writer, bytes32 anyHead) external { + function testApplyMigrationHistoryIdCheckedBeforeTimestamp(address writer, bytes32 anyHead) external { vm.assume(writer != address(0)); vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); vm.prank(writer); - sRegistry.applyMigration(anyHead, bytes32(0), 0); + sRegistry.applyMigrationHistory(anyHead, bytes32(0), 0); vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); vm.prank(writer); - sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS, 0); + sRegistry.applyMigrationHistory(anyHead, MIGRATION_HEAD_GENESIS, 0); } /// The refusals that describe the NAMESPACE come before the future-moment /// one, so a re-dispatched script is told its migration already ran, and a /// script at the wrong point in the sequence is told where the namespace is, /// rather than either of them being sent to look at a clock. - function testApplyMigrationNamespaceCheckedBeforeTheFuture( + function testApplyMigrationHistoryNamespaceCheckedBeforeTheFuture( address writer, bytes32 migrationA, bytes32 migrationB, @@ -894,27 +895,27 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(9000); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 5000); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 5000); // Already applied, and in the future: told it already ran. vm.expectRevert( abi.encodeWithSelector(IMigrationRegistryV1.MigrationAlreadyApplied.selector, writer, migrationA) ); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationA, 9001); + sRegistry.applyMigrationHistory(migrationA, migrationA, 9001); // The wrong head, and in the future: told where the namespace is. vm.expectRevert( abi.encodeWithSelector(IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, skipped, migrationA) ); vm.prank(writer); - sRegistry.applyMigration(skipped, migrationB, 9001); + sRegistry.applyMigrationHistory(skipped, migrationB, 9001); } /// A record may NOT carry a moment earlier than the record it is applied /// onto. Nothing is written and the head does not move, so the migration is /// still applicable with a moment the chain admits. - function testApplyMigrationMomentBeforeHeadReverts( + function testApplyMigrationHistoryMomentBeforeHeadReverts( address writer, bytes32 migrationA, bytes32 migrationB, @@ -930,27 +931,27 @@ contract MigrationRegistryApplyMigrationTest is Test { earlier = bound(earlier, 1, uint256(now_) - 1); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(now_)); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, uint256(now_)); vm.expectRevert( abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, earlier, uint256(now_)) ); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, earlier); + sRegistry.applyMigrationHistory(migrationA, migrationB, earlier); assertEq(sRegistry.applied(writer, migrationB), 0); assertEq(sRegistry.appliedOnto(writer, migrationB), bytes32(0)); assertEq(sRegistry.head(writer), migrationA); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, uint256(now_)); + sRegistry.applyMigrationHistory(migrationA, migrationB, uint256(now_)); assertEq(sRegistry.applied(writer, migrationB), uint256(now_)); assertEq(sRegistry.head(writer), migrationB); } /// The boundary is the head's own moment, inclusive: exactly it is /// accepted, and one second below it is refused. - function testApplyMigrationBeforeHeadBoundary( + function testApplyMigrationHistoryBeforeHeadBoundary( address writer, bytes32 migrationA, bytes32 migrationB, @@ -964,7 +965,7 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(headAppliedAt); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(headAppliedAt)); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, uint256(headAppliedAt)); vm.expectRevert( abi.encodeWithSelector( @@ -972,16 +973,16 @@ contract MigrationRegistryApplyMigrationTest is Test { ) ); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, uint256(headAppliedAt) - 1); + sRegistry.applyMigrationHistory(migrationA, migrationB, uint256(headAppliedAt) - 1); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, uint256(headAppliedAt)); + sRegistry.applyMigrationHistory(migrationA, migrationB, uint256(headAppliedAt)); assertEq(sRegistry.applied(writer, migrationB), uint256(headAppliedAt)); } /// A moment AFTER the head's is the ordinary case, and it is the moment the /// record carries rather than anything derived from the one before it. - function testApplyMigrationMomentAfterHeadIsAccepted( + function testApplyMigrationHistoryMomentAfterHeadIsAccepted( address writer, bytes32 migrationA, bytes32 migrationB, @@ -997,9 +998,9 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(second); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, uint256(first)); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, uint256(first)); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, uint256(second)); + sRegistry.applyMigrationHistory(migrationA, migrationB, uint256(second)); assertEq(sRegistry.applied(writer, migrationA), uint256(first)); assertEq(sRegistry.applied(writer, migrationB), uint256(second)); @@ -1010,7 +1011,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// onto `MIGRATION_HEAD_GENESIS`, which is refused as a migration and so /// holds no record in any namespace ever — which is why the smallest moment /// a record may carry is accepted at genesis in the latest block. - function testApplyMigrationNoMomentBoundAtGenesis(address writer, bytes32 migration, uint32 now_) external { + function testApplyMigrationHistoryNoMomentBoundAtGenesis(address writer, bytes32 migration, uint32 now_) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(now_ != 0); @@ -1019,10 +1020,10 @@ contract MigrationRegistryApplyMigrationTest is Test { // Nothing can put a record at genesis to be bounded by. vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_HEAD_GENESIS, uint256(now_)); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, MIGRATION_HEAD_GENESIS, uint256(now_)); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, 1); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 1); assertEq(sRegistry.applied(writer, migration), 1); assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); @@ -1033,7 +1034,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// ran and a script at the wrong point is told where the namespace is, /// rather than either being told about the moment of a record it was never /// going to be chained onto. The zero moment still comes before all of them. - function testApplyMigrationNamespaceCheckedBeforeTheHeadsMoment( + function testApplyMigrationHistoryNamespaceCheckedBeforeTheHeadsMoment( address writer, bytes32 migrationA, bytes32 migrationB, @@ -1049,14 +1050,14 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(9000); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 9000); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 9000); // Already applied, and before the head's moment: told it already ran. vm.expectRevert( abi.encodeWithSelector(IMigrationRegistryV1.MigrationAlreadyApplied.selector, writer, migrationA) ); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationA, 1); + sRegistry.applyMigrationHistory(migrationA, migrationA, 1); // The wrong head, and before the head's moment: told where the // namespace is. @@ -1064,22 +1065,22 @@ contract MigrationRegistryApplyMigrationTest is Test { abi.encodeWithSelector(IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, skipped, migrationA) ); vm.prank(writer); - sRegistry.applyMigration(skipped, migrationB, 1); + sRegistry.applyMigrationHistory(skipped, migrationB, 1); // A zero moment, which is also before the head's: told about the zero. vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, 0); + sRegistry.applyMigrationHistory(migrationA, migrationB, 0); // With nothing else wrong, the head's moment is what refuses it. vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1, 9000)); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, 1); + sRegistry.applyMigrationHistory(migrationA, migrationB, 1); } /// A head's moment bounds only its own namespace. Another writer at genesis /// is bounded by nothing, whatever moment the first namespace recorded. - function testApplyMigrationHeadMomentIsPerWriter( + function testApplyMigrationHistoryHeadMomentIsPerWriter( address writer, address other, bytes32 migrationA, @@ -1094,24 +1095,24 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(9000); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 9000); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 9000); vm.prank(other); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB, 1); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationB, 1); assertEq(sRegistry.applied(other, migrationB), 1); // And the other namespace's own head bounds it from there. vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1, 9000)); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, 1); + sRegistry.applyMigrationHistory(migrationA, migrationB, 1); } /// Two records may carry the SAME moment. Two migrations applied in one /// transaction share a block, and two backfilled migrations known only to /// the same day share a moment; the chain is what orders them, so the /// moments are not asked to. - function testApplyMigrationMomentsMayBeEqual( + function testApplyMigrationHistoryMomentsMayBeEqual( address writer, bytes32 migrationA, bytes32 migrationB, @@ -1125,9 +1126,9 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(appliedAt); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, appliedAt); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, appliedAt); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, appliedAt); + sRegistry.applyMigrationHistory(migrationA, migrationB, appliedAt); assertEq(sRegistry.applied(writer, migrationA), appliedAt); assertEq(sRegistry.applied(writer, migrationB), appliedAt); @@ -1161,7 +1162,7 @@ contract MigrationRegistryApplyMigrationTest is Test { /// The chain is the order the migrations ran in, and it says so where the /// moments cannot. Three records carrying one moment walk back from the head /// in the order they were APPLIED, ending at genesis. - function testApplyMigrationChainIsTheOrderWhateverTheMoments( + function testApplyMigrationHistoryChainIsTheOrderWhateverTheMoments( address writer, bytes32 migrationA, bytes32 migrationB, @@ -1177,11 +1178,11 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.warp(9000); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 3000); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 3000); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB, 3000); + sRegistry.applyMigrationHistory(migrationA, migrationB, 3000); vm.prank(writer); - sRegistry.applyMigration(migrationB, migrationC, 3000); + sRegistry.applyMigrationHistory(migrationB, migrationC, 3000); // Every moment is the same as the one before it, so nothing about the // order can be read out of them. diff --git a/test/src/concrete/MigrationRegistryHead.t.sol b/test/src/concrete/MigrationRegistryHead.t.sol index 4702534..f3f594e 100644 --- a/test/src/concrete/MigrationRegistryHead.t.sol +++ b/test/src/concrete/MigrationRegistryHead.t.sol @@ -11,7 +11,7 @@ import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; /// @title MigrationRegistryHeadTest /// @notice A test suite for `MigrationRegistry.head`: where a namespace is, what /// an empty one answers, that the answer is never a value that is not a head, -/// and that it is the same answer `applyMigration` checks against. +/// and that it is the same answer a write checks against. contract MigrationRegistryHeadTest is Test { /// The registry under test. Stateful, so a fresh one per test. MigrationRegistry internal sRegistry; @@ -70,8 +70,8 @@ contract MigrationRegistryHeadTest is Test { assertEq(sRegistry.head(other), MIGRATION_HEAD_GENESIS); } - /// The head `head` reports is exactly the head `applyMigration` demands: - /// whatever this answers is accepted, and it is the only value that is. The + /// The head `head` reports is exactly the head a write demands: whatever + /// this answers is accepted, and it is the only value that is. The /// two go through one translation of an empty namespace, so they cannot /// disagree about where one is. function testHeadIsWhatApplyMigrationAccepts(address writer, bytes32 migrationA, bytes32 migrationB) external { diff --git a/test/src/lib/LibMigrationRegistry.t.sol b/test/src/lib/LibMigrationRegistry.t.sol index 63d7721..ac5ac3b 100644 --- a/test/src/lib/LibMigrationRegistry.t.sol +++ b/test/src/lib/LibMigrationRegistry.t.sol @@ -87,13 +87,13 @@ contract LibMigrationRegistryTest is Test { LibMigrationRegistry.applyMigration(expectedHead, migration); } - /// External wrapper for the `appliedAt` form of `applyMigration` so that - /// `vm.expectRevert` works at the correct call depth. + /// External wrapper for `applyMigrationHistory` so that `vm.expectRevert` + /// works at the correct call depth. /// @param expectedHead The head this contract believes it is at. /// @param migration The migration to apply. /// @param appliedAt The moment to record against it. - function externalApplyMigration(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { - LibMigrationRegistry.applyMigration(expectedHead, migration, appliedAt); + function externalApplyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + LibMigrationRegistry.applyMigrationHistory(expectedHead, migration, appliedAt); } /// The Zoltu deploy really does land the registry on its pinned address @@ -451,9 +451,9 @@ contract LibMigrationRegistryTest is Test { } /// A migration recorded with a supplied moment reads back as that moment - /// through the library, so what the `appliedAt` form writes is what + /// through the library, so what `applyMigrationHistory` writes is what /// `applied` finds — and it is not the block the write landed in. - function testApplyMigrationWithAppliedAtThenApplied(bytes32 migration, uint32 appliedAt, uint32 writtenAt) + function testApplyMigrationHistoryThenApplied(bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { LibMigrationFuzz.assumeMigration(vm, migration); @@ -462,7 +462,7 @@ contract LibMigrationRegistryTest is Test { deployRegistry(); vm.warp(writtenAt); - LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + LibMigrationRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); assertEq(LibMigrationRegistry.applied(address(this), migration), appliedAt); } @@ -472,7 +472,7 @@ contract LibMigrationRegistryTest is Test { /// whose migrations ran before this registry reached the chain recording /// what actually happened rather than the day it got round to writing it /// down. - function testApplyMigrationBackfillsAHistoricalSequence(bytes32 migrationA, bytes32 migrationB, bytes32 migrationC) + function testApplyMigrationHistoryBackfillsASequence(bytes32 migrationA, bytes32 migrationB, bytes32 migrationC) external { LibMigrationFuzz.assumeMigration(vm, migrationA); @@ -484,9 +484,9 @@ contract LibMigrationRegistryTest is Test { deployRegistry(); vm.warp(9000); - LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 1000); - LibMigrationRegistry.applyMigration(migrationA, migrationB, 2000); - LibMigrationRegistry.applyMigration(migrationB, migrationC, 3000); + LibMigrationRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 1000); + LibMigrationRegistry.applyMigrationHistory(migrationA, migrationB, 2000); + LibMigrationRegistry.applyMigrationHistory(migrationB, migrationC, 3000); assertEq(LibMigrationRegistry.applied(address(this), migrationA), 1000); assertEq(LibMigrationRegistry.applied(address(this), migrationB), 2000); @@ -503,19 +503,20 @@ contract LibMigrationRegistryTest is Test { } /// The registry's zero-moment refusal arrives unmodified through - /// `applyMigration`, so a consumer that left its `appliedAt` uninitialised - /// is told so rather than writing a record that reads back as none. - function testApplyMigrationZeroTimestampReverts(bytes32 migration) external { + /// `applyMigrationHistory`, so a consumer that left its `appliedAt` + /// uninitialised is told so rather than writing a record that reads back as + /// none. + function testApplyMigrationHistoryZeroTimestampReverts(bytes32 migration) external { LibMigrationFuzz.assumeMigration(vm, migration); deployRegistry(); vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); - this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, 0); + this.externalApplyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 0); } /// The registry's future-moment refusal arrives unmodified through - /// `applyMigration`. - function testApplyMigrationFutureTimestampReverts(bytes32 migration, uint32 now_) external { + /// `applyMigrationHistory`. + function testApplyMigrationHistoryFutureTimestampReverts(bytes32 migration, uint32 now_) external { LibMigrationFuzz.assumeMigration(vm, migration); deployRegistry(); vm.warp(now_); @@ -523,36 +524,36 @@ contract LibMigrationRegistryTest is Test { vm.expectRevert( abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) ); - this.externalApplyMigration(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); + this.externalApplyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); } /// The registry's before-the-head refusal arrives unmodified through - /// `applyMigration`, so a consumer backfilling its history out of order is - /// told which moment it contradicted. - function testApplyMigrationTimestampBeforeHeadReverts(bytes32 migrationA, bytes32 migrationB) external { + /// `applyMigrationHistory`, so a consumer backfilling its history out of + /// order is told which moment it contradicted. + function testApplyMigrationHistoryTimestampBeforeHeadReverts(bytes32 migrationA, bytes32 migrationB) external { LibMigrationFuzz.assumeMigration(vm, migrationA); LibMigrationFuzz.assumeMigration(vm, migrationB); vm.assume(migrationA != migrationB); deployRegistry(); vm.warp(9000); - LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA, 2000); + LibMigrationRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 2000); vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1999, 2000)); - this.externalApplyMigration(migrationA, migrationB, 1999); + this.externalApplyMigrationHistory(migrationA, migrationB, 1999); } - /// The namespace of the `appliedAt` form is the calling CONTRACT too, so a - /// consumer backfilling its history writes its own namespace and nobody + /// The namespace of `applyMigrationHistory` is the calling CONTRACT too, so + /// a consumer backfilling its history writes its own namespace and nobody /// else's. - function testApplyMigrationWithAppliedAtLandsUnderTheCallingContract(bytes32 migration, uint32 appliedAt) external { + function testApplyMigrationHistoryLandsUnderTheCallingContract(bytes32 migration, uint32 appliedAt) external { LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); deployRegistry(); vm.warp(appliedAt); MockMigrationApplier applier = new MockMigrationApplier(); - applier.applyMigration(MIGRATION_HEAD_GENESIS, migration, appliedAt); + applier.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); assertEq(LibMigrationRegistry.applied(address(applier), migration), appliedAt); assertEq(LibMigrationRegistry.appliedOnto(address(applier), migration), MIGRATION_HEAD_GENESIS); @@ -649,9 +650,9 @@ contract LibMigrationRegistryTest is Test { this.externalAppliedOnto(writer, migration); } - /// The `appliedAt` form checks the code hash too, so a backfill is never + /// `applyMigrationHistory` checks the code hash too, so a backfill is never /// written to a chain with no registry. - function testApplyMigrationWithAppliedAtNoRegistry(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) + function testApplyMigrationHistoryNoRegistry(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { assertEq(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS.code.length, 0); @@ -663,11 +664,11 @@ contract LibMigrationRegistryTest is Test { bytes32(0) ) ); - this.externalApplyMigration(expectedHead, migration, appliedAt); + this.externalApplyMigrationHistory(expectedHead, migration, appliedAt); } /// Nor into ordinary occupying code. - function testApplyMigrationWithAppliedAtWrongCode( + function testApplyMigrationHistoryWrongCode( bytes32 expectedHead, bytes32 migration, uint256 appliedAt, @@ -684,7 +685,7 @@ contract LibMigrationRegistryTest is Test { keccak256(code) ) ); - this.externalApplyMigration(expectedHead, migration, appliedAt); + this.externalApplyMigrationHistory(expectedHead, migration, appliedAt); } /// Nor into a delegated account. @@ -692,7 +693,7 @@ contract LibMigrationRegistryTest is Test { /// @param migration The migration being applied. /// @param appliedAt The moment being recorded. /// @param delegate The account the registry address is delegated to. - function testApplyMigrationWithAppliedAtDelegatedCode( + function testApplyMigrationHistoryDelegatedCode( bytes32 expectedHead, bytes32 migration, uint256 appliedAt, @@ -709,6 +710,6 @@ contract LibMigrationRegistryTest is Test { keccak256(designator) ) ); - this.externalApplyMigration(expectedHead, migration, appliedAt); + this.externalApplyMigrationHistory(expectedHead, migration, appliedAt); } } From bcde3a525a259482cb09caafb6aebee76e53d2f7 Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 18 Aug 2026 11:27:37 +0000 Subject: [PATCH 8/8] Regenerate the candidate snapshot for the renamed selector applyMigrationHistory changes the creation code, so the pinned bytecode hash and deterministic address move with it. Co-Authored-By: Claude Opus 5 (1M context) --- src/generated/candidate/MigrationRegistry.sol | 8 ++++---- .../MigrationRegistryApplyMigration.t.sol | 17 ++++++++++++----- test/src/lib/LibMigrationRegistry.t.sol | 8 ++------ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/generated/candidate/MigrationRegistry.sol b/src/generated/candidate/MigrationRegistry.sol index 9b44db5..a031048 100644 --- a/src/generated/candidate/MigrationRegistry.sol +++ b/src/generated/candidate/MigrationRegistry.sol @@ -5,19 +5,19 @@ pragma solidity ^0.8.25; // THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0xaa8bc7e0eab188014af2732a60e9109f17183b2623604c56557b7b8b65f9b0e4); +bytes32 constant BYTECODE_HASH = bytes32(0xb9a3ed001b8e4acff189216b31f4598759df873dbb9f7c114bf2995941e3723c); /// @dev The deterministic deploy address of the contract when deployed via /// the Zoltu factory. -address constant DEPLOYED_ADDRESS = address(0x0253603e9b8b2BDCd57d8A7D96b6c989C736D321); +address constant DEPLOYED_ADDRESS = address(0x13175E90969fE4977834210F25Fb3ED3ABBA64C7); /// @dev The creation bytecode of the contract. bytes constant CREATION_CODE = - hex"6080604052348015600e575f80fd5b506106428061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610064575f3560e01c8063a56d39e01161004d578063a56d39e0146100a2578063bda4fec5146100b5578063e2930416146100c8575f80fd5b80635965d2a6146100685780638c3f2ba51461007d575b5f80fd5b61007b610076366004610590565b6100db565b005b61009061008b3660046105e1565b6100eb565b60405190815260200160405180910390f35b61007b6100b0366004610609565b61012c565b6100906100c3366004610629565b61013b565b6100906100d63660046105e1565b6101e3565b6100e6838383610221565b505050565b5f6100f683836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052206001015490565b610137828242610221565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610189576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156101ba57806101dc565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f6101ee83836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b81610258576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036102b1576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f036102ea576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f9081526020818152604080832085845290915290205415610347576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6103513361013b565b905080841461039c576040517facbe6852000000000000000000000000000000000000000000000000000000008152336004820152602481018590526044810182905260640161033e565b335f90815260208181526040808320848452909152902054808310156103f8576040517f29d1b347000000000000000000000000000000000000000000000000000000008152600481018490526024810182905260440161033e565b4283111561043b576040517f8fdce34f0000000000000000000000000000000000000000000000000000000081526004810184905242602482015260440161033e565b6040805180820182528481526020808201858152335f8181528084528581208a82528452858120945185559151600194850155808252928252839020879055915185815286927f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b73ffffffffffffffffffffffffffffffffffffffff8216610500576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80610537576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610137576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805f606084860312156105a2575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff811681146105dc575f80fd5b919050565b5f80604083850312156105f2575f80fd5b6105fb836105b9565b946020939093013593505050565b5f806040838503121561061a575f80fd5b50508035926020909101359150565b5f60208284031215610639575f80fd5b6101dc826105b956"; + hex"6080604052348015600e575f80fd5b506106428061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610064575f3560e01c8063a56d39e01161004d578063a56d39e0146100a2578063bda4fec5146100b5578063e2930416146100c8575f80fd5b8063280a6565146100685780638c3f2ba51461007d575b5f80fd5b61007b610076366004610590565b6100db565b005b61009061008b3660046105e1565b6100eb565b60405190815260200160405180910390f35b61007b6100b0366004610609565b61012c565b6100906100c3366004610629565b61013b565b6100906100d63660046105e1565b6101e3565b6100e6838383610221565b505050565b5f6100f683836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052206001015490565b610137828242610221565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610189576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156101ba57806101dc565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f6101ee83836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b81610258576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036102b1576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f036102ea576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f9081526020818152604080832085845290915290205415610347576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6103513361013b565b905080841461039c576040517facbe6852000000000000000000000000000000000000000000000000000000008152336004820152602481018590526044810182905260640161033e565b335f90815260208181526040808320848452909152902054808310156103f8576040517f29d1b347000000000000000000000000000000000000000000000000000000008152600481018490526024810182905260440161033e565b4283111561043b576040517f8fdce34f0000000000000000000000000000000000000000000000000000000081526004810184905242602482015260440161033e565b6040805180820182528481526020808201858152335f8181528084528581208a82528452858120945185559151600194850155808252928252839020879055915185815286927f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b73ffffffffffffffffffffffffffffffffffffffff8216610500576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80610537576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610137576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805f606084860312156105a2575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff811681146105dc575f80fd5b919050565b5f80604083850312156105f2575f80fd5b6105fb836105b9565b946020939093013593505050565b5f806040838503121561061a575f80fd5b50508035926020909101359150565b5f60208284031215610639575f80fd5b6101dc826105b956"; /// @dev The runtime bytecode of the contract. bytes constant RUNTIME_CODE = - hex"608060405234801561000f575f80fd5b5060043610610064575f3560e01c8063a56d39e01161004d578063a56d39e0146100a2578063bda4fec5146100b5578063e2930416146100c8575f80fd5b80635965d2a6146100685780638c3f2ba51461007d575b5f80fd5b61007b610076366004610590565b6100db565b005b61009061008b3660046105e1565b6100eb565b60405190815260200160405180910390f35b61007b6100b0366004610609565b61012c565b6100906100c3366004610629565b61013b565b6100906100d63660046105e1565b6101e3565b6100e6838383610221565b505050565b5f6100f683836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052206001015490565b610137828242610221565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610189576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156101ba57806101dc565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f6101ee83836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b81610258576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036102b1576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f036102ea576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f9081526020818152604080832085845290915290205415610347576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6103513361013b565b905080841461039c576040517facbe6852000000000000000000000000000000000000000000000000000000008152336004820152602481018590526044810182905260640161033e565b335f90815260208181526040808320848452909152902054808310156103f8576040517f29d1b347000000000000000000000000000000000000000000000000000000008152600481018490526024810182905260440161033e565b4283111561043b576040517f8fdce34f0000000000000000000000000000000000000000000000000000000081526004810184905242602482015260440161033e565b6040805180820182528481526020808201858152335f8181528084528581208a82528452858120945185559151600194850155808252928252839020879055915185815286927f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b73ffffffffffffffffffffffffffffffffffffffff8216610500576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80610537576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610137576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805f606084860312156105a2575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff811681146105dc575f80fd5b919050565b5f80604083850312156105f2575f80fd5b6105fb836105b9565b946020939093013593505050565b5f806040838503121561061a575f80fd5b50508035926020909101359150565b5f60208284031215610639575f80fd5b6101dc826105b956"; + hex"608060405234801561000f575f80fd5b5060043610610064575f3560e01c8063a56d39e01161004d578063a56d39e0146100a2578063bda4fec5146100b5578063e2930416146100c8575f80fd5b8063280a6565146100685780638c3f2ba51461007d575b5f80fd5b61007b610076366004610590565b6100db565b005b61009061008b3660046105e1565b6100eb565b60405190815260200160405180910390f35b61007b6100b0366004610609565b61012c565b6100906100c3366004610629565b61013b565b6100906100d63660046105e1565b6101e3565b6100e6838383610221565b505050565b5f6100f683836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052206001015490565b610137828242610221565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610189576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156101ba57806101dc565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f6101ee83836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b81610258576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036102b1576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f036102ea576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f9081526020818152604080832085845290915290205415610347576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6103513361013b565b905080841461039c576040517facbe6852000000000000000000000000000000000000000000000000000000008152336004820152602481018590526044810182905260640161033e565b335f90815260208181526040808320848452909152902054808310156103f8576040517f29d1b347000000000000000000000000000000000000000000000000000000008152600481018490526024810182905260440161033e565b4283111561043b576040517f8fdce34f0000000000000000000000000000000000000000000000000000000081526004810184905242602482015260440161033e565b6040805180820182528481526020808201858152335f8181528084528581208a82528452858120945185559151600194850155808252928252839020879055915185815286927f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b73ffffffffffffffffffffffffffffffffffffffff8216610500576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80610537576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610137576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805f606084860312156105a2575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff811681146105dc575f80fd5b919050565b5f80604083850312156105f2575f80fd5b6105fb836105b9565b946020939093013593505050565b5f806040838503121561061a575f80fd5b50508035926020909101359150565b5f60208284031215610639575f80fd5b6101dc826105b956"; /// @dev The addresses that MUST already have code on a network before /// this release can be broadcast there, `abi.encode`d as an `address[]` diff --git a/test/src/concrete/MigrationRegistryApplyMigration.t.sol b/test/src/concrete/MigrationRegistryApplyMigration.t.sol index d3f1aeb..df0ebe7 100644 --- a/test/src/concrete/MigrationRegistryApplyMigration.t.sol +++ b/test/src/concrete/MigrationRegistryApplyMigration.t.sol @@ -566,7 +566,9 @@ contract MigrationRegistryApplyMigrationTest is Test { /// in order ARE its chain of heads. It does carry the moment, which the /// block a log entry sits in does not — that block says when the record was /// written, and the moment says when the migration ran. - function testApplyMigrationHistoryEvent(address writer, bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { + function testApplyMigrationHistoryEvent(address writer, bytes32 migration, uint32 appliedAt, uint32 writtenAt) + external + { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); @@ -746,9 +748,12 @@ contract MigrationRegistryApplyMigrationTest is Test { /// run, so a future one is not a late record of anything, and a consumer /// measuring an interval since the migration would be subtracting a moment /// later than the one it is measuring from. - function testApplyMigrationHistoryFutureTimestampReverts(address writer, bytes32 migration, uint32 now_, uint256 appliedAt) - external - { + function testApplyMigrationHistoryFutureTimestampReverts( + address writer, + bytes32 migration, + uint32 now_, + uint256 appliedAt + ) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.warp(now_); @@ -807,7 +812,9 @@ contract MigrationRegistryApplyMigrationTest is Test { /// refused as a moment, and every other moment is still in the future. The /// head does not move, so the namespace goes on describing something true /// and the migration is still applicable once the clock has moved. - function testApplyMigrationHistoryZeroBlockRecordsNothing(address writer, bytes32 migration, uint256 appliedAt) external { + function testApplyMigrationHistoryZeroBlockRecordsNothing(address writer, bytes32 migration, uint256 appliedAt) + external + { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); diff --git a/test/src/lib/LibMigrationRegistry.t.sol b/test/src/lib/LibMigrationRegistry.t.sol index ac5ac3b..0956c92 100644 --- a/test/src/lib/LibMigrationRegistry.t.sol +++ b/test/src/lib/LibMigrationRegistry.t.sol @@ -453,9 +453,7 @@ contract LibMigrationRegistryTest is Test { /// A migration recorded with a supplied moment reads back as that moment /// through the library, so what `applyMigrationHistory` writes is what /// `applied` finds — and it is not the block the write landed in. - function testApplyMigrationHistoryThenApplied(bytes32 migration, uint32 appliedAt, uint32 writtenAt) - external - { + function testApplyMigrationHistoryThenApplied(bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); vm.assume(writtenAt > appliedAt); @@ -652,9 +650,7 @@ contract LibMigrationRegistryTest is Test { /// `applyMigrationHistory` checks the code hash too, so a backfill is never /// written to a chain with no registry. - function testApplyMigrationHistoryNoRegistry(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) - external - { + function testApplyMigrationHistoryNoRegistry(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { assertEq(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS.code.length, 0); vm.expectRevert(