Skip to main content

Escrow

Overview​

The Escrow contract (EscrowV2) in V3 focuses on deposits and token custody. It lets sellers configure supported payment methods (bytes32), currencies and minimum conversion rates, and manages locking/unlocking when intents are signaled/fulfilled via authorized Orchestrators.

Compared to older versions, Escrow no longer verifies payments directly or manages the full intent lifecycle — that responsibility moved to the Orchestrator. EscrowV2 also introduces oracle-driven rate floors, delegated rate management, dust sweeping, and multi-orchestrator support via OrchestratorRegistry.


Constants​

  • uint256 internal constant PRECISE_UNIT = 1e18;
  • uint256 internal constant BPS = 10_000;
  • int256 internal constant SIGNED_BPS = 10_000;
  • uint256 internal constant MAX_DUST_THRESHOLD = 1e6; // 1 USDC
  • uint256 internal constant MAX_TOTAL_INTENT_EXPIRATION_PERIOD = 5 days;
  • uint256 internal constant MAX_ADAPTER_CONFIG_BYTES = 256;

Key State​

  • IOrchestratorRegistry public orchestratorRegistry — Registry of authorized orchestrator contracts that can lock/unlock funds.
  • IPaymentVerifierRegistry public paymentVerifierRegistry — Registry providing allowed payment methods/currencies.
  • uint256 public immutable chainId — Deployment chain id.
  • address public dustRecipient — Address that receives dust swept from auto-closed deposits.
  • uint256 public dustThreshold — Remaining deposit amount below which dust sweep triggers (max 1 USDC).
  • uint256 public maxIntentsPerDeposit — Maximum concurrent intents per deposit.
  • uint256 public intentExpirationPeriod — Default intent expiration duration.
  • mapping(address => uint256[]) accountDeposits — Deposit ids per account.
  • mapping(uint256 => Deposit) deposits — Core deposit data:
    • depositor, delegate, token, intentAmountRange (min,max), acceptingIntents, remainingDeposits, outstandingIntentAmount, intentGuardian, retainOnEmpty.
  • Per-deposit payment configuration:
    • depositPaymentMethodData[depositId][paymentMethod] → { intentGatingService, payeeDetails (bytes32), data (bytes) }
    • depositPaymentMethods[depositId] — list of methods; depositPaymentMethodActive — active flag; depositPaymentMethodListed — ever-listed flag.
    • depositCurrencies[depositId][paymentMethod] — list of currency codes (bytes32)
    • depositCurrencyMinRate[depositId][paymentMethod][currency] — min conversion rate (1e18)
  • Per-deposit oracle and rate manager configuration:
    • depositOracleRateConfig[depositId][paymentMethod][currency] → OracleRateConfig
    • depositRateManagerConfig[depositId] → RateManagerConfig

Create Deposit​

function createDeposit(CreateDepositParams calldata _params)

function depositTo(address _depositor, CreateDepositParams calldata _params) — Third-party funded deposits. Allows anyone to create a deposit on behalf of _depositor.

CreateDepositParams

struct Range { uint256 min; uint256 max; }

struct OracleRateConfig {
address adapter; // IOracleAdapter address (address(0) = disabled)
bytes adapterConfig; // Adapter-specific config (max 256 bytes)
int16 spreadBps; // Signed spread applied to oracle market rate
uint32 maxStaleness; // Max age of oracle data in seconds
}

struct Currency {
bytes32 code; // keccak256(utf8(currencyCode))
uint256 minConversionRate; // Fixed rate floor (1e18 precision)
OracleRateConfig oracleRateConfig; // Optional oracle rate config (adapter == address(0) means disabled)
}

struct DepositPaymentMethodData {
address intentGatingService;
bytes32 payeeDetails; // hashed off-chain identifier
bytes data; // optional verifier data (e.g., witness set)
}

struct CreateDepositParams {
IERC20 token;
uint256 amount;
Range intentAmountRange;
bytes32[] paymentMethods; // e.g., keccak256("venmo")
DepositPaymentMethodData[] paymentMethodData;// 1:1 with paymentMethods
Currency[][] currencies; // per method (includes optional oracle config)
address delegate; // optional
address intentGuardian; // optional
bool retainOnEmpty; // keep config when empty
}

Notes

  • paymentMethods and currencies must be whitelisted in PaymentVerifierRegistry.
  • payeeDetails is bytes32 (do not store raw handles on-chain).
  • All min rates use 1e18 precision.
  • OracleRateConfig can be set inline at creation or later via setOracleRateConfig.

