Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/lib/LibMigrationInvariant.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ library LibMigrationInvariant {

/// @notice `address` overload. Casts each address to `bytes32` under the
/// hood via `uint160`.
///
/// The zero address is rejected as `actual` UNCONDITIONALLY — on either
/// side of the deadline, even when a migration side equals zero. Every
/// address-valued surface this lib asserts (owners, role holders) has
/// zero as its default/renounced/unset value, making it the single most
/// likely drift reading — so it is its own checked case, never an
/// accepted side. In particular an unhydrated `post` pin of zero means
/// "post-state unreachable", and a renounced-to-zero owner must trip
/// drift rather than read as "already migrated".
/// @param label Human-readable identifier for the invariant surfaced in
/// revert data.
/// @param actual The value read from the live chain.
Expand All @@ -83,6 +92,11 @@ library LibMigrationInvariant {
internal
view
{
if (actual == address(0)) {
revert MigrationStateDrift(
label, bytes32(uint256(uint160(pre))), bytes32(uint256(uint160(post))), bytes32(0)
);
}
assertMigration(
label,
bytes32(uint256(uint160(actual))),
Expand Down
2 changes: 1 addition & 1 deletion src/lib/LibTimelockInvariants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ library LibTimelockInvariants {

/// @notice The ST0x governance timelock on **Base**. Equals
/// `expectedTimelockAddress(STOX_TOKEN_OWNER_SAFE)`, which
/// `testPinsMatchDerivedAddressesOnceHydrated` asserts.
/// `testPinsMatchDerivedAddresses` asserts.
/// https://basescan.org/address/0xdb4b2187a685310e6b64170c97b80e90dd4a9b71
address internal constant STOX_GOVERNANCE_TIMELOCK = address(0xdb4b2187A685310E6b64170c97B80E90DD4a9B71);

Expand Down
45 changes: 45 additions & 0 deletions test/src/lib/LibMigrationInvariant.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,49 @@ contract LibMigrationInvariantTest is Test {
);
harness.callAssertMigrationUint256(LABEL, 1, 1, 3, DEADLINE);
}

/// @notice The zero address is rejected as `actual` on BOTH sides of
/// the deadline, even when a migration side equals zero. Zero is
/// Solidity's default-everything value — renounced ownership, unset
/// storage, a failed lookup — the single most likely drift reading, so
/// it is its own checked case, never an accepted side. The load-bearing
/// branch is the third: with an UNHYDRATED `post` pin of zero, a
/// renounced-to-zero owner must trip drift rather than read as
/// "already migrated".
function testAddressZeroActualAlwaysRejected() external {
address pre = address(0xAAAA);
address post = address(0xBBBB);

vm.warp(DEADLINE - 1);
vm.expectRevert(
abi.encodeWithSelector(
MigrationStateDrift.selector,
LABEL,
bytes32(uint256(uint160(pre))),
bytes32(uint256(uint160(post))),
bytes32(0)
)
);
harness.callAssertMigrationAddress(LABEL, address(0), pre, post, DEADLINE);

vm.warp(DEADLINE);
vm.expectRevert(
abi.encodeWithSelector(
MigrationStateDrift.selector,
LABEL,
bytes32(uint256(uint160(pre))),
bytes32(uint256(uint160(post))),
bytes32(0)
)
);
harness.callAssertMigrationAddress(LABEL, address(0), pre, post, DEADLINE);

vm.warp(DEADLINE - 1);
vm.expectRevert(
abi.encodeWithSelector(
MigrationStateDrift.selector, LABEL, bytes32(uint256(uint160(pre))), bytes32(0), bytes32(0)
)
);
harness.callAssertMigrationAddress(LABEL, address(0), pre, address(0), DEADLINE);
}
}
45 changes: 21 additions & 24 deletions test/src/lib/LibTimelockInvariants.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -240,30 +240,27 @@ contract LibTimelockInvariantsTest is Test {
harness.callTimelockForChainId(31337);
}

/// @notice The hydration contract for the per-chain pins: while a pin is
/// a placeholder it must be zero; once hydrated it must equal the
/// derived Zoltu address for that chain's Safe. Both directions are
/// asserted here so the post-deploy pin PR turns this from the
/// placeholder branch to the equality branch with no test change.
function testPinsMatchDerivedAddressesOnceHydrated() external pure {
address basePin = LibTimelockInvariants.STOX_GOVERNANCE_TIMELOCK;
if (basePin != address(0)) {
assertEq(basePin, LibTimelockInvariants.expectedTimelockAddress(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE));
}
address ethereumPin = LibTimelockInvariants.STOX_GOVERNANCE_TIMELOCK_ETHEREUM;
if (ethereumPin != address(0)) {
assertEq(
ethereumPin,
LibTimelockInvariants.expectedTimelockAddress(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_ETHEREUM)
);
}
address hyperevmPin = LibTimelockInvariants.STOX_GOVERNANCE_TIMELOCK_HYPEREVM;
if (hyperevmPin != address(0)) {
assertEq(
hyperevmPin,
LibTimelockInvariants.expectedTimelockAddress(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_HYPEREVM)
);
}
/// @notice Every per-chain pin equals the Zoltu address derived from the
/// frozen creation code and that chain's Safe — UNCONDITIONALLY. The
/// not-zero guards that let this test ride through the hydration window
/// are retired with it: all three deploys have executed and the pins are
/// deploy history, so a zeroed or drifted pin must fail loudly here
/// rather than silently skip. A future chain's placeholder phase gets
/// its own guarded branch when its arm is added; these three never go
/// back.
function testPinsMatchDerivedAddresses() external pure {
assertEq(
LibTimelockInvariants.STOX_GOVERNANCE_TIMELOCK,
LibTimelockInvariants.expectedTimelockAddress(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE)
);
assertEq(
LibTimelockInvariants.STOX_GOVERNANCE_TIMELOCK_ETHEREUM,
LibTimelockInvariants.expectedTimelockAddress(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_ETHEREUM)
);
assertEq(
LibTimelockInvariants.STOX_GOVERNANCE_TIMELOCK_HYPEREVM,
LibTimelockInvariants.expectedTimelockAddress(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_HYPEREVM)
);
}

/// @notice Every chain with a pinned token-owner Safe resolves through
Expand Down
Loading