Skip to main content

Overview

The Escrow contract is the liquidity hub of the zkp2p protocol. It holds depositor funds, manages deposit configurations, and coordinates with the Orchestrator to lock/unlock liquidity as intents progress through their lifecycle.
Each deposit can support multiple payment methods (Venmo, PayPal, Wise, etc.), each with its own verification requirements and supported currencies.

Key Responsibilities

  • Deposit Management: Create and manage deposits with flexible intent ranges
  • Liquidity Locking: Lock funds when intents are signaled, release when fulfilled or cancelled
  • Payment Method Configuration: Support multiple payment services with independent verification data
  • Intent Expiry: Track and reclaim liquidity from expired intents
  • Dust Collection: Automatically close small deposits and sweep remainder to protocol

Core Data Structures

Deposit Struct

Every deposit created on the Escrow is represented by this struct (from contracts/interfaces/IEscrow.sol:24):
struct Deposit {
    address depositor;                  // Address of depositor
    address delegate;                   // Address that can manage this deposit (address(0) if no delegate)
    IERC20 token;                       // Address of deposit token
    Range intentAmountRange;            // Range of take amount per intent
    // Deposit state
    bool acceptingIntents;              // True if the deposit is accepting intents
    uint256 remainingDeposits;          // Amount of liquidity immediately available to lock
    uint256 outstandingIntentAmount;    // Amount of outstanding intents (may include expired intents)
    // Intent guardian
    address intentGuardian;             // Address that can extend intent expiry times
    // Retention behavior
    bool retainOnEmpty;                 // If true, do not auto-close/sweep when empty; keep config for reuse
}
Delegate: An optional address that can manage deposit parameters (conversion rates, intent ranges, payment methods) on behalf of the depositor.

Intent Struct

Each intent locked against a deposit is tracked with this struct (from contracts/interfaces/IEscrow.sol:12):
struct Intent {
    bytes32 intentHash;     // Unique identifier for the intent
    uint256 amount;         // Amount locked
    uint256 timestamp;      // When this intent was created
    uint256 expiryTime;     // When this intent expires
}

Payment Method Data

Deposits can support multiple payment methods. Each has its own configuration (from contracts/interfaces/IEscrow.sol:44):
struct DepositPaymentMethodData {
    address intentGatingService;    // Public key of gating service that will verify intents
    bytes32 payeeDetails;          // Payee details, hash of payee details
    bytes data;                    // Verification Data: Additional data used for payment verification
}
The payeeDetails field stores a hash of the depositor’s payment account ID (e.g., Venmo username hash) to preserve privacy on-chain.

Deposit Lifecycle

Creating a Deposit

Depositors create liquidity pools by calling createDeposit() with comprehensive configuration (from contracts/Escrow.sol:139):
function createDeposit(CreateDepositParams calldata _params) external whenNotPaused
CreateDepositParams includes:
  • token: ERC20 token address
  • amount: Initial deposit amount
  • intentAmountRange: Min/max amounts per intent
  • paymentMethods: Array of supported payment method hashes
  • paymentMethodData: Verification data for each payment method
  • currencies: Supported currencies and min conversion rates for each method
  • delegate: Optional management delegate
  • intentGuardian: Optional address that can extend intent expiry
  • retainOnEmpty: Whether to keep deposit config when balance reaches zero

Example: Multi-Currency Deposit

A depositor might configure:
  • Venmo: USD only, min rate 1.0
  • Wise: USD, EUR, GBP with different conversion rates
  • PayPal: USD, EUR
Each payment method has independent payeeDetails (the depositor’s account ID for that service).

Liquidity Management

Adding Funds

Anyone can add funds to an existing deposit (from contracts/Escrow.sol:187):
function addFunds(uint256 _depositId, uint256 _amount) external whenNotPaused
Funds are added to remainingDeposits, making them immediately available for new intents.

Removing Funds

Only the depositor can remove funds (from contracts/Escrow.sol:213):
function removeFunds(uint256 _depositId, uint256 _amount) external nonReentrant whenNotPaused
If remainingDeposits is insufficient, the function automatically prunes expired intents to reclaim liquidity before attempting removal.

Intent Amount Range

The intentAmountRange (min/max) controls the size of intents accepted:
struct Range {
    uint256 min;    // Minimum value
    uint256 max;    // Maximum value
}
Depositors can update this range via setIntentRange() (from contracts/Escrow.sol:352).

Intent Lifecycle on Escrow

Locking Funds

When an intent is signaled on the Orchestrator, it calls lockFunds() (from contracts/Escrow.sol:557):
function lockFunds(
    uint256 _depositId, 
    bytes32 _intentHash,
    uint256 _amount
) external nonReentrant onlyOrchestrator
The function:
  1. Validates deposit state (acceptingIntents == true)
  2. Checks amount is within intentAmountRange
  3. Prunes expired intents if needed to free liquidity
  4. Moves amount from remainingDeposits to outstandingIntentAmount
  5. Stores the intent with expiry time (block.timestamp + intentExpirationPeriod)
  6. Emits FundsLocked event

Unlocking Funds (Cancel)

When an intent is cancelled, the Orchestrator calls unlockFunds() (from contracts/Escrow.sol:620):
function unlockFunds(uint256 _depositId, bytes32 _intentHash) 
    external 
    nonReentrant
    onlyOrchestrator
This returns the locked amount to remainingDeposits.

Unlocking & Transferring Funds (Fulfill)

