What you’ll be able to do
- Withdraw shielded funds to an address on another chain.
Check notes, route and reserves
Start with recoverable, unspent bridged notes and the owner’s spending authorization. Obtain the route provider’s authenticated gateway, pool, route ID, destination vault and token, RPC endpoints, inbox and light-client configuration, public contract artifacts, and destination finality policy. The Metal signing account’s balance in the gas token pays its transaction fee. Fund the destination relayer’s signing account for that chain’s transaction fees. Use the bridge read interfaces to verify gatewaypool(), pool bridgeGateway(), gateway routeOf(routeId),
and foreignTokenOf(routeId, asset). Check the destination vault’s
bridgeGateway(), metalInbox(), metalChainId() and
supportedToken(token). The message route must match those values and accept
Metal’s certificate epoch. The destination verifier accepts only epoch zero.
Read assetRouteOf(routeId, token) and require the selected route’s reserve
to cover the amount. This is the amount already minted through that route and
available for withdrawal. Read pool bridgeLiabilityOf(asset) and the vault’s
token balance, and verify both cover the withdrawal. Use withdrawalDelay() to
display the destination-chain waiting period in the application.
Obtain the route provider’s public note and key tools, matching BridgeWithdraw
circuit artifact, and Barretenberg proving instructions. Prepare current note
membership paths and a sanctions nonmembership witness, then check that the
selected notes are unspent and their input lock IDs are unlocked. Bridge amounts
retain the route’s raw units and must satisfy 0 < amount < 2^128.
Save the claimant’s recovery data
Choose a secp256k1 claimant key and a cryptographically random 32-byte salt. Store both securely before preparing the withdrawal, since both are needed to sweep the funds. The claimant key may differ from the keys used to spend notes or sign transactions. The proof’sdestCommitment commits to this claimant and salt. The payout
address is chosen when authorizing the sweep. Install viem with
pnpm add viem and compute the commitment as follows:
claimCommitment(claimantAddress, salt) result. An RPC call
reveals the salt to the endpoint, so use an endpoint you trust with that data.
Prove and submit the withdrawal
Use the public proving tools to construct the withdrawal request. It binds the pool asset, amount, route ID, claimant commitment, the note tree’s Merkle root, two input lock IDs, two nullifiers, change note commitment and sanctions root. The proof also binds Metal chain ID and pool contract address. Arrays contain exactly two entries, with zero padding for an unused input. Save the opening of any change note before submitting. The owner authorizes the complete statement with the spending key. Generate and locally verify theBridgeWithdraw proof against the pool’s current circuit.
The sanctions root must match the pool’s current root at execution, so read it
again before submission and regenerate the proof if it changed.
Recheck the selected route’s reserve, then simulate and submit
gateway.withdraw(request, withdrawProof) using the
withdrawal ABI. Any
Metal signing account may submit because the proof carries the owner’s
authorization. Save signed transaction bytes and hash before broadcasting.
Require a successful receipt with the pool’s BridgeWithdrawn and gateway’s
WithdrawalRouted. Match asset, amount, route, claimant commitment and
destination token. Save the actual withdrawal ID and outbox nonce. Verify the
outbox’s message targets the configured vault with
queueRelease(withdrawalId, token, amount, destCommitment) and authenticates
the gateway as source sender.
Confirm spent nullifiers, any appended change note, and the amount decrease in
bridge liability and route reserve, accounting for concurrent activity when
reading aggregate balances. After checking the receipt’s canonical block and
Metal finality, mark inputs spent and change available
in private wallet records. An insufficient reserve or invalid proof reverts the
whole transaction, leaving the notes unspent.
Deliver the release and wait for the vault
Follow sending messages to construct a proof and deliver the saved outbox message through the destination’sMetalInbox.
Check the inbox execution event and vault ReleaseQueued, then read
releaseOf(withdrawalId). Verify token, amount and claimant commitment.
The waiting period begins when the release is queued at the vault.
releasableAt equals that destination block’s timestamp plus the vault’s
withdrawalDelay. Compare the destination chain’s current block timestamp with
releasableAt to decide when the release can be settled.
At or after releasableAt, any destination signing account funded for
transaction fees can call settle(withdrawalId). Use the
release ABI, saving the
signed attempt first. Require a successful receipt, the matching Settled event
and releaseOf(withdrawalId).settled == true. Read
claimAddress(withdrawalId) and verify the token transfer of the recorded
amount from the vault to that holding address. Apply destination finality before
recording settlement.
The funds now rest in the holding address. Keep the withdrawal pending final
delivery until the authorized sweep succeeds.
Authorize and submit the sweep
The claimant chooses the final destination and a Unix deadline in seconds on the destination chain. Validate the address against the application’s payment instruction and allow enough time for transaction inclusion. The signature binds destination chain ID, vault address, withdrawal ID, destination and deadline. With a viem local account or compatible signer for the claimant, sign the inner hash as a personal message:claimant is the signer for the address derived from the saved claimant key.
destinationChainId and vaultAddress come from the authenticated route, and
deadline is a bigint within the uint64 range. Sign the inner hash shown
above so signMessage adds the EIP-191 prefix once. The vault’s sweepDigest
returns the already-prefixed digest. The claimant must use a secp256k1 key
because the vault validates the signature by recovering the signing address.
Simulate the call, then submit it from a signing account funded for
destination-chain transaction fees:
sweep(withdrawalId, claimantAddress, salt, destination, deadline, signature).
Save its signed transaction and hash. The claimant address and salt become
public in this transaction. If the authorization expires before execution, the
claimant can sign a fresh deadline while the release remains unswept.
Verify the successful receipt, vault Swept event, and
releaseOf(withdrawalId).swept == true. Match the authorized destination and
check the actual token transfer from the holding address to that destination.
Reconcile the destination’s balance increase with the transferred amount,
accounting for other transfers in the block. The sweep transfers the holding
address’s entire token balance, so unsolicited funds can make it larger than the
nominal release amount reported by Swept. Confirm the receipt’s canonical
block and destination finality before marking the withdrawal delivered.
Recover each stage
Keep the withdrawal ID, message nonce, transaction attempts and finality
evidence with the application record. Protect private notes, claimant keys and
salt in the recovery store. A failed
settle or sweep transaction rolls back
its state change and remains retryable after the cause is resolved.
Test the route with an approved withdrawal. Early settlement, wrong claimant or
salt, altered destination, expired signature, and replay must fail without
moving funds. Repeating the withdrawal proof must not spend the same notes
again. Complete the sweep and verify that the final destination receives the
funds.