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
48 changes: 24 additions & 24 deletions test/lib/mocks/MockB20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ contract MockB20 is IB20 {
// Read the executor policy ID out of the transfer-side packed
// slot. Cold here; warm by the time _transfer reads the same
// slot for sender + receiver.
uint64 executorPolicyId = uint64(MockB20Storage.layout().transferPolicyIds >> 128);
uint64 executorPolicyId = MockB20Storage.layout().transferPolicyIds.executor;
if (!IPolicyRegistry(POLICY_REGISTRY).isAuthorized(executorPolicyId, msg.sender)) {
revert PolicyForbids(TRANSFER_EXECUTOR_POLICY, executorPolicyId);
}
Expand Down Expand Up @@ -189,7 +189,7 @@ contract MockB20 is IB20 {
function transferFromWithMemo(address from, address to, uint256 amount, bytes32 memo) external returns (bool) {
if (!_isPrivileged() && msg.sender != from) {
_consumeAllowance(from, msg.sender, amount);
uint64 executorPolicyId = uint64(MockB20Storage.layout().transferPolicyIds >> 128);
uint64 executorPolicyId = MockB20Storage.layout().transferPolicyIds.executor;
if (!IPolicyRegistry(POLICY_REGISTRY).isAuthorized(executorPolicyId, msg.sender)) {
revert PolicyForbids(TRANSFER_EXECUTOR_POLICY, executorPolicyId);
}
Expand Down Expand Up @@ -243,7 +243,7 @@ contract MockB20 is IB20 {
// accounts. Read the transfer-sender policy ID out of the
// transfer-side packed slot and reject if the target is
// currently authorized.
uint64 senderPolicyId = uint64(MockB20Storage.layout().transferPolicyIds);
uint64 senderPolicyId = MockB20Storage.layout().transferPolicyIds.sender;
if (IPolicyRegistry(POLICY_REGISTRY).isAuthorized(senderPolicyId, from)) {
revert AccountNotBlocked(from);
}
Expand Down Expand Up @@ -390,31 +390,31 @@ contract MockB20 is IB20 {
/// falling through to `super`.
function _readPolicyId(bytes32 policyScope) internal view virtual returns (uint64) {
MockB20Storage.Layout storage $ = MockB20Storage.layout();
if (policyScope == TRANSFER_SENDER_POLICY) return uint64($.transferPolicyIds);
if (policyScope == TRANSFER_RECEIVER_POLICY) return uint64($.transferPolicyIds >> 64);
if (policyScope == TRANSFER_EXECUTOR_POLICY) return uint64($.transferPolicyIds >> 128);
if (policyScope == MINT_RECEIVER_POLICY) return uint64($.mintPolicyIds);
if (policyScope == TRANSFER_SENDER_POLICY) return $.transferPolicyIds.sender;
if (policyScope == TRANSFER_RECEIVER_POLICY) return $.transferPolicyIds.receiver;
if (policyScope == TRANSFER_EXECUTOR_POLICY) return $.transferPolicyIds.executor;
if (policyScope == MINT_RECEIVER_POLICY) return $.mintPolicyIds.receiver;
revert UnsupportedPolicyType(policyScope);
}

/// @dev Writes a policy ID to storage. Hot-path types update their
/// per-operation packed slot in-place (preserving the other
/// three packed lanes); anything else reverts
/// named field on the per-operation packed-struct slot
/// (`TransferPolicyIds.sender` etc.); Solidity emits the
/// appropriate mask/shift sequence to preserve the other
/// lanes in the slot. Anything else reverts
/// `UnsupportedPolicyType` — the token has no slot for it.
/// Variants override to handle their own policy types before
/// falling through to `super`. Mask + shift are explicit so
/// the Rust impl can replicate the exact bit layout.
/// falling through to `super`.
function _writePolicyId(bytes32 policyScope, uint64 newPolicyId) internal virtual {
MockB20Storage.Layout storage $ = MockB20Storage.layout();
uint256 mask = uint256(type(uint64).max);
if (policyScope == TRANSFER_SENDER_POLICY) {
$.transferPolicyIds = ($.transferPolicyIds & ~mask) | uint256(newPolicyId);
$.transferPolicyIds.sender = newPolicyId;
} else if (policyScope == TRANSFER_RECEIVER_POLICY) {
$.transferPolicyIds = ($.transferPolicyIds & ~(mask << 64)) | (uint256(newPolicyId) << 64);
$.transferPolicyIds.receiver = newPolicyId;
} else if (policyScope == TRANSFER_EXECUTOR_POLICY) {
$.transferPolicyIds = ($.transferPolicyIds & ~(mask << 128)) | (uint256(newPolicyId) << 128);
$.transferPolicyIds.executor = newPolicyId;
} else if (policyScope == MINT_RECEIVER_POLICY) {
$.mintPolicyIds = ($.mintPolicyIds & ~mask) | uint256(newPolicyId);
$.mintPolicyIds.receiver = newPolicyId;
} else {
revert UnsupportedPolicyType(policyScope);
}
Expand Down Expand Up @@ -611,14 +611,14 @@ contract MockB20 is IB20 {
// One SLOAD pulls both policy IDs we need for the transfer
// check (and was already warmed if we came in via transferFrom,
// which reads the executor lane of the same slot first).
uint256 packed = MockB20Storage.layout().transferPolicyIds;
uint64 senderPolicyId = uint64(packed);
uint64 receiverPolicyId = uint64(packed >> 64);
if (!IPolicyRegistry(POLICY_REGISTRY).isAuthorized(senderPolicyId, from)) {
revert PolicyForbids(TRANSFER_SENDER_POLICY, senderPolicyId);
// Solidity emits a single SLOAD for the struct read + masked
// extracts for the named fields.
MockB20Storage.TransferPolicyIds memory packed = MockB20Storage.layout().transferPolicyIds;
if (!IPolicyRegistry(POLICY_REGISTRY).isAuthorized(packed.sender, from)) {
revert PolicyForbids(TRANSFER_SENDER_POLICY, packed.sender);
}
if (!IPolicyRegistry(POLICY_REGISTRY).isAuthorized(receiverPolicyId, to)) {
revert PolicyForbids(TRANSFER_RECEIVER_POLICY, receiverPolicyId);
if (!IPolicyRegistry(POLICY_REGISTRY).isAuthorized(packed.receiver, to)) {
revert PolicyForbids(TRANSFER_RECEIVER_POLICY, packed.receiver);
}
}

Expand All @@ -637,7 +637,7 @@ contract MockB20 is IB20 {

if (!_isPrivileged()) {
if (_isPaused(PausableFeature.MINT)) revert ContractPaused(PausableFeature.MINT);
uint64 mintReceiverPolicyId = uint64(MockB20Storage.layout().mintPolicyIds);
uint64 mintReceiverPolicyId = MockB20Storage.layout().mintPolicyIds.receiver;
if (!IPolicyRegistry(POLICY_REGISTRY).isAuthorized(mintReceiverPolicyId, to)) {
revert PolicyForbids(MINT_RECEIVER_POLICY, mintReceiverPolicyId);
}
Expand Down
8 changes: 3 additions & 5 deletions test/lib/mocks/MockB20Security.sol
Original file line number Diff line number Diff line change
Expand Up @@ -255,16 +255,14 @@ contract MockB20Security is MockB20, IB20Security {
/// revert is the terminal case.
function _readPolicyId(bytes32 policyScope) internal view virtual override returns (uint64) {
if (policyScope == REDEEM_SENDER_POLICY) {
return uint64(MockB20RedeemStorage.layout().redeemPolicyIds);
return MockB20RedeemStorage.layout().redeemPolicyIds.sender;
}
return super._readPolicyId(policyScope);
}

function _writePolicyId(bytes32 policyScope, uint64 newPolicyId) internal virtual override {
if (policyScope == REDEEM_SENDER_POLICY) {
MockB20RedeemStorage.Layout storage $ = MockB20RedeemStorage.layout();
uint256 mask = uint256(type(uint64).max);
$.redeemPolicyIds = ($.redeemPolicyIds & ~mask) | uint256(newPolicyId);
MockB20RedeemStorage.layout().redeemPolicyIds.sender = newPolicyId;
return;
}
super._writePolicyId(policyScope, newPolicyId);
Expand All @@ -290,7 +288,7 @@ contract MockB20Security is MockB20, IB20Security {
function _redeemBurn(uint256 amount) internal returns (uint256 ratio) {
if (_isPaused(PausableFeature.REDEEM)) revert ContractPaused(PausableFeature.REDEEM);
MockB20RedeemStorage.Layout storage $ = MockB20RedeemStorage.layout();
uint64 REDEEMSenderPolicyId = uint64($.redeemPolicyIds);
uint64 REDEEMSenderPolicyId = $.redeemPolicyIds.sender;
if (!IPolicyRegistry(POLICY_REGISTRY).isAuthorized(REDEEMSenderPolicyId, msg.sender)) {
revert PolicyForbids(REDEEM_SENDER_POLICY, REDEEMSenderPolicyId);
}
Expand Down
111 changes: 82 additions & 29 deletions test/lib/mocks/MockB20Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ pragma solidity ^0.8.20;
/// variant-fixed (`18` for default, `6` for stablecoin/security)
/// and read from code, not storage.
library MockB20Storage {
// ============================================================
// PACKED POLICY STRUCTS
// ============================================================
// Solidity packs struct fields LSB-first into a single 256-bit slot
// (all fields fit because each is `uint64` and there are at most
// four of them per struct). The struct definition IS the binary
// layout spec — the Rust impl mirrors the field order with `u64`s
// of the same names. Reserved bits are implicit: any uint64 lane
// not declared as a field is simply uninitialized (zero) and the
// struct cannot accidentally write to it.

/// @notice Transfer-side policy IDs (read by `_transfer`,
/// `transferFrom*`, and the seize check in `burnBlocked`).
/// @dev Bit layout (Solidity LSB-first):
/// bits 0.. 63 : sender
/// bits 64..127 : receiver
/// bits 128..191 : executor
/// bits 192..255 : reserved (implicit, no field declared)
struct TransferPolicyIds {
uint64 sender;
uint64 receiver;
uint64 executor;
}

/// @notice Mint-side policy IDs (read by `_mint`). Only the
/// receiver policy is defined today; future granular
/// mint-side policy types get added as additional `uint64`
/// fields here so adding one doesn't force a second SLOAD.
/// @dev Bit layout:
/// bits 0.. 63 : receiver
/// bits 64..255 : reserved (implicit)
struct MintPolicyIds {
uint64 receiver;
}

/// @custom:storage-location erc7201:base.b20
struct Layout {
// ---------- Identity (mutable via admin) ----------
Expand Down Expand Up @@ -65,25 +100,24 @@ library MockB20Storage {
// matches `uint64 policyId` everywhere else in the system, and
// four IDs fit exactly into the 256-bit slot.
//
// **Layout via Solidity packed structs.** Each per-op packed slot
// is declared as a struct of `uint64` fields. Solidity packs
// struct fields LSB-first into the slot, which is the exact
// convention the Rust precompile impl must reproduce — so the
// struct field DECLARATION ORDER is the binary layout spec, with
// no comment-vs-code drift surface. Bit-identical to the prior
// hand-rolled `uint256` layout; consumer code uses named field
// access (`$.transferPolicyIds.sender = id;`) instead of inline
// shifts and mask operations.
//
// Transfer-side policies (read by `_transfer`, `transferFrom*`,
// and the seize check in `burnBlocked`).
// Packed as four uint64 lanes from least-significant to most-significant:
// word0 (bytes 0.. 7, bits 0.. 63): transferSenderPolicyId
// word1 (bytes 8..15, bits 64..127): transferReceiverPolicyId
// word2 (bytes 16..23, bits 128..191): transferExecutorPolicyId
// word3 (bytes 24..31, bits 192..255): reserved
// Note: bit ranges are shown low..high to avoid [end:start] ambiguity.
uint256 transferPolicyIds;
// Mint-side policies (read by `_mint`). Only `MINT_RECEIVER_POLICY` is
// defined today; the remaining three uint64 slots are reserved
// for future granular mint-side policy types (e.g.
// MINT_AUTHORIZER) so adding one doesn't force a second SLOAD.
// Layout uses the same uint64 lane convention:
// word0 (bytes 0.. 7, bits 0.. 63): mintReceiverPolicyId
// word1 (bytes 8..15, bits 64..127): reserved
// word2 (bytes 16..23, bits 128..191): reserved
// word3 (bytes 24..31, bits 192..255): reserved
uint256 mintPolicyIds;
TransferPolicyIds transferPolicyIds;
// Mint-side policies (read by `_mint`). Only `MINT_RECEIVER_POLICY`
// is defined today; future granular mint-side policy types (e.g.
// `MINT_AUTHORIZER`) get added as additional `uint64` fields on
// `MintPolicyIds` so adding one doesn't force a second SLOAD.
MintPolicyIds mintPolicyIds;
// There is no generic fallback mapping for "other" policy
// types. Each supported `policyType` lives in a fixed slot on
// this struct (or, for variant-specific operations, in the
Expand Down Expand Up @@ -239,12 +273,17 @@ library MockB20Storage {
// ============================================================
// PACKED-SLOT CODECS
// ============================================================
// The transfer-side and mint-side policy slots each pack four
// uint64 lanes into a single 256-bit slot, low-to-high (lane 0 in
// bits 0..63, lane 1 in bits 64..127, lane 2 in bits 128..191,
// lane 3 in bits 192..255). These pure codecs let test callers
// extract / compose lane values without re-deriving the shifts at
// every callsite.
// Production code accesses the packed policy slots via the
// `TransferPolicyIds` / `MintPolicyIds` structs (named fields on
// `Layout`) — Solidity handles the bit math automatically. These
// pure codecs operate on a raw `uint256` (what `vm.load` returns
// for the slot) and exist for test-side use only: layout-pin tests
// that read the raw slot bytes can use them to extract lanes
// without re-deriving the shifts at every callsite.
//
// The roundtrip tests in `MockB20SlotHelpers.t.sol` verify that
// these codecs' bit math matches Solidity's struct packing — so a
// codec drifting away from the canonical struct layout fails CI.

/// @notice Extracts the TRANSFER_SENDER policy id (lane 0) from the packed slot.
function transferSenderPolicyId(uint256 packed) internal pure returns (uint64) {
Expand Down Expand Up @@ -413,6 +452,21 @@ library MockB20RedeemStorage {
}
}

/// @notice Redeem-side policy IDs (read by `_redeemBurn` on the
/// security variant). Mirrors the per-op packed-slot
/// convention of `MockB20Storage.TransferPolicyIds` /
/// `MintPolicyIds`. Only the sender policy is defined
/// today; future granular redeem-side types
/// (e.g. `REDEEMER_RECEIVER`) get added as additional
/// `uint64` fields here so adding one doesn't force a
/// second SLOAD.
/// @dev Bit layout (Solidity LSB-first packing):
/// bits 0.. 63 : sender
/// bits 64..255 : reserved (implicit)
struct RedeemPolicyIds {
uint64 sender;
}

/// @custom:storage-location erc7201:base.b20.redeem
struct Layout {
// ---------- Redemption ----------
Expand All @@ -421,12 +475,11 @@ library MockB20RedeemStorage {
uint256 minimumRedeemable;

// ---------- Redeem-side policies (PACKED) ----------
// Layout:
// [63:0] redeemerSenderPolicyId
// [127:64] reserved
// [191:128] reserved
// [255:192] reserved
uint256 redeemPolicyIds;
// Solidity-packed struct: field declarations are the binary
// layout spec. Bit-identical to the prior hand-rolled
// `uint256` layout; consumer code uses named field access
// (`$.redeemPolicyIds.sender = id;`) instead of inline shifts.
RedeemPolicyIds redeemPolicyIds;
}

/// @notice Returns the storage location derived per the ERC-7201 formula
Expand Down
Loading