Skip to content
home/overview/02 · settlement

Custody, routing and a single event.

The settlement layer is deliberately boring. It takes tokens, swaps them, emits one event and later checks one proof. Everything clever happens somewhere it can be verified rather than trusted.

The contract surface

ZRelayEscrow.sol lives on Robinhood Chain at 0x3F8a9Db62e109d93Eb69145Ac4E0f5D409941aB2. It accepts any governance-approved ERC-20 — USDG, wrapped ETH, or a tokenized equity such as NVDA — together with a flat native-ETH surcharge that pays for the relayer's downstream gas.

ZRelayEscrow.sol
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.24;3 4import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";5import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";6 7/// @title ZRelayEscrow8/// @notice Locks EVM assets and signals the relayer ring to mint the9///         equivalent value as a shielded Orchard note.10/// @dev    The contract holds no protocol seed capital. Each user forwards a11///         flat native-ETH surcharge that funds the relayer's own gas and the12///         Zcash miner fee, so the ring is self-sustaining from block one.13contract ZRelayEscrow {14    using SafeERC20 for IERC20;15 16    address public owner;17    address public relayerAddress;18 19    uint256 public flatGasSurcharge = 0.0002 ether;20 21    event ShieldRequested(22        bytes32 indexed depositId,23        address indexed token,24        address indexed sender,25        uint256 amount,26        bytes recipientZAddress,27        bytes memo28    );29 30    error InsufficientGasCoverage();31    error SurchargeTransferFailed();32 33    constructor(address _relayer) {34        owner = msg.sender;35        relayerAddress = _relayer;36    }37 38    /// @notice Escrow `amount` of `token` and request an Orchard shielding.39    function shieldAsset(40        address token,41        uint256 amount,42        bytes calldata recipientZAddress,43        bytes calldata memo44    ) external payable returns (bytes32 depositId) {45        if (msg.value < flatGasSurcharge) revert InsufficientGasCoverage();46 47        // Forward the surcharge with call() — transfer()'s 2300-gas stipend48        // breaks for smart-contract relayer wallets.49        (bool sent, ) = payable(relayerAddress).call{value: msg.value}("");50        if (!sent) revert SurchargeTransferFailed();51 52        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);53 54        depositId = keccak256(55            abi.encodePacked(block.chainid, block.number, msg.sender, amount, memo)56        );57 58        emit ShieldRequested(59            depositId,60            token,61            msg.sender,62            amount,63            recipientZAddress,64            memo65        );66    }67}
The surcharge is the whole business model
Forwarding msg.value straight to the relayer means every shield transaction funds its own execution — the EVM gas, the Zcash miner fee and the prover cycles. No treasury subsidy, no runway to burn, no moment where the ring stops relaying because a grant ran out.

Why the surcharge uses call(), not transfer()

The reference sketch for this contract used payable(relayer).transfer(msg.value). That forwards a fixed 2300-gas stipend, which is enough for an EOA and not enough for a multisig, a smart account or anything with a receive hook. Since relayer operators are exactly the people likely to run a Safe, the production contract uses call with an explicit success check and a custom error.

Asset routing

Zcash has one asset. Robinhood Chain has hundreds. The escrow reconciles this by routing every deposit through on-chain DEX liquidity into the bridge collateral asset before the ring mints anything:

  • Direct pairs settle in a single hop where liquidity allows it.
  • Equity tokens route through USDG, which carries the deepest book on the chain.
  • Slippage bounds are supplied by the caller and enforced in the same transaction — a quote that cannot be honoured reverts rather than settling badly.

The event is the API

The relayer ring has no privileged channel, no webhook and no shared database. It watches the chain. ShieldRequested is the entire instruction set:

ShieldRequested
event ShieldRequested(
    bytes32 indexed depositId,
    address indexed token,
    address indexed sender,
    uint256 amount,
    bytes recipientZAddress,
    bytes memo
);

Because the instruction is a public log, any staked node can serve any request. A relayer that ignores a deposit does not censor it; it just forfeits the fee to whichever node picks it up next.

Threshold custody

Between escrow and Orchard, funds are controlled by the ring's FROST key. This is the one window where users are trusting people rather than mathematics, so the parameters are deliberately conservative:

Scheme
FROST (threshold Schnorr, RedPallas)
Ring size
11 signers at mainnet launch
Threshold
7 of 11
Key refresh
Every epoch, without changing the group key
Stake requirement
Denominated in $ZRL, slashable
Timeout refund
30 minutes, claimable by the depositor

Failure modes

  • Threshold never met. The deposit becomes refundable after the timeout. The depositor calls reclaim(depositId) and receives the original token back, minus nothing.
  • Ring signs the wrong output. The attestation proof will not verify against the submitted claim hash, the escrow never marks the deposit settled, and the responsible shares are slashable.
  • DEX route fails. The swap and the escrow happen in one transaction, so the whole call reverts and the user keeps their tokens.
Audit status
The escrow and verifier are under review as Milestone 2. Treat the deployed testnet addresses as pre-production and do not send mainnet value to them until the audit report is published on the security page.