How I Saved 20,000 Gas Per Transaction by Reordering One Line in Solidity
Source: Dev.to
Smart‑Wallet Storage Optimization – A Quick Code Review
While building Fishnet – an AI‑agent transaction‑security proxy – I ran a self‑imposed code review and uncovered a subtle optimization that every Solidity developer should know about: re‑ordering state variables can save ~20 k gas per transaction.
Original State Layout
address public owner; // 20 bytes → Slot 0
address public fishnetSigner; // 20 bytes → Slot 1
mapping(uint256 => bool) public usedNonces; // Slot 2
bool public paused; // 1 byte → Slot 3 ← wasting 31 bytes
bool pausedoccupies its own 32‑byte slot, wasting 31 bytes and incurring an extraSLOAD/SSTOREon every pause check.
The EVM works with 32‑byte words. The Solidity compiler lays out storage top‑to‑bottom in declaration order:
| Slot | Layout |
|---|---|
| 0 | [owner ─────── 20 B][── 12 B empty] |
| 1 | [fishnetSigner ─────── 20 B][── 12 B empty] |
| 2 | [usedNonces mapping hash ─────────────────────── 32 B] |
| 3 | [paused ─ 1 B][─────── 31 B empty] |
The compiler does not reorder variables for you. If a variable cannot fit in the remaining space of the current slot, it starts a new one.
Optimized Layout
Move paused right after owner:
address public owner; // 20 bytes ─┐
bool public paused; // 1 byte ──┘ → Slot 0 (21/32 B)
address public fishnetSigner; // 20 bytes → Slot 1
mapping(uint256 => bool) public usedNonces; // Slot 2
Resulting storage:
| Slot | Layout |
|---|---|
| 0 | [owner ─────── 20 B][paused ─ 1 B][── 11 B empty] |
| 1 | [fishnetSigner ─────── 20 B][── 12 B empty] |
| 2 | [usedNonces mapping hash ─────────────────────── 32 B] |
Benefit: 4 slots → 3 slots. One fewer storage slot is touched at runtime.
Gas Savings in Practice
| Operation | Before (separate slots) | After (packed) | Approx. Savings |
|---|---|---|---|
Cold SLOAD (first read in tx) | 2,100 gas × 2 slots | 2,100 gas × 1 slot | 2,100 gas |
Cold SSTORE (pause/unpause) | ~20,000 gas | 0 (slot already warm from owner) | ~20,000 gas |
whenNotPaused modifier per call | Reads its own slot + owner’s slot (often warm) | Same reads, but now both are in the same slot | Up to 2,000 gas saved |
The biggest win is eliminating the cold SSTORE. Writing to a storage slot that hasn’t been accessed in the current transaction costs ~20 k gas. If owner has already been read (which is typical), the slot containing paused is now warm, and a warm SSTORE costs only ~2.9 k gas.
Verifying Layout with Foundry
forge inspect YourContract storage-layout
The command prints each variable’s slot, offset, and byte size.
Example output:
| Name | Type | Slot | Offset | Bytes |
|---|---|---|---|---|
| owner | address | 0 | 0 | 20 |
| paused | bool | 0 | 20 | 1 |
| fishnetSigner | address | 1 | 0 | 20 |
| usedNonces | mapping(uint256 => bool) | 2 | 0 | 32 |
- When
Offset > 0, packing is already happening. - When a small type has
Offset = 0and its own slot, that’s a packing opportunity.
Other Findings from the Review
| Issue | Before | After |
|---|---|---|
| Permit value mismatch | No check that permit.value == msg.value. | require(permit.value == msg.value, InsufficientValue()); |
Stale DOMAIN_SEPARATOR | Cached at deployment, never recomputed on a fork. | Re‑compute when block.chainid != _CACHED_CHAIN_ID. |
| Unnecessary hash | keccak256 computed before validating signature length. | Validate length first, then hash. |
| String revert messages | require(..., "Not authorized") (stores string in bytecode). | Use custom errors: error Unauthorized(); if (msg.sender != owner) revert Unauthorized(); |
| Dead code | Leftover console.log imports & unused test helpers. | Removed – reduces deployment bytecode size. |
Takeaway
A code review isn’t just about finding bugs; it’s about understanding the machine your code runs on.
- The EVM’s 32‑byte word size means every storage slot costs real money.
- Knowing how the compiler packs storage can be the difference between a contract that costs users $2 vs $5 per transaction.
Action items:
- Run
forge inspect YourContract storage-layout. - Look for variables that could be packed together (combined size ≤ 32 bytes) but are in separate slots.
- Re‑order or group small types (
bool,uint8,uint16,address) accordingly.
You might be surprised what you find.
This optimization came out of building Fishnet – an open‑source security proxy for AI agents.
## Rarity Proxy for AI Agent Transactions on Ethereum
Rarity proxy for AI agent transactions on Ethereum. If you're working on **AI × Web3** infrastructure, check it out.