Managing Deposits​

  • addFunds(depositId, amount) — Add liquidity (ERC20 must be approved). Callable by anyone.
  • removeFunds(depositId, amount) — May prune expired intents to reclaim liquidity.
  • withdrawDeposit(depositId) — Returns remaining funds (+expired intent funds), and closes when no outstanding intents.
  • Payment method management:
    • addPaymentMethods(depositId, methods, methodData, currencies)
    • setPaymentMethodActive(depositId, method, active)
    • addCurrencies(depositId, method, currencies[])
    • deactivateCurrency(depositId, method, currency)
    • setCurrencyMinRate(depositId, method, currency, newMinRate)
  • Other controls:
    • setAcceptingIntents(depositId, bool)
    • setIntentRange(depositId, Range)
    • setDelegate(depositId, address) / removeDelegate(depositId)
    • extendIntentExpiry(depositId, intentHash, newExpiry) (only intentGuardian)

Oracle Rate Configuration​

Depositors can configure oracle-driven rate floors for any currency on their deposit. When the configured oracle returns a usable rate, the escrow floor for a currency is max(fixedRate, oracleSpreadRate) — the oracle can only raise the floor, never lower it below the fixed rate.

struct OracleRateConfig {
address adapter; // IOracleAdapter implementation (e.g., ChainlinkOracleAdapter)
bytes adapterConfig; // Feed-specific config (max 256 bytes)
int16 spreadBps; // Signed spread in basis points applied to oracle market rate
uint32 maxStaleness; // Max staleness in seconds
}

The oracle spread rate is computed as: oracleRate = marketRate * (10_000 + spreadBps) / 10_000. Negative spreads are allowed (discount below market). The constraint (10_000 + spreadBps) > 0 is enforced.

If a configured oracle returns invalid data or the price is staler than maxStaleness, the pair halts for new intents. A positive fixed floor does not override that halt. With no oracle configured, a positive fixed floor is sufficient.

Management functions:

  • setOracleRateConfig(depositId, paymentMethod, currencyCode, config) — Set oracle config for a single currency.
  • setOracleRateConfigBatch(depositId, paymentMethods[], currencyCodes[][], configs[][]) — Batch set.
  • removeOracleRateConfig(depositId, paymentMethod, currencyCode) — Remove oracle config (reverts to fixed rate only).
  • updateCurrencyConfigBatch(depositId, paymentMethods[], CurrencyRateUpdate[][]) — Batch update fixed rates and/or oracle configs in a single call.
  • deactivateCurrenciesBatch(depositId, paymentMethods[], currencyCodes[][]) — Batch deactivate currencies.

Delegated Rate Management​

Depositors can delegate rate management to an external IRateManager contract (e.g., RateManagerV1). The delegated rate manager can set rates on behalf of the depositor without requiring deposit ownership.

struct RateManagerConfig {
address rateManager; // IRateManager contract address
bytes32 rateManagerId; // Identifier within the rate manager contract
}

With a usable positive escrow floor and a positive delegated rate, the effective rate is max(escrowFloor, delegatedRate). A zero escrow floor halts the pair before querying the manager. A manager returning zero also halts the pair, even when a positive floor exists. A manager call reverting instead falls back to the usable escrow floor.

Management functions:

  • setRateManager(depositId, rateManager, rateManagerId) — Opt into delegated rate management.
  • clearRateManager(depositId) — Remove delegated rate manager.

View functions:

  • getEffectiveRate(depositId, paymentMethod, currencyCode) — Returns the effective rate after the oracle, escrow-floor, and manager halt rules above.
  • getManagerFee(depositId) — Returns the fee recipient and fee amount from the rate manager. The manager fee is snapshotted at intent signal time and distributed at fulfillment.

Dust Sweep & Auto-Close​

When a deposit's remainingDeposits drops to or below dustThreshold and there are no outstanding intents:

  • If retainOnEmpty = false: the deposit is auto-closed and any remaining dust is swept to dustRecipient.
  • If retainOnEmpty = true: the deposit config is kept for reuse (e.g., the depositor can add more funds later without reconfiguring payment methods).

Orchestrator Integration​

EscrowV2 uses OrchestratorRegistry to authorize callers. Any orchestrator registered in the registry can call these functions:

  • lockFunds(depositId, intentHash, amount) — Called by Orchestrator on signal.
  • unlockFunds(depositId, intentHash) — Called on cancel/expire.
  • unlockAndTransferFunds(depositId, intentHash, transferAmount, to) — Called on fulfillment; returns net amount (after fees) to Orchestrator for onward distribution.

Reference: zkp2p-contracts/contracts/EscrowV2.sol