Inspired by SSTORE2
12, the SSTORE3
library enables a new way of using the "code-as-storage"
to efficiently store and retrieve large amounts of data programmatically. The core advantage of SSTORE3
is that it allows for smaller pointer sizes, allowing you to more easily pack the data pointer with
other storage variables, saving more gas.
Data Size (1 word = 32 bytes) | SSTORE2 | SSTORE3_S | SSTORE3_M (est. w/ EIP1153) | SSTORE3_L |
---|---|---|---|---|
1 word | 42.1k (1,316.5 g/b) | 48.0k (1,498.8 g/b) | 43.5k (1,360.0 g/b) | 76.1k (2,376.9 g/b) |
2 words | 48.4k (757.0 g/b) | 56.9k (888.7 g/b) | 50.2k (784.6 g/b) | 82.5k (1,288.3 g/b) |
3 words | 54.9k (572.1 g/b) | 65.9k (686.3 g/b) | 57.0k (593.8 g/b) | 88.9k (925.9 g/b) |
5 words | 67.6k (422.8 g/b) | 83.9k (524.4 g/b) | 70.6k (441.1 g/b) | 101.7k (635.7 g/b) |
10 words | 99.8k (311.8 g/b) | 129.0k (403.1 g/b) | 104.6k (326.8 g/b) | 133.8k (418.1 g/b) |
15 words | 131.8k (274.6 g/b) | 174.0k (362.5 g/b) | 138.5k (288.5 g/b) | 165.9k (345.6 g/b) |
25 words | 195.9k (244.9 g/b) | 265.3k (331.6 g/b) | 207.6k (259.4 g/b) | 230.0k (287.5 g/b) |
50 words | 356.1k (222.5 g/b) | 501.4k (313.4 g/b) | 388.2k (242.6 g/b) | 390.3k (243.9 g/b) |
100 words | 676.5k (211.4 g/b) | 973.7k (304.3 g/b) | 749.5k (234.2 g/b) | 711.1k (222.2 g/b) |
250 words | 1,637.7k (204.7 g/b) | 2,390.8k (298.8 g/b) | 1,833.5k (229.2 g/b) | 1,673.3k (209.2 g/b) |
500 words | 3,240.1k (202.5 g/b) | 4,752.7k (297.0 g/b) | 3,640.5k (227.5 g/b) | 3,277.7k (204.9 g/b) |
24,575 bytes (maximum) | 4,958.0k (201.7 g/b) | 7,285.0k (296.4 g/b) | 5,577.9k (227.0 g/b) | 4,997.7k (203.4 g/b) |
The Code-as-Storage (CaS) pattern leverages the relative cost of loading bytecode vs. the cost of loading storage:
- 32-byte Storage Read (Cold, first slot access within tx):
2100
gas - n-byte Code Read (Cold, first address access within tx):
2600 + ceil(n / 32) * 3
gas3
From the points above we can see that loading just 2 EVM words (64 bytes) is already cheaper to do from bytecode (2606 gas) vs. from storage (4200 gas). The problem is smart contract code is immutable4, unlike storage it cannot easily be modified. To work around this you need to store & update a "pointer", some information that lets you know what contract stores the current data. To "mutate" the data you then simply initialize a new store (deploy contract which holds new data) and update the pointer:
contract MyContract is SSTORE3 {
uint40 internal pointer;
// ...
function updateData(bytes memory data) internal {
sstore3(pointer++, data);
}
}
In practice this will increase the cost of employing CaS as it require an additional storage read
to first get the pointer. This can be minimized by packing the pointer together with other
variables. This is also where the benefit of SSTORE3
comes in, because the pointer can be
arbitrary (only requirement is all pointers are single use) you can use a much smaller type for the
pointer, allowing you to pack it with other values in more situations.
Standalone, SSTORE2-based storage is slightly cheaper to read from than SSTORE3, however SSTORE2 requires you to update & store a full address as the data pointer. If you're able to store a full address in stoage alongside your other variables without increasing the amount of unique storage slots to be read in your function(s) it'll be net-cheaper to use SSTORE2.
Reducing SSTORE2 Pointer Size
The pointer size for SSTORE2 can practically be reduced from a full 20-byte address by up to 6 bytes down to
a 14-byte pointer by leveraging deterministic SSTORE2 deployment via CREATE2, however this will
require you to mine a salt for every update that results in an address with X-leading zeros (where
X
is the amount of bytes you're shrinking the pointer by).
Footnotes
-
Code is read using the
EXTCODECOPY
opcode which copies bytes to memory, if copying to fresh memory sections this will incur an additional memory expansion cost,previous_cost - new_cost
↩ -
Proxy patterns do not mutate actual bytecode but what implementation address the proxy points to in storage. As of the Shanghai upgrade bytecode is still mutable via the
SELFDESTRUCT
+CREATE2
enabled metamorphic contract pattern, but this will be deprecated by EIP-6780 which is likely to be included in the upcoming Cancun hardfork. ↩