Skip to main content

Overview

The Orchestrator is the intent coordination hub of the zkp2p protocol. It manages the complete lifecycle of intents from signal to fulfillment, coordinates with the Escrow for liquidity management, and integrates with payment verifiers to validate off-chain payments.
The Orchestrator acts as the “order book” for P2P fiat-to-crypto trades, matching buyers (who signal intents) with sellers (who provide liquidity via deposits).

Key Responsibilities

  • Intent Signaling: Capture buyer commitment to pay off-chain for on-chain assets
  • Intent Verification: Validate gating service signatures and deposit parameters
  • Fulfillment Coordination: Verify payment proofs and release funds to buyers
  • Fee Management: Collect protocol fees and referrer fees
  • Hook Execution: Execute post-intent actions (bridging, swapping, etc.)

Core Data Structures

Intent Struct

Every intent signaled on the Orchestrator is represented by this comprehensive struct (from contracts/interfaces/IOrchestrator.sol:12):
struct Intent {
    address owner;                   // Address of the intent owner  
    address to;                      // Address to forward funds to (can be same as owner)
    address escrow;                  // Address of the escrow contract holding the deposit
    uint256 depositId;               // ID of the deposit the intent is associated with
    uint256 amount;                  // Amount of deposit.token the owner wants to take
    uint256 timestamp;               // Timestamp of the intent
    bytes32 paymentMethod;           // Payment method to be used for offchain payment
    bytes32 fiatCurrency;            // Currency code that the owner is paying in offchain
    uint256 conversionRate;          // Conversion rate of deposit token to fiat at time of intent
    bytes32 payeeId;                 // Hashed payee identifier to whom the owner will pay offchain
    address referrer;                // Address of the referrer who brought this intent (if any)
    uint256 referrerFee;             // Fee to be paid to the referrer in preciseUnits (1e16 = 1%)
    IPostIntentHook postIntentHook;  // Address of the post-intent hook that will execute actions
    bytes data;                      // Additional data to be passed to the post-intent hook
}
Intent Immutability: Once signaled, intent parameters are frozen on-chain. This gives strong guarantees to both buyer (locked exchange rate) and seller (payment details).

SignalIntentParams

When signaling an intent, users provide these parameters (from contracts/interfaces/IOrchestrator.sol:29):
struct SignalIntentParams {
    address escrow;                      // The escrow contract where the deposit is held
    uint256 depositId;                   // The ID of the deposit the taker intends to use
    uint256 amount;                      // The amount of deposit.token the user wants to take
    address to;                          // Address to forward funds to
    bytes32 paymentMethod;               // Payment method to be used for offchain payment
    bytes32 fiatCurrency;                // Currency code for offchain payment
    uint256 conversionRate;              // The conversion rate agreed offchain
    address referrer;                    // Address of the referrer (address(0) if no referrer)
    uint256 referrerFee;                 // Fee to be paid to the referrer
    bytes gatingServiceSignature;        // Signature from the deposit's gating service
    uint256 signatureExpiration;         // Timestamp when the gating service signature expires
    IPostIntentHook postIntentHook;      // Optional post-intent hook (address(0) for no hook)
    bytes data;                          // Additional data for the intent
}

Intent Lifecycle

Signaling Intents

Buyers signal intent to purchase on-chain assets by paying off-chain (from contracts/Orchestrator.sol:102):
function signalIntent(SignalIntentParams calldata _params)
    external
    whenNotPaused

Signal Validation

The _validateSignalIntent() function performs extensive checks (from contracts/Orchestrator.sol:390):
  1. Multiple Intent Check: Verify user can have multiple active intents (whitelisted relayer or allowMultipleIntents == true)
  2. Address Validation: Ensure to address is not zero
  3. Fee Validation:
    • Referrer fee must be ≤ 50% (MAX_REFERRER_FEE)
    • If no referrer, fee must be 0
  4. Post-Intent Hook Validation: If set, must be whitelisted in postIntentHookRegistry
  5. Escrow Validation: Must be whitelisted or registry accepts all escrows
  6. Payment Method Validation: Must exist in paymentVerifierRegistry and be active on the deposit
  7. Currency Validation: Must be supported by the payment method with non-zero min conversion rate
  8. Conversion Rate Validation: Must meet or exceed deposit’s min conversion rate
  9. Gating Service Signature: If deposit has a gating service, validate EIP-712 signature

Intent Hash Calculation

Intent hashes are deterministically generated (from contracts/Orchestrator.sol:444):
function _calculateIntentHash() internal view returns (bytes32 intentHash) {
    uint256 intermediateHash = uint256(
        keccak256(
            abi.encodePacked(
                address(this),    // Orchestrator address for global uniqueness
                intentCounter     // Unique counter within this orchestrator
            )
        ));
    intentHash = bytes32(intermediateHash % CIRCOM_PRIME_FIELD);
}
The hash is modded by CIRCOM_PRIME_FIELD (254-bit prime) to ensure compatibility with circom-based ZK circuits.

