├── .gitmodules ├── funding.json ├── .gitignore ├── foundry.toml ├── src ├── ReentrancyGuard.sol ├── TransientPrimitives.sol ├── PayableMulticallable.sol └── TransientBytesLib.sol ├── .github └── workflows │ └── test.yml ├── test ├── mocks │ ├── Multicallable.sol │ ├── GuardedDeposit.sol │ └── Reenterer.sol ├── ReentrancyGuard.t.sol ├── PayableMulticallable.t.sol ├── TransientBytes.t.sol └── TransientPrimitives.t.sol ├── LICENSE └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0xbf681923643d10f7412c4886c71f5d5525e5ae687273e0ec69d8bc68454fe307" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | evm_version = "cancun" 7 | ignored_error_codes = [2394] 8 | viaIR = true 9 | 10 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 11 | -------------------------------------------------------------------------------- /src/ReentrancyGuard.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {tuint256} from "./TransientPrimitives.sol"; 5 | 6 | /// @author philogy 7 | abstract contract ReentrancyGuard { 8 | tuint256 private _lockState; 9 | 10 | uint256 internal constant DEFAULT_UNLOCKED = 0; 11 | uint256 internal constant LOCKED = 1; 12 | 13 | error Reentering(); 14 | 15 | modifier nonReentrant() { 16 | if (_lockState.get() != DEFAULT_UNLOCKED) revert Reentering(); 17 | _lockState.set(LOCKED); 18 | _; 19 | _lockState.set(DEFAULT_UNLOCKED); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /test/mocks/Multicallable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {PayableMulticallable} from "../../src/PayableMulticallable.sol"; 5 | 6 | /// @author philogy 7 | contract Multicallable is PayableMulticallable { 8 | mapping(address => uint256) public balanceOf; 9 | 10 | function deposit(uint256 amount) external payable standalonePayable { 11 | balanceOf[msg.sender] += useValue(amount); 12 | } 13 | 14 | function withdraw(uint256 amount) external payable { 15 | balanceOf[msg.sender] -= amount; 16 | (bool suc,) = msg.sender.call{value: amount}(""); 17 | require(suc, "CALL_FAILED"); 18 | } 19 | 20 | function returnRemainder() external payable { 21 | _returnRemainingValue(msg.sender); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Philippe Dumonet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/mocks/GuardedDeposit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {ReentrancyGuard} from "../../src/ReentrancyGuard.sol"; 5 | 6 | /// @author philogy 7 | contract GuardedDeposit is ReentrancyGuard { 8 | mapping(address => uint256) public balanceOf; 9 | 10 | function deposit() external payable { 11 | balanceOf[msg.sender] += msg.value; 12 | } 13 | 14 | function withdraw1() external nonReentrant { 15 | _withdraw(); 16 | } 17 | 18 | function withdraw2() external nonReentrant { 19 | _withdraw(); 20 | } 21 | 22 | function vulnerableWithdraw() external { 23 | _withdraw(); 24 | } 25 | 26 | function _withdraw() internal { 27 | uint256 amount = balanceOf[msg.sender]; 28 | 29 | (bool success,) = msg.sender.call{value: amount}(""); 30 | if (!success) _bubbleError(); 31 | 32 | balanceOf[msg.sender] = 0; 33 | } 34 | 35 | function _bubbleError() internal pure { 36 | assembly ("memory-safe") { 37 | let m := mload(0x40) 38 | returndatacopy(m, 0, returndatasize()) 39 | revert(m, returndatasize()) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/mocks/Reenterer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {GuardedDeposit} from "./GuardedDeposit.sol"; 5 | 6 | /// @author philogy 7 | contract Reenterer { 8 | GuardedDeposit internal immutable victim; 9 | 10 | bytes4 private targetSelector; 11 | uint8 private loops; 12 | 13 | constructor(address target) { 14 | victim = GuardedDeposit(target); 15 | } 16 | 17 | receive() external payable { 18 | if (--loops == 0) return; 19 | (bool success,) = address(victim).call(abi.encodePacked(targetSelector)); 20 | if (!success) _bubbleError(); 21 | } 22 | 23 | function deposit() external payable { 24 | victim.deposit{value: msg.value}(); 25 | } 26 | 27 | function attack(bytes4 entry, bytes4 reentry, uint8 total) external { 28 | targetSelector = reentry; 29 | loops = total; 30 | 31 | (bool success,) = address(victim).call(abi.encodePacked(entry)); 32 | if (!success) _bubbleError(); 33 | } 34 | 35 | function _bubbleError() internal pure { 36 | assembly ("memory-safe") { 37 | let m := mload(0x40) 38 | returndatacopy(m, 0, returndatasize()) 39 | revert(m, returndatasize()) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/ReentrancyGuard.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {GuardedDeposit} from "./mocks/GuardedDeposit.sol"; 6 | import {Reenterer} from "./mocks/Reenterer.sol"; 7 | 8 | /// @author philogy 9 | contract ReentrancyGuardTest is Test { 10 | GuardedDeposit target; 11 | Reenterer attacker; 12 | 13 | function setUp() public { 14 | target = new GuardedDeposit(); 15 | attacker = new Reenterer(address(target)); 16 | } 17 | 18 | function test_mock_vulnerableWithoutGuard() public { 19 | uint256 value = 1 ether; 20 | for (uint256 i = 0; i < 10; i++) { 21 | hoax(vm.addr(1 + i), value); 22 | target.deposit{value: value}(); 23 | } 24 | 25 | address trigger = makeAddr("trigger"); 26 | hoax(trigger, value); 27 | attacker.deposit{value: value}(); 28 | 29 | attacker.attack(target.vulnerableWithdraw.selector, target.vulnerableWithdraw.selector, 5); 30 | assertEq(address(attacker).balance, value * 5); 31 | } 32 | 33 | function test_blocksReentrancy() public { 34 | uint256 value = 1 ether; 35 | for (uint256 i = 0; i < 10; i++) { 36 | hoax(vm.addr(1 + i), value); 37 | target.deposit{value: value}(); 38 | } 39 | 40 | address trigger = makeAddr("trigger"); 41 | hoax(trigger, value); 42 | attacker.deposit{value: value}(); 43 | 44 | vm.expectRevert(abi.encodeWithSignature("Reentering()")); 45 | attacker.attack(target.withdraw1.selector, target.withdraw1.selector, 5); 46 | 47 | vm.expectRevert(abi.encodeWithSignature("Reentering()")); 48 | attacker.attack(target.withdraw2.selector, target.withdraw2.selector, 5); 49 | 50 | // Cross-function reentrancy 51 | vm.expectRevert(abi.encodeWithSignature("Reentering()")); 52 | attacker.attack(target.withdraw2.selector, target.withdraw1.selector, 5); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/PayableMulticallable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {stdError} from "forge-std/StdError.sol"; 6 | import {Multicallable} from "./mocks/Multicallable.sol"; 7 | 8 | /// @author philogy 9 | contract PayableMulticallableTest is Test { 10 | Multicallable multicall = new Multicallable(); 11 | 12 | function test_preventsSimpleValueDoubleSpend() public { 13 | address attacker = makeAddr("attacker"); 14 | uint256 amount = 3 ether; 15 | hoax(attacker, amount); 16 | bytes[] memory data = new bytes[](2); 17 | data[0] = abi.encodeCall(multicall.deposit, (amount)); 18 | data[1] = abi.encodeCall(multicall.deposit, (amount)); 19 | vm.expectRevert(stdError.arithmeticError); 20 | multicall.multicall{value: amount}(data); 21 | } 22 | 23 | function test_allowsNormalMulticall() public { 24 | address user = makeAddr("user"); 25 | hoax(user, 5 ether); 26 | bytes[] memory data = new bytes[](3); 27 | data[0] = abi.encodeCall(multicall.deposit, (2 ether)); 28 | data[1] = abi.encodeCall(multicall.deposit, (2.9 ether)); 29 | data[2] = abi.encodeCall(multicall.returnRemainder, ()); 30 | multicall.multicall{value: 5 ether}(data); 31 | 32 | assertEq(multicall.balanceOf(user), 4.9 ether); 33 | assertEq(user.balance, 0.1 ether); 34 | } 35 | 36 | function test_returnResetsValue() public { 37 | address user = makeAddr("user"); 38 | hoax(user, 5 ether); 39 | bytes[] memory data = new bytes[](3); 40 | data[0] = abi.encodeCall(multicall.deposit, (2 ether)); 41 | data[1] = abi.encodeCall(multicall.deposit, (2.9 ether)); 42 | data[2] = abi.encodeCall(multicall.returnRemainder, ()); 43 | multicall.multicall{value: 5 ether}(data); 44 | 45 | assertEq(multicall.balanceOf(user), 4.9 ether); 46 | assertEq(user.balance, 0.1 ether); 47 | } 48 | 49 | function test_standalonePaybable() public { 50 | address user = makeAddr("user"); 51 | uint256 amount = 3.238 ether; 52 | hoax(user, amount); 53 | multicall.deposit{value: amount}(amount); 54 | 55 | assertEq(multicall.balanceOf(user), amount); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/TransientBytes.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {TransientBytes} from "../src/TransientBytesLib.sol"; 6 | 7 | /// @author philogy 8 | contract TransientBytesTest is Test { 9 | TransientBytes tbytes; 10 | 11 | uint256 internal constant MAX_LENGTH = type(uint32).max; 12 | 13 | function test_defaultEmpty() public { 14 | assertEq(tbytes.get(), ""); 15 | assertEq(tbytes.length(), 0); 16 | } 17 | 18 | function test_setMem(bytes memory inner) public { 19 | vm.assume(inner.length <= MAX_LENGTH); 20 | tbytes.set(inner); 21 | assertEq(tbytes.get(), inner); 22 | assertEq(tbytes.length(), inner.length); 23 | tbytes.agus(); 24 | assertEq(tbytes.get(), ""); 25 | assertEq(tbytes.length(), 0); 26 | } 27 | 28 | function test_setCd(bytes calldata inner) public { 29 | vm.assume(inner.length <= MAX_LENGTH); 30 | tbytes.setCd(inner); 31 | assertEq(tbytes.get(), inner); 32 | assertEq(tbytes.length(), inner.length); 33 | tbytes.agus(); 34 | assertEq(tbytes.get(), ""); 35 | assertEq(tbytes.length(), 0); 36 | } 37 | 38 | function test_multipleSetMemNoAgus(bytes memory inner1, bytes memory inner2) public { 39 | vm.assume(inner1.length <= MAX_LENGTH); 40 | vm.assume(inner2.length <= MAX_LENGTH); 41 | 42 | tbytes.set(inner1); 43 | assertEq(tbytes.get(), inner1); 44 | tbytes.set(inner2); 45 | assertEq(tbytes.get(), inner2); 46 | } 47 | 48 | function test_multipleSetCdNoAgus(bytes calldata inner1, bytes calldata inner2) public { 49 | vm.assume(inner1.length <= MAX_LENGTH); 50 | vm.assume(inner2.length <= MAX_LENGTH); 51 | 52 | tbytes.setCd(inner1); 53 | assertEq(tbytes.get(), inner1); 54 | tbytes.setCd(inner2); 55 | assertEq(tbytes.get(), inner2); 56 | } 57 | 58 | function test_gasUsed_setTillBoundary() public { 59 | bytes32 a = keccak256("a"); 60 | bytes32 b = keccak256("b"); 61 | bytes memory data = abi.encodePacked(bytes28(a), b); 62 | assertEq(data.length, 0x20 * 2 - 4); 63 | 64 | uint256 g0 = gasleft(); 65 | tbytes.set(data); 66 | uint256 g1 = gasleft(); 67 | emit log_named_uint("used", g0 - g1); 68 | } 69 | 70 | function test_gasUsed_setTillBoundary_five() public { 71 | bytes32 w1 = keccak256("w1"); 72 | bytes32 w2 = keccak256("w2"); 73 | bytes32 w3 = keccak256("w3"); 74 | bytes32 w4 = keccak256("w4"); 75 | bytes32 w5 = keccak256("w5"); 76 | bytes memory data = abi.encodePacked(bytes28(w1), w2, w3, w4, w5); 77 | assertEq(data.length, 0x20 * 5 - 4); 78 | 79 | uint256 g0 = gasleft(); 80 | tbytes.set(data); 81 | uint256 g1 = gasleft(); 82 | emit log_named_uint("used", g0 - g1); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/TransientPrimitives.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | /// @author philogy 5 | 6 | struct tuint256 { 7 | uint256 __placeholder; 8 | } 9 | 10 | struct tint256 { 11 | uint256 __placeholder; 12 | } 13 | 14 | struct tbytes32 { 15 | uint256 __placeholder; 16 | } 17 | 18 | struct taddress { 19 | uint256 __placeholder; 20 | } 21 | 22 | using TransientPrimitivesLib for tuint256 global; 23 | using TransientPrimitivesLib for tint256 global; 24 | using TransientPrimitivesLib for tbytes32 global; 25 | using TransientPrimitivesLib for taddress global; 26 | 27 | library TransientPrimitivesLib { 28 | error ArithmeticOverflowUnderflow(); 29 | 30 | function get(tuint256 storage ptr) internal view returns (uint256 value) { 31 | /// @solidity memory-safe-assembly 32 | assembly { 33 | value := tload(ptr.slot) 34 | } 35 | } 36 | 37 | function get(tint256 storage ptr) internal view returns (int256 value) { 38 | /// @solidity memory-safe-assembly 39 | assembly { 40 | value := tload(ptr.slot) 41 | } 42 | } 43 | 44 | function get(tbytes32 storage ptr) internal view returns (bytes32 value) { 45 | /// @solidity memory-safe-assembly 46 | assembly { 47 | value := tload(ptr.slot) 48 | } 49 | } 50 | 51 | function get(taddress storage ptr) internal view returns (address value) { 52 | /// @solidity memory-safe-assembly 53 | assembly { 54 | value := tload(ptr.slot) 55 | } 56 | } 57 | 58 | function set(tuint256 storage ptr, uint256 value) internal { 59 | /// @solidity memory-safe-assembly 60 | assembly { 61 | tstore(ptr.slot, value) 62 | } 63 | } 64 | 65 | function inc(tuint256 storage ptr, uint256 change) internal returns (uint256 newValue) { 66 | ptr.set(newValue = ptr.get() + change); 67 | } 68 | 69 | function dec(tuint256 storage ptr, uint256 change) internal returns (uint256 newValue) { 70 | ptr.set(newValue = ptr.get() - change); 71 | } 72 | 73 | function inc(tuint256 storage ptr, int256 change) internal returns (uint256 newValue) { 74 | uint256 currentValue = ptr.get(); 75 | assembly ("memory-safe") { 76 | newValue := add(currentValue, change) 77 | if iszero(eq(lt(newValue, currentValue), slt(change, 0))) { 78 | mstore(0x00, 0xc9654ed4 /* ArithmeticOverflowUnderflow() */ ) 79 | revert(0x1c, 0x04) 80 | } 81 | } 82 | ptr.set(newValue); 83 | } 84 | 85 | function dec(tuint256 storage ptr, int256 change) internal returns (uint256 newValue) { 86 | uint256 currentValue = ptr.get(); 87 | assembly ("memory-safe") { 88 | newValue := sub(currentValue, change) 89 | if iszero(eq(lt(newValue, currentValue), sgt(change, 0))) { 90 | mstore(0x00, 0xc9654ed4 /* ArithmeticOverflowUnderflow() */ ) 91 | revert(0x1c, 0x04) 92 | } 93 | } 94 | ptr.set(newValue); 95 | } 96 | 97 | function set(tint256 storage ptr, int256 value) internal { 98 | /// @solidity memory-safe-assembly 99 | assembly { 100 | tstore(ptr.slot, value) 101 | } 102 | } 103 | 104 | function inc(tint256 storage ptr, int256 change) internal returns (int256 newValue) { 105 | ptr.set(newValue = ptr.get() + change); 106 | } 107 | 108 | function dec(tint256 storage ptr, int256 change) internal returns (int256 newValue) { 109 | ptr.set(newValue = ptr.get() - change); 110 | } 111 | 112 | function set(tbytes32 storage ptr, bytes32 value) internal { 113 | /// @solidity memory-safe-assembly 114 | assembly { 115 | tstore(ptr.slot, value) 116 | } 117 | } 118 | 119 | function set(taddress storage ptr, address value) internal { 120 | /// @solidity memory-safe-assembly 121 | assembly { 122 | tstore(ptr.slot, value) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/PayableMulticallable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {tuint256} from "./TransientPrimitives.sol"; 5 | 6 | /// @author philogy 7 | abstract contract PayableMulticallable { 8 | error AmountOverflow(); 9 | error EthTransferFailed(); 10 | 11 | uint256 private constant _LOCK_FLAG_BIT = 1; 12 | 13 | /// @dev Lowest bit indicates that the lock is set. 14 | tuint256 private _topLevelValueAndLock; 15 | 16 | modifier standalonePayable() { 17 | uint256 valueAndLock = _topLevelValueAndLock.get(); 18 | bool topLevel = valueAndLock & 1 == 0; 19 | if (topLevel) _topLevelValueAndLock.set(_LOCK_FLAG_BIT | (msg.value << 1)); 20 | 21 | _; 22 | if (topLevel) _topLevelValueAndLock.set(0); 23 | } 24 | 25 | function multicall(bytes[] calldata data) external payable returns (bytes[] memory) { 26 | // Taken from Solady's Multicallable (https://github.com/Vectorized/solady/blob/main/src/utils/Multicallable.sol). 27 | assembly { 28 | let wasLocked := and(tload(_topLevelValueAndLock.slot), _LOCK_FLAG_BIT) 29 | if iszero(wasLocked) { tstore(_topLevelValueAndLock.slot, or(_LOCK_FLAG_BIT, shl(1, callvalue()))) } 30 | 31 | mstore(0x00, 0x20) 32 | mstore(0x20, data.length) // Store `data.length` into `results`. 33 | // Early return if no data. 34 | if iszero(data.length) { return(0x00, 0x40) } 35 | 36 | let results := 0x40 37 | // `shl` 5 is equivalent to multiplying by 0x20. 38 | let end := shl(5, data.length) 39 | // Copy the offsets from calldata into memory. 40 | calldatacopy(0x40, data.offset, end) 41 | // Offset into `results`. 42 | let resultsOffset := end 43 | // Pointer to the end of `results`. 44 | end := add(results, end) 45 | 46 | for {} 1 {} { 47 | // The offset of the current bytes in the calldata. 48 | let o := add(data.offset, mload(results)) 49 | let m := add(resultsOffset, 0x40) 50 | // Copy the current bytes from calldata to the memory. 51 | calldatacopy( 52 | m, 53 | add(o, 0x20), // The offset of the current bytes' bytes. 54 | calldataload(o) // The length of the current bytes. 55 | ) 56 | if iszero(delegatecall(gas(), address(), m, calldataload(o), codesize(), 0x00)) { 57 | // Bubble up the revert if the delegatecall reverts. 58 | returndatacopy(0x00, 0x00, returndatasize()) 59 | revert(0x00, returndatasize()) 60 | } 61 | // Append the current `resultsOffset` into `results`. 62 | mstore(results, resultsOffset) 63 | results := add(results, 0x20) 64 | // Append the `returndatasize()`, and the return data. 65 | mstore(m, returndatasize()) 66 | returndatacopy(add(m, 0x20), 0x00, returndatasize()) 67 | // Advance the `resultsOffset` by `returndatasize() + 0x20`, 68 | // rounded up to the next multiple of 32. 69 | resultsOffset := and(add(add(resultsOffset, returndatasize()), 0x3f), 0xffffffffffffffe0) 70 | if iszero(lt(results, end)) { break } 71 | } 72 | 73 | if iszero(wasLocked) { tstore(_topLevelValueAndLock.slot, 0) } 74 | 75 | return(0x00, add(resultsOffset, 0x40)) 76 | } 77 | } 78 | 79 | function useValue(uint256 amount) internal returns (uint256) { 80 | uint256 shifted = amount << 1; 81 | // Will underflow and revert if amount above remaining value. (If lock not acquired amount 82 | // is zero anyway). 83 | _topLevelValueAndLock.dec(shifted); 84 | if (shifted >> 1 != amount) revert AmountOverflow(); 85 | return amount; 86 | } 87 | 88 | function useAllValue() internal returns (uint256 value) { 89 | uint256 valueAndLock = _topLevelValueAndLock.get(); 90 | value = valueAndLock >> 1; 91 | _topLevelValueAndLock.set(valueAndLock & _LOCK_FLAG_BIT); 92 | } 93 | 94 | function _returnRemainingValue(address to) internal { 95 | uint256 valueAndLock = _topLevelValueAndLock.get(); 96 | uint256 value = valueAndLock >> 1; 97 | if (value > 0) { 98 | (bool suc,) = to.call{value: value}(""); 99 | if (!suc) revert EthTransferFailed(); 100 | // If value was above zero the lock *must've* been acquired, return to base state. 101 | _topLevelValueAndLock.set(_LOCK_FLAG_BIT); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transient Goodies 2 | 3 | As of 0.8.24 [solc](https://github.com/ethereum/solidity) does not grant access to a `storage`-type 4 | specifier to be able to easily define transient storage data structures. This library is meant to be a 5 | collection of various transient storage helpers and data structures. 6 | 7 | Note that under the hood most of the transient types in this library are implemented as structs with 8 | custom library methods mapped onto them. This means they will receive unique slots and are by 9 | default composable into mappings and other structs. When defining a custom struct you 10 | can also mix transient and persistent storage data. However it's important to note that **variables 11 | defined as `public` will not create valid getters**, this is because Solidity will default to 12 | reading the underlying storage struct and always default to 0. 13 | 14 | 15 | ## [Transient Primitives (`uint256`, `bytes32`, `address`)](./src/TransientPrimitives.sol) 16 | 17 | Mimics the main 3 solidity primitive types (`uint256`, `bytes32`, `address`) but has them use 18 | EIP-1153 transient storage. 19 | 20 | 21 | Usage (taken from [`TransientPrimitives.t.sol`](./test/TransientPrimitives.t.sol)): 22 | 23 | ```solidity 24 | // SPDX-License-Identifier: MIT 25 | pragma solidity ^0.8.24; 26 | 27 | import {Test} from "forge-std/Test.sol"; 28 | import {tuint256, tbytes32, taddress} from "../src/TransientPrimitives.sol"; 29 | 30 | /// @author philogy 31 | contract TransientPrimitivesTest is Test { 32 | // Definable as if they were normal storage variables. 33 | tuint256 uint256_var; 34 | tbytes32 bytes32_var; 35 | taddress address_var; 36 | 37 | // Can even compose to create a transient mapping. 38 | mapping(address => tuint256) transient_addr_to_uint; 39 | 40 | function test_defaultValues() public { 41 | // Default to 0 like storage variables. 42 | assertEq(uint256_var.get(), 0); 43 | assertEq(bytes32_var.get(), 0); 44 | assertEq(address_var.get(), address(0)); 45 | } 46 | 47 | function test_setUint256(uint256 value1, uint256 value2) public { 48 | // Can set and get values. 49 | uint256_var.set(value1); 50 | assertEq(uint256_var.get(), value1); 51 | uint256_var.set(value2); 52 | assertEq(uint256_var.get(), value2); 53 | } 54 | 55 | function test_setBytes32(bytes32 value1, bytes32 value2) public { 56 | // Can set and get values. 57 | bytes32_var.set(value1); 58 | assertEq(bytes32_var.get(), value1); 59 | bytes32_var.set(value2); 60 | assertEq(bytes32_var.get(), value2); 61 | } 62 | 63 | function test_setAddress(address value1, address value2) public { 64 | // Can set and get values. 65 | address_var.set(value1); 66 | assertEq(address_var.get(), value1); 67 | address_var.set(value2); 68 | assertEq(address_var.get(), value2); 69 | } 70 | 71 | function test_setAddrUintMap(address key, uint256 value) public { 72 | // Mapping works as you'd expect. 73 | assertEq(transient_addr_to_uint[key].get(), 0); 74 | transient_addr_to_uint[key].set(value); 75 | assertEq(transient_addr_to_uint[key].get(), value); 76 | } 77 | } 78 | ``` 79 | 80 | ## [Transient Bytes](./src/TransientBytesLib.sol) 81 | 82 | Mimics Solidity `bytes` but instead of being stored in persistent storage uses EIP-1153 transient 83 | storage. (Note while technically defined as a storage struct the underlying library never interacts 84 | with persistent storage and merely uses the `storage` definition to get access to a unique base slot). 85 | 86 | Usage (taken from [`TransientBytes.t.sol`](./test/TransientBytes.t.sol)): 87 | 88 | ```solidity 89 | 90 | // SPDX-License-Identifier: MIT 91 | pragma solidity 0.8.24; 92 | 93 | import {TransientBytes} from "../src/TransientBytesLib.sol"; 94 | 95 | /// @author philogy 96 | contract TransientBytesTest { 97 | TransientBytes data; 98 | 99 | // Has an unreachable theoretical maximum length of 2^32 bytes. 100 | uint256 internal constant MAX_LENGTH = type(uint32).max; 101 | 102 | function test_defaultEmpty() public { 103 | assertEq(data.get(), ""); 104 | } 105 | 106 | function test_setMem(bytes memory inner) public { 107 | vm.assume(inner.length <= MAX_LENGTH); 108 | // Store some value. 109 | data.set(inner); 110 | // Retrieve the value. 111 | assertEq(data.get(), inner); 112 | } 113 | 114 | function test_setCd(bytes calldata inner) public { 115 | vm.assume(inner.length <= MAX_LENGTH); 116 | // Store some value directly from calldata (more gas efficient than calling the memory 117 | // variant with the calldata argument). 118 | data.setCd(inner); 119 | // Retrieve data with the same endpoint. 120 | assertEq(data.get(), inner); 121 | } 122 | } 123 | ``` 124 | -------------------------------------------------------------------------------- /src/TransientBytesLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | struct TransientBytes { 5 | uint256 __placeholder; 6 | } 7 | 8 | using TransientBytesLib for TransientBytes global; 9 | 10 | /// @author philogy 11 | library TransientBytesLib { 12 | error DataTooLarge(); 13 | error OutOfOrderSlots(); 14 | error RangeTooLarge(); 15 | 16 | /// @dev 4-bytes is way above current max contract size, meant to account for future EVM 17 | /// versions. 18 | uint256 internal constant LENGTH_MASK = 0xffffffff; 19 | uint256 internal constant MAX_LENGTH = LENGTH_MASK; 20 | uint256 internal constant LENGTH_BYTES = 4; 21 | 22 | function length(TransientBytes storage self) internal view returns (uint256 len) { 23 | /// @solidity memory-safe-assembly 24 | assembly { 25 | let head := tload(self.slot) 26 | len := shr(sub(256, mul(LENGTH_BYTES, 8)), head) 27 | } 28 | } 29 | 30 | function setCd(TransientBytes storage self, bytes calldata data) internal { 31 | /// @solidity memory-safe-assembly 32 | assembly { 33 | let len := data.length 34 | 35 | if gt(len, LENGTH_MASK) { 36 | mstore(0x00, 0x54ef47ee /* DataTooLarge() */ ) 37 | revert(0x1c, 0x04) 38 | } 39 | 40 | // Store first word packed with length 41 | let head := calldataload(sub(data.offset, LENGTH_BYTES)) 42 | tstore(self.slot, head) 43 | 44 | if gt(len, sub(32, LENGTH_BYTES)) { 45 | // Derive extended slots. 46 | mstore(0x00, self.slot) 47 | let slot := keccak256(0x00, 0x20) 48 | 49 | // Store remainder. 50 | let offset := add(data.offset, sub(0x20, LENGTH_BYTES)) 51 | // Ensure each loop can do cheap comparison to see if it's at the end. 52 | let endOffset := sub(add(data.offset, len), 1) 53 | for {} 1 {} { 54 | tstore(slot, calldataload(offset)) 55 | offset := add(offset, 0x20) 56 | if gt(offset, endOffset) { break } 57 | slot := add(slot, 1) 58 | } 59 | } 60 | } 61 | } 62 | 63 | function set(TransientBytes storage self, bytes memory data) internal { 64 | /// @solidity memory-safe-assembly 65 | assembly { 66 | let len := mload(data) 67 | 68 | if gt(len, LENGTH_MASK) { 69 | mstore(0x00, 0x54ef47ee /* DataTooLarge() */ ) 70 | revert(0x1c, 0x04) 71 | } 72 | 73 | // Store first word packed with length 74 | let dataStart := add(data, 0x20) 75 | let head := mload(sub(dataStart, LENGTH_BYTES)) 76 | tstore(self.slot, head) 77 | 78 | if gt(len, sub(0x20, LENGTH_BYTES)) { 79 | // Derive extended slots. 80 | mstore(0x00, self.slot) 81 | let slot := keccak256(0x00, 0x20) 82 | 83 | // Store remainder. 84 | let offset := add(dataStart, sub(0x20, LENGTH_BYTES)) 85 | // Ensure each loop can do cheap comparison to see if it's at the end. 86 | let endOffset := sub(add(dataStart, len), 1) 87 | for {} 1 {} { 88 | tstore(slot, mload(offset)) 89 | offset := add(offset, 0x20) 90 | if gt(offset, endOffset) { break } 91 | slot := add(slot, 1) 92 | } 93 | } 94 | } 95 | } 96 | 97 | function get(TransientBytes storage self) internal view returns (bytes memory data) { 98 | /// @solidity memory-safe-assembly 99 | assembly { 100 | // Allocate and load head. 101 | data := mload(0x40) 102 | mstore(data, 0) 103 | mstore(add(data, sub(0x20, LENGTH_BYTES)), tload(self.slot)) 104 | // Get length and update free pointer. 105 | let dataStart := add(data, 0x20) 106 | let len := mload(data) 107 | mstore(0x40, add(dataStart, len)) 108 | 109 | if gt(len, sub(0x20, LENGTH_BYTES)) { 110 | // Derive extended slots. 111 | mstore(0x00, self.slot) 112 | let slot := keccak256(0x00, 0x20) 113 | 114 | // Store remainder. 115 | let offset := add(dataStart, sub(0x20, LENGTH_BYTES)) 116 | let endOffset := add(dataStart, len) 117 | for {} 1 {} { 118 | mstore(offset, tload(slot)) 119 | offset := add(offset, 0x20) 120 | if gt(offset, endOffset) { break } 121 | slot := add(slot, 1) 122 | } 123 | mstore(endOffset, 0) 124 | } 125 | } 126 | } 127 | 128 | function agus(TransientBytes storage self) internal { 129 | /// @solidity memory-safe-assembly 130 | assembly { 131 | // Resetting head automatically sets length to 0, rest remains in accessible. 132 | tstore(self.slot, 0) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/TransientPrimitives.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.24; 3 | 4 | import {Test, stdError} from "forge-std/Test.sol"; 5 | import {TransientPrimitivesLib, tuint256, tint256, tbytes32, taddress} from "../src/TransientPrimitives.sol"; 6 | 7 | /// @author philogy 8 | contract TransientPrimitivesTest is Test { 9 | tuint256 uint256_var; 10 | tint256 int256_var; 11 | tbytes32 bytes32_var; 12 | taddress address_var; 13 | 14 | mapping(address => tuint256) transient_addr_to_uint; 15 | 16 | function test_defaultValues() public { 17 | assertEq(uint256_var.get(), 0); 18 | assertEq(int256_var.get(), 0); 19 | assertEq(bytes32_var.get(), 0); 20 | assertEq(address_var.get(), address(0)); 21 | } 22 | 23 | function test_setUint256(uint256 value1, uint256 value2) public { 24 | uint256_var.set(value1); 25 | assertEq(uint256_var.get(), value1); 26 | uint256_var.set(value2); 27 | assertEq(uint256_var.get(), value2); 28 | } 29 | 30 | function test_setInt256(int256 value1, int256 value2) public { 31 | int256_var.set(value1); 32 | assertEq(int256_var.get(), value1); 33 | int256_var.set(value2); 34 | assertEq(int256_var.get(), value2); 35 | } 36 | 37 | function test_setBytes32(bytes32 value1, bytes32 value2) public { 38 | bytes32_var.set(value1); 39 | assertEq(bytes32_var.get(), value1); 40 | bytes32_var.set(value2); 41 | assertEq(bytes32_var.get(), value2); 42 | } 43 | 44 | function test_setAddress(address value1, address value2) public { 45 | address_var.set(value1); 46 | assertEq(address_var.get(), value1); 47 | address_var.set(value2); 48 | assertEq(address_var.get(), value2); 49 | } 50 | 51 | function test_increaseUint256(uint256 start, uint256 increase) public { 52 | increase = bound(increase, 0, type(uint256).max - start); 53 | uint256_var.set(start); 54 | assertEq(start + increase, uint256_var.inc(increase)); 55 | assertEq(start + increase, uint256_var.get()); 56 | } 57 | 58 | function test_increaseUint256RevertsOnOverflow(uint256 start, uint256 increase) public { 59 | start = bound(start, 1, type(uint256).max); 60 | increase = bound(increase, type(uint256).max - start + 1, type(uint256).max); 61 | uint256_var.set(start); 62 | vm.expectRevert(stdError.arithmeticError); 63 | uint256_var.inc(increase); 64 | } 65 | 66 | function test_signedUint256Increase(uint256 start, int256 delta) public { 67 | uint256 absDelta; 68 | unchecked { 69 | absDelta = delta < 0 ? uint256(-delta) : uint256(delta); 70 | } 71 | bool overflows = delta < 0 ? absDelta > start : type(uint256).max - start < absDelta; 72 | 73 | uint256_var.set(start); 74 | if (overflows) { 75 | vm.expectRevert(TransientPrimitivesLib.ArithmeticOverflowUnderflow.selector); 76 | uint256_var.inc(delta); 77 | } else { 78 | uint256_var.inc(delta); 79 | uint256 end = uint256_var.get(); 80 | if (delta < 0) { 81 | assertEq(end, start - absDelta); 82 | } else { 83 | assertEq(end, start + absDelta); 84 | } 85 | } 86 | } 87 | 88 | function test_signedUint256Decrease(uint256 start, int256 delta) public { 89 | uint256 absDelta; 90 | unchecked { 91 | absDelta = delta < 0 ? uint256(-delta) : uint256(delta); 92 | } 93 | bool overflows = delta < 0 ? type(uint256).max - start < absDelta : absDelta > start; 94 | 95 | uint256_var.set(start); 96 | if (overflows) { 97 | vm.expectRevert(TransientPrimitivesLib.ArithmeticOverflowUnderflow.selector); 98 | uint256_var.dec(delta); 99 | } else { 100 | uint256_var.dec(delta); 101 | uint256 end = uint256_var.get(); 102 | if (delta < 0) { 103 | assertEq(end, start + absDelta); 104 | } else { 105 | assertEq(end, start - absDelta); 106 | } 107 | } 108 | } 109 | 110 | function test_decreaseUint256(uint256 start, uint256 decrease) public { 111 | decrease = bound(decrease, 0, start); 112 | uint256_var.set(start); 113 | assertEq(start - decrease, uint256_var.dec(decrease)); 114 | assertEq(start - decrease, uint256_var.get()); 115 | } 116 | 117 | function test_decreaseUint256RevertsOnUnderflow(uint256 start, uint256 increase) public { 118 | start = bound(start, 0, type(uint256).max - 1); 119 | increase = bound(increase, start + 1, type(uint256).max); 120 | uint256_var.set(start); 121 | vm.expectRevert(stdError.arithmeticError); 122 | uint256_var.dec(increase); 123 | } 124 | 125 | function test_increaseInt256(int256 start, int256 change) public { 126 | int256 upperBound = start <= 0 ? type(int256).max : type(int256).max - start; 127 | int256 lowerBound = start >= 0 ? type(int256).min : type(int256).min - start; 128 | change = bound(change, lowerBound, upperBound); 129 | int256_var.set(start); 130 | assertEq(start + change, int256_var.inc(change)); 131 | assertEq(start + change, int256_var.get()); 132 | } 133 | 134 | function test_increaseInt256RevertsOnOverflow(int256 start, int256 change) public { 135 | // Ensure `start` is any non-zero number. 136 | start = int256(bound(uint256(start), 1, type(uint256).max)); 137 | assertTrue(start != 0); 138 | // Guarantee overflow. 139 | int256 lowerBound = start > 0 ? type(int256).max - start + 1 : type(int256).min; 140 | int256 upperBound = start < 0 ? type(int256).min - start - 1 : type(int256).max; 141 | change = bound(change, lowerBound, upperBound); 142 | int256_var.set(start); 143 | vm.expectRevert(stdError.arithmeticError); 144 | int256_var.inc(change); 145 | } 146 | 147 | function test_decreaseInt256(int256 start, int256 change) public { 148 | int256 upperBound = start >= 0 ? type(int256).max : start - type(int256).min; 149 | int256 lowerBound = start < 0 ? type(int256).min : start - type(int256).max; 150 | change = bound(change, lowerBound, upperBound); 151 | int256_var.set(start); 152 | assertEq(start - change, int256_var.dec(change)); 153 | assertEq(start - change, int256_var.get()); 154 | } 155 | 156 | function test_decreaseInt256RevertsOnUnderflow(int256 start, int256 change) public { 157 | // Ensure `start` is not 0 or -1. 158 | start = int256(bound(uint256(start), 1, type(uint256).max - 1)); 159 | int256 upperBound = start >= 0 ? start - type(int256).max - 1 : type(int256).max; 160 | int256 lowerBound = start < 0 ? start - type(int256).min + 1 : type(int256).min; 161 | change = bound(change, lowerBound, upperBound); 162 | int256_var.set(start); 163 | vm.expectRevert(stdError.arithmeticError); 164 | int256_var.dec(change); 165 | } 166 | 167 | function test_setAddrUintMap(address key, uint256 value) public { 168 | assertEq(transient_addr_to_uint[key].get(), 0); 169 | transient_addr_to_uint[key].set(value); 170 | assertEq(transient_addr_to_uint[key].get(), value); 171 | } 172 | } 173 | --------------------------------------------------------------------------------