Skip to main content

EIP-712 typed data

Every owner and attestor action on the FeeVault is an EIP-712 signature. The strings below are exact; any difference produces a different digest and the vault rejects the signature.

Domain​

FieldValue
name"ZkPadFeeVault"
version"1"
chainIdthe chain id (56 BSC, 97 BSC testnet, 31337 local)
verifyingContractthe FeeVault address
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)

The vault exposes DOMAIN_SEPARATOR() and hashClaim / hashRotateOwner / hashPing / hashCancelBind / hashBindOwner so clients can check their digests on-chain.

Types​

TypeSigned byNonce
Claimaccount ownernonces(id)
RotateOwneraccount ownernonces(id)
Pingaccount ownernonces(id)
CancelBindaccount ownernonces(id)
BindOwnereach attestorbindNonces(id)
Consolidateaccount ownernonces(id)
RegisterShieldTemplatesaccount ownernonces(id)
ClearShieldTemplatesaccount ownernonces(id)

Owner actions share one nonce sequence per account, consumed in order. Every message has a deadline (unix seconds); the vault rejects it after that time.

Type strings and typehashes​

Claim(bytes32 beneficiaryId,address asset,uint256 amount,address adapter,bytes32 dataHash,address relayer,uint256 relayerFee,uint256 nonce,uint256 deadline)
0x45a74d21aea80eff47faf64f7ab938e4f40e6da037183a57a0e733f7393ec1c1

RotateOwner(bytes32 beneficiaryId,address newOwner,uint256 nonce,uint256 deadline)
0xc157ea48fda20f57af23ed2a8a45b61ef8ce2db3d05f9f2081f848a9557705af

Ping(bytes32 beneficiaryId,uint256 nonce,uint256 deadline)
0xd3e64a2eac63a84951d5998ccfdd00bec16f495f877caf678bef81668d86a300

CancelBind(bytes32 beneficiaryId,uint256 nonce,uint256 deadline)
0x2f8a11d99cfc6b95a1774c7471a6e6bc1a27f9b4a445f7a6166d23309f4b7e53

BindOwner(bytes32 beneficiaryId,address newOwner,uint256 bindNonce,uint256 deadline)
0xe60ce31d79e3ded52631f772232b52e697c0a22d8294df4c9effda89bf550872

Consolidate(bytes32 beneficiaryId,address assetIn,uint256 amountIn,uint256 minOut,uint256 nonce,uint256 deadline)
0x8326daafef3ea9a1e67573b536d462b863e98d810fbcce7cad228554eae56835

RegisterShieldTemplates(bytes32 beneficiaryId,bytes32 templatesHash,uint256 nonce,uint256 deadline)
0x705410f832f84d01467d08adf008c769a9c235ebc759ee870107cab679cb1acf

ClearShieldTemplates(bytes32 beneficiaryId,uint256 nonce,uint256 deadline)
0x1b3d72506a98dc545eb910f4b3d53f4cd831e608e79aa4f293d3f3cd3abe9b07

templatesHash = keccak256(keccak256(t_0) ‖ … ‖ keccak256(t_n-1)), which is also EIP-712's encoding of a bytes[] member.

The typehashes are keccak256 of the type strings, computed for this page. The FeeVault exposes them as CLAIM_TYPEHASH, ROTATE_OWNER_TYPEHASH, PING_TYPEHASH, CANCEL_BIND_TYPEHASH, BIND_OWNER_TYPEHASH, CONSOLIDATE_TYPEHASH, REGISTER_SHIELD_TEMPLATES_TYPEHASH and CLEAR_SHIELD_TEMPLATES_TYPEHASH, plus hashConsolidate, hashRegisterShieldTemplates, hashClearShieldTemplates and templatesHash views. The SDK tests check every digest against a deployed FeeVault.

Claim fields​

FieldMeaning
beneficiaryIdThe account
assetERC20 to withdraw
amountTotal debited from the account, including the relayer fee. 2^256 − 1 means "the whole balance at execution" (it must be non-zero and cover relayerFee); Claimed reports the amount actually taken
adapteraddress(0) for a direct transfer, or an allow-listed adapter
dataHashkeccak256(adapterData)
relayerThe only address allowed to submit, or address(0) for anyone (fee then goes to the submitter)
relayerFeePaid to the submitter in asset; ≤ amount
nonceMust equal nonces(beneficiaryId)
deadlineUnix seconds

A fee-only claim pays a relayer for a non-claim action (consolidate, register templates, rotate, ping): adapter = address(0), amount == relayerFee > 0, dataHash = keccak256(0x), relayer = that relayer, and nonce = nonces(id) + 1, because it runs right after the action in the same multicall (SDK buildFeeClaim). For RotateOwner it is signed by the new owner.

Consolidate amounts​

Consolidate.amountIn is either an exact amount or one of two whole-balance forms:

amountInSwapsminOut
exact amountthat amountabsolute
2^255 | quotedAmountIn (SDK consolidateAllAmountIn)the whole balance at executionscaled by balance / quotedAmountIn (rounded up) when less than quoted is left, so the signed rate holds
2^256 − 1the whole balance at executionabsolute (a creator consolidation larger than the slippage margin landing first makes it revert)

Clients use the quoted form; effectiveConsolidateMinOut computes the bound the vault will apply. Exception: the FeeVault of the superseded 2026-10-04 deployment (which never held funds) predates the quoted form and reads 2^255 | quotedAmountIn as a literal amount (the request reverts), so messages for that vault would use 2^256 − 1 or an exact amount.

Adapter data​

DestinationadapterData
Directabi.encode(address recipient, bool unwrapNative) (unwrapNative only for WBNB)
Railgunabi.encode(bytes32 beneficiaryId, ShieldRequest req)

Example (viem / SDK)​

import {buildClaim, encodeDirectData, DIRECT_ADAPTER, signFeeVaultMessage, hashFeeVaultMessage} from '@zk-pad/sdk';
import {privateKeyToAccount} from 'viem/accounts';

const {claim, data} = buildClaim({
beneficiaryId: id,
asset: USDT,
amount: 100n * 10n ** 18n, // total debited, fee included
adapter: DIRECT_ADAPTER, // address(0)
data: encodeDirectData(freshAddress),
relayer: relayerAddress,
relayerFee: BigInt(quote.relayerFee),
nonce, // nonces(id), from RelayerClient.accountNonces(id) (no RPC read)
deadline: BigInt(Math.floor(Date.now() / 1000) + 600),
});
const stealth = privateKeyToAccount(kit.privateKey);
const signature = await signFeeVaultMessage(stealth, 'Claim', claim, 56, feeVault);
// Optional: compare with FeeVault.hashClaim(claim) on-chain
const digest = hashFeeVaultMessage('Claim', claim, 56, feeVault);

The exact SDK signatures are on the SDK page.

BindOwner signatures​

proposeBind requires at least attestorThreshold signatures, each from a current attestor, with recovered signers in strictly ascending address order (duplicates are rejected). Attestor signatures are verified with ECDSA recovery (EOA keys only).