Skip to content
Zeno Docs

Binding and proofs

A fill has to be safe against two distinct failures: someone rewriting the settlement terms after the proof is made, and a filler delivering a note that does not match the order. ZenoLOP addresses each with a different mechanism, and gates the escrow release on a third check at claim time.

The adapt-params binding

ZenoLOP is a RAILGUN adapt contract. Every transaction passed to fillOrder must have committed, inside its own zero-knowledge proof, to both the adapt contract and a hash of the entire settlement bundle:

adaptContract == address(ZenoLOP)
adaptParams   == keccak256(abi.encode(nullifiers, transactions.length, fills, recipient))
                 % SNARK_SCALAR_FIELD

Because the nullifiers, the transaction count, the full fill set, and the payout recipient are all inside the hash, a shielded proof cannot be replayed, redirected to a different recipient, partially executed with a subset of the transactions, or have its fills tampered with. This mirrors RAILGUN's own RelayAdapt.getAdaptParams.

The note-requirements proof

The binding proves the settlement terms were not altered, but not that the delivered note is the right one. That is the job of a Noir circuit, verified on-chain per fill.

fn main(random: Field, token: pub Field, value: pub Field, mpk: pub Field) -> pub Field {
    let npk = hash_2([mpk, random]);
    let commitment = hash_3([npk, token, value]);
    commitment
}

The proof attests: I know a blinding random such that the delivered commitment equals poseidon3(poseidon2(mpk, random), token, value). In other words the note is spendable by the maker's pool key, carries the token the order asked for, and holds the value the filler claims.

The contract checks this with public inputs in exactly this order:

IndexValue
0railgun.getTokenID(order.makingToken)
1the fill's amount
2the order's mpk
3the delivered commitment

Before the proof is checked, fillOrder also requires that the commitment actually appears among the commitments created by the RAILGUN transactions in the same call, that the order exists and has not expired, and that the commitment has not already been used.

Verification runs through an UltraHonk HonkVerifier exposing:

interface IVerifier {
    function verify(bytes calldata _proof, bytes32[] calldata _publicInputs)
        external view returns (bool);
}

Binding the claim to the delivered note

The escrow is not released by fillOrder. At claim time the filler reveals the note blinding, and the contract recomputes the commitment from the order's mpk, the delivered note amount, and that blinding, requiring it to match the note that was delivered:

npk        = railgun.hashLeftRight(order.mpk, random)
commitment = railgun.hashCommitment({npk, order.makingToken, payout.noteAmount})
require(commitment == payout.commitment)

This ties the revealed blinding to the real note, so an escrow cannot be claimed against a note the filler never delivered. random becomes public in the PayoutExecuted event.

What is not enforced

ZenoLOP verifies the structure of a delivered note, not the provenance of the funds behind it. The note-requirements proof attests that the note carries the right token, the right value, and the maker's pool key. Nothing in the protocol attests where those funds came from.

Concretely, a fill is not checked against any blocklist or screening list, and no proof is required that the funds backing the delivered note trace to an unblocked origin. Orders carry a listProviderPPoIKey field and it is emitted in OrderCreated, but the contract never reads it.

Integrators who need provenance screening must enforce it out of band, before deciding which orders to fill or which fills to accept.