Skip to content

refactor(policy): migrate policies[id] slot to Solidity packed struct (bit-identical)#77

Draft
amiecorso wants to merge 1 commit into
mainfrom
amie/policy-struct
Draft

refactor(policy): migrate policies[id] slot to Solidity packed struct (bit-identical)#77
amiecorso wants to merge 1 commit into
mainfrom
amie/policy-struct

Conversation

@amiecorso
Copy link
Copy Markdown
Collaborator

@amiecorso amiecorso commented May 23, 2026

Summary

Continues the structural-slot-packing work from #75. The lane-based
slots (transferPolicyIds, mintPolicyIds, redeemPolicyIds) now
use Solidity packed structs; this PR does the same for the
PolicyRegistry's policies[id] slot — bit-identical to the
existing layout
(no Rust coordination required).

Per Conner's Slack thread.

Layout is frozen — zero Rust change required

Unlike the lane slots (where Solidity's natural LSB-first packing
matched the hand-rolled convention), the policy slot has its exists
flag at bit 255 with a 95-bit gap above admin. A clean
struct { address admin; bool exists; } would have Solidity place
exists at bit 160 (right above admin), changing the layout.

To preserve the bit-255 location, the struct uses Conner's first
sketch shape:

struct PolicyPacked {
    address admin;          // bits 0..159
    uint88 reservedMiddle;  // bits 160..247 (always zero)
    uint8 existsByte;       // bits 248..255 — only bit 7 (slot bit 255) is the actual flag
}

This is bit-identical to the prior hand-rolled uint256 layout —
verified by the existing layout-pin tests in
PolicyRegistryFullLayout.t.sol passing unchanged against the
migrated storage. Rust side stays as-is.

The footgun and how it's hidden

The existsByte field is a single byte but only its high bit
(= slot bit 255) is the existence signal. Writing existsByte = 1
puts the bit at slot position 248 instead of 255 — wrong location.

Hidden behind two library helpers, which are the only sanctioned
API for the existence flag:

  • newPolicy(address admin) — always writes existsByte = 0x80
    (= slot bit 255). All construction goes through this.
  • existsSet(PolicyPacked memory packed) — returns
    packed.existsByte != 0. Tolerant of any non-zero byte value so
    a buggy writer that lands the bit at the wrong position still
    triggers existence semantics; the layout-pin tests catch the
    wrong bit position separately.

Consumer code never touches existsByte directly:

// _create
\$.policies[id] = MockPolicyRegistryStorage.newPolicy(admin);

// _requireCustom
PolicyPacked memory packed = \$.policies[policyId];
if (!MockPolicyRegistryStorage.existsSet(packed)) revert PolicyNotFound();
if (packed.admin != msg.sender) revert Unauthorized();

// renounceAdmin — only admin lane updated, existsByte preserved
\$.policies[policyId].admin = address(0);

_encode and _decodeAdmin helpers in MockPolicyRegistry removed
newPolicy(a) and packed.admin cover both paths.

Intentional non-goal: "polymorphic" policy struct

Conner's Slack message also raised future UNION/INTERSECT composite
policy types and asked whether the struct should be "somewhat
polymorphic". Documented in the storage library NatSpec as an
explicit non-goal:

Polymorphism non-goal. This struct holds the admin'd
ALLOWLIST/BLOCKLIST policy shape. Future composite policy types
(e.g. UNION/INTERSECT, immutable + multi-reference) must NOT be
overloaded onto this struct: Solidity's lack of sum-types means
doing so would force an awkward worst-of-both layout. When
composite types land, they get their own struct and a parallel
`mapping(uint64 => CompositePolicyPacked)`, with the policy ID's
top byte routing lookups between mappings.

Tests

465 passed / 0 failed. Tests stay valid because the binary layout is
bit-identical to the hand-rolled version. The codec helpers
(packPolicy, policyAdminFromPacked, policyExistsFromPacked)
retained — they operate on raw `uint256` slot reads from `vm.load`
and now match what Solidity emits for the struct.

@amiecorso amiecorso force-pushed the amie/policy-struct branch from 1840648 to b56d6c1 Compare May 23, 2026 02:29
@amiecorso amiecorso changed the title refactor(policy): migrate policies[id] slot to Solidity packed struct refactor(policy): migrate policies[id] slot to Solidity packed struct (bit-identical) May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant