Understanding Transient Storage

Understanding Transient Storage

Solidity
EIP-1153
Yul
2m

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 and TSTORE 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

ReentrancyGuard.sol
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
ReentrancyGuard.sol
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

UniswapV3Pool.sol
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

UniswapV3PoolDeployer.sol
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.

UniswapV3Pool.sol
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
UniswapV3PoolDeployer.sol
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

UniswapV2Pair.sol
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.

UniswapV2Pair.sol
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
UniswapV2PairDeployer.sol
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, and token1 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.


Explore more posts