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 diff --git a/docs/utils/libtransient.md b/docs/utils/libtransient.md index 0e8344f898..79dd418716 100644 --- a/docs/utils/libtransient.md +++ b/docs/utils/libtransient.md @@ -75,6 +75,28 @@ 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. +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 + +### StackIsEmpty() + +```solidity +error StackIsEmpty() +``` + +The transient stack is empty. + ## Constants ### REGISTRY @@ -756,6 +778,81 @@ 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. +This method can help avoid an additional `TLOAD`. + +### 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) diff --git a/src/utils/LibTransient.sol b/src/utils/LibTransient.sol index 621da5a7eb..bf1e40ee7f 100644 --- a/src/utils/LibTransient.sol +++ b/src/utils/LibTransient.sol @@ -43,6 +43,20 @@ library LibTransient { uint256 _spacer; } + /// @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; + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The transient stack is empty. + error StackIsEmpty(); + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* CONSTANTS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ @@ -51,6 +65,10 @@ library LibTransient { /// `bytes4(keccak256("_LIB_TRANSIENT_COMPAT_SLOT_SEED"))`. uint256 private constant _LIB_TRANSIENT_COMPAT_SLOT_SEED = 0x5a0b45f2; + /// @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. /// See: https://gist.github.com/Vectorized/4ab665d7a234ef5aaaff2e5091ec261f address internal constant REGISTRY = 0x000000000000297f64C7F8d9595e43257908F170; @@ -694,6 +712,95 @@ 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))) // Removes the base offset and stride. + } + } + + /// @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 { + // Clears the length and increments the base pointer by `1 << 128`. + 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)) // Increments by a stride. + tstore(ptr.slot, topPtr) + topPtr := add(mul(_STACK_BASE_SALT, ptr.slot), topPtr) + } + } + + /// @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 { + let t := tload(ptr.slot) + topPtr := mul(iszero(iszero(shl(128, t))), add(mul(_STACK_BASE_SALT, ptr.slot), t)) + } + } + + /// @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)) // Decrements by a stride. + 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..268324900f 100644 --- a/src/utils/g/LibTransient.sol +++ b/src/utils/g/LibTransient.sol @@ -37,12 +37,20 @@ struct TBytes { uint256 _spacer; } +/// @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; +} + 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 +60,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 +75,10 @@ library LibTransient { /// `bytes4(keccak256("_LIB_TRANSIENT_COMPAT_SLOT_SEED"))`. uint256 private constant _LIB_TRANSIENT_COMPAT_SLOT_SEED = 0x5a0b45f2; + /// @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. /// See: https://gist.github.com/Vectorized/4ab665d7a234ef5aaaff2e5091ec261f address internal constant REGISTRY = 0x000000000000297f64C7F8d9595e43257908F170; @@ -703,6 +722,95 @@ 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))) // Removes the base offset and stride. + } + } + + /// @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 { + // Clears the length and increments the base pointer by `1 << 128`. + 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)) // Increments by a stride. + tstore(ptr.slot, topPtr) + topPtr := add(mul(_STACK_BASE_SALT, ptr.slot), topPtr) + } + } + + /// @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 { + let t := tload(ptr.slot) + topPtr := mul(iszero(iszero(shl(128, t))), add(mul(_STACK_BASE_SALT, ptr.slot), t)) + } + } + + /// @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)) // Decrements by a stride. + 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..8a921ac4d7 100644 --- a/test/LibTransient.t.sol +++ b/test/LibTransient.t.sol @@ -266,6 +266,104 @@ 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"); + 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) { + 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) { + 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) { + 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 - i]; + assertEq(LibTransient.tStack(bStackSlot).pop().tBytes().get(), expected); + } + } + + 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).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) { + assertEq(LibTransient.tStack(stackSlot).length(), i); + assertEq(LibTransient.tStack(stackSlot).place().tBytes32().get(), 0); + } + } + + 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); + } + + 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)) {