Yul mstore of a 20-byte chunk into bytes memory clobbers the next 12 bytes — mask and OR
Task: in a bytes memory blob, replace the low 20 bytes of the second 32-byte word (an ABI-encoded address slot) and leave everything else untouched.
The direct approach appears to work on a short input:
assembly {
chunk := shl(mul(12, 8), substringBytes) // left-align the low 20 bytes
mstore(add(originalBytes, 76), chunk) // 32 (length) + 32 (word 0) + 12
}and it does produce the right output — until the array has non-zero data further along, at which point bytes after the target start coming back zeroed. mstore always writes a full 32 bytes. Writing a left-aligned 20-byte value at offset 76 therefore covers bytes 76..108, so the 12 bytes it overwrites past the intended field are the first 12 bytes of the next word. With zero-padded test data the damage is invisible.
The correct primitive is read-modify-write on the whole word, keeping the high 12 bytes:
assembly {
let slot := add(originalBytes, 64) // 32 length prefix + word 0
let mask := not(0xffffffffffffffffffffffffffffffffffffffff) // clears low 20 bytes
mstore(slot, or(and(mload(slot), mask), addr)) // addr right-aligned uint160
}Two offsets worth memorising for bytes memory b: b points at the 32-byte length, data begins at add(b, 32), and word n is at add(b, add(32, mul(32, n))). Anything you mstore must be 32-byte aligned, or you must mask.
The general rule: in Yul, use mstore only for whole words, mstore8 for single bytes, and mask-and-OR for anything in between. Verify with a fixture whose neighbouring bytes are non-zero — zero-padded fixtures hide exactly this bug.
Source: adapted from a Stack Exchange answer by Mila A (question by dNyrM), https://ethereum.stackexchange.com/questions/159781/manipulate-bytes-with-assembly — CC BY-SA 4.0