BidPayload library
Source: contracts/src/libs/BidPayload.sol
A pure-Solidity library that owns the wire format for a sealed payload and the
signature verification used at settle. Used internally by SealedAuction;
mirrored by the SDK for off-chain encoding and verification.
The same format is used for both sealed bids (signer = bidder) and the sealed reserve (signer = seller). The signer field in the digest is the address being committed to; the recovery logic is identical in both cases.
Wire format
Each payload is exactly 149 bytes:
| field | type | bytes | offset |
|------------|----------|-------|--------|
| bidder | address | 20 | 0 |
| amount | uint256 | 32 | 20 |
| nonce | bytes32 | 32 | 52 |
| signature | bytes65 | 65 | 84 |
signature is a 65-byte secp256k1 signature in r || s || v format.
For the reserve payload, the bidder field holds the seller's address.
Functions
encode
function encode(
address bidder,
uint256 amount,
bytes32 nonce,
bytes memory signature
) internal pure returns (bytes memory);
Returns the 149-byte concatenated payload. Used off-chain by the SDK to construct the payload before encryption.
Reverts: InvalidSignatureLength if signature.length != 65.
decode
function decode(bytes memory payload)
internal pure
returns (address bidder, uint256 amount, bytes32 nonce, bytes memory signature);
The inverse of encode. Used by the orchestrator after CDR decryption.
Reverts: InvalidPayloadLength if input is not exactly 149 bytes.
recoverSigner
function recoverSigner(
uint256 auctionId,
address bidder,
uint256 amount,
bytes32 nonce,
bytes memory signature
) internal pure returns (address);
Verification function used by SealedAuction.settle for both bids and the
reserve reveal. Reconstructs the signed message hash and returns the recovered
address.
The digest and signed hash:
bytes32 digest = keccak256(abi.encode(auctionId, bidder, amount, nonce));
bytes32 ethSignedHash = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)
);
address signer = ecrecover(ethSignedHash, v, r, s);
The "\x19Ethereum Signed Message:\n32" prefix matches what browser wallets
produce via personal_sign. This means EOA wallets, viem's
signMessage({message: {raw: digest}}), and ethers'
signMessage(getBytes(digest)) all produce signatures that pass verification
without any client-side adjustment.
EIP-2 low-s is enforced: high-s signatures return address(0) rather than
reverting, which causes the caller to treat the reveal as invalid and skip or
revert accordingly.
Reverts: InvalidSignatureLength if signature.length != 65.
The personal_sign prefix
Earlier SealedAuction versions (v1, v2) called ecrecover on the raw
unprefixed digest. This worked for SDK clients signing with raw private keys
but failed for browser wallets, which always apply the prefix. v3 adds the
prefix in recoverSigner, aligning browser-wallet and SDK signing paths.
Off-chain mirror (SDK)
import {keccak256, encodeAbiParameters, hashMessage, recoverAddress} from 'viem'
function recoverSigner(
auctionId: bigint,
bidder: `0x${string}`,
amount: bigint,
nonce: `0x${string}`,
signature: `0x${string}`,
): Promise<`0x${string}`> {
const digest = keccak256(
encodeAbiParameters(
[{type: 'uint256'}, {type: 'address'}, {type: 'uint256'}, {type: 'bytes32'}],
[auctionId, bidder, amount, nonce],
),
)
const ethSigned = hashMessage({raw: digest})
return recoverAddress({hash: ethSigned, signature})
}
Use this to verify your signing path produces the expected address before submitting ciphertext on-chain.
Sealed reserve reuse
When the seller seals a reserve, the SDK sets bidder = seller address in the
payload. At settle, SealedAuction.settle calls recoverSigner(auctionId, auction.seller, reserveReveal.amount, reserveReveal.nonce, reserveReveal.signature) and checks that the result equals auction.seller. The same
circuit — encode, encrypt, decrypt, recoverSigner — applies to both sides of
the auction.
Tests
The Foundry suite covers:
- Round-trip encode / decode preserves all fields
recoverSignerreturns the correct address for valid signaturesrecoverSignerrejects tampered amountsrecoverSignerrejects tampered nonces- High-s signatures return
address(0)(EIP-2 enforcement) - Invalid lengths revert with the documented errors
See contracts/test/libs/BidPayload.t.sol.