Skip to main content

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
  • recoverSigner returns the correct address for valid signatures
  • recoverSigner rejects tampered amounts
  • recoverSigner rejects 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.