From 8ddfa08cc18133917199969fce0f196a1df63790 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 19 May 2025 17:55:42 +0000 Subject: [PATCH 1/8] Add generalized transient stack operations --- src/utils/LibTransient.sol | 93 +++++++++++++++++++++++++++++++++++ src/utils/g/LibTransient.sol | 94 ++++++++++++++++++++++++++++++++++++ test/LibTransient.t.sol | 83 +++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+) diff --git a/src/utils/LibTransient.sol b/src/utils/LibTransient.sol index 621da5a7eb..75618b8045 100644 --- a/src/utils/LibTransient.sol +++ b/src/utils/LibTransient.sol @@ -43,6 +43,18 @@ library LibTransient { uint256 _spacer; } + /// @dev Pointer struct to a stack pointer generator in transient storage. + struct TStack { + uint256 _spacer; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The transient stack is empty. + error StackIsEmpty(); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CONSTANTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -51,6 +63,9 @@ library LibTransient { /// `bytes4(keccak256("_LIB_TRANSIENT_COMPAT_SLOT_SEED"))`. uint256 private constant _LIB_TRANSIENT_COMPAT_SLOT_SEED = 0x5a0b45f2; + /// @dev Multiplier to spread the stack base. A prime. + uint256 private constant _STACK_BASE_SALT = 0x9e076501211e1371b; + /// @dev The canonical address of the transient registry. /// See: https://gist.github.com/Vectorized/4ab665d7a234ef5aaaff2e5091ec261f address internal constant REGISTRY = 0x000000000000297f64C7F8d9595e43257908F170; @@ -694,6 +709,84 @@ library LibTransient { _compat(ptr)._spacer = 0; } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STACK OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns a pointer to a stack in transient storage. + function tStack(bytes32 tSlot) internal pure returns (TStack storage ptr) { + /// @solidity memory-safe-assembly + assembly { + ptr.slot := tSlot + } + } + + /// @dev Returns a pointer to a stack in transient storage. + function tStack(uint256 tSlot) internal pure returns (TStack storage ptr) { + /// @solidity memory-safe-assembly + assembly { + ptr.slot := tSlot + } + } + + /// @dev Returns the number of elements in the stack. + function length(TStack storage ptr) internal view returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := shr(160, shl(128, tload(ptr.slot))) + } + } + + /// @dev Clears the stack at `ptr`. + /// Note: Future usage of the stack will point to a fresh transient storage region. + function clear(TStack storage ptr) internal { + /// @solidity memory-safe-assembly + assembly { + tstore(ptr.slot, shl(128, add(1, shr(128, tload(ptr.slot))))) + } + } + + /// @dev Increments the stack length by 1, and returns a pointer to the top element. + /// We don't want to call this `push` as it does not take in an element value. + /// Note: The value pointed to might not be cleared from previous usage. + function place(TStack storage ptr) internal returns (bytes32 topPtr) { + /// @solidity memory-safe-assembly + assembly { + topPtr := add(0x100000000, tload(ptr.slot)) + tstore(ptr.slot, topPtr) + topPtr := add(mul(_STACK_BASE_SALT, ptr.slot), topPtr) + } + } + + /// @dev Returns a pointer to the top element. Reverts if the stack is empty. + function top(TStack storage ptr) internal view returns (bytes32 topPtr) { + /// @solidity memory-safe-assembly + assembly { + topPtr := tload(ptr.slot) + if iszero(topPtr) { + mstore(0x00, 0xbb704e21) // `StackIsEmpty()`. + revert(0x1c, 0x04) + } + topPtr := add(mul(_STACK_BASE_SALT, ptr.slot), topPtr) + } + } + + /// @dev Decrements the stack length by 1, returns a pointer to the top element + /// before the popping. Reverts if the stack is empty. + /// Note: Popping from the stack does NOT auto-clear the top value. + function pop(TStack storage ptr) internal returns (bytes32 lastTopPtr) { + /// @solidity memory-safe-assembly + assembly { + lastTopPtr := tload(ptr.slot) + if iszero(lastTopPtr) { + mstore(0x00, 0xbb704e21) // `StackIsEmpty()`. + revert(0x1c, 0x04) + } + tstore(ptr.slot, sub(lastTopPtr, 0x100000000)) + lastTopPtr := add(mul(_STACK_BASE_SALT, ptr.slot), lastTopPtr) + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* TRANSIENT REGISTRY OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/src/utils/g/LibTransient.sol b/src/utils/g/LibTransient.sol index 6cc661b3e0..8508ba3929 100644 --- a/src/utils/g/LibTransient.sol +++ b/src/utils/g/LibTransient.sol @@ -37,12 +37,18 @@ struct TBytes { uint256 _spacer; } +/// @dev Pointer struct to a stack pointer generator in transient storage. +struct TStack { + uint256 _spacer; +} + using LibTransient for TUint256 global; using LibTransient for TInt256 global; using LibTransient for TBytes32 global; using LibTransient for TAddress global; using LibTransient for TBool global; using LibTransient for TBytes global; +using LibTransient for TStack global; /// @notice Library for transient storage operations. /// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/g/LibTransient.sol) @@ -52,6 +58,13 @@ using LibTransient for TBytes global; /// L2s are super cheap anyway. /// For best safety, always clear the storage after use. library LibTransient { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The transient stack is empty. + error StackIsEmpty(); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CONSTANTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -60,6 +73,9 @@ library LibTransient { /// `bytes4(keccak256("_LIB_TRANSIENT_COMPAT_SLOT_SEED"))`. uint256 private constant _LIB_TRANSIENT_COMPAT_SLOT_SEED = 0x5a0b45f2; + /// @dev Multiplier to spread the stack base. A prime. + uint256 private constant _STACK_BASE_SALT = 0x9e076501211e1371b; + /// @dev The canonical address of the transient registry. /// See: https://gist.github.com/Vectorized/4ab665d7a234ef5aaaff2e5091ec261f address internal constant REGISTRY = 0x000000000000297f64C7F8d9595e43257908F170; @@ -703,6 +719,84 @@ library LibTransient { _compat(ptr)._spacer = 0; } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STACK OPERATIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Returns a pointer to a stack in transient storage. + function tStack(bytes32 tSlot) internal pure returns (TStack storage ptr) { + /// @solidity memory-safe-assembly + assembly { + ptr.slot := tSlot + } + } + + /// @dev Returns a pointer to a stack in transient storage. + function tStack(uint256 tSlot) internal pure returns (TStack storage ptr) { + /// @solidity memory-safe-assembly + assembly { + ptr.slot := tSlot + } + } + + /// @dev Returns the number of elements in the stack. + function length(TStack storage ptr) internal view returns (uint256 result) { + /// @solidity memory-safe-assembly + assembly { + result := shr(160, shl(128, tload(ptr.slot))) + } + } + + /// @dev Clears the stack at `ptr`. + /// Note: Future usage of the stack will point to a fresh transient storage region. + function clear(TStack storage ptr) internal { + /// @solidity memory-safe-assembly + assembly { + tstore(ptr.slot, shl(128, add(1, shr(128, tload(ptr.slot))))) + } + } + + /// @dev Increments the stack length by 1, and returns a pointer to the top element. + /// We don't want to call this `push` as it does not take in an element value. + /// Note: The value pointed to might not be cleared from previous usage. + function place(TStack storage ptr) internal returns (bytes32 topPtr) { + /// @solidity memory-safe-assembly + assembly { + topPtr := add(0x100000000, tload(ptr.slot)) + tstore(ptr.slot, topPtr) + topPtr := add(mul(_STACK_BASE_SALT, ptr.slot), topPtr) + } + } + + /// @dev Returns a pointer to the top element. Reverts if the stack is empty. + function top(TStack storage ptr) internal view returns (bytes32 topPtr) { + /// @solidity memory-safe-assembly + assembly { + topPtr := tload(ptr.slot) + if iszero(topPtr) { + mstore(0x00, 0xbb704e21) // `StackIsEmpty()`. + revert(0x1c, 0x04) + } + topPtr := add(mul(_STACK_BASE_SALT, ptr.slot), topPtr) + } + } + + /// @dev Decrements the stack length by 1, returns a pointer to the top element + /// before the popping. Reverts if the stack is empty. + /// Note: Popping from the stack does NOT auto-clear the top value. + function pop(TStack storage ptr) internal returns (bytes32 lastTopPtr) { + /// @solidity memory-safe-assembly + assembly { + lastTopPtr := tload(ptr.slot) + if iszero(lastTopPtr) { + mstore(0x00, 0xbb704e21) // `StackIsEmpty()`. + revert(0x1c, 0x04) + } + tstore(ptr.slot, sub(lastTopPtr, 0x100000000)) + lastTopPtr := add(mul(_STACK_BASE_SALT, ptr.slot), lastTopPtr) + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* TRANSIENT REGISTRY OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/test/LibTransient.t.sol b/test/LibTransient.t.sol index 11af513995..07fac523b9 100644 --- a/test/LibTransient.t.sol +++ b/test/LibTransient.t.sol @@ -266,6 +266,89 @@ contract LibTransientTest is SoladyTest { LibTransient.tBytes(uint256(0)).setCalldataCompat(data); } + function testStackPlacePopBytes() public { + testStackPlacePopBytes(type(uint256).max, 0, 1); + } + + function testStackPlacePopBytes(uint256 r, uint256 aStackSlot, uint256 bStackSlot) public { + bytes[] memory aValues = new bytes[]((r >> 8) & 0x7); + bytes[] memory bValues = new bytes[]((r >> 16) & 0x7); + if (aStackSlot == bStackSlot) { + bStackSlot = aStackSlot ^ 1; + } + for (uint256 i; i < aValues.length; ++i) { + aValues[i] = abi.encodePacked(keccak256(abi.encode(i, aStackSlot)), "hehe"); + } + for (uint256 i; i < bValues.length; ++i) { + bValues[i] = abi.encodePacked(keccak256(abi.encode(i, bStackSlot))); + } + for (uint256 i; i < aValues.length; ++i) { + LibTransient.tStack(aStackSlot).place().tBytes().set(aValues[i]); + } + for (uint256 i; i < bValues.length; ++i) { + LibTransient.tStack(bStackSlot).place().tBytes().set(bValues[i]); + } + if (aValues.length > 0) { + assertEq( + LibTransient.tStack(aStackSlot).top().tBytes().get(), aValues[aValues.length - 1] + ); + } + if (bValues.length > 0) { + assertEq( + LibTransient.tStack(bStackSlot).top().tBytes().get(), bValues[bValues.length - 1] + ); + } + for (uint256 i; i < aValues.length; ++i) { + assertEq( + LibTransient.tStack(aStackSlot).pop().tBytes().get(), + aValues[aValues.length - 1 - i] + ); + } + for (uint256 i; i < bValues.length; ++i) { + assertEq( + LibTransient.tStack(bStackSlot).pop().tBytes().get(), + bValues[bValues.length - 1 - i] + ); + } + } + + function testStackPlacePopClear(bytes32 stackSlot) public { + uint256 n = _randomUniform() & 7; + for (uint256 i; i < n; ++i) { + assertEq(LibTransient.tStack(stackSlot).length(), i); + bytes32 x = keccak256(abi.encode(i)); + LibTransient.tStack(stackSlot).place().tBytes32().set(x); + assertEq(LibTransient.tStack(stackSlot).top().tBytes32().get(), x); + } + assertEq(LibTransient.tStack(stackSlot).length(), n); + + LibTransient.tStack(stackSlot).clear(); + + assertEq(LibTransient.tStack(stackSlot).length(), 0); + for (uint256 i; i < n; ++i) { + assertEq(LibTransient.tStack(stackSlot).length(), i); + assertEq(LibTransient.tStack(stackSlot).place().tBytes32().get(), 0); + } + } + + function testEmptyStackTopReverts() public { + vm.expectRevert(LibTransient.StackIsEmpty.selector); + this.stackTop(0); + } + + function testEmptyStackPopReverts() public { + vm.expectRevert(LibTransient.StackIsEmpty.selector); + this.stackPop(0); + } + + function stackTop(uint256 stackSlot) public view returns (bytes32) { + return LibTransient.tStack(stackSlot).top(); + } + + function stackPop(uint256 stackSlot) public returns (bytes32) { + return LibTransient.tStack(stackSlot).pop(); + } + function testRegistry(bytes32 key, bytes memory value) public { _etchTransientRegistry(); if (_randomChance(2)) { From d16b71e1e7f346041349e6bf77cb0e6439a07492 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 19 May 2025 18:42:24 +0000 Subject: [PATCH 2/8] Add peek --- src/utils/LibTransient.sol | 9 +++++++++ src/utils/g/LibTransient.sol | 9 +++++++++ test/LibTransient.t.sol | 12 ++++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/utils/LibTransient.sol b/src/utils/LibTransient.sol index 75618b8045..0d966db0c3 100644 --- a/src/utils/LibTransient.sol +++ b/src/utils/LibTransient.sol @@ -758,6 +758,15 @@ library LibTransient { } } + /// @dev Returns a pointer to the top element. Returns zero if the stack is empty. + function peek(TStack storage ptr) internal view returns (bytes32 topPtr) { + /// @solidity memory-safe-assembly + assembly { + topPtr := tload(ptr.slot) + topPtr := mul(iszero(iszero(topPtr)), add(mul(_STACK_BASE_SALT, ptr.slot), topPtr)) + } + } + /// @dev Returns a pointer to the top element. Reverts if the stack is empty. function top(TStack storage ptr) internal view returns (bytes32 topPtr) { /// @solidity memory-safe-assembly diff --git a/src/utils/g/LibTransient.sol b/src/utils/g/LibTransient.sol index 8508ba3929..648dac0ac0 100644 --- a/src/utils/g/LibTransient.sol +++ b/src/utils/g/LibTransient.sol @@ -768,6 +768,15 @@ library LibTransient { } } + /// @dev Returns a pointer to the top element. Returns zero if the stack is empty. + function peek(TStack storage ptr) internal view returns (bytes32 topPtr) { + /// @solidity memory-safe-assembly + assembly { + topPtr := tload(ptr.slot) + topPtr := mul(iszero(iszero(topPtr)), add(mul(_STACK_BASE_SALT, ptr.slot), topPtr)) + } + } + /// @dev Returns a pointer to the top element. Reverts if the stack is empty. function top(TStack storage ptr) internal view returns (bytes32 topPtr) { /// @solidity memory-safe-assembly diff --git a/test/LibTransient.t.sol b/test/LibTransient.t.sol index 07fac523b9..4a6bae247c 100644 --- a/test/LibTransient.t.sol +++ b/test/LibTransient.t.sol @@ -331,6 +331,18 @@ contract LibTransientTest is SoladyTest { } } + function testStackPeekTrick(uint256 base, uint256 n, uint256 r) public pure { + check_StackPeekTrick(base, n, r); + } + + function check_StackPeekTrick(uint256 base, uint256 n, uint256 r) public pure { + n = (n & 0xffffffffffffffff) | 1; + unchecked { + uint256 s = base * 0x9e076501211e1371b + ((n * 0x100000000) | (r << 128)); + assert(s != 0); + } + } + function testEmptyStackTopReverts() public { vm.expectRevert(LibTransient.StackIsEmpty.selector); this.stackTop(0); From aec35242678dd6f1b075c61799e90f8d635b5bd4 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 19 May 2025 18:42:39 +0000 Subject: [PATCH 3/8] Regen docs --- docs/utils/libtransient.md | 94 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/utils/libtransient.md b/docs/utils/libtransient.md index 0e8344f898..2fefa47226 100644 --- a/docs/utils/libtransient.md +++ b/docs/utils/libtransient.md @@ -75,6 +75,26 @@ struct TBytes { Pointer struct to a `bytes` in transient storage. +### TStack + +```solidity +struct TStack { + uint256 _spacer; +} +``` + +Pointer struct to a stack pointer generator in transient storage. + +## Custom Errors + +### StackIsEmpty() + +```solidity +error StackIsEmpty() +``` + +The transient stack is empty. + ## Constants ### REGISTRY @@ -756,6 +776,80 @@ function clearCompat(TBytes storage ptr) internal Clears the value at transient `ptr`. +## Stack Operations + +### tStack(bytes32) + +```solidity +function tStack(bytes32 tSlot) internal pure returns (TStack storage ptr) +``` + +Returns a pointer to a stack in transient storage. + +### tStack(uint256) + +```solidity +function tStack(uint256 tSlot) internal pure returns (TStack storage ptr) +``` + +Returns a pointer to a stack in transient storage. + +### length(TStack) + +```solidity +function length(TStack storage ptr) + internal + view + returns (uint256 result) +``` + +Returns the number of elements in the stack. + +### clear(TStack) + +```solidity +function clear(TStack storage ptr) internal +``` + +Clears the stack at `ptr`. +Note: Future usage of the stack will point to a fresh transient storage region. + +### place(TStack) + +```solidity +function place(TStack storage ptr) internal returns (bytes32 topPtr) +``` + +Increments the stack length by 1, and returns a pointer to the top element. +We don't want to call this `push` as it does not take in an element value. +Note: The value pointed to might not be cleared from previous usage. + +### peek(TStack) + +```solidity +function peek(TStack storage ptr) internal view returns (bytes32 topPtr) +``` + +Returns a pointer to the top element. Returns zero if the stack is empty. + +### top(TStack) + +```solidity +function top(TStack storage ptr) internal view returns (bytes32 topPtr) +``` + +Returns a pointer to the top element. Reverts if the stack is empty. + +### pop(TStack) + +```solidity +function pop(TStack storage ptr) internal returns (bytes32 lastTopPtr) +``` + +Decrements the stack length by 1, returns a pointer to the top element +before the popping. Reverts if the stack is empty. +Note: Popping from the stack does NOT auto-clear the top value. + ## Transient Registry Operations ### registrySet(bytes32,bytes) From f41dccd688f42b6040da566a19b28f03ba99034b Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 19 May 2025 19:45:36 +0000 Subject: [PATCH 4/8] Fix ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97c6ee65c0..b6cfa57db6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable - name: Install Dependencies run: forge install - name: Run prep scripts and forge fmt From e105c97babb96c23eb09df64c330b2b290a3b257 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 19 May 2025 19:50:05 +0000 Subject: [PATCH 5/8] Edit comments --- docs/utils/libtransient.md | 3 ++- src/utils/LibTransient.sol | 4 +++- src/utils/g/LibTransient.sol | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/utils/libtransient.md b/docs/utils/libtransient.md index 2fefa47226..a2a0558b70 100644 --- a/docs/utils/libtransient.md +++ b/docs/utils/libtransient.md @@ -830,7 +830,8 @@ Note: The value pointed to might not be cleared from previous usage. function peek(TStack storage ptr) internal view returns (bytes32 topPtr) ``` -Returns a pointer to the top element. Returns zero if the stack is empty. +Returns a pointer to the top element. Returns zero if the stack is empty. +This method can help avoid an additional `TLOAD`. ### top(TStack) diff --git a/src/utils/LibTransient.sol b/src/utils/LibTransient.sol index 0d966db0c3..becf368a26 100644 --- a/src/utils/LibTransient.sol +++ b/src/utils/LibTransient.sol @@ -63,7 +63,8 @@ library LibTransient { /// `bytes4(keccak256("_LIB_TRANSIENT_COMPAT_SLOT_SEED"))`. uint256 private constant _LIB_TRANSIENT_COMPAT_SLOT_SEED = 0x5a0b45f2; - /// @dev Multiplier to spread the stack base. A prime. + /// @dev Multiplier to stack base slot, so that in the case where two stacks + /// share consecutive base slots, their pointers will likely not overlap. A prime. uint256 private constant _STACK_BASE_SALT = 0x9e076501211e1371b; /// @dev The canonical address of the transient registry. @@ -759,6 +760,7 @@ library LibTransient { } /// @dev Returns a pointer to the top element. Returns zero if the stack is empty. + /// This method can help avoid an additional `TLOAD`. function peek(TStack storage ptr) internal view returns (bytes32 topPtr) { /// @solidity memory-safe-assembly assembly { diff --git a/src/utils/g/LibTransient.sol b/src/utils/g/LibTransient.sol index 648dac0ac0..9c03ed1e24 100644 --- a/src/utils/g/LibTransient.sol +++ b/src/utils/g/LibTransient.sol @@ -73,7 +73,8 @@ library LibTransient { /// `bytes4(keccak256("_LIB_TRANSIENT_COMPAT_SLOT_SEED"))`. uint256 private constant _LIB_TRANSIENT_COMPAT_SLOT_SEED = 0x5a0b45f2; - /// @dev Multiplier to spread the stack base. A prime. + /// @dev Multiplier to stack base slot, so that in the case where two stacks + /// share consecutive base slots, their pointers will likely not overlap. A prime. uint256 private constant _STACK_BASE_SALT = 0x9e076501211e1371b; /// @dev The canonical address of the transient registry. @@ -769,6 +770,7 @@ library LibTransient { } /// @dev Returns a pointer to the top element. Returns zero if the stack is empty. + /// This method can help avoid an additional `TLOAD`. function peek(TStack storage ptr) internal view returns (bytes32 topPtr) { /// @solidity memory-safe-assembly assembly { From 878cb5a060cde72dd098ed9b3c3acb5180f25705 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 19 May 2025 19:53:55 +0000 Subject: [PATCH 6/8] Edit comments --- docs/utils/libtransient.md | 4 +++- src/utils/LibTransient.sol | 2 ++ src/utils/g/LibTransient.sol | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/utils/libtransient.md b/docs/utils/libtransient.md index a2a0558b70..79dd418716 100644 --- a/docs/utils/libtransient.md +++ b/docs/utils/libtransient.md @@ -83,7 +83,9 @@ struct TStack { } ``` -Pointer struct to a stack pointer generator in transient storage. +Pointer struct to a stack pointer generator in transient storage. +This stack does not directly take in values. Instead, it generates pointers +that can be casted to any of the other transient storage pointer struct. ## Custom Errors diff --git a/src/utils/LibTransient.sol b/src/utils/LibTransient.sol index becf368a26..371ba0a85e 100644 --- a/src/utils/LibTransient.sol +++ b/src/utils/LibTransient.sol @@ -44,6 +44,8 @@ library LibTransient { } /// @dev Pointer struct to a stack pointer generator in transient storage. + /// This stack does not directly take in values. Instead, it generates pointers + /// that can be casted to any of the other transient storage pointer struct. struct TStack { uint256 _spacer; } diff --git a/src/utils/g/LibTransient.sol b/src/utils/g/LibTransient.sol index 9c03ed1e24..93ec36949a 100644 --- a/src/utils/g/LibTransient.sol +++ b/src/utils/g/LibTransient.sol @@ -38,6 +38,8 @@ struct TBytes { } /// @dev Pointer struct to a stack pointer generator in transient storage. +/// This stack does not directly take in values. Instead, it generates pointers +/// that can be casted to any of the other transient storage pointer struct. struct TStack { uint256 _spacer; } From ce20f02e0e368e0545a8cfde751692a14a582d02 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 19 May 2025 20:02:12 +0000 Subject: [PATCH 7/8] Tidy --- src/utils/LibTransient.sol | 7 ++++--- src/utils/g/LibTransient.sol | 7 ++++--- test/LibTransient.t.sol | 38 ++++++++++++++++++------------------ 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/utils/LibTransient.sol b/src/utils/LibTransient.sol index 371ba0a85e..a3c534705f 100644 --- a/src/utils/LibTransient.sol +++ b/src/utils/LibTransient.sol @@ -736,7 +736,7 @@ library LibTransient { function length(TStack storage ptr) internal view returns (uint256 result) { /// @solidity memory-safe-assembly assembly { - result := shr(160, shl(128, tload(ptr.slot))) + result := shr(160, shl(128, tload(ptr.slot))) // Removes the base offset and stride. } } @@ -745,6 +745,7 @@ library LibTransient { function clear(TStack storage ptr) internal { /// @solidity memory-safe-assembly assembly { + // Clears the length and increments the base pointer by `1 << 128`. tstore(ptr.slot, shl(128, add(1, shr(128, tload(ptr.slot))))) } } @@ -755,7 +756,7 @@ library LibTransient { function place(TStack storage ptr) internal returns (bytes32 topPtr) { /// @solidity memory-safe-assembly assembly { - topPtr := add(0x100000000, tload(ptr.slot)) + topPtr := add(0x100000000, tload(ptr.slot)) // Increments by a stride. tstore(ptr.slot, topPtr) topPtr := add(mul(_STACK_BASE_SALT, ptr.slot), topPtr) } @@ -795,7 +796,7 @@ library LibTransient { mstore(0x00, 0xbb704e21) // `StackIsEmpty()`. revert(0x1c, 0x04) } - tstore(ptr.slot, sub(lastTopPtr, 0x100000000)) + tstore(ptr.slot, sub(lastTopPtr, 0x100000000)) // Decrements by a stride. lastTopPtr := add(mul(_STACK_BASE_SALT, ptr.slot), lastTopPtr) } } diff --git a/src/utils/g/LibTransient.sol b/src/utils/g/LibTransient.sol index 93ec36949a..b5e6ef2f28 100644 --- a/src/utils/g/LibTransient.sol +++ b/src/utils/g/LibTransient.sol @@ -746,7 +746,7 @@ library LibTransient { function length(TStack storage ptr) internal view returns (uint256 result) { /// @solidity memory-safe-assembly assembly { - result := shr(160, shl(128, tload(ptr.slot))) + result := shr(160, shl(128, tload(ptr.slot))) // Removes the base offset and stride. } } @@ -755,6 +755,7 @@ library LibTransient { function clear(TStack storage ptr) internal { /// @solidity memory-safe-assembly assembly { + // Clears the length and increments the base pointer by `1 << 128`. tstore(ptr.slot, shl(128, add(1, shr(128, tload(ptr.slot))))) } } @@ -765,7 +766,7 @@ library LibTransient { function place(TStack storage ptr) internal returns (bytes32 topPtr) { /// @solidity memory-safe-assembly assembly { - topPtr := add(0x100000000, tload(ptr.slot)) + topPtr := add(0x100000000, tload(ptr.slot)) // Increments by a stride. tstore(ptr.slot, topPtr) topPtr := add(mul(_STACK_BASE_SALT, ptr.slot), topPtr) } @@ -805,7 +806,7 @@ library LibTransient { mstore(0x00, 0xbb704e21) // `StackIsEmpty()`. revert(0x1c, 0x04) } - tstore(ptr.slot, sub(lastTopPtr, 0x100000000)) + tstore(ptr.slot, sub(lastTopPtr, 0x100000000)) // Decrements by a stride. lastTopPtr := add(mul(_STACK_BASE_SALT, ptr.slot), lastTopPtr) } } diff --git a/test/LibTransient.t.sol b/test/LibTransient.t.sol index 4a6bae247c..d4bf8e969d 100644 --- a/test/LibTransient.t.sol +++ b/test/LibTransient.t.sol @@ -278,37 +278,37 @@ contract LibTransientTest is SoladyTest { } for (uint256 i; i < aValues.length; ++i) { aValues[i] = abi.encodePacked(keccak256(abi.encode(i, aStackSlot)), "hehe"); - } - for (uint256 i; i < bValues.length; ++i) { - bValues[i] = abi.encodePacked(keccak256(abi.encode(i, bStackSlot))); - } - for (uint256 i; i < aValues.length; ++i) { LibTransient.tStack(aStackSlot).place().tBytes().set(aValues[i]); } for (uint256 i; i < bValues.length; ++i) { + bValues[i] = abi.encodePacked(keccak256(abi.encode(i, bStackSlot))); LibTransient.tStack(bStackSlot).place().tBytes().set(bValues[i]); } if (aValues.length > 0) { - assertEq( - LibTransient.tStack(aStackSlot).top().tBytes().get(), aValues[aValues.length - 1] - ); + bytes memory expected = aValues[aValues.length - 1]; + assertEq(LibTransient.tStack(aStackSlot).top().tBytes().get(), expected); + assertEq(LibTransient.tStack(aStackSlot).peek().tBytes().get(), expected); + assertGt(uint256(LibTransient.tStack(aStackSlot).peek()), 0); + } else { + assertEq(uint256(LibTransient.tStack(aStackSlot).peek()), 0); + assertEq(LibTransient.tStack(aStackSlot).peek().tBytes().get(), ""); } if (bValues.length > 0) { - assertEq( - LibTransient.tStack(bStackSlot).top().tBytes().get(), bValues[bValues.length - 1] - ); + bytes memory expected = bValues[bValues.length - 1]; + assertEq(LibTransient.tStack(bStackSlot).top().tBytes().get(), expected); + assertEq(LibTransient.tStack(bStackSlot).peek().tBytes().get(), expected); + assertGt(uint256(LibTransient.tStack(bStackSlot).peek()), 0); + } else { + assertEq(uint256(LibTransient.tStack(bStackSlot).peek()), 0); + assertEq(LibTransient.tStack(bStackSlot).peek().tBytes().get(), ""); } for (uint256 i; i < aValues.length; ++i) { - assertEq( - LibTransient.tStack(aStackSlot).pop().tBytes().get(), - aValues[aValues.length - 1 - i] - ); + bytes memory expected = aValues[aValues.length - 1]; + assertEq(LibTransient.tStack(aStackSlot).pop().tBytes().get(), expected); } for (uint256 i; i < bValues.length; ++i) { - assertEq( - LibTransient.tStack(bStackSlot).pop().tBytes().get(), - bValues[bValues.length - 1 - i] - ); + bytes memory expected = bValues[bValues.length - 1]; + assertEq(LibTransient.tStack(bStackSlot).pop().tBytes().get(), expected); } } From de909b8d14203336e09651817bf1769748ec0d75 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 19 May 2025 20:12:05 +0000 Subject: [PATCH 8/8] Tidy --- src/utils/LibTransient.sol | 4 ++-- src/utils/g/LibTransient.sol | 4 ++-- test/LibTransient.t.sol | 7 +++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/utils/LibTransient.sol b/src/utils/LibTransient.sol index a3c534705f..bf1e40ee7f 100644 --- a/src/utils/LibTransient.sol +++ b/src/utils/LibTransient.sol @@ -767,8 +767,8 @@ library LibTransient { function peek(TStack storage ptr) internal view returns (bytes32 topPtr) { /// @solidity memory-safe-assembly assembly { - topPtr := tload(ptr.slot) - topPtr := mul(iszero(iszero(topPtr)), add(mul(_STACK_BASE_SALT, ptr.slot), topPtr)) + let t := tload(ptr.slot) + topPtr := mul(iszero(iszero(shl(128, t))), add(mul(_STACK_BASE_SALT, ptr.slot), t)) } } diff --git a/src/utils/g/LibTransient.sol b/src/utils/g/LibTransient.sol index b5e6ef2f28..268324900f 100644 --- a/src/utils/g/LibTransient.sol +++ b/src/utils/g/LibTransient.sol @@ -777,8 +777,8 @@ library LibTransient { function peek(TStack storage ptr) internal view returns (bytes32 topPtr) { /// @solidity memory-safe-assembly assembly { - topPtr := tload(ptr.slot) - topPtr := mul(iszero(iszero(topPtr)), add(mul(_STACK_BASE_SALT, ptr.slot), topPtr)) + let t := tload(ptr.slot) + topPtr := mul(iszero(iszero(shl(128, t))), add(mul(_STACK_BASE_SALT, ptr.slot), t)) } } diff --git a/test/LibTransient.t.sol b/test/LibTransient.t.sol index d4bf8e969d..8a921ac4d7 100644 --- a/test/LibTransient.t.sol +++ b/test/LibTransient.t.sol @@ -303,11 +303,11 @@ contract LibTransientTest is SoladyTest { assertEq(LibTransient.tStack(bStackSlot).peek().tBytes().get(), ""); } for (uint256 i; i < aValues.length; ++i) { - bytes memory expected = aValues[aValues.length - 1]; + bytes memory expected = aValues[aValues.length - 1 - i]; assertEq(LibTransient.tStack(aStackSlot).pop().tBytes().get(), expected); } for (uint256 i; i < bValues.length; ++i) { - bytes memory expected = bValues[bValues.length - 1]; + bytes memory expected = bValues[bValues.length - 1 - i]; assertEq(LibTransient.tStack(bStackSlot).pop().tBytes().get(), expected); } } @@ -319,10 +319,13 @@ contract LibTransientTest is SoladyTest { bytes32 x = keccak256(abi.encode(i)); LibTransient.tStack(stackSlot).place().tBytes32().set(x); assertEq(LibTransient.tStack(stackSlot).top().tBytes32().get(), x); + assertEq(LibTransient.tStack(stackSlot).peek().tBytes32().get(), x); } assertEq(LibTransient.tStack(stackSlot).length(), n); LibTransient.tStack(stackSlot).clear(); + assertEq(LibTransient.tStack(stackSlot).peek(), 0); + assertEq(LibTransient.tStack(stackSlot).peek().tBytes32().get(), 0); assertEq(LibTransient.tStack(stackSlot).length(), 0); for (uint256 i; i < n; ++i) {