# Solidity 0.8.30 — What's New and Why It Matters
Solidity 0.8.30 dropped in April 2026 with several long-awaited features. Transient storage improvements, better error messages, and gas optimizations make this a worthwhile upgrade.
Here is what changed and when you should upgrade.
Transient Storage Improvements (EIP-1153)
Transient storage was introduced in 0.8.24, but 0.8.30 makes it easier to use with native syntax.
Before (0.8.24-0.8.29)
You had to use inline assembly:
contract ReentrancyGuard {
bytes32 constant LOCK_SLOT = keccak256("lock");
modifier nonReentrant() {
assembly {
if tload(LOCK_SLOT) { revert(0, 0) }
tstore(LOCK_SLOT, 1)
}
_;
assembly {
tstore(LOCK_SLOT, 0)
}
}
}
After (0.8.30)
You can now use transient storage variables directly:
contract ReentrancyGuard {
transient bool locked;
modifier nonReentrant() {
require(!locked, "Reentrant");
locked = true;
_;
locked = false;
}
}
No assembly required. The compiler generates TLOAD and TSTORE opcodes automatically.
Transient storage is cleared at the end of each transaction, making it perfect for reentrancy locks, flash loan guards, and temporary flags.
Gas Savings
Transient storage is ~90% cheaper than SSTORE:
SSTORE(cold): 20,000 gas
TSTORE: 100 gas
For a reentrancy guard:
- Before (SSTORE): ~20,000 gas per call
- After (TSTORE): ~200 gas per call
That is a 100x reduction.
Named Return Destructuring
You can now destructure named returns directly:
Before
function getPosition(address user) external view
returns (uint256 shares, uint256 debt)
{
Position memory pos = positions[user];
return (pos.shares, pos.debt);
}
function liquidate(address user) external {
uint256 shares;
uint256 debt;
(shares, debt) = getPosition(user);
// use shares and debt
}
After
function liquidate(address user) external {
(uint256 shares, uint256 debt) = getPosition(user);
// use shares and debt
}
Cleaner and less verbose.
User-Defined Operators (Experimental)
0.8.30 adds experimental support for user-defined operators. This lets you overload operators like +, *, == for custom types.
Example: Fixed-Point Math
type Fixed18 is int256;
using {add as +, mul as *, eq as ==} for Fixed18 global;
function add(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) + Fixed18.unwrap(b));
}
function mul(Fixed18 a, Fixed18 b) pure returns (Fixed18) {
return Fixed18.wrap(Fixed18.unwrap(a) * Fixed18.unwrap(b) / 1e18);
}
function eq(Fixed18 a, Fixed18 b) pure returns (bool) {
return Fixed18.unwrap(a) == Fixed18.unwrap(b);
}
contract Math {
function test() external pure returns (Fixed18) {
Fixed18 a = Fixed18.wrap(2e18);
Fixed18 b = Fixed18.wrap(3e18);
return a + b * a; // Readable!
}
}
This is still experimental, but it makes math libraries much more readable.
Better Error Messages
The compiler now gives more specific error messages for common mistakes.
Before
Error: Type uint256 is not implicitly convertible to address.
After
Error: Type uint256 is not implicitly convertible to address.
→ Did you mean to use address(uint160(...))?
The compiler now suggests fixes for:
- Unsafe type casts
- Missing
payableon addresses
- Incorrect function visibility
- Unused return values
Gas Optimizations
0.8.30 improves codegen for:
- Struct packing (better slot allocation)
- Memory expansion (fewer
MSTOREops)
- Short-circuiting in conditionals (skip expensive checks early)
Benchmark on a real protocol (Uniswap V3 pool):
- 0.8.29: 2,450,000 gas (deploy)
- 0.8.30: 2,380,000 gas (deploy)
~3% reduction in deploy cost, 1-2% reduction in runtime gas.
Breaking Changes
1. Stricter Type Checking for abi.decode
// This now fails:
bytes memory data = ...;
uint256 value = abi.decode(data, (uint256, uint256)); // ERROR
// Fix:
(uint256 a, uint256 b) = abi.decode(data, (uint256, uint256));
2. internal Functions in Interfaces No Longer Allowed
Interfaces can only have external functions now:
interface IToken {
function transfer(address to, uint256 amount) external; // OK
function _internal() internal; // ERROR in 0.8.30
}
3. Deprecated selfdestruct
selfdestruct is deprecated (following EIP-6780). It no longer deletes code or sends ETH unless called in the same transaction as contract creation.
Use this pattern instead:
function destroy() external onlyOwner {
payable(owner).transfer(address(this).balance);
// Code remains, but funds are withdrawn
}
Should You Upgrade?
Upgrade if:
- You use reentrancy guards (huge gas savings with
transient)
- You want better error messages (helpful for juniors)
- You are starting a new project
Wait if:
- You rely on
selfdestructbehavior
- You have complex inline assembly (test thoroughly)
- You need 100% backwards compatibility with old tooling
How to Upgrade
foundry.toml:[profile.default]
solc_version = "0.8.30"
forge test
forge snapshot --diff
Summary
0.8.30 is a solid upgrade. Transient storage syntax alone is worth it for protocols with reentrancy guards. Better error messages and gas optimizations are nice bonuses.
Breaking changes are minimal — most codebases will upgrade smoothly.
If you are on 0.8.24+, upgrade. If you are on 0.8.20 or earlier, plan for a bigger migration.