fix: keep domain-tagged salts a full 32 bytes wide - #963
Open
Xowiek wants to merge 1 commit into
Open
Conversation
generateRandomSalt builds a domain-tagged salt by concatenating the first
four bytes of keccak256(domain), twenty zero bytes and eight random bytes,
then passes the result through toBeHex. concat already yields 32 bytes, but
toBeHex without a width re-encodes the value as a number, so any leading
zero byte is dropped.
A domain whose hash starts with 0x00 -- about one in 256 -- therefore gets a
31-byte salt with every byte shifted one to the left. Reading the tag back
off the salt returns the wrong four bytes, so the order can no longer be
attributed to the domain that created it:
domain "d362", tag 00b508f2
salt 0xb508f200000000000000000000000000000000000000005... (31 bytes)
salt.slice(2, 10) -> b508f200, not 00b508f2
test/create-order.spec.ts already asserts this invariant with
`expect(order.parameters.salt.slice(0, 10)).eq(openseaMagicValue)`; it only
holds today because keccak256("opensea.io") happens to start with 0x36.
Pass the 32-byte width to toBeHex. The order itself is unaffected either
way, since salt is a uint256 and both forms carry the same value -- the
order hash and signature already matched. What changes is that the tag
survives in the serialized salt for every domain, and the generated salt is
now padded the same way a caller-supplied one already is in _formatOrder.
Xowiek
force-pushed
the
fix/domain-tagged-salt-width
branch
from
August 16, 2026 22:15
dba56de to
92e9eaf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Roughly one domain in 256 ends up with a salt that is a byte short, and the domain tag can no longer be read back off it.
Problem
A domain-tagged salt is four bytes of
keccak256(domain), twenty zero bytes, then eight random ones.concatassembles exactly 32 bytes. ThetoBeHexwrapped around it has no width argument, so it re-encodes that as a number, and numbers do not keep leading zeros.Domain
d362has the tag00b508f2. What comes out today:The first four bytes now read
b508f200. The tag is still in there, just shifted a place left, so nothing downstream can tell which domain produced the order, which is the only reason to tag the salt.test/create-order.spec.tsalready pins the invariant withexpect(order.parameters.salt.slice(0, 10)).eq(openseaMagicValue). It is green purely becausekeccak256("opensea.io")happens to start with0x36.Worth being clear on scope: the order itself is fine.
saltis auint256and both forms carry the same value. I checked against a local chain thatgetOrderHashstill agrees with the contract andvalidate()returns true. What breaks is the serialized salt, which is not a well formedbytes32and no longer yields the tag.Fix
Pass the width:
toBeHex(concat([...]), 32). That also makes the size explicit, so ethers raises instead of quietly shortening if the input ever grows past 32 bytes.It lines the two salt paths up too. A caller supplied salt is already widened to 32 bytes in
_formatOrder, andgetOrderHashpads as well, both from #250. The generated branch was the one that never got the same treatment. The no-domain branch needs nothing, it already pads to 64 characters.Test plan
test/utils/order.spec.ts, no fixture required:/^0x[0-9a-f]{64}$/for those and for ordinary domainsopensea.iostill tags correctly and the no-domain path still padsThe first two fail on
main. Suite green at 175.