What if your storage model matched your hardware?

The storage engine touches 4,096 bytes for a 32-byte read.
MIP-8 makes the EVM account for that page-sized reality.

The information on this page should not be quoted. Please refer to MIP-8 for the authoritative spec.

waiting...

128 slots × 32 bytes = 4,096 bytes = 1 page

127 sibling slots stay unused in this example

Same struct, different cost model

Solidity lays out struct fields at contiguous slots, but trie/backend hashing can scatter them across different physical locations. In this worst-case illustration, each field lands on a separate backend page. MIP-8 groups contiguous slots into one page.

Click each field to load it and compare the gas cost side by side. Cold read costs 8,100 gas, warm read costs 100 gas on Monad.

struct Token { owner, balance, timestamp, approved }

Monad (current)

0 cold reads

page 0x7a2f...

page 0x3e81...

page 0xb4c5...

page 0x91d7...

0 x 8,100

0

MIP-8

0

page 0 (contiguous)

0

0

Compare the cost

Select a scenario to see the gas breakdown under Monad's current model versus MIP-8's page-aware model.

The read examples use Monad's gas constants (8,100 cold / 100 warm) and assume the accessed run fits in one page. The write example is qualitative because MIP-8 defines abstract page-write and state-growth parameters instead of fixed numbers.

Loading 4 contiguous struct fields that fit in one page

Monad (current)

32,400

gas

4 × 8,100 (distinct cold slots on Monad)

MIP-8

8,400

gas

1 × 8,100 (first page touch) + 3 × 100 (warm reads in same page)

Gas savings

74% cheaper

SavedMonad (current)

What to write differently

Storage-layout patterns that the MIP-8 spec implies should win. Each savings number comes from a mainnet measurement, not spec math, and the measurements reveal that the spec's predicted gains don't yet materialize at the gas-schedule level. We're publishing the raw numbers below so you can verify and judge for yourself.

Each suggestion below is backed by a transaction on Monad mainnet. Click any link to verify the gas cost on-chain.

Pattern 8A

Replace dense-key mappings with arrays

Mappings hash each key to a slot scattered across the storage space. Consecutive logical indices land on different MIP-8 pages and pay a fresh cold-page cost each time.

Before

// Keys hash to keccak-scattered slots — consecutive
// indices land on different MIP-8 pages.
mapping(uint256 => uint256) private values;

function readEight(uint256 start) external view returns (uint256 sum) {
    for (uint i = 0; i < 8; i++) {
        sum += values[start + i];   // 8 cold-page loads
    }
}

After

// Adjacent slots — 8 entries fit comfortably in one page.
uint256[256] private values;

function readEight(uint256 start) external view returns (uint256 sum) {
    for (uint i = 0; i < 8; i++) {
        sum += values[start + i];   // 1 cold page + 7 warm
    }
}

Before

116,281 gas

Verify on Monadscan

After

115,729 gas

Verify on Monadscan

Savings

≈ 0%

The MIP-8 spec predicts a large saving here (8 cold pages → 1 cold + 7 warm, roughly 7× cheaper). Our mainnet measurement shows only a 0.5% difference: 116,281 gas scattered vs 115,729 gas packed. The page-locality charges the spec describes don't appear to differentiate these two layouts at the gas-schedule level on current Monad mainnet. Both tx hashes are linked so you can verify. For now, treat this pattern as forward-compatible (it will likely become a real win as MIP-8's full gas schedule lands), but don't refactor existing contracts expecting an immediate gas drop.

Pattern 8B

Co-locate hot struct fields

If two fields of a struct are usually read together, put them adjacent in the declaration, not separated by unrelated fields.

Before

struct User {
    uint256 stake;       // hot
    uint256 _filler0;    // 6 cold fields
    uint256 _filler1;
    uint256 _filler2;
    uint256 _filler3;
    uint256 _filler4;
    uint256 _filler5;
    uint256 lastClaim;   // hot — but 7 slots away
}

// Reading both pays two cold-page loads.

After

struct User {
    uint256 stake;       // hot
    uint256 lastClaim;   // hot — adjacent
    uint256 _filler0;
    uint256 _filler1;
    uint256 _filler2;
    uint256 _filler3;
    uint256 _filler4;
    uint256 _filler5;
}

// One cold-page load + one warm slot read.

Before

49,267 gas

Verify on Monadscan

After

49,292 gas

Verify on Monadscan

Savings

≈ 0%

Measured on mainnet: scattered layout (fields 128 slots apart, guaranteed different pages) cost 49,267 gas; reordered layout (fields adjacent) cost 49,292 gas. The reordered version was actually 25 gas more expensive, essentially identical. As with pattern 8a, the page-locality benefit the spec describes doesn't yet materialize at the measured-gas level. The pattern is still good hygiene (cleaner code, better cache behavior off-chain) but isn't currently delivering a measurable on-chain saving.

Pattern 8C

Batch fresh-state writes into the same page

Growing storage charges STATE_GROWTH_COST per page, not per slot. Writing 10 new entries across 10 different pages costs 10× more than writing them into one page.

Before

// Mapping keys hash to slots on different pages.
mapping(uint256 => uint256) private entries;

function allocate(uint256 count, uint256 base) external {
    for (uint i = 0; i < count; i++) {
        entries[i] = base + i;   // each on a new page
    }                            // → count × STATE_GROWTH_COST
}

After

// Adjacent storage slots — one page covers many entries.
uint256[1024] private entries;

