Documentation Index Fetch the complete documentation index at: https://mintlify.com/zkp2p/zkp2p-contracts/llms.txt
Use this file to discover all available pages before exploring further.
System Overview
ZKP2P v2.1 implements a modular, intent-based architecture for trustless peer-to-peer fiat-to-crypto exchanges. The system consists of five main components that work together to facilitate secure, verifiable trades:
Escrow Contract - Liquidity management and deposit custody
Orchestrator Contract - Intent lifecycle coordination
Unified Payment Verifier - Multi-platform payment validation
Registry System - Permission and configuration management
Protocol Viewer - Read-only state aggregation
The v2.1 architecture introduces a unified verification system that consolidates multiple payment method verifiers into a single, configurable contract, significantly reducing deployment complexity.
Architecture Diagram
Core Components
Escrow Contract
The Escrow contract manages liquidity deposits from makers (liquidity providers) and handles secure fund custody.
Deposit Management Creates, updates, and closes liquidity deposits with configurable parameters
Payment Method Config Supports multiple payment methods per deposit with currency-specific rates
Intent Locking Temporarily locks funds when takers signal intent to trade
Liquidity Reclaim Automatically reclaims liquidity from expired intents
Key Responsibilities
Deposit Lifecycle Management
Makers can create deposits with specific parameters: function createDeposit ( CreateDepositParams calldata _params )
external
whenNotPaused
{
// Validates parameters
if (_params.intentAmountRange.min == 0 ) revert ZeroMinValue ();
if (_params.amount < _params.intentAmountRange.min) {
revert AmountBelowMin (_params.amount, _params.intentAmountRange.min);
}
// Creates deposit with unique ID
uint256 depositId = depositCounter ++ ;
deposits[depositId] = Deposit ({
depositor : msg.sender ,
token : _params.token,
intentAmountRange : _params.intentAmountRange,
acceptingIntents : true ,
remainingDeposits : _params.amount,
outstandingIntentAmount : 0 ,
// ...
});
// Transfers tokens to escrow
_params.token. safeTransferFrom ( msg.sender , address ( this ), _params.amount);
}
Each deposit includes:
Token type (USDC)
Amount range for individual intents (min/max)
Supported payment methods and currencies
Optional delegate for deposit management
Optional intent guardian for expiry extensions
Each deposit can accept multiple payment methods, and each method can support multiple currencies: // Example: Deposit supports Venmo and Revolut
// Venmo accepts: USD (1:1 rate)
// Revolut accepts: USD (1:1), EUR (1.2:1), GBP (1.3:1)
mapping ( uint256 => mapping ( bytes32 => mapping ( bytes32 => uint256 )))
internal depositCurrencyMinRate;
This flexibility allows makers to:
Accept payments from multiple platforms
Support international currencies
Set custom conversion rates per currency
Update rates dynamically based on market conditions
When a taker signals intent, the Orchestrator calls lockFunds: function lockFunds (
uint256 _depositId ,
bytes32 _intentHash ,
uint256 _amount
)
external
onlyOrchestrator
{
// Validates deposit state
Deposit storage deposit = deposits[_depositId];
if ( ! deposit.acceptingIntents) revert DepositNotAcceptingIntents (_depositId);
// Reclaims expired intent liquidity if needed
bytes32 [] memory expiredIntents = _reclaimLiquidityIfNecessary (
deposit, _depositId, _amount
);
// Locks liquidity
deposit.remainingDeposits -= _amount;
deposit.outstandingIntentAmount += _amount;
// Creates intent with expiry
uint256 expiryTime = block .timestamp + intentExpirationPeriod;
depositIntents[_depositId][_intentHash] = Intent ({
intentHash : _intentHash,
amount : _amount,
timestamp : block .timestamp,
expiryTime : expiryTime
});
}
This ensures:
Liquidity is reserved for the specific intent
Intents expire after a configurable period
Expired intents are pruned to reclaim liquidity
Dust Collection & Deposit Closure
To prevent small leftover balances: function _closeDepositIfNecessary ( uint256 _depositId , Deposit storage _deposit )
internal
{
uint256 totalRemaining = _deposit.remainingDeposits;
if (_deposit.outstandingIntentAmount == 0 &&
totalRemaining <= dustThreshold &&
! _deposit.retainOnEmpty)
{
// Close deposit and sweep dust
IERC20 token = _deposit.token;
_closeDeposit (_depositId, _deposit);
if (totalRemaining > 0 ) {
token. safeTransfer (dustRecipient, totalRemaining);
emit DustCollected (_depositId, totalRemaining, dustRecipient);
}
}
}
Dust threshold prevents tiny balances from remaining
retainOnEmpty flag allows makers to keep deposit config
Protocol collects dust to avoid locked funds
Orchestrator Contract
The Orchestrator coordinates the entire intent lifecycle from creation to settlement.
Intent Coordination Manages intent creation, cancellation, and fulfillment
Payment Verification Routes verification requests to appropriate verifiers
Fee Collection Distributes protocol fees and referrer commissions
Hook Execution Executes optional post-intent hooks for custom logic
Intent Lifecycle
Signal Intent
Taker signals their intention to trade: Orchestrator.sol (line 102)
function signalIntent ( SignalIntentParams calldata _params )
external
whenNotPaused
{
// Validates intent parameters
_validateSignalIntent (_params);
// Calculates unique intent hash
bytes32 intentHash = _calculateIntentHash ();
// Stores intent with all parameters
intents[intentHash] = Intent ({
owner : msg.sender ,
to : _params.to,
escrow : _params.escrow,
depositId : _params.depositId,
amount : _params.amount,
paymentMethod : _params.paymentMethod,
fiatCurrency : _params.fiatCurrency,
conversionRate : _params.conversionRate,
payeeId : depData.payeeDetails,
timestamp : block .timestamp,
referrer : _params.referrer,
referrerFee : _params.referrerFee,
postIntentHook : _params.postIntentHook
});
// Locks funds on escrow
IEscrow (_params.escrow). lockFunds (_params.depositId, intentHash, _params.amount);
}
Off-Chain Payment
Taker sends fiat payment through the specified payment platform (Venmo, PayPal, etc.) to the maker’s payee details. This step happens entirely off-chain. The protocol does not control or monitor the payment itself.
Fulfill Intent
Anyone can submit payment proof to fulfill the intent: Orchestrator.sol (line 184)
function fulfillIntent ( FulfillIntentParams calldata _params )
external
nonReentrant
whenNotPaused
{
Intent memory intent = intents[_params.intentHash];
// Gets verifier from registry
address verifier = paymentVerifierRegistry. getVerifier (intent.paymentMethod);
// Verifies payment proof
IPaymentVerifier.PaymentVerificationResult memory result =
IPaymentVerifier (verifier). verifyPayment (
IPaymentVerifier. VerifyPaymentData ({
intentHash : _params.intentHash,
paymentProof : _params.paymentProof,
data : _params.verificationData
})
);
if ( ! result.success) revert PaymentVerificationFailed ();
// Unlocks and transfers funds
IEscrow (intent.escrow). unlockAndTransferFunds (
intent.depositId,
_params.intentHash,
result.releaseAmount,
address ( this )
);
// Distributes fees and transfers to recipient
_collectFeesTransferFundsAndExecuteAction (
deposit.token,
_params.intentHash,
intent,
result.releaseAmount,
_params.postIntentHookData
);
}
Settlement
Funds are distributed to all parties:
Protocol Fee - Sent to protocol fee recipient
Referrer Fee - Sent to referrer (if specified)
Net Amount - Sent to taker or post-intent hook
Orchestrator.sol (line 475)
function _calculateAndTransferFees (
IERC20 _token ,
Intent memory _intent ,
uint256 _releaseAmount
) internal returns ( uint256 netFees ) {
// Protocol fee (1% default)
if (protocolFeeRecipient != address ( 0 ) && protocolFee > 0 ) {
protocolFeeAmount = (_releaseAmount * protocolFee) / PRECISE_UNIT;
_token. safeTransfer (protocolFeeRecipient, protocolFeeAmount);
}
// Referrer fee (up to 50%)
if (_intent.referrer != address ( 0 ) && _intent.referrerFee > 0 ) {
referrerFeeAmount = (_releaseAmount * _intent.referrerFee) / PRECISE_UNIT;
_token. safeTransfer (_intent.referrer, referrerFeeAmount);
}
netFees = protocolFeeAmount + referrerFeeAmount;
}
Intent Gating
Orchestrator supports optional signature-based gating:
Orchestrator.sol (line 428)
address intentGatingService = IEscrow (_intent.escrow). getDepositGatingService (
_intent.depositId, _intent.paymentMethod
);
if (intentGatingService != address ( 0 )) {
if ( block .timestamp > _intent.signatureExpiration) {
revert SignatureExpired (_intent.signatureExpiration, block .timestamp);
}
if ( ! _isValidIntentGatingSignature (_intent, intentGatingService)) {
revert InvalidSignature ();
}
}
Gating allows makers to restrict who can take their liquidity, enabling compliance, KYC requirements, or whitelist-based access.
Unified Payment Verifier
The Unified Payment Verifier consolidates verification logic for all payment methods into a single contract.
Architecture Benefits
Reduced Complexity One contract instead of 8+ separate verifiers
Consistent Interface Standardized verification across all payment methods
Easy Configuration Per-method settings without deploying new contracts
Lower Gas Costs Shared logic and optimized verification flow
Verification Flow
UnifiedPaymentVerifier.sol
function verifyPayment ( VerifyPaymentData calldata _data )
external
returns ( PaymentVerificationResult memory )
{
// 1. Decode payment attestation (EIP-712 signed)
PaymentAttestation memory attestation = abi . decode (
_data.paymentProof, (PaymentAttestation)
);
// 2. Verify attestation signatures
bool isValid = attestationVerifier. verifyAttestation (
attestation.dataHash,
attestation.signatures
);
if ( ! isValid) revert InvalidAttestation ();
// 3. Decode and validate payment details
PaymentDetails memory payment = abi . decode (
attestation.data, (PaymentDetails)
);
// 4. Verify payment matches intent
IntentSnapshot memory intentSnap = _getIntentSnapshot (
attestation.intentHash
);
if (payment.method != intentSnap.paymentMethod) revert MethodMismatch ();
if (payment.payeeId != intentSnap.payeeDetails) revert PayeeMismatch ();
if (payment.currency != intentSnap.fiatCurrency) revert CurrencyMismatch ();
// 5. Verify timestamp within buffer
uint256 timeDiff = payment.timestamp - intentSnap.signalTimestamp;
if (timeDiff > intentSnap.timestampBuffer) revert TimestampOutOfRange ();
// 6. Nullify payment to prevent double-spend
nullifierRegistry. addNullifier (payment.paymentId);
return PaymentVerificationResult ({
success : true ,
intentHash : attestation.intentHash,
releaseAmount : attestation.releaseAmount
});
}
Critical Security : The nullifier registry prevents payment proofs from being reused. Each payment ID can only be used once across the entire protocol.
Registry System
The registry system provides modular permission management:
Payment Verifier Registry
Maps payment methods to verifier contracts and supported currencies: // Payment method => Verifier address
mapping ( bytes32 => address ) public verifiers;
// Payment method => Currency => Supported
mapping ( bytes32 => mapping ( bytes32 => bool )) public supportedCurrencies;
function addPaymentMethod (
bytes32 _method ,
address _verifier ,
bytes32 [] calldata _currencies
) external onlyOwner {
verifiers[_method] = _verifier;
for ( uint256 i = 0 ; i < _currencies.length; i ++ ) {
supportedCurrencies[_method][_currencies[i]] = true ;
}
}
Supported payment methods:
Venmo (USD)
PayPal (USD, EUR, GBP)
Wise (USD, EUR, GBP, SGD, etc.)
Zelle (USD)
CashApp (USD)
Revolut (USD, EUR, GBP)
MercadoPago (BRL, ARS)
Monzo (GBP)
Whitelists valid escrow implementations: mapping ( address => bool ) public whitelistedEscrows;
bool public acceptAllEscrows; // Emergency flag
function addEscrow ( address _escrow ) external onlyOwner {
whitelistedEscrows[_escrow] = true ;
}
This allows protocol upgrades without migrating all deposits.
Globally tracks used payment proofs: mapping ( bytes32 => bool ) public usedNullifiers;
function addNullifier ( bytes32 _nullifier ) external onlyVerifier {
if (usedNullifiers[_nullifier]) revert AlreadyNullified ();
usedNullifiers[_nullifier] = true ;
}
Prevents double-spending across all deposits and orchestrators.
Post Intent Hook Registry
Manages approved post-fulfillment hooks: mapping ( address => bool ) public whitelistedHooks;
function addHook ( address _hook ) external onlyOwner {
whitelistedHooks[_hook] = true ;
}
Example hooks:
Across Bridge Hook - Automatically bridge USDC to another chain
Swap Hook - Convert USDC to another token
Multi-recipient Hook - Split payment among multiple addresses
Authorizes relayers for gasless transactions: mapping ( address => bool ) public whitelistedRelayers;
function isWhitelistedRelayer ( address _relayer ) external view returns ( bool ) {
return whitelistedRelayers[_relayer];
}
Relayers can signal multiple intents simultaneously for better UX.
Protocol Viewer
A read-only contract for efficient state aggregation:
contract ProtocolViewer {
function getDepositWithIntents ( uint256 _depositId )
external
view
returns (
IEscrow . Deposit memory deposit ,
bytes32 [] memory intentHashes ,
IEscrow . Intent [] memory intents
)
{
deposit = escrow. getDeposit (_depositId);
intentHashes = escrow. getDepositIntentHashes (_depositId);
intents = new IEscrow.Intent[](intentHashes.length);
for ( uint256 i = 0 ; i < intentHashes.length; i ++ ) {
intents[i] = escrow. getDepositIntent (_depositId, intentHashes[i]);
}
}
}
Use Protocol Viewer for frontend queries to reduce RPC calls and improve performance.
Complete User Flow
Here’s the end-to-end flow with all components:
Step-by-Step Breakdown
Maker Deposits Liquidity
Maker calls Escrow.createDeposit() with:
1000 USDC
Accepted payment methods (Venmo, PayPal)
Min/max intent amounts (10-500 USDC)
Supported currencies and rates
Escrow locks funds and assigns a unique depositId.
Taker Signals Intent
Taker calls Orchestrator.signalIntent() with:
Target deposit and amount (100 USDC)
Payment method (Venmo)
Currency (USD) and rate (1:1)
Recipient address
Orchestrator:
Validates all parameters
Generates unique intent hash
Calls Escrow.lockFunds() to reserve liquidity
Stores intent parameters for verification
Taker Sends Fiat Payment
Taker sends $100 via Venmo to maker’s payee details. This is a standard Venmo payment - no special protocol interaction required.
Payment Proof Generation
Taker obtains payment receipt and sends to attestation service:
Attestation service verifies payment via zkTLS
Extracts payment details (amount, currency, payee, timestamp)
Creates EIP-712 typed data structure
Signs with trusted witness keys
Returns attestation to taker
Taker Fulfills Intent
Taker (or relayer) calls Orchestrator.fulfillIntent() with attestation. Orchestrator:
Retrieves intent parameters
Gets verifier from payment registry
Calls UnifiedPaymentVerifier.verifyPayment()
Verifier:
Validates EIP-712 signatures
Checks payment details match intent
Verifies timestamp within buffer
Nullifies payment ID
Returns verification result
Settlement & Distribution
If verification succeeds:
Orchestrator calls Escrow.unlockAndTransfer()
Escrow transfers 100 USDC to Orchestrator
Orchestrator deducts 1% protocol fee (1 USDC)
Orchestrator transfers 99 USDC to taker
Intent is pruned from both contracts
If post-intent hook specified:
Funds go to hook contract instead
Hook executes custom logic (bridge, swap, etc.)
Security Considerations
Reentrancy Protection All state-changing functions use OpenZeppelin’s ReentrancyGuard
Pausable Contracts Emergency pause functionality preserves fund recovery options
Access Control Role-based permissions via Ownable and custom modifiers
Signature Validation EIP-712 typed data and EIP-1271 contract signatures
Nullifier System Global prevention of double-spending payment proofs
Intent Expiration Time-bounded locks prevent indefinite liquidity freezing
Pausable Functionality
Both Escrow and Orchestrator implement careful pause logic:
// PAUSED functions (for safety):
- createDeposit
- addFunds
- removeFunds
- signalIntent
- fulfillIntent
// ALWAYS AVAILABLE (for recovery):
- withdrawDeposit
- cancelIntent
- releaseFundsToPayer
- pruneExpiredIntents
Pausing does NOT prevent users from recovering their funds. Withdrawal and cancellation remain available.
Gas Optimization
The architecture includes several gas optimizations:
solidity : {
version : "0.8.18" ,
settings : {
optimizer : { enabled : true , runs : 200 },
viaIR : true // Enables IR-based optimizer
}
}
struct Deposit {
address depositor; // 20 bytes
address delegate; // 20 bytes
IERC20 token; // 20 bytes
Range intentAmountRange; // 64 bytes
bool acceptingIntents; // 1 byte
// Packed into same slots where possible
}
Functions support array parameters to batch operations: function addPaymentMethods (
bytes32 [] calldata _paymentMethods ,
DepositPaymentMethodData [] calldata _paymentMethodData ,
Currency [][] calldata _currencies
)
View Functions for State Aggregation
Protocol Viewer aggregates multiple reads: function getDepositWithIntents ( uint256 _depositId )
external view
returns (
Deposit memory deposit ,
bytes32 [] memory intentHashes ,
Intent [] memory intents
)
Upgrade Path
The modular architecture supports upgrades without full migration:
Deploy New Components
Deploy updated contracts (e.g., OrchestratorV2, EscrowV2)
Register in System
Add new contracts to respective registries
Gradual Migration
Old deposits remain on old Escrow
New deposits use new Escrow
Both can coexist using registry pattern
Update References
Point Orchestrator to new Escrow via setEscrowRegistry()
The registry pattern allows multiple versions to coexist, enabling gradual migration without disrupting active trades.
Extension Points
The architecture provides several extension points:
Post Intent Hooks
Custom logic executed after intent fulfillment:
interface IPostIntentHook {
function execute (
IOrchestrator . Intent memory intent ,
uint256 amount ,
bytes memory data
) external ;
}
Example: Across Bridge Hook automatically bridges USDC to another chain after fulfillment.
Custom Verifiers
New payment methods via IPaymentVerifier interface:
interface IPaymentVerifier {
function verifyPayment ( VerifyPaymentData calldata data )
external
returns ( PaymentVerificationResult memory );
}
Delegate Management
Deposits can have delegates for automated management:
function setDelegate ( uint256 _depositId , address _delegate ) external ;
Delegates can update rates, add currencies, and manage deposit config.
Next Steps
Contract API Reference Detailed documentation for all contract interfaces
Integration Guide Learn how to integrate ZKP2P into your application
Smart Contracts Review core contract documentation and implementation details
Testing Guide Code examples and testing patterns for integration