What Happens on Signal

  1. Validate all parameters via _validateSignalIntent()
  2. Generate unique intentHash via _calculateIntentHash()
  3. Fetch deposit and payment method data from Escrow
  4. Store snapshot of deposit’s min intent amount (intentMinAtSignal)
  5. Create and store the full Intent struct
  6. Add intent hash to accountIntents[msg.sender]
  7. Increment intentCounter
  8. Emit IntentSignaled event
  9. Call IEscrow(escrow).lockFunds(depositId, intentHash, amount)
If lockFunds() on the Escrow reverts (insufficient liquidity, deposit not accepting intents, etc.), the entire transaction reverts.

Cancelling Intents

Intent owners can cancel their own intents before fulfillment (from contracts/Orchestrator.sol:161):
function cancelIntent(bytes32 _intentHash) external
Process:
  1. Verify intent exists (timestamp != 0)
  2. Verify caller is intent owner
  3. Prune intent (delete from storage, remove from accountIntents)
  4. Call IEscrow(escrow).unlockFunds(depositId, intentHash) to release liquidity
  5. Emit IntentPruned event

Fulfilling Intents

Anyone can submit a fulfillment with valid payment proof (from contracts/Orchestrator.sol:184):
function fulfillIntent(FulfillIntentParams calldata _params) external nonReentrant whenNotPaused

FulfillIntentParams

struct FulfillIntentParams {
    bytes paymentProof;           // Payment proof (attestation, ZK proof, etc.)
    bytes32 intentHash;           // Identifier of intent being fulfilled
    bytes verificationData;       // Additional data for payment verifier
    bytes postIntentHookData;     // Additional data for post intent hook
}

Fulfillment Flow

Payment Verification

The Orchestrator calls the payment verifier registered for the intent’s payment method (from contracts/Orchestrator.sol:194):
address verifier = paymentVerifierRegistry.getVerifier(intent.paymentMethod);

IPaymentVerifier.PaymentVerificationResult memory verificationResult = 
    IPaymentVerifier(verifier).verifyPayment(
        IPaymentVerifier.VerifyPaymentData({
            intentHash: _params.intentHash,
            paymentProof: _params.paymentProof,
            data: _params.verificationData
        })
    );
The verifier returns:
struct PaymentVerificationResult {
    bool success;              // Whether verification succeeded
    bytes32 intentHash;        // The intent hash (must match)
    uint256 releaseAmount;     // The amount to release (may be partial)
}

Min-At-Signal Enforcement

To prevent sub-minimum partial fulfillments, the Orchestrator enforces the deposit’s min intent amount at the time of signal (from contracts/Orchestrator.sol:205):
uint256 minAtSignal = intentMinAtSignal[_params.intentHash];
if (minAtSignal > 0 && verificationResult.releaseAmount < minAtSignal) {
    revert AmountBelowMin(verificationResult.releaseAmount, minAtSignal);
}
This prevents a depositor from raising their min intent amount after an intent is signaled, then getting a partial fulfillment below the original minimum.

Fee Management

Protocol Fee

The protocol charges a fee on the release amount (from contracts/Orchestrator.sol:485):
if (protocolFeeRecipient != address(0) && protocolFee > 0) {
    protocolFeeAmount = (_releaseAmount * protocolFee) / PRECISE_UNIT;
    _token.safeTransfer(protocolFeeRecipient, protocolFeeAmount);
}
  • Max protocol fee: 10% (MAX_PROTOCOL_FEE = 1e17 at line 41)
  • Fee is in preciseUnits (1e16 = 1%)

Referrer Fee

Referrers can earn a fee for bringing users to the protocol (from contracts/Orchestrator.sol:490):
if (_intent.referrer != address(0) && _intent.referrerFee > 0) {
    referrerFeeAmount = (_releaseAmount * _intent.referrerFee) / PRECISE_UNIT;
    _token.safeTransfer(_intent.referrer, referrerFeeAmount);
}
  • Max referrer fee: 50% (MAX_REFERRER_FEE = 5e17 at line 40)
  • Paid from the release amount

Net Amount Calculation

uint256 netFees = protocolFeeAmount + referrerFeeAmount;
uint256 netAmount = releaseAmount - netFees;
The netAmount is what the buyer receives (or is passed to the post-intent hook).

Post-Intent Hooks

Post-intent hooks enable custom actions after fulfillment, such as:
  • Bridging tokens to another chain (Across, Stargate)
  • Swapping tokens (Uniswap, 1inch)
  • Depositing to yield protocols
  • Multi-step DeFi operations

Hook Execution

