Skip to content
Merged
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
5 changes: 0 additions & 5 deletions .soldeerignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
.DS_Store
.coderabbitai.yaml
.gas-snapshot
.git
.github
.gitignore
.gitmodules
.pre-commit-config.yaml
.soldeerignore
.vscode
Expand All @@ -15,12 +13,9 @@ CLAUDE.md
/docs
/flake.lock
/flake.nix
/foundry.lock
/foundry.toml
/lib
/out
/remappings.txt
/slither.config.json
/soldeer.lock
/target
/test
3 changes: 0 additions & 3 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ version = 1

[[annotations]]
path = [
".gas-snapshot",
".github/**/",
".gitignore",
".gitmodules",
".soldeerignore",
"audit/**/",
"README.md",
"flake.lock",
"flake.nix",
"foundry.toml",
"foundry.lock",
"remappings.txt",
"slither.config.json",
"REUSE.toml",
Expand Down
6 changes: 0 additions & 6 deletions foundry.lock

This file was deleted.

61 changes: 61 additions & 0 deletions test/lib/LibAccountCode.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity ^0.8.25;

/// @dev The two bytes that open an EIP-7702 delegation designator, and the only
/// thing that separates one from ordinary contract code.
bytes2 constant DELEGATION_PREFIX = 0xef01;

/// @dev The one length EIP-7702 gives a delegation designator: the three-byte
/// `0xef0100` header and a twenty-byte address.
uint256 constant DELEGATION_DESIGNATOR_LENGTH = 23;

/// @title LibAccountCode
/// @notice What an account's code is allowed to BE, for tests that fuzz whatever
/// occupies a pinned address.
///
/// An account holds one of exactly two kinds of code, and a test that fuzzes
/// `bytes` alone does not have that domain — it has every byte string, most of
/// which no EVM can put in an account:
///
/// 1. ordinary contract code, which is any byte string that does not open with
/// `DELEGATION_PREFIX`;
/// 2. an EIP-7702 delegation designator, which opens with that prefix and is
/// `DELEGATION_DESIGNATOR_LENGTH` bytes, never any other length.
///
/// `vm.etch` enforces exactly that: a byte string opening `0xef01` at any other
/// length is refused with `Eip7702 is not 23 bytes long`, so a fuzzer that
/// reaches one fails the run on the cheatcode rather than finding anything
/// about the code under test. That failure is seed-dependent, so it arrives as a
/// test that was green yesterday and is red today over a fixture nobody touched.
///
/// The two kinds are covered by two tests rather than one, split on
/// `hasDelegationPrefix`, because they are different kinds of account and not
/// merely different values. The designator carries 23 bytes while the account
/// executes whatever the delegate holds, so it is the shape a test over
/// arbitrary `bytes` can never construct and the one an attacker reaches for.
library LibAccountCode {
/// Whether `code` opens with the delegation designator prefix, and is
/// therefore in kind 2 rather than kind 1.
///
/// The prefix alone decides it. A byte string that opens `0xef01` at the
/// wrong length is not ordinary code that happens to start awkwardly — it is
/// a malformed designator, which is why `vm.etch` refuses it rather than
/// storing it.
/// @param code The candidate account code.
/// @return True when `code` is in the delegation-designator family.
function hasDelegationPrefix(bytes memory code) internal pure returns (bool) {
return code.length >= 2 && code[0] == DELEGATION_PREFIX[0] && code[1] == DELEGATION_PREFIX[1];
}

/// The delegation designator that points an account at `delegate`.
///
/// `delegate` of zero is the CLEARING form: it leaves the account with no
/// code at all rather than 23 bytes of designator, so it is the empty-account
/// case and not this one. Callers fuzzing a delegate assume it away.
/// @param delegate The account being delegated to.
/// @return designator The 23-byte designator.
function delegationDesignator(address delegate) internal pure returns (bytes memory designator) {
designator = abi.encodePacked(DELEGATION_PREFIX, bytes1(0), delegate);
}
}
28 changes: 25 additions & 3 deletions test/src/abstract/RainDeployVerifyChain.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ import {
/// signal this group exists to raise for its own repo, and precisely the wrong
/// thing to import into this one.
///
/// ## Absence is CONSTRUCTED here, never assumed
///
/// Every state this contract puts the matrix in is one it etches: present, wrong
/// code, and — the case below — absent. Absent is `vm.etch(addr, hex"")` on an
/// account that STAYS persistent, so each fork the matrix creates carries the
/// empty account no matter what that network holds.
///
/// Revoking persistence instead hands the matrix the real network, which turns
/// "nothing is deployed at this address" from something the fixture arranged
/// into a claim about the world. That claim is false here: the exemplar's
/// addresses come from `src/generated/candidate/`, which is exactly what
/// `Manual sol artifacts` broadcasts, and `AddressRegistry` is live on all five
/// supported networks. A negative case resting on it asserts nothing and
/// reports `next call did not revert as expected` — a fixture that only worked
/// while the repo had not yet done the thing it exists to do.
///
/// Pointing the fixture at a mock nobody deploys would move that dependency
/// rather than remove it: the Zoltu factory is permissionless, so no address is
/// structurally unoccupiable. Etching the state the assertion is about is what
/// removes it.
///
/// The etch does not make the passing case circular. It writes the runtime code
/// the compiler emits, while the expectation is derived independently by running
/// the recorded CREATION code through the Zoltu factory. That the two agree is
Expand Down Expand Up @@ -75,8 +96,9 @@ contract RainDeployVerifyChainTest is ExampleDeploySuites, RainDeployVerifyChain
/// release that reached four chains of five, or a chain added after a
/// release that therefore never got it, is invisible to every other check.
function testChainNotDeployedReverts() external {
// Present locally, but no longer carried onto forks.
vm.revokePersistent(ADDRESS_REGISTRY_DEPLOYED_ADDRESS);
// Emptied, and left persistent, so every fork carries an empty account
// here rather than whatever the network holds.
vm.etch(ADDRESS_REGISTRY_DEPLOYED_ADDRESS, hex"");

vm.expectRevert(
abi.encodeWithSelector(
Expand All @@ -93,7 +115,7 @@ contract RainDeployVerifyChainTest is ExampleDeploySuites, RainDeployVerifyChain
/// reaches. The version missing here is the LAST one, so a matrix that
/// stopped after the first version would pass.
function testChainNotDeployedRevertsForALaterSuite() external {
vm.revokePersistent(secondDeployedAddress());
vm.etch(secondDeployedAddress(), hex"");

vm.expectRevert(
abi.encodeWithSelector(
Expand Down
23 changes: 20 additions & 3 deletions test/src/abstract/RainDeployVerifyChainCandidate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,32 @@ contract RainDeployVerifyChainCandidateTest is RainDeployVerifyChain {
});
}

/// The RELEASE is live everywhere. The candidate is not touched.
/// The RELEASE is live everywhere and the CANDIDATE is nowhere, both by
/// construction: each account is etched to the state this contract is about
/// and made persistent, so every fork carries that state rather than
/// whatever the network holds.
///
/// The candidate's absence is etched for the same reason the release's
/// presence is. Reading it off the real chain would make the premise of
/// `testChainIgnoresAnUndeployedCandidate` a claim about the world, and the
/// Zoltu factory is permissionless — anyone can put `MockDeployableV2` at
/// that address, and this contract would then assert nothing while still
/// passing right up until they did.
function setUp() external {
vm.etch(ADDRESS_REGISTRY_DEPLOYED_ADDRESS, ADDRESS_REGISTRY_RUNTIME_CODE);
vm.makePersistent(ADDRESS_REGISTRY_DEPLOYED_ADDRESS);

address candidateAddress = LibRainDeploy.zoltuAddress(type(MockDeployableV2).creationCode);
vm.etch(candidateAddress, hex"");
vm.makePersistent(candidateAddress);
}

/// The matrix MUST pass with the candidate on no network at all, and the
/// candidate MUST really be absent — otherwise this passes for the wrong
/// reason and says nothing about the scope.
/// candidate MUST really be absent on each of them — otherwise this passes
/// for the wrong reason and says nothing about the scope. The per-fork
/// reads are what say `setUp`'s construction reached every fork, so a
/// persistent empty account that stopped carrying would fail here rather
/// than quietly hand the matrix a live network.
function testChainIgnoresAnUndeployedCandidate() external {
address candidateAddress = LibRainDeploy.zoltuAddress(type(MockDeployableV2).creationCode);
assertNotEq(candidateAddress, ADDRESS_REGISTRY_DEPLOYED_ADDRESS);
Expand Down
47 changes: 44 additions & 3 deletions test/src/lib/LibAddressRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {LibAddressRegistryDeploy} from "../../../src/lib/LibAddressRegistryDeplo
import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol";
import {IAddressRegistryV1} from "../../../src/interface/IAddressRegistryV1.sol";
import {AddressRegistry, ADDRESS_REGISTRY_ROOT} from "../../../src/concrete/AddressRegistry.sol";
import {DELEGATION_DESIGNATOR_LENGTH, LibAccountCode} from "../../lib/LibAccountCode.sol";

/// @title LibAddressRegistryTest
/// Tests for `LibAddressRegistry`. The registry is not mocked: the real
Expand Down Expand Up @@ -88,11 +89,18 @@ contract LibAddressRegistryTest is Test {
this.externalResolve(name);
}

/// A chain where something other than the pinned registry occupies the
/// address reverts on the code hash, so a name is never resolved by code
/// the caller did not compile against.
/// A chain where ORDINARY code — anything that is not a delegation
/// designator — occupies the address reverts on the code hash, so a name is
/// never resolved by code the caller did not compile against.
///
/// The designator family is excluded here and covered by
/// `testResolveDelegatedCode` instead. `LibAccountCode` is what says why:
/// it is the other KIND of account code, not another value of this one, and
/// the arbitrary-`bytes` domain this used to fuzz is not the domain of
/// things an account can hold.
function testResolveWrongCode(bytes32 name, bytes memory code) external {
vm.assume(code.length > 0);
vm.assume(!LibAccountCode.hasDelegationPrefix(code));
vm.assume(keccak256(code) != LibAddressRegistryDeploy.ADDRESS_REGISTRY_DEPLOYED_CODEHASH);
vm.etch(LibAddressRegistryDeploy.ADDRESS_REGISTRY_DEPLOYED_ADDRESS, code);

Expand All @@ -105,4 +113,37 @@ contract LibAddressRegistryTest is Test {
);
this.externalResolve(name);
}

/// A chain where an EOA has DELEGATED the registry address under EIP-7702
/// reverts on the code hash too. This is the other way an address gets
/// occupied, and the more dangerous one: the account carries only 23 bytes
/// of designator while executing whatever the delegate holds, so an address
/// that looks nothing like a registry can answer `get` however it likes.
///
/// The code hash is what refuses it, without knowing anything about 7702:
/// the account hashes its designator, never the delegate's code, so a
/// delegation can never present the pinned registry's hash.
///
/// A delegation to the zero address is the CLEARING form — it leaves the
/// account with no code at all, which is `testResolveNoRegistry`, not this.
/// @param name The name a caller would resolve.
/// @param delegate The account the registry address is delegated to.
function testResolveDelegatedCode(bytes32 name, address delegate) external {
vm.assume(delegate != address(0));
bytes memory designator = LibAccountCode.delegationDesignator(delegate);
assertEq(designator.length, DELEGATION_DESIGNATOR_LENGTH);
assertTrue(LibAccountCode.hasDelegationPrefix(designator));

vm.etch(LibAddressRegistryDeploy.ADDRESS_REGISTRY_DEPLOYED_ADDRESS, designator);
assertEq(LibAddressRegistryDeploy.ADDRESS_REGISTRY_DEPLOYED_ADDRESS.codehash, keccak256(designator));

vm.expectRevert(
abi.encodeWithSelector(
LibAddressRegistry.UnexpectedAddressRegistryCodeHash.selector,
LibAddressRegistryDeploy.ADDRESS_REGISTRY_DEPLOYED_CODEHASH,
keccak256(designator)
)
);
this.externalResolve(name);
}
}
Loading
Loading