Solidity 0.8.26 transient storage is the biggest EVM gas optimization since the merge — and most developers are still not using it. Introduced via EIP-1153 and enabled on mainnet with the Cancun upgrade, transient variables let you store data that lives only for the duration of a transaction, at a fraction of the cost of regular storage.
This guide walks through exactly how it works, where to use it, and how to migrate existing patterns like reentrancy guards and flash loan callbacks.
You'll learn:
- What
transientstorage is and how it differs fromstorageandmemory - How to declare and use
transientvariables in Solidity 0.8.26+ - How to rewrite a reentrancy guard to cut gas by ~80%
- How to test transient storage in Foundry and Hardhat
Time: 20 min | Difficulty: Intermediate
Why Transient Storage Exists
Before Cancun, you had two options for temporary per-transaction state:
storage— persists across transactions, costs 20,000 gas to write a cold slot (SSTORE), 100 gas to read (SLOAD).memory— lives only in the current call frame, can't cross call boundaries.
Neither was ideal for patterns that need cross-call, within-transaction state — like a reentrancy lock or a flash loan validation flag. You had to use storage for those, paying the full cold-write penalty every single transaction.
EIP-1153 introduces two new opcodes: TSTORE and TLOAD. They work exactly like SSTORE and SLOAD but write to a separate "transient" memory space that is automatically cleared at the end of each transaction. No refund needed. No cleanup code required. The EVM just wipes it.
Cost comparison on Cancun mainnet:
| Operation | Opcode | Gas Cost |
|---|---|---|
| Cold storage write | SSTORE | 20,000 |
| Warm storage write | SSTORE | 2,900 |
| Transient write | TSTORE | 100 |
| Cold storage read | SLOAD | 2,100 |
| Warm storage read | SLOAD | 100 |
| Transient read | TLOAD | 100 |
Writing a reentrancy lock with storage costs 20,000 gas cold. With transient, it costs 100. That's a 200× reduction on the first access.
Transient storage clears automatically after every transaction — no manual reset, no refund.
Requirements
- Solidity 0.8.24+ (transient keyword and EIP-1153 support)
- EVM version Cancun or later (
evmVersion: "cancun"in config) - Network: Ethereum mainnet post-March 2024, or any L2 with Cancun support (Arbitrum One, Base, Optimism)
- Foundry ≥ 0.2.0 or Hardhat ≥ 2.22.0 with
hardhat-toolboxv4+
Verify your Solidity version:
# Check installed solc version
solc --version
# Or with Foundry
forge --version
How to Declare Transient Variables in Solidity
The transient keyword works similarly to storage — you declare it at the contract level, not inside a function.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract TransientExample {
// Regular storage — persists forever, costs 20k gas cold write
uint256 private _normalLock;
// Transient storage — cleared after every tx, costs 100 gas
uint256 private transient _lock;
// Transient can hold any value type: bool, address, bytes32, uint256
address private transient _flashLoanCaller;
bool private transient _entered;
}
Key rules:
- Only value types are supported:
uint,int,bool,address,bytes1–bytes32 - Reference types (
mapping, arrays, structs) are not supported yet in Solidity syntax — use assembly for those transientvariables cannot beimmutableorconstant- They cannot be initialized with a value at declaration — they always start as zero
Rewrite a Reentrancy Guard with Transient Storage
This is the most common migration. Here's a standard ReentrancyGuard using storage:
// BEFORE — uses storage, 20,000 gas cold write
abstract contract ReentrancyGuard {
uint256 private _status;
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
constructor() {
_status = NOT_ENTERED; // 20,000 gas in constructor
}
modifier nonReentrant() {
require(_status != ENTERED, "ReentrancyGuard: reentrant call");
_status = ENTERED; // 2,900 gas warm write (100 if already warm)
_;
_status = NOT_ENTERED; // 2,900 gas to reset
}
}
And here's the transient version:
// AFTER — uses transient storage, 100 gas write, auto-reset
abstract contract TransientReentrancyGuard {
// Zero by default each tx — no constructor init needed
uint256 private transient _status;
modifier nonReentrant() {
// TLOAD costs 100 gas
require(_status == 0, "ReentrancyGuard: reentrant call");
// TSTORE costs 100 gas
_status = 1;
_;
// No reset needed — EVM clears transient storage after the tx
_status = 0; // Optional: set back to 0 to signal clean exit
}
}
Gas savings per protected function call:
- Before (cold): 20,000 (constructor) + 2,900 (enter) + 2,900 (reset) = ~25,800 gas
- After: 100 (TLOAD) + 100 (TSTORE) = ~200 gas
- Savings: ~99% on first interaction, ~93% warm
Flash Loan Callback Validation
Flash loan contracts need to verify that a callback is coming from their own trusted context. The old pattern used storage:
// BEFORE — storage-based flash loan callback guard
contract FlashLoanOld {
address private _activeBorrower; // storage slot — costs 20k first write
function flashLoan(address receiver, uint256 amount) external {
_activeBorrower = receiver; // SSTORE
// ... transfer tokens, call receiver.onFlashLoan() ...
_activeBorrower = address(0); // SSTORE reset — get partial refund
}
function onFlashLoan(...) external {
require(msg.sender == address(this), "not from self");
require(_activeBorrower == tx.origin, "invalid caller"); // SLOAD
}
}
With transient storage:
// AFTER — transient flash loan callback guard
contract FlashLoanNew {
address private transient _activeBorrower; // transient — 100 gas, auto-clears
function flashLoan(address receiver, uint256 amount) external {
_activeBorrower = receiver; // TSTORE — 100 gas
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(receiver, amount);
IFlashLoanReceiver(receiver).onFlashLoan(msg.sender, amount, "");
// No manual reset — transient clears at end of tx
require(
token.balanceOf(address(this)) >= balanceBefore,
"flash loan not repaid"
);
}
function validateFlashLoanCaller() internal view {
require(_activeBorrower != address(0), "no active loan"); // TLOAD
}
}
Using Assembly for Transient Mappings
Solidity 0.8.26 does not yet support mapping(...) transient. For per-address transient state, use inline assembly:
// Per-user transient balance — not possible in native Solidity syntax yet
function _setTransientBalance(address user, uint256 amount) internal {
// Derive a unique storage slot: keccak256(user ++ TRANSIENT_SLOT_BASE)
bytes32 slot = keccak256(abi.encodePacked(user, uint256(0x1)));
assembly {
tstore(slot, amount) // TSTORE opcode — 100 gas
}
}
function _getTransientBalance(address user) internal view returns (uint256 amount) {
bytes32 slot = keccak256(abi.encodePacked(user, uint256(0x1)));
assembly {
amount := tload(slot) // TLOAD opcode — 100 gas
}
}
Warning: Slot collision is your responsibility here. Use a well-defined base constant and document it. OpenZeppelin's SlotDerivation library (v5.1+) handles this safely.
Testing Transient Storage in Foundry
Configure your foundry.toml first:
# foundry.toml
[profile.default]
evm_version = "cancun" # Required — transient opcodes not available on older EVM
solc_version = "0.8.26"
optimizer = true
optimizer_runs = 200
Write your test:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "../src/TransientReentrancyGuard.sol";
contract TransientStorageTest is Test {
MockVault vault;
function setUp() public {
vault = new MockVault();
}
function test_ReentrancyBlocked() public {
// First call succeeds
vault.deposit(1 ether);
// Reentrancy attempt must revert
MaliciousReceiver attacker = new MaliciousReceiver(address(vault));
vm.expectRevert("ReentrancyGuard: reentrant call");
attacker.attack{value: 1 ether}();
}
function test_TransientClearsAcrossTx() public {
// Status should be 0 at start of every new tx
vault.deposit(1 ether);
// Second transaction — transient slot is cleared, so deposit works again
vault.deposit(1 ether);
}
function test_GasTransientVsStorage() public {
uint256 gasBefore = gasleft();
vault.deposit(1 ether);
uint256 gasUsed = gasBefore - gasleft();
// Should be well under 5,000 gas for the lock ops alone
assertLt(gasUsed, 50_000, "unexpectedly high gas");
}
}
Run the tests:
forge test --match-contract TransientStorageTest -vvv
Expected output:
[PASS] test_ReentrancyBlocked() (gas: 42183)
[PASS] test_TransientClearsAcrossTx() (gas: 38210)
[PASS] test_GasTransientVsStorage() (gas: 38210)
Testing Transient Storage in Hardhat
// hardhat.config.js
module.exports = {
solidity: {
version: "0.8.26",
settings: {
evmVersion: "cancun", // Required for TSTORE / TLOAD
optimizer: { enabled: true, runs: 200 }
}
}
};
// test/TransientGuard.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("TransientReentrancyGuard", function () {
let vault;
beforeEach(async function () {
const Vault = await ethers.getContractFactory("MockVault");
vault = await Vault.deploy();
});
it("blocks reentrant calls", async function () {
const Attacker = await ethers.getContractFactory("MaliciousReceiver");
const attacker = await Attacker.deploy(vault.target);
await expect(
attacker.attack({ value: ethers.parseEther("1") })
).to.be.revertedWith("ReentrancyGuard: reentrant call");
});
it("transient slot resets between transactions", async function () {
// Each of these is a separate tx — transient clears automatically
await vault.deposit({ value: ethers.parseEther("1") });
await vault.deposit({ value: ethers.parseEther("1") }); // must not revert
});
});
npx hardhat test --grep "TransientReentrancyGuard"
When NOT to Use Transient Storage
Transient storage solves a specific problem. Don't reach for it when:
- Data must persist — use
storage. Transient is gone after the transaction. - Data is local to one function — use
memoryor stack variables; they're cheaper. - You need mappings or dynamic arrays — native Solidity syntax doesn't support transient reference types yet. Assembly workarounds work but add audit surface.
- Targeting pre-Cancun networks —
TSTORE/TLOADwill revert on Shanghai and earlier. Check your deployment targets. Polygon PoS enabled Cancun in March 2024; BSC in April 2024. - Your audit team isn't ready — transient storage is new. Many auditors will flag it as unreviewed territory until tooling (Slither, Echidna) fully supports it.
Verification
After deployment, verify that your contract correctly uses transient opcodes:
# Disassemble and grep for TSTORE / TLOAD
forge inspect src/TransientReentrancyGuard.sol:TransientReentrancyGuard opcodes | grep -E "TSTORE|TLOAD"
You should see:
TLOAD
TSTORE
If you see SLOAD/SSTORE where you expected transient opcodes, your evmVersion is not set to cancun.
What You Learned
- Transient storage (
transientkeyword +TSTORE/TLOAD) is cleared after every transaction — no manual reset. - Gas cost is 100 gas per read and write — ~200× cheaper than a cold
SSTOREfor reentrancy locks. - Best use cases: reentrancy guards, flash loan callbacks, cross-contract temporary flags in the same tx.
- Limitation: native syntax only supports value types; reference types require assembly. Full
mappingsupport is planned for a future Solidity release. - Cancun is required — always set
evmVersion: "cancun"in Foundry and Hardhat configs.
Tested on Solidity 0.8.26, Foundry 0.2.0, Hardhat 2.22.3, EVM Cancun, Ubuntu 22.04 & macOS 14.
FAQ
Q: Does transient storage work on all EVM-compatible chains? A: Only on chains that have adopted the Cancun upgrade (EIP-1153). Ethereum mainnet, Arbitrum One, Base, Optimism, and Polygon PoS all support it as of 2026. BSC and zkSync Era require checking current hardfork status.
Q: What happens if I use transient but set evmVersion to shanghai?
A: The compiler will reject it with TypeError: Transient storage is not supported for this EVM version. You must set evmVersion: "cancun" or later.
Q: Can I use transient storage inside a view or pure function?
A: TLOAD is allowed in view functions — reading transient state does not modify persistent state. TSTORE is not allowed in view or pure functions.
Q: Is OpenZeppelin's ReentrancyGuard updated to use transient storage?
A: Yes — OpenZeppelin Contracts v5.1 introduced ReentrancyGuardTransient that uses TSTORE/TLOAD. You can drop it in as a direct replacement for ReentrancyGuard on Cancun-compatible networks.
Q: How much does transient storage cost compared to storage on a $0.10 ETH gas scenario? A: At 20 gwei gas price and ETH at ~$3,200 USD, a cold SSTORE costs ~$1.28 per call. A TSTORE costs ~$0.0064 — roughly $1.27 in savings per reentrancy guard interaction. High-frequency DeFi contracts save hundreds of dollars per day at moderate volume.