Understanding Transient Storage
Transient storage, introduced by EIP-1153, is a concept in Ethereum's EVM that provides temporary storage within a transaction. Unlike traditional storage, which persists on the blockchain across transactions, transient storage is cleared at the end of each transaction. This makes it ideal for storing temporary data that doesn't need to be permanently saved on the blockchain.
Key Features
- Temporary Data: Data stored in transient storage lasts only for the duration of the transaction and is erased afterward.
- New Opcodes: The
TLOAD
andTSTORE
opcodes enable reading from and writing to transient storage, making it an essential tool for short-lived data. - Cost Efficiency: Using transient storage reduces gas costs compared to permanent storage, making smart contracts more efficient.
Benefits
- Reduced Gas Costs: By avoiding the use of permanent storage for temporary data, gas costs can be significantly reduced.
- Increased Efficiency: Contracts can perform more efficiently by utilizing transient storage for short-lived data.
- Simplified State Management: Managing transient state within a transaction becomes easier without the need to clean up or reset state manually.
Use Cases
Transient storage can be used in various contexts where temporary data needs to be handled efficiently. Below are a couple of notable examples where transient storage can significantly improve the performance and security of smart contracts.
Reentrancy Locks
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract ReentrancyGuard {
5 error ReentrantCall();
6
7 uint256 private constant UNLOCKED = 1;
8 uint256 private constant LOCKED = 2;
9
10 uint256 private _status;
11
12 modifier lock() {
13 if (_status == LOCKED) revert ReentrantCall();
14 _status = LOCKED;
15 _;
16 _status = UNLOCKED;
17 }
18}
19
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract ReentrancyGuard {
5 // keccak256(abi.encode(uint256(keccak256("ReentrancyGuard.storage.tslot")) - 1)) & ~bytes32(uint256(0xff))
6 bytes32 private constant SLOT = 0x7fa101c675504f300024665bb98368fa249f0de168a726bc582d6a21883adf00;
7
8 modifier lock() {
9 assembly ("memory-safe") {
10 if iszero(iszero(tload(SLOT))) {
11 mstore(0x00, 0x37ed32e8) // ReentrantCall()
12 revert(0x1c, 0x04)
13 }
14
15 tstore(SLOT, 0x01)
16 }
17
18 _;
19
20 assembly ("memory-safe") {
21 tstore(SLOT, 0x00)
22 }
23 }
24}
25
Constructing Uniswap V3 Pool
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract UniswapV3Pool {
5 constructor() {
6 int24 _tickSpacing;
7 (factory, token0, token1, fee, _tickSpacing) = IUniswapV3PoolDeployer(msg.sender).parameters();
8 tickSpacing = _tickSpacing;
9 maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(_tickSpacing);
10 }
11
12 function initialize(uint160 sqrtPriceX96) external {
13 require(slot0.sqrtPriceX96 == 0, 'AI');
14
15 int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
16
17 (uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp());
18
19 slot0 = Slot0({
20 sqrtPriceX96: sqrtPriceX96,
21 tick: tick,
22 observationIndex: 0,
23 observationCardinality: cardinality,
24 observationCardinalityNext: cardinalityNext,
25 feeProtocol: 0,
26 unlocked: true
27 });
28
29 emit Initialize(sqrtPriceX96, tick);
30 }
31}
32
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract UniswapV3PoolDeployer {
5 struct Parameters {
6 address factory;
7 address token0;
8 address token1;
9 uint24 fee;
10 int24 tickSpacing;
11 }
12
13 Parameters public parameters;
14
15 function deploy(
16 address factory,
17 address token0,
18 address token1,
19 uint24 fee,
20 int24 tickSpacing
21 ) internal returns (address pool) {
22 parameters = Parameters({factory: factory, token0: token0, token1: token1, fee: fee, tickSpacing: tickSpacing});
23 pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());
24 delete parameters;
25 }
26}
27
The pool parameters struct can be stored in transient storage and used to construct the immutable states of the pool. Also the pool can be initialized inside of the constructor if necessary.
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract UniswapV3Pool {
5 constructor() {
6 int24 _tickSpacing;
7 uint160 sqrtPriceX96;
8 (factory, token0, token1, fee, _tickSpacing, sqrtPriceX96) = IUniswapV3PoolDeployer(msg.sender).parameters();
9 tickSpacing = _tickSpacing;
10 maxLiquidityPerTick = Tick.tickSpacingToMaxLiquidityPerTick(_tickSpacing);
11
12 int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
13
14 (uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp());
15
16 slot0 = Slot0({
17 sqrtPriceX96: sqrtPriceX96,
18 tick: tick,
19 observationIndex: 0,
20 observationCardinality: cardinality,
21 observationCardinalityNext: cardinalityNext,
22 feeProtocol: 0,
23 unlocked: true
24 });
25
26 emit Initialize(sqrtPriceX96, tick);
27 }
28}
29
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract UniswapV3PoolDeployer {
5 // bytes32(uint256(keccak256("UniswapV3Factory.poolContext.slot")) - 1) & ~bytes32(uint256(0xff))
6 bytes32 private constant SLOT = 0x4cbc4600075b2f8c615834a56221934e92a9a18daaa9939c06932f192e907900;
7
8 function deploy(
9 address factory,
10 address token0,
11 address token1,
12 uint24 fee,
13 int24 tickSpacing,
14 uint160 sqrtPriceX96
15 ) internal returns (address pool) {
16 bytes memory bytecode = type(UniswapV3Pool).creationCode;
17
18 bytes32 salt = keccak256(abi.encode(token0, token1, fee));
19
20 assembly ("memory-safe") {
21 if iszero(iszero(tload(SLOT))) {
22 mstore(0x00, 0x55b9fb08) // SlotNotEmpty()
23 revert(0x1c, 0x04)
24 }
25
26 tstore(SLOT, factory)
27 tstore(add(SLOT, 0x20), token0)
28 tstore(add(SLOT, 0x40), token1)
29 tstore(add(SLOT, 0x60), fee)
30 tstore(add(SLOT, 0x80), tickSpacing)
31 tstore(add(SLOT, 0xa0), sqrtPriceX96)
32
33 pool := create2(0x00, add(bytecode, 0x20), mload(bytecode), salt)
34
35 if iszero(pool) {
36 mstore(0x00, 0xdc802c74) // PoolCreationFailed()
37 revert(0x1c, 0x04)
38 }
39
40 tstore(SLOT, 0x00)
41 tstore(add(SLOT, 0x20), 0x00)
42 tstore(add(SLOT, 0x40), 0x00)
43 tstore(add(SLOT, 0x60), 0x00)
44 tstore(add(SLOT, 0x80), 0x00)
45 tstore(add(SLOT, 0xa0), 0x00)
46 }
47 }
48
49 function parameters()
50 external
51 view
52 returns (address factory, address token0, address token1, uint24 fee, int24 tickSpacing, uint160 sqrtPriceX96)
53 {
54 assembly ("memory-safe") {
55 if iszero(tload(SLOT)) {
56 mstore(0x00, 0xce174065) // SlotEmpty()
57 revert(0x1c, 0x04)
58 }
59
60 factory := tload(SLOT)
61 token0 := tload(add(SLOT, 0x20))
62 token1 := tload(add(SLOT, 0x40))
63 fee := tload(add(SLOT, 0x60))
64 tickSpacing := tload(add(SLOT, 0x80))
65 sqrtPriceX96 := tload(add(SLOT, 0xa0))
66 }
67 }
68}
69
Constructing Uniswap V2 Pair
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract UniswapV2Pair {
5 address public factory;
6 address public token0;
7 address public token1;
8
9 constructor() {
10 factory = msg.sender;
11 }
12
13 function initialize(address _token0, address _token1) external {
14 require(msg.sender == factory, 'UniswapV2: FORBIDDEN');
15 token0 = _token0;
16 token1 = _token1;
17 }
18}
19
The Uniswap V2 factory and pair contracts can also be optimized similar to V3.
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract UniswapV2Pair {
5 address public immutable factory;
6 address public immutable token0;
7 address public immutable token1;
8
9 constructor() {
10 factory = msg.sender;
11 (token0, token1) = IUniswapV2PairDeployer(msg.sender).parameters();
12 }
13
14 function initialize(address, address) external {
15 revert();
16 }
17}
18
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.25;
3
4contract UniswapV2PairDeployer {
5 // bytes32(uint256(keccak256("UniswapV2Factory.pairContext.slot")) - 1) & ~bytes32(uint256(0xff))
6 bytes32 private constant SLOT = 0x7795f0288f84ebc3248ba2ece8a261d88f3c68aa04c040b6eed48a2eafe53400;
7
8 function deploy(
9 address token0,
10 address token1
11 ) internal returns (address pair) {
12 bytes memory bytecode = type(UniswapV2Pair).creationCode;
13
14 bytes32 salt = keccak256(abi.encodePacked(token0, token1));
15
16 assembly ("memory-safe") {
17 if iszero(iszero(tload(SLOT))) {
18 mstore(0x00, 0x55b9fb08) // SlotNotEmpty()
19 revert(0x1c, 0x04)
20 }
21
22 tstore(SLOT, token0)
23 tstore(add(SLOT, 0x20), token1)
24
25 pair := create2(0x00, add(bytecode, 0x20), mload(bytecode), salt)
26
27 if iszero(pair) {
28 mstore(0x00, 0xe1745f83) // PairCreationFailed()
29 revert(0x1c, 0x04)
30 }
31
32 tstore(SLOT, 0x00)
33 tstore(add(SLOT, 0x20), 0x00)
34 }
35 }
36
37 function parameters()
38 external
39 view
40 returns (address token0, address token1)
41 {
42 assembly ("memory-safe") {
43 if iszero(tload(SLOT)) {
44 mstore(0x00, 0xce174065) // SlotEmpty()
45 revert(0x1c, 0x04)
46 }
47
48 token0 := tload(SLOT)
49 token1 := tload(add(SLOT, 0x20))
50 }
51 }
52}
53
The addresses of
factory
,token0
, andtoken1
are now stored as immutable states which can save gas in other executions. Also,initialize
function is no longer required to be called after the deployment.
Conclusion
Transient storage is a powerful feature in Solidity that enables developers to store temporary data more efficiently and cost-effectively than using the traditional storage location. By reducing the gas costs of temporary computations, transient storage opens up new possibilities for optimizing smart contracts, especially for security-critical operations like reentrancy protection or complex decentralized finance applications.
However, developers should use transient storage carefully, particularly when dealing with multi-step processes or composability with other smart contracts. As Solidity evolves, transient storage is expected to become a critical tool for developers looking to build more efficient and secure smart contracts.