Skip to content

fix: keep domain-tagged salts a full 32 bytes wide - #963

Open
Xowiek wants to merge 1 commit into
ProjectOpenSea:mainfrom
Xowiek:fix/domain-tagged-salt-width
Open

fix: keep domain-tagged salts a full 32 bytes wide#963
Xowiek wants to merge 1 commit into
ProjectOpenSea:mainfrom
Xowiek:fix/domain-tagged-salt-width

Conversation

@Xowiek

@Xowiek Xowiek commented Aug 16, 2026

Copy link
Copy Markdown

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. concat assembles exactly 32 bytes. The toBeHex wrapped around it has no width argument, so it re-encodes that as a number, and numbers do not keep leading zeros.

Domain d362 has the tag 00b508f2. What comes out today:

0xb508f2000000000000000000000000000000000000000051be822dec93b6f2   (31 bytes)

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.ts already pins the invariant with expect(order.parameters.salt.slice(0, 10)).eq(openseaMagicValue). It is green purely because keccak256("opensea.io") happens to start with 0x36.

Worth being clear on scope: the order itself is fine. salt is a uint256 and both forms carry the same value. I checked against a local chain that getOrderHash still agrees with the contract and validate() returns true. What breaks is the serialized salt, which is not a well formed bytes32 and 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, and getOrderHash pads 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:

  • three domains hashing to a leading zero byte keep their tag readable off the salt
  • salts match /^0x[0-9a-f]{64}$/ for those and for ordinary domains
  • control: opensea.io still tags correctly and the no-domain path still pads

The first two fail on main. Suite green at 175.

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
Xowiek force-pushed the fix/domain-tagged-salt-width branch from dba56de to 92e9eaf Compare August 16, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant