Skip to content
Zeno Docs

Contract reference

ZenoLOP is Ownable and ReentrancyGuard, compiled with Solidity ^0.8.7 and abicoder v2. Every order and payout function is nonReentrant; the owner-only setters are not.

Constructor and configuration

constructor(
    RailgunSmartWallet _railgun,
    IVerifier _noteRequirementsVerifier,
    address _treasury,
    uint256 _claimWindow
)

railgun and noteRequirementsVerifier are immutable. The owner can update the rest:

FunctionAccessConstraint
setTreasury(address)ownercannot be the zero address
setClaimWindow(uint256)ownermust be greater than zero

Constants

NameValueMeaning
takerFee10taker fee in basis points (0.1%), skimmed from each payout to the treasury. public constant, readable on-chain
BASIS_POINTS10000denominator for the fee calculation. private constant, not readable on-chain

Structs

struct EntryOrder {
    address   maker;                // who submitted the order
    bytes32   mpk;                  // pool key that must be able to spend the delivered note
    TokenData makingToken;          // token wanted as a shielded note
    uint120   makingAmount;         // total note value wanted
    TokenData takingToken;          // token escrowed as payment
    uint256   takingAmount;         // total escrowed
    uint256   expiry;               // fillable before; cancellable at or after
    bytes32   listProviderPPoIKey;  // recorded and emitted; not read by the contract
    uint256   remainingAmount;      // unallocated escrow; starts at takingAmount
}
 
struct Fill {
    uint256 orderId;
    uint120 amount;      // makingToken delivered in this note
    bytes32 commitment;  // delivered note commitment
    bytes   proof;       // Noir note-requirements proof
}
 
struct PendingPayout {
    uint256 orderId;
    uint256 amount;      // takingToken owed to the filler
    address recipient;
    uint256 deadline;    // last timestamp the filler can claim
    bytes32 commitment;
    uint120 noteAmount;
}

TokenData is RAILGUN's (uint8 tokenType, address tokenAddress, uint256 tokenSubID).

Functions

FunctionCallerNotes
submitOrder(mpk, makingToken, makingAmount, takingToken, takingAmount, expiry, listProviderPPoIKey)makerescrows takingAmount up front
fillOrder(Transaction[], Fill[], address recipient)filler or relayerexecutes the RAILGUN transactions and escrows payouts
claimPayout(payoutId, random, proof)anyonereleases escrow within the claim window; proof is currently ignored. There is no msg.sender check, so a relayer can submit it: funds always go to the recorded recipient
reclaimPayout(payoutId)makerrecovers escrow after the deadline passes
cancelOrder(orderId)makeronly at or after expiry; refunds remainingAmount

Public getters: orders(orderId), pendingPayouts(payoutId), usedCommitment(commitment), nextOrderId, nextPendingPayoutId, claimWindow, treasury, takerFee, railgun, noteRequirementsVerifier, and owner() from Ownable.

Payout math

payout = deliveredNoteAmount * order.takingAmount / order.makingAmount

Floored, so rounding dust favors the maker. The fee is then payout * takerFee / BASIS_POINTS to the treasury, with the remainder to the recipient.

Events

event OrderCreated(uint256 indexed orderId, address indexed maker, bytes32 mpk,
                   TokenData makingToken, uint120 makingAmount, TokenData takingToken,
                   uint256 takingAmount, uint256 expiry, bytes32 listProviderPPoIKey);
event OrderCancelled(uint256 indexed orderId, address indexed maker, uint256 refundedAmount);
event OrderFilled(uint256 indexed payoutId, uint256 indexed orderId, address indexed recipient,
                  uint256 payoutAmount, uint120 noteAmount, bytes32 commitment, uint256 deadline);
event PayoutExecuted(uint256 indexed payoutId, uint256 indexed orderId, address indexed recipient,
                     uint256 payoutAmount, bytes32 commitment, bytes32 random);
event PayoutReclaimed(uint256 indexed payoutId, uint256 indexed orderId, address indexed maker,
                      uint256 amount);

These drive the subgraph; regenerate subgraph/abis/ZenoLOP.json after changing any of them.

Reverts

MessageRaised when
Taking amount must be greater than zerosubmitOrder with zero escrow
Making amount must be greater than zerosubmitOrder with zero note value
Expiry must be in the futuresubmitOrder with a past expiry
Custodied amount mismatchthe escrow token takes a transfer fee
Unsupported token typetoken id is not an ERC20
Invalid recipientfillOrder with a zero recipient
Invalid adapt contract / Invalid adapt paramsa transaction is not bound to this bundle
Commitment not createdthe fill names a note the transactions did not create
Order does not exist / Order expiredorder lookup or expiry check failed
Commitment already usedthe note was already used for a fill
Invalid note proofthe note-requirements proof failed
Fill too smallthe pro-rata payout rounds to zero
Payout does not existunknown payout id
Claim window closed / Claim window still openclaim or reclaim outside its window
random not in fieldclaimPayout with random >= SNARK_SCALAR_FIELD
random does not match delivered notethe revealed blinding does not reproduce the commitment
Only maker can cancel / Only maker can reclaimcaller is not the maker
Order not yet expiredcancelOrder before expiry
Claim window must be positive / Treasury cannot be zero addressowner setters, invalid argument

A fill whose payout exceeds the order's remainingAmount reverts on the subtraction with an arithmetic panic (Panic(0x11)) rather than a message, so there is no string to match on.

Token support

Only ERC20 is supported. Token ids with bits above 160 are rejected, and fee-on-transfer tokens fail the custody balance-delta check. ERC721 and ERC1155 remain TODOs.

Deployments

Current Sepolia deployment (chain id 11155111), matching the subgraph's indexed address:

ContractAddress
ZenoLOP0x960974EC73a2534DE907a610d623Aa61EA696804
HonkVerifier (note requirements)0x07408A1e804FC93fC588f28f550c82347f37E03c
RailgunSmartWallet0xeCFCf3b4eC647c4Ca6D49108b311b7a7C9543fea

Deployed at block 11238525 with claimWindow = 86400 (one day).