When an intent is fulfilled, the Orchestrator calls unlockAndTransferFunds() (from contracts/Escrow.sol:651):
function unlockAndTransferFunds(
    uint256 _depositId, 
    bytes32 _intentHash,
    uint256 _transferAmount,   // May be less than intent amount (partial fulfillment)
    address _to                 // Orchestrator address
) external nonReentrant onlyOrchestrator
Partial Fulfillment: The _transferAmount can be less than the original intent amount. The difference is returned to remainingDeposits.

Intent Expiration

Intents have a configurable expiration period (default 5 days max per MAX_TOTAL_INTENT_EXPIRATION_PERIOD at line 42).

Automatic Expiry Pruning

Expired intents are automatically pruned when:
  • lockFunds() needs more liquidity
  • removeFunds() is called
  • withdrawDeposit() is called
  • Anyone calls pruneExpiredIntents() (from contracts/Escrow.sol:533)

Extending Expiry

The intentGuardian (if set) can extend intent expiry via extendIntentExpiry() (from contracts/Escrow.sol:702):
function extendIntentExpiry(
    uint256 _depositId, 
    bytes32 _intentHash,
    uint256 _additionalTime
) external
This is useful when off-chain payment is delayed but legitimate.

Delegate System

Depositors can assign a delegate to manage deposit parameters without transferring ownership. Delegate can:
  • Update conversion rates (setCurrencyMinRate)
  • Update intent ranges (setIntentRange)
  • Add/remove payment methods and currencies
  • Toggle accepting intents state
  • Set retention behavior
Delegate cannot:
  • Withdraw funds (only depositor)
  • Change the delegate itself (only depositor)
// Set delegate (contracts/Escrow.sol:286)
function setDelegate(uint256 _depositId, address _delegate) external whenNotPaused

// Remove delegate (contracts/Escrow.sol:301)
function removeDelegate(uint256 _depositId) external whenNotPaused

Dust Collection

When a deposit’s remainingDeposits falls below dustThreshold (max 1 USDC, see line 41) and has no outstanding intents:
  • Deposit is automatically closed
  • Remaining balance is swept to dustRecipient
  • All payment method and currency data is deleted
Set retainOnEmpty = true to prevent auto-closure and keep the deposit configuration for future reuse.

Payment Method & Currency Management

Adding Payment Methods

Depositors can add new payment methods after creation (from contracts/Escrow.sol:381):
function addPaymentMethods(
    uint256 _depositId,
    bytes32[] calldata _paymentMethods,
    DepositPaymentMethodData[] calldata _paymentMethodData,
    Currency[][] calldata _currencies
) external whenNotPaused onlyDepositorOrDelegate(_depositId)

Toggling Payment Methods

Payment methods can be deactivated without deletion (from contracts/Escrow.sol:404):
function setPaymentMethodActive(
    uint256 _depositId,
    bytes32 _paymentMethod,
    bool _isActive
) external whenNotPaused onlyDepositorOrDelegate(_depositId)

Managing Currencies

Each payment method can support multiple currencies with independent min conversion rates:
struct Currency {
    bytes32 code;                  // Currency code (keccak256 hash)
    uint256 minConversionRate;     // Minimum rate in preciseUnits (1e18)
}
Functions:
  • addCurrencies(): Add new currencies to a payment method (line 430)
  • setCurrencyMinRate(): Update min conversion rate (line 324)
  • deactivateCurrency(): Set conversion rate to 0 (line 458)

State Variables

VariableDescriptionLocation
orchestratorOrchestrator contract referenceLine 47
paymentVerifierRegistryPayment verifier registryLine 48
chainIdImmutable chain identifierLine 49
depositCounterIncrementing deposit ID counterLine 78
dustRecipientReceives dust from closed depositsLine 80
dustThresholdThreshold below which deposits are dustLine 81
maxIntentsPerDepositMax concurrent intents (prevents DOS)Line 82
intentExpirationPeriodHow long intents remain validLine 83

Access Control

Owner (Governance)

  • Set orchestrator address
  • Update payment verifier registry
  • Configure dust parameters
  • Set max intents per deposit
  • Set intent expiration period
  • Pause/unpause deposit operations

Depositor

  • Create deposits
  • Withdraw deposits
  • Remove funds
  • Set delegate
  • Remove delegate

Depositor OR Delegate

  • Update conversion rates
  • Update intent ranges
  • Add/remove payment methods
  • Add/remove currencies
  • Toggle accepting intents state
  • Set retention behavior

Orchestrator Only

  • Lock funds
  • Unlock funds
  • Unlock and transfer funds

Intent Guardian Only

  • Extend intent expiry

Key Events

event DepositReceived(uint256 indexed depositId, address indexed depositor, ...)
event FundsLocked(uint256 indexed depositId, bytes32 indexed intentHash, uint256 amount, uint256 expiryTime)
event FundsUnlocked(uint256 indexed depositId, bytes32 indexed intentHash, uint256 amount)
event FundsUnlockedAndTransferred(uint256 indexed depositId, bytes32 indexed intentHash, ...)
event DepositClosed(uint256 depositId, address depositor)
event DustCollected(uint256 indexed depositId, uint256 dustAmount, address indexed dustRecipient)

Security Features

Reentrancy Protection

All state-changing functions use nonReentrant modifier from OpenZeppelin

Pausability

Owner can pause deposit creation/modification while leaving withdrawals active

Intent Limits

maxIntentsPerDeposit prevents gas DOS attacks on withdrawal

Expiry Enforcement

Intents automatically expire after intentExpirationPeriod

Build docs developers (and LLMs) love