function allocate(uint256 count, uint256 base) external {
    for (uint i = 0; i < count; i++) {
        entries[i] = base + i;   // same page after the first
    }                            // → 1 × STATE_GROWTH_COST
}

Before

400,500 gas

Verify on Monadscan

After

400,017 gas

Verify on Monadscan

Savings

≈ 0%

Spec prediction: 10 new entries spread across 10 different pages should pay 10 × state-growth (~170k); 10 entries packed into one page should pay 1 × state-growth + 9 × warm-write (~18k). Measured on mainnet: scattered cost 400,500 gas, packed cost 400,017 gas, a 0.1% difference. State growth appears to currently be charged per-slot rather than per-page on Monad mainnet, so the packed layout doesn't yet win. Same recommendation as 8a / 8b: keep this in mind as a forward-compatible improvement, but don't expect today's gas drop.

Watch storage accesses in real time

Step through real contract code line by line. Each SLOAD/SSTORE lights up the corresponding storage slot and shows whether it's a cold or warm access under MIP-8.

8 unique storage slots (5-12) accessed during swap(), all in one page

Uniswap V2 swap()

0 unique slots accessed

1
// UniswapV2Pair.sol
2
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
3
// lock modifier
4
require(unlocked == 1);  unlocked = 0;
5
 
6
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
7
require(amount0Out > 0 || amount1Out > 0);
8
require(amount0Out < _reserve0 && amount1Out < _reserve1);
9
 
10
uint balance0 = IERC20(token0).balanceOf(address(this));
11
uint balance1 = IERC20(token1).balanceOf(address(this));
12
 
13
// _mintFee (called internally)
14
address feeTo = IUniswapV2Factory(factory).feeTo();
15
uint _kLast = kLast;
16
 
17
// _update
18
price0CumulativeLast += ...;
19
price1CumulativeLast += ...;
20
 
21
// end lock modifier
22
unlocked = 1;
23
}

Design for pages, get 10X+

MIP-8 doesn't just help existing contracts. It opens a new design space where page-aware storage yields order-of-magnitude improvements.

Consider an ERC-1155 multi-token contract. The standard implementation hashes each token balance to a random storage location. A page-aware design stores balances contiguously, so batch operations read one page instead of N scattered slots.

Standard ERC-1155

// balances scattered by keccak256

mapping(uint256 =>

mapping(address => uint256))

balances;

Each token ID hashes to a different page

page 0x7a..

page 0x3e..

page 0xb4..

page 0x91..

20 tokens = 20 cold reads = 162,000 gas

Page-aware design

// balances packed contiguously

uint256[128] balances;

// token IDs 0-127 map to one page

All token balances in one page

page 0

20 tokens = 1 cold + 19 warm = 10,000 gas

Batch size

Number of token balances read in one operation

20

tokens

212 (10X threshold)64

Standard layout

162,000

gas

20 x 8,100 (all cold)

Page-aware + MIP-8

10,000

gas

8,100 + 19 x 100

Improvement

16.2x

cheaper

94% gas saved

Cold-access gas comparison

16.2x cheaper

Page-aware + MIP-8: 10,000Standard layout: 162,000

This example shows a page-aware ERC-1155 that stores token balances in a contiguous array instead of a double mapping. With MIP-8, batch reads from the same page scale at 100 gas per additional slot instead of 8,100.

Slot → Page mapping

Every slot maps deterministically to a page. The math is simple: shift right by 7 bits to get the page, mask the low 7 bits to get the offset within it.

0127255383511

page_index(slot) = slot >> 7

0 >> 7 = 0

offset(slot) = slot & 0x7F

0 & 127 = 0

Page 0

Page 1

Page 2

Page 3

Try your own contract

Paste a GitHub repo URL or Solidity source to see how your contract's storage layout maps to pages.

Works best with small-to-medium repos. Large repos with many dependencies (e.g. Aave, Chainlink) may time out.

Try:

What this means for you

Structs get cheaper

Solidity stores struct members and array elements contiguously. Under MIP-8, a contiguous run that fits in one page is typically 1 cold page touch plus N - 1 warm slot accesses instead of N cold slot accesses.

Mappings change less

Mappings still derive storage locations from keccak256, so unrelated keys almost always land on different pages. MIP-8 rarely helps or hurts truly random access; it mostly rewards contiguous layouts.

New optimization patterns

Page-aware arrays, careful packing, and low-level layouts that keep related data inside the same 128-slot page open a new optimization space for page-aware gas costs.

Execution stays compatible

At the opcode level, execution semantics stay the same: SLOAD still returns 32 bytes and SSTORE still writes 32 bytes. What changes is the storage commitment/proof layer and the gas model, which become page-aware. The effective key space narrows from 2²⁵⁶ hashed slots to 2²⁴⁹ page indices.

Contracts that read consecutive storage slots often get cheaper because Solidity stores struct members, fixed arrays, and runs of dynamic-array elements contiguously once their base location is known. Mappings still use hashed locations, so mapping-heavy access patterns tend to change less. The main contracts at risk are those that hardcode opcode-gas assumptions for consecutive storage accesses.

Each 4,096-byte page is committed via a fixed binary tree built from the BLAKE3 compression function. 128 slots pair into 64 leaves, which hash through 6 levels into a single 32-byte root. An inclusion proof for any slot is about 257 bytes (1-byte index + target word + sibling word + 6 parent hashes), plus the MPT proof for the page commitment.

Continue the discussion on Monad Forum

Questions, feedback, or a better idea? Weigh in on the forum thread.

Open forum thread