If an intent has a postIntentHook set (from contracts/Orchestrator.sol:534):
if (address(_intent.postIntentHook) != address(0)) {
    // Grant exact allowance to the hook
    _token.safeApprove(address(_intent.postIntentHook), 0);
    _token.safeApprove(address(_intent.postIntentHook), netAmount);
    
    // Execute the hook
    _intent.postIntentHook.execute(_intent, netAmount, _postIntentHookData);
    
    // Enforce exact consumption
    uint256 postBalance = _token.balanceOf(address(this));
    require(postBalance <= preBalance, "PostIntentHook: unexpected balance increase");
    uint256 spent = preBalance - postBalance;
    require(spent == netAmount, "PostIntentHook: must pull exact netAmount");
    
    // Reset allowance
    _token.safeApprove(address(_intent.postIntentHook), 0);
}
Security: The hook must pull exactly netAmount from the Orchestrator. Any deviation causes the transaction to revert. This prevents hooks from leaving stranded funds or draining extra tokens.

Manual Release by Depositor

Depositors can manually release funds to the buyer in case of disputes or off-chain arrangements (from contracts/Orchestrator.sol:232):
function releaseFundsToPayer(bytes32 _intentHash) external nonReentrant
Process:
  1. Verify caller is the depositor for the intent’s deposit
  2. Prune intent from storage
  3. Call unlockAndTransferFunds() with full intent amount (no partial)
  4. Calculate and transfer fees
  5. Transfer net amount to intent.to
  6. Emit IntentFulfilled(intentHash, to, netAmount, isManualRelease: true)
Manual releases still charge protocol and referrer fees.

Intent Pruning

Expired intents are pruned by the Escrow contract, which calls back to the Orchestrator (from contracts/Orchestrator.sol:257):
function pruneIntents(bytes32[] calldata _intents) external
Only the Escrow that owns the intent can prune it:
if (
    intent.timestamp != 0 &&           // Intent exists
    intent.escrow == msg.sender        // Caller is the owning escrow
) {
    _pruneIntent(intentHash);
}

Gating Service Signatures

Deposits can require buyers to obtain a signature from a gating service (KYC provider, allowlist manager, etc.) before signaling intents.

Signature Validation

The signature covers (from contracts/Orchestrator.sol:578):
bytes memory message = abi.encodePacked(
    address(this),              // Orchestrator address
    _intent.escrow, 
    _intent.depositId, 
    _intent.amount, 
    _intent.to, 
    _intent.paymentMethod, 
    _intent.fiatCurrency, 
    _intent.conversionRate, 
    _intent.signatureExpiration,
    chainId
);

bytes32 verifierPayload = keccak256(message).toEthSignedMessageHash();
return _intentGatingService.isValidSignatureNow(verifierPayload, _intent.gatingServiceSignature);
The signature includes signatureExpiration to prevent replay attacks. Signatures must be used before expiry.

State Variables

VariableDescriptionLocation
chainIdImmutable chain identifierLine 45
intentsMapping of intentHash to Intent structLine 47
accountIntentsMapping of account to array of intent hashesLine 48
intentMinAtSignalSnapshot of min intent amount at signal timeLine 52
escrowRegistryRegistry of whitelisted escrowsLine 55
paymentVerifierRegistryRegistry of payment verifiersLine 56
postIntentHookRegistryRegistry of whitelisted hooksLine 57
relayerRegistryRegistry of whitelisted relayersLine 58
protocolFeeProtocol fee in preciseUnits (1e16 = 1%)Line 61
protocolFeeRecipientRecipient of protocol feesLine 62
allowMultipleIntentsWhether all users can have multiple intentsLine 64
intentCounterIncrementing nonce for intent hash generationLine 66

Access Control

Owner (Governance)

  • Set escrow registry
  • Set protocol fee (max 10%)
  • Set protocol fee recipient
  • Set allow multiple intents
  • Set post-intent hook registry
  • Set relayer registry
  • Pause/unpause orchestrator

Users

  • Signal intents (with valid gating signature if required)
  • Cancel own intents
  • Fulfill any intent (with valid payment proof)

Depositors

  • Manually release funds to buyers

Escrows

  • Prune expired intents

Key Events

event IntentSignaled(
    bytes32 indexed intentHash, 
    address indexed escrow,
    uint256 indexed depositId, 
    bytes32 paymentMethod, 
    address owner, 
    address to, 
    uint256 amount, 
    bytes32 fiatCurrency, 
    uint256 conversionRate, 
    uint256 timestamp
);

event IntentPruned(bytes32 indexed intentHash);

event IntentFulfilled(
    bytes32 indexed intentHash,
    address indexed fundsTransferredTo,   // Can be intent.to or hook address
    uint256 amount,
    bool isManualRelease
);

Security Features

Reentrancy Protection

fulfillIntent and releaseFundsToPayer use nonReentrant guard

Intent Immutability

Once signaled, intent parameters cannot be changed

Min-At-Signal

Prevents depositors from raising minimums after intent signal

Hook Safety

Post-intent hooks must consume exact allowance or revert

Build docs developers (and LLMs) love