Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x53 |
| Mnemonic | MSTORE8 |
| Gas | 3 + memory expansion cost |
| Stack Input | offset, val |
| Stack Output | (none) |
| Behavior | Writes the least significant byte of val (i.e., val & 0xFF) to memory at the given byte offset. Unlike MSTORE, which writes a full 32-byte word, MSTORE8 writes exactly one byte, leaving all other bytes in the surrounding 32-byte memory word untouched. Memory expansion gas is charged if offset extends beyond the current memory size, identical to MSTORE’s expansion model. |
Threat Surface
MSTORE8 is deceptively simple — a single-byte memory write — but its threat surface arises from three properties that interact in ways developers routinely misjudge:
-
Implicit value truncation. MSTORE8 silently discards the upper 248 bits of
val, writing onlyval & 0xFFto memory. The EVM performs no check, no revert, and no event when a 256-bit stack value is truncated to 8 bits. Developers who confuse MSTORE8 with MSTORE, or who pass a multi-byte value expecting full storage, lose data silently. In assembly-heavy code (custom ABI encoders, cryptographic routines, bitwise packing), this truncation is the root cause of subtle corruption bugs where the output looks plausible but is cryptographically or semantically wrong. -
Byte-granularity memory writes enable surgical corruption. MSTORE always overwrites an aligned 32-byte word. MSTORE8 can modify any single byte within a 32-byte word without disturbing its neighbors. This gives assembly code sub-word precision for building packed data structures (ABI encoding, byte array construction), but it equally gives attackers sub-word precision for corrupting specific fields within packed memory layouts. A single MSTORE8 to the wrong offset can flip a boolean flag, alter a length prefix, change a function selector byte, or corrupt one byte of an address — all without triggering any visible memory expansion or gas anomaly.
-
Memory expansion cost is identical to MSTORE but the write is 32x smaller. Writing one byte at offset
Nexpands memory toceil((N + 1) / 32) * 32bytes — the same expansion boundary asMSTORE(N, val)would trigger. An attacker who controls theoffsetparameter can force quadratic memory expansion by specifying a very large offset, paying minimal stack setup cost (just PUSH the offset and a byte value) for potentially enormous gas consumption. Because MSTORE8 writes only one byte, the ratio of gas cost to useful work is maximally unfavorable — it is the most gas-wasteful way to expand EVM memory.
Smart Contract Threats
T1: Silent Value Truncation to Single Byte (High)
MSTORE8 writes val & 0xFF — the lowest 8 bits of a 256-bit stack value. The remaining 248 bits are silently discarded. This is correct by specification, but dangerous when developers misunderstand which opcode they are using or when upstream code passes values wider than 8 bits:
-
MSTORE/MSTORE8 confusion in assembly. A developer writing inline Yul who intends
mstore(ptr, value)but writesmstore8(ptr, value)will store only one byte instead of 32, silently corrupting the memory layout. The inverse mistake — usingmstorewhenmstore8was intended — overwrites 31 adjacent bytes. Both directions produce memory that parses incorrectly in subsequent ABI decoding or hash computations, but neither causes a revert. -
Truncation of multi-byte values. If contract logic computes a value (e.g., a hash, an address, a uint256 amount) and passes it to MSTORE8 expecting the full value to be stored, only the least significant byte survives. A
uint256balance of0x0100(256) truncates to0x00, reading back as zero. Auint256of0x01FF(511) truncates to0xFF(255). The truncation is not an error — it is a silent, specification-compliant data loss. -
Cross-compiler inconsistencies. Solidity, Vyper, and Huff emit MSTORE8 in different contexts. Hand-written assembly or compiler plugins that incorrectly emit MSTORE8 where MSTORE was intended produce contracts that deploy successfully, pass basic tests (especially when test values happen to fit in one byte), but fail catastrophically with larger real-world values.
Why it matters: Truncation bugs are the most insidious class of memory errors because they produce plausible-looking but incorrect data rather than immediate reverts. In DeFi protocols where amounts, addresses, and selectors are constructed in assembly, a truncation bug can silently alter financial outcomes.
T2: Memory Expansion Gas Griefing (High)
MSTORE8 triggers memory expansion using the same formula as MSTORE:
memory_cost = (words² / 512) + (3 × words)
where words = ceil((offset + 1) / 32)
A single mstore8(offset, 0) at a large offset forces the EVM to expand memory to that offset and charge quadratic gas. The attacker writes one byte but pays (and forces the transaction to pay) for the entire memory expansion:
-
Attacker-controlled offset. Any code path where an external caller influences the
offsetparameter of MSTORE8 — even indirectly through array index calculations, string operations, or ABI decoding — is a gas griefing vector. An offset of0xFFFFFF(~16 million) forces expansion to ~512K words, costing approximately 520 million gas — exceeding the block gas limit and reverting the transaction. -
Griefing critical operations. If an attacker can force an MSTORE8 with a large offset inside a liquidation, governance execution, or withdrawal function, the operation becomes permanently unexecutable. The function reverts with out-of-gas before reaching the critical logic.
-
Worst gas-to-work ratio among memory opcodes. MSTORE writes 32 bytes per expansion event; MSTORE8 writes 1 byte. For the same memory expansion cost, MSTORE8 produces 32x less useful output, making it the least efficient memory opcode for legitimate use and the most efficient for pure gas burning.
Why it matters: Memory expansion griefing via MSTORE8 is functionally identical to the MSTORE/CALLDATACOPY variants, but the single-byte write size makes the attack surface less obvious during code review. Auditors looking for “large memory writes” may overlook a single mstore8 call with a large offset.
T3: Byte-Level Memory Corruption in Assembly (High)
MSTORE8’s byte-granularity writes enable precise memory corruption that is invisible to word-level analysis:
-
Overwriting packed struct fields. Solidity packs multiple variables into single 32-byte memory words when using
abi.encodePackedor hand-rolled assembly. An MSTORE8 to an incorrect offset within a packed word corrupts one field without disturbing others. For example, a packed(address, uint96)pair occupies 32 bytes: an MSTORE8 at the wrong offset within this word can alter one byte of the address or the amount, producing a valid-looking but incorrect value. -
Length prefix corruption. Dynamic types (
bytes,string, dynamic arrays) store a 32-byte length prefix before the data. An errant MSTORE8 that overwrites one byte of the length prefix can cause the ABI decoder to read too many or too few bytes, leading to out-of-bounds reads, truncated data, or garbage inclusion. A length of0x00000020(32 bytes) corrupted to0x00000120(288 bytes) causes subsequent MLOAD/CALLDATACOPY to read 256 extra bytes of unrelated memory. -
Function selector corruption. Contracts that build
msg.datain assembly for internal calls (proxy patterns, multicall implementations) store the 4-byte selector in memory. An MSTORE8 targeting any of those 4 bytes redirects the call to a different function entirely, potentially bypassing access control or triggering unintended state changes.
Why it matters: Byte-level corruption produces valid memory layouts that parse without error but contain wrong values. Unlike word-level overwrites (via MSTORE), which are more likely to destroy the entire structure and trigger obvious failures, single-byte corruption creates subtle, hard-to-detect bugs.
T4: Partial Word Overwrites and Dirty Memory (Medium)
MSTORE8 modifies one byte within a 32-byte memory word without clearing or affecting the other 31 bytes. This creates interaction hazards with MLOAD, which always reads a full 32-byte word:
-
Dirty higher-order bytes. Memory is initialized to zero, but after a sequence of MSTORE8 writes, only the written byte positions contain intended values. MLOAD from any offset in that region reads 32 bytes, including bytes that may be zero (never written) or stale (from a prior operation). If the developer assumes MSTORE8 initializes the entire word, the MLOAD result contains garbage in the non-written positions.
-
Interaction with MSTORE. Code that mixes MSTORE (32-byte writes) and MSTORE8 (1-byte writes) to the same memory region must carefully reason about which bytes are overwritten. An MSTORE followed by an MSTORE8 to the same base offset overwrites only the byte at that exact position, leaving the other 31 bytes from the MSTORE intact. An MSTORE8 followed by an MSTORE to the same aligned word erases the MSTORE8 write entirely. This ordering sensitivity is a source of bugs in hand-optimized assembly.
-
ABI encoding corruption. Solidity’s ABI encoder writes full 32-byte words via MSTORE. If assembly code uses MSTORE8 to “patch” a specific byte within an ABI-encoded payload, the patch may be overwritten by a subsequent MSTORE from the compiler, or the patch may leave dirty bytes that cause the recipient to decode unexpected values.
Why it matters: The interaction between byte-level (MSTORE8) and word-level (MSTORE/MLOAD) memory operations is a rich source of bugs in gas-optimized assembly code, particularly in custom ABI encoders, token implementations, and cryptographic libraries.
T5: Confusion with MSTORE Behavior (Medium)
MSTORE and MSTORE8 have nearly identical names and stack signatures (offset, val) but fundamentally different behavior:
| Property | MSTORE | MSTORE8 |
|---|---|---|
| Bytes written | 32 (full word) | 1 (LSB of val) |
| Value used | All 256 bits | Lower 8 bits only |
| Memory alignment | Writes to offset..offset+31 | Writes to offset only |
| Typical use | Store full words | Build byte arrays |
-
Name-based confusion. The “8” suffix suggests “8-byte” or “8-word” to some developers rather than “8-bit.” This misreading leads to using MSTORE8 to store 8-byte (uint64) values, which truncates to one byte.
-
Parameter interpretation. Both opcodes pop
offset, valfrom the stack in the same order, reinforcing the assumption that they work identically. A developer switching from MSTORE to MSTORE8 may not realize the value semantics change from “store the full 256-bit word” to “store only the least significant byte.” -
Testing blind spots. Contracts tested with small values (0-255) produce identical results whether MSTORE or MSTORE8 is used, because the values fit in one byte. The bug only manifests with values >= 256, which may not appear in unit tests but will appear in production.
Why it matters: MSTORE/MSTORE8 confusion is a known class of inline assembly bugs documented in security audit reports. The bug passes compilation, passes tests with small inputs, and produces incorrect results only with production-scale values.
Protocol-Level Threats
P1: Memory Expansion Gas Asymmetry (Low)
At the protocol level, MSTORE8 and MSTORE trigger identical memory expansion costs, but MSTORE8 writes 32x less data. This creates a gas accounting asymmetry: the marginal cost of writing one byte via MSTORE8 is dominated by memory expansion rather than the write itself. In contexts where gas metering accuracy matters — gas estimation for meta-transactions, EIP-4337 bundlers, L2 gas oracles — the quadratic memory expansion triggered by MSTORE8 can cause unexpected gas overruns if the estimator models MSTORE8 as a “cheap single-byte write” without accounting for expansion.
P2: Solidity Optimizer Removing MSTORE8 Writes (Medium)
The Solidity compiler’s Yul optimizer has a documented class of bugs where memory writes in inline assembly are incorrectly removed. The InlineAssemblyMemorySideEffects bug (Solidity 0.8.13-0.8.14) affected both mstore and mstore8 instructions: the legacy code generation pipeline removed memory writes from assembly blocks that did not reference surrounding Solidity variables, even when the written memory was read by subsequent code. An MSTORE8 write in one assembly block that is consumed by an MLOAD in a subsequent block could be silently eliminated, causing the MLOAD to return zero instead of the written byte.
This was fixed in Solidity 0.8.15, but contracts compiled with 0.8.13 or 0.8.14 using the legacy pipeline with optimizer enabled remain vulnerable if they use the affected pattern.
P3: zkEVM Circuit Constraints for Byte-Level Writes (Low)
zkEVM implementations must prove MSTORE8’s byte-level write semantics in arithmetic circuits. Unlike MSTORE, which writes an aligned 32-byte word, MSTORE8 requires the circuit to prove that exactly one byte was modified at a specific offset within a 32-byte word while the remaining 31 bytes were preserved. This “read-modify-write” proof is more complex than MSTORE’s “full-word overwrite” proof and has been a source of under-constrained circuit bugs in zkEVM implementations. The Scroll/PSE zkEVM project has documented multiple missing-constraint bugs in memory operation circuits, and any zkEVM handling MSTORE8 must correctly constrain the byte-extraction (val & 0xFF), the offset calculation, and the partial-word write semantics.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
val > 0xFF (e.g., mstore8(0, 0x1234)) | Only 0x34 (least significant byte) is written; upper bytes silently discarded | Silent data truncation. Contracts that pass multi-byte values to MSTORE8 lose data without any revert or warning. |
val = 0 | Writes 0x00 to the target byte; memory expansion still occurs if offset is beyond current size | Can be used to zero out a specific byte without affecting neighbors; still charges expansion gas. |
offset at word boundary (e.g., offset = 32) | Writes to the first byte of the next 32-byte word; expands memory to 64 bytes if not already allocated | Off-by-one errors at word boundaries can write to the wrong word, corrupting adjacent data structures. |
offset within a word (e.g., offset = 15) | Modifies byte 15 only; MLOAD from offset 0 reads bytes 0-31 including the modified byte 15 | Mixed MSTORE8/MLOAD patterns read back partial modifications, producing values with some bytes modified and others stale. |
Very large offset (e.g., 0xFFFFFF) | Memory expands to ceil((0xFFFFFF + 1) / 32) * 32 bytes; quadratic gas cost likely exceeds block gas limit | Gas griefing via memory expansion. A single mstore8 with attacker-controlled offset can consume all transaction gas. |
offset = 2^256 - 1 (maximum) | Memory expansion calculation overflows; behavior is implementation-defined but typically reverts OOG | Overflow in memory size calculation. Clients must handle this correctly; consensus-critical if implementations differ. |
Sequential mstore8 to build a word | Writing 32 individual bytes via MSTORE8 to offsets N..N+31 produces the same memory state as one MSTORE | 32x more gas consumed than equivalent MSTORE for the same result; inefficient patterns may appear in unoptimized code. |
mstore8 after mstore to same region | The MSTORE8 overwrites one byte of the previously MSTORE’d word; 31 bytes remain from MSTORE | Ordering-sensitive; the final memory state depends on which operation executed last at each byte position. |
mstore8 in a loop with index from calldata | Each iteration may expand memory incrementally; attacker-controlled calldata controls total expansion | Gas consumption scales with attacker-chosen iteration count and maximum offset, enabling DoS. |
Real-World Exploits
Exploit 1: Solidity Optimizer Silently Removes MSTORE8 Writes — InlineAssemblyMemorySideEffects (June 2022)
Root cause: The Solidity compiler’s Yul optimizer, introduced in version 0.8.13, incorrectly removed mstore and mstore8 instructions from inline assembly blocks when the written memory was not read within the same block — even if subsequent code depended on those writes.
Details: The optimizer enhancement was designed to eliminate unused memory and storage writes for gas savings. For the via-IR pipeline (where the entire contract is optimized as a single Yul program), this logic is correct: a memory write that is never read is genuinely dead. However, the legacy code generation pipeline (the default for most contracts) runs the optimizer on each inline assembly block in isolation. If an assembly block writes to memory via mstore8 (or mstore) but does not read that memory within the same block, the optimizer considers the write unused and removes it — even if a subsequent assembly block or Solidity expression reads the memory.
The dangerous pattern is:
assembly {
mstore8(0x80, 0x42) // Optimizer removes this write
}
assembly {
result := mload(0x80) // Reads zero instead of 0x42
}The Certora development team discovered and reported this bug on June 5, 2022. The optimizer’s UnusedStoreEliminator did not account for the cross-block memory dependency in the legacy pipeline.
MSTORE8’s role: MSTORE8 was explicitly affected alongside MSTORE. Any inline assembly using mstore8 to write bytes that were later consumed by code outside that assembly block had the write silently removed. This is particularly dangerous for MSTORE8 because its typical use case — building byte arrays or packed data one byte at a time across helper functions — naturally separates the write and read into different code regions.
Impact: Medium severity. Contracts compiled with Solidity 0.8.13 or 0.8.14 using the legacy pipeline with optimizer enabled could silently return incorrect values. The Solidity team noted the pattern is “unlikely to occur in practice” because most assembly blocks reference Solidity variables (which prevents isolated optimization), but acknowledged “the consequences in affected cases can be severe.” Fixed in Solidity 0.8.15 (PR #13100).
References:
- Solidity Blog: Optimizer Bug Regarding Memory Side Effects of Inline Assembly
- Certora: Overly Optimistic Optimizer Bug Disclosure
- Solidity PR #13100: Fix unused store inline assembly
Exploit 2: Vyper concat() Memory Corruption — Byte-Level Buffer Overflow (CVE-2024-22419, January 2024)
Root cause: The Vyper compiler’s concat built-in function generated incorrect bytecode that wrote past the allocated memory buffer bounds, corrupting adjacent memory. The underlying mechanism involved byte-level memory writes (including MSTORE8-equivalent operations) that exceeded the buffer length calculated during compilation.
Details: Vyper versions 0.3.0 through 0.3.10 contained a critical bug in the concat() function’s code generation. When concatenating multiple bytes or string values, the compiler emitted memory copy operations that did not properly respect the allocated buffer boundary. The generated bytecode could write beyond the end of the destination buffer, overwriting valid data in subsequent memory slots.
The root cause was an improper implementation of the copy_bytes API in the concat code path. The compiler calculated the destination buffer size correctly but generated copy instructions that could overshoot by up to 32 bytes. Since EVM memory is byte-addressable and concat() builds results byte-by-byte for non-aligned inputs, the overflow corrupted specific bytes in adjacent memory rather than entire words, making detection extremely difficult.
MSTORE8’s relevance: The Vyper compiler uses MSTORE8 (and byte-level memory copy loops built on MSTORE8) when handling non-32-byte-aligned concatenation operations. The buffer overflow in concat() is a concrete example of byte-level memory corruption producing semantically valid but incorrect memory layouts — the exact threat pattern that MSTORE8’s byte granularity enables. The corrupted bytes were not random garbage; they were bytes from the source operands written to the wrong offset.
Impact: Critical severity (CVSS 9.8). While no vulnerable contracts were confirmed exploited in production, the bug could alter contract semantics undetectably — financial calculations, access control checks, or storage writes downstream of a concat() call could produce silently wrong results. Fixed in Vyper 0.4.0.
References:
Exploit 3: Gas Griefing via Memory Expansion — Recurring Pattern in DeFi Audits (2022-2025)
Root cause: Contracts that accept user-controlled offsets for memory operations (MSTORE, MSTORE8, CALLDATACOPY, RETURNDATACOPY) without bounds checking allow attackers to force quadratic memory expansion, consuming all transaction gas and reverting critical operations.
Details: Memory expansion gas griefing is not a single exploit but a recurring vulnerability class found in approximately 20% of smart contract audits according to industry reports. The attack exploits the EVM’s quadratic memory cost formula: expanding memory to N 32-byte words costs N² / 512 + 3N gas. A single memory access at a large offset — including a single mstore8(large_offset, 0) — triggers expansion to that offset.
In assembly-heavy contracts (DEX routers, bridge adapters, account abstraction modules), MSTORE8 is used to construct byte arrays, pack calldata, and build dynamic return values. When the offset calculation depends on user input — an array index from calldata, a string length, or a loop counter derived from an external parameter — the attacker controls memory expansion. Concrete patterns include:
-
Loop-based byte array construction:
for (uint i = 0; i < userLength; i++) { mstore8(ptr + i, data[i]); }whereuserLengthis attacker-controlled. Each iteration expands memory incrementally, and the total cost is quadratic inuserLength. -
Unchecked offset arithmetic:
mstore8(basePtr + userOffset, value)whereuserOffsetis read from calldata. AuserOffsetof 10 million forces ~312K words of memory expansion. -
Dynamic ABI encoding in assembly: Contracts that build ABI-encoded responses manually using MSTORE8 for byte-precise packing, where the payload length is derived from external data.
MSTORE8’s role: MSTORE8 is the most gas-inefficient memory expander — one byte written per expansion event versus MSTORE’s 32 bytes. A loop writing N bytes via MSTORE8 costs N individual expansion charges (though the gas is amortized since expansion only charges for new memory), while MSTORE would write the same data in N/32 operations. More critically, MSTORE8’s single-byte nature makes the gas griefing less visible during code review: a lone mstore8 call appears innocuous compared to an MCOPY or CALLDATACOPY with an explicit length parameter.
Impact: High severity across the ecosystem. Gas griefing via memory expansion has been reported in audits of major protocols including EigenLayer, Uniswap, and multiple bridge implementations. The attack does not steal funds directly but can permanently DoS critical operations (liquidations, withdrawals, governance execution).
References:
- Gas Griefing Attacks: How They Exploit the EVM (2026)
- Vitalik Buterin: Proposals to Adjust Memory Gas Costs
- EIP-3336: Paged Memory Allocation for the EVM
Attack Scenarios
Scenario A: Value Truncation in Custom Token Transfer
contract VulnerableToken {
mapping(address => uint256) public balances;
function unsafePackedTransfer(
address to,
uint256 amount
) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
// Emit a "packed" event using assembly for gas savings
// VULNERABLE: mstore8 truncates the amount to one byte!
assembly {
let ptr := mload(0x40)
mstore(ptr, to) // Store recipient (32 bytes)
mstore8(add(ptr, 32), amount) // BUG: stores amount & 0xFF
// A transfer of 256 tokens is logged as 0 tokens
// A transfer of 511 tokens is logged as 255 tokens
log1(ptr, 33, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef)
}
}
}
// Impact: Off-chain indexers, block explorers, and accounting systems
// read truncated amounts from events. A 1000-token transfer appears
// as a 232-token transfer (1000 & 0xFF = 0xE8 = 232).Scenario B: Memory Expansion Gas Griefing via Controlled Offset
contract VulnerableByteWriter {
// Intended: write a single byte at a user-specified index in a result buffer
function writeByte(
uint256 index,
uint8 value
) external pure returns (bytes memory result) {
result = new bytes(32);
assembly {
// VULNERABLE: 'index' is user-controlled with no upper bound.
// mstore8(add(add(result, 0x20), index), value)
// If index = 0xFFFFFF (~16M), memory expands to 512K words.
// Gas cost: (512K)^2 / 512 + 3 * 512K ≈ 500M gas -> exceeds block limit.
mstore8(add(add(result, 0x20), index), value)
}
}
}
// Attack: Call writeByte(0xFFFFFF, 0) -> transaction reverts OOG.
// If this function is part of a liquidation or withdrawal path,
// the attacker can permanently DoS the operation.Scenario C: Byte-Level Corruption of ABI-Encoded Calldata
contract VulnerableMulticall {
function execute(address target, bytes calldata data) external {
// Build modified calldata in memory with a "corrected" selector
bytes memory payload;
assembly {
payload := mload(0x40)
let len := data.length
mstore(payload, len)
calldatacopy(add(payload, 0x20), data.offset, len)
mstore(0x40, add(add(payload, 0x20), len))
// VULNERABLE: "Patch" the 4th byte of the selector
// Developer intended to fix byte index 3 of the selector,
// but an off-by-one error patches byte 4 (first byte of args)
mstore8(add(add(payload, 0x20), 4), 0xAB)
}
// The first byte of the first argument is now 0xAB
// If the argument was an address, it's corrupted
// If it was a uint256 amount, the most significant byte changed
(bool ok,) = target.call(payload);
require(ok, "call failed");
}
}
// Impact: The function selector is correct but the first argument byte
// is corrupted. The target contract receives a valid-looking call with
// a subtly wrong argument value -- potentially a different recipient
// address or a wildly different amount.Scenario D: Partial Word Overwrite Leaving Dirty Bytes
contract DirtyMemoryBug {
function packValues(
uint8 flag,
address recipient
) external pure returns (bytes32 packed) {
assembly {
let ptr := 0x00
// Write the flag byte at position 0
mstore8(ptr, flag) // Writes 1 byte: memory[0] = flag
// Developer assumes memory[1..31] are zero (fresh memory)
// and writes the 20-byte address starting at byte 12
// to produce a packed (flag, address) in 32 bytes.
//
// BUG: If this function is called after other assembly
// that wrote to memory[0..31], the bytes at positions
// 1..11 contain STALE DATA from the previous operation.
// mstore8 only wrote byte 0; it did NOT zero bytes 1-31.
mstore(add(ptr, 12), recipient)
packed := mload(ptr)
// packed = [flag][stale 11 bytes][20-byte address]
// instead of [flag][11 zero bytes][20-byte address]
}
}
}
// Impact: The returned 'packed' value contains dirty bytes at
// positions 1-11. If this value is used as a storage key, mapping
// index, or is hashed, the result depends on prior memory state,
// creating a nondeterministic bug.Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Value truncation | Validate input fits in one byte before MSTORE8 | if gt(val, 0xFF) { revert(0, 0) } in assembly; or use explicit uint8 casting in Solidity before the assembly block |
| T1: MSTORE/MSTORE8 confusion | Code review checklist for inline assembly | Verify every mstore8 call is intentional; search codebase for mstore8 and confirm each expects a single-byte write |
| T2: Memory expansion griefing | Bound all user-influenced offsets | require(offset <= MAX_SAFE_OFFSET) before any assembly block; MAX_SAFE_OFFSET should keep memory under ~1KB for most use cases |
| T2: Gas DoS via large offset | Cap memory size at function entry | In assembly: if gt(offset, 0xFFFF) { revert(0, 0) } to limit memory to 64KB |
| T3: Byte-level memory corruption | Minimize use of MSTORE8 in critical paths | Prefer MSTORE for word-aligned writes; use MSTORE8 only when byte-precise writes are necessary and thoroughly tested |
| T3: Length prefix corruption | Validate length fields after assembly construction | After building packed data, verify mload(ptr) (length word) matches expected value before returning or hashing |
| T4: Dirty memory bytes | Zero memory before partial writes | mstore(ptr, 0) to clear a full word before using MSTORE8 to set individual bytes within that word |
| T4: MSTORE/MSTORE8 ordering | Document memory ownership in assembly | Comment which bytes are “owned” by MSTORE vs. MSTORE8 writes; never assume byte positions are zero without explicit clearing |
| T5: Name confusion | Use wrapper functions with explicit semantics | function writeByteToMemory(uint256 offset, uint8 value) in Yul to make intent clear; prohibit raw mstore8 in style guides |
| General: All memory attacks | Avoid assembly when possible | Use Solidity’s built-in bytes operations (abi.encodePacked, bytes.concat) instead of manual MSTORE8 byte construction |
Compiler/EIP-Based Protections
- Solidity >= 0.8.15: Fixes the
InlineAssemblyMemorySideEffectsoptimizer bug that could remove MSTORE8 writes. All production contracts using inline assembly with MSTORE8 should compile with 0.8.15 or later. - Solidity
memory-safeannotation: Marking assembly blocks asassembly ("memory-safe") { ... }tells the optimizer the block only accesses memory through the free memory pointer, enabling safer optimization. However, this annotation can paradoxically worsen gas performance in some cases (Solidity issue #14934), so it should be used judiciously. - EIP-3336 (Proposed): Paged Memory Allocation: Proposes replacing the continuous quadratic memory cost model with page-based billing, which would reduce the effectiveness of memory expansion griefing attacks. If adopted, MSTORE8 to a large offset would only expand the containing page rather than all memory up to that offset.
- Vyper >= 0.4.0: Fixes the
concat()buffer overflow (CVE-2024-22419) that involved byte-level memory corruption. All Vyper contracts usingconcat()should compile with 0.4.0 or later.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | Medium | Solidity optimizer removing mstore8 writes (0.8.13-0.8.14); Vyper concat buffer overflow (CVE-2024-22419) |
| T2 | Smart Contract | High | Medium | Gas griefing via memory expansion (recurring DeFi audit finding, ~20% of audits) |
| T3 | Smart Contract | High | Medium | Vyper concat() byte-level memory corruption; inline assembly memory corruption patterns |
| T4 | Smart Contract | Medium | Low | Dirty bytes array to storage bug (Solidity, 2022); ecrecover dirty bits contamination |
| T5 | Smart Contract | Medium | Medium | Common inline assembly audit finding; testing blind spot with small values |
| P1 | Protocol | Low | Low | Gas estimation inaccuracies in EIP-4337 bundlers |
| P2 | Protocol | Medium | Low | Solidity InlineAssemblyMemorySideEffects bug (0.8.13-0.8.14), Certora disclosure |
| P3 | Protocol | Low | Low | Scroll/PSE zkEVM missing constraint bugs in memory operation circuits |
Related Opcodes
| Opcode | Relationship |
|---|---|
| MSTORE (0x52) | Writes a full 32-byte word to memory. MSTORE8’s closest sibling — same stack signature (offset, val), same memory expansion model, but writes 32 bytes instead of 1. Confusion between the two is a primary source of MSTORE8 bugs. MSTORE overwrites entire 32-byte words; MSTORE8 modifies a single byte within a word. |
| MLOAD (0x51) | Reads a 32-byte word from memory. Always reads 32 bytes regardless of how the memory was written. After MSTORE8 writes a single byte, MLOAD from the same word returns 32 bytes including 31 potentially uninitialized positions. The byte-vs-word asymmetry between MSTORE8 and MLOAD is a source of dirty-memory bugs. |
| BYTE (0x1A) | Extracts a single byte from a 256-bit stack value. BYTE reads bytes from the stack; MSTORE8 writes bytes to memory. Together they form the byte-level data manipulation pair: BYTE for extracting, MSTORE8 for storing. Both operate on the same 0-255 value range. |
| MCOPY (0x5E) | Copies a range of bytes within memory (EIP-5656, Cancun). MCOPY can replicate what multiple MSTORE8 calls do (copying individual bytes) in a single operation with better gas efficiency. For bulk byte-level memory operations, MCOPY is the preferred alternative to MSTORE8 loops. |