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.
Overview
The UnifiedPaymentVerifier contract verifies payment proofs for multiple payment methods (Venmo, PayPal, Wise, etc.) using a unified, configurable architecture. This contract replaces individual payment verifiers with a single contract that can be easily swapped without affecting critical state.
Key Features:
- Supports multiple payment methods with custom configuration
- Uses EIP-712 signature validation for payment attestations
- Validates offchain payment attestations through the AttestationVerifier
- Prevents double-spending via nullifier registry
- Ensures trust anchor integrity for off-chain verification
Contract Location: contracts/unifiedVerifier/UnifiedPaymentVerifier.sol
Architecture
The UnifiedPaymentVerifier inherits from BaseUnifiedPaymentVerifier and implements the IPaymentVerifier interface. It coordinates with several other contracts:
- AttestationVerifier: Validates witness signatures on payment attestations
- NullifierRegistry: Prevents payment reuse (double-spending)
- OrchestratorRegistry: Authorizes orchestrators to call verification
- Orchestrator: Provides intent data for validation
Payment Attestation Structure
Payment attestations are EIP-712 signed messages containing:
struct PaymentAttestation {
bytes32 intentHash; // Binds the payment to the intent on Orchestrator
uint256 releaseAmount; // Final token amount to release on-chain after FX
bytes32 dataHash; // Hash of the additional data to verify integrity
bytes[] signatures; // Array of signatures from witnesses
bytes data; // Data for verification
bytes metadata; // Additional metadata; isn't signed by the witnesses
}
The data field contains:
struct PaymentDetails {
bytes32 method; // Payment method hash (e.g., "venmo", "paypal", "wise")
bytes32 payeeId; // Payment recipient ID (hashed payee details)
uint256 amount; // Payment amount in smallest currency unit (cents)
bytes32 currency; // Payment currency hash (e.g., "USD", "EUR")
uint256 timestamp; // Payment timestamp in UTC in milliseconds
bytes32 paymentId; // Hashed payment identifier from the service
}
struct IntentSnapshot {
bytes32 intentHash;
uint256 amount;
bytes32 paymentMethod;
bytes32 fiatCurrency;
bytes32 payeeDetails;
uint256 conversionRate;
uint256 signalTimestamp;
uint256 timestampBuffer;
}
EIP-712 Signature Validation
The contract uses EIP-712 typed structured data hashing and signing. The domain separator is computed at deployment:
DOMAIN_SEPARATOR = keccak256(
abi.encode(
DOMAIN_TYPEHASH,
keccak256(bytes("UnifiedPaymentVerifier")), // name
keccak256(bytes("1")), // version
block.chainid, // chainId
address(this) // verifyingContract
)
);
The PaymentAttestation type hash is:
bytes32 private constant PAYMENT_ATTESTATION_TYPEHASH = keccak256(
"PaymentAttestation(bytes32 intentHash,uint256 releaseAmount,bytes32 dataHash)"
);
Verification Process
The _verifyAttestation function:
- Constructs the struct hash from attestation fields
- Creates the EIP-712 digest:
keccak256("\x19\x01" || DOMAIN_SEPARATOR || structHash)
- Verifies data integrity by checking
keccak256(attestation.data) == attestation.dataHash
- Calls the AttestationVerifier to validate witness signatures
Source: contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:183-214
Payment Verification Flow
The verifyPayment function executes the following steps:
1. Decode Attestation
PaymentAttestation memory attestation = _decodeAttestation(_verifyPaymentData.paymentProof);
2. Decode Payment Details and Intent Snapshot
(
PaymentDetails memory paymentDetails,
IntentSnapshot memory intentSnapshot
) = _decodeAttestationPayload(attestation.data);
3. Validate Payment Method
require(isPaymentMethod[paymentDetails.method], "UPV: Invalid payment method");
4. Validate Intent Snapshot
Reads the intent from the Orchestrator and validates all fields match:
- Intent hash
- Payee details
- Amount
- Payment method
- Fiat currency
- Conversion rate
- Signal timestamp
- Timestamp buffer (must be ≤ 48 hours)
Source: contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:220-234
5. Verify Attestation Signatures
bool isValid = _verifyAttestation(attestation);
require(isValid, "UPV: Invalid attestation");
6. Nullify Payment
Prevents double-spending by creating a unique nullifier:
bytes32 nullifier = keccak256(abi.encodePacked(paymentMethod, paymentId));
_validateAndAddNullifier(nullifier);
The nullifier combines payment method and payment ID to prevent collisions across different payment systems.
Source: contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:242-245
7. Calculate Release Amount
Caps the release amount to the intent amount:
uint256 releaseAmount = _calculateReleaseAmount(attestation.releaseAmount, intentSnapshot.amount);
8. Emit Payment Details
Emits the PaymentVerified event for off-chain reconciliation:
event PaymentVerified(
bytes32 indexed intentHash,
bytes32 indexed method,
bytes32 indexed currency,
uint256 amount,
uint256 timestamp,
bytes32 paymentId,
bytes32 payeeId
);
Source: contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:53-61
Access Control
The verifyPayment function can only be called by authorized orchestrators:
modifier onlyOrchestrator() {
require(orchestratorRegistry.isOrchestrator(msg.sender), "Only orchestrator can call");
_;
}
This prevents griefing attacks since the verifier has write permissions on the nullifier registry.
Configuration Management
Inherited from BaseUnifiedPaymentVerifier:
Adding Payment Methods
function addPaymentMethod(bytes32 _paymentMethod) external onlyOwner
Adds a new supported payment method. The payment method should be the keccak256 hash of the lowercase method name (e.g., keccak256("venmo")).
Removing Payment Methods
function removePaymentMethod(bytes32 _paymentMethod) external onlyOwner
Updating Attestation Verifier
function setAttestationVerifier(address _newVerifier) external onlyOwner
Allows swapping the attestation verifier implementation.
Source: contracts/unifiedVerifier/BaseUnifiedPaymentVerifier.sol:74-111
Security Considerations
Double-Spend Prevention
The nullifier combines both payment method and payment ID to create a unique identifier:
bytes32 nullifier = keccak256(abi.encodePacked(paymentMethod, paymentId));
This prevents collisions where the same payment ID could exist across different payment methods.
Data Integrity
The contract verifies that the data hash in the attestation matches the actual data:
require(
keccak256(attestation.data) == attestation.dataHash,
"UPV: Data hash mismatch"
);
This ensures witnesses signed the exact payment details being verified.
Intent Validation
All intent fields are validated against the on-chain intent state to prevent attestation reuse or manipulation:
- Prevents attestations from being used for different intents
- Ensures payment details match what was agreed upon
- Validates timestamp buffer is within acceptable range (≤ 48 hours)
Release Amount Capping
The release amount is always capped to the intent amount:
if (releaseAmount > intentAmount) {
return intentAmount;
}
This prevents over-release of tokens even if the attestation specifies a higher amount.
Example Usage
// Called by Orchestrator contract
IPaymentVerifier.VerifyPaymentData memory verifyData = IPaymentVerifier.VerifyPaymentData({
intentHash: intentHash,
paymentProof: encodedAttestation, // ABI-encoded PaymentAttestation
data: "" // Additional data if needed
});
IPaymentVerifier.PaymentVerificationResult memory result =
unifiedPaymentVerifier.verifyPayment(verifyData);
if (result.success) {
// Release result.releaseAmount tokens to the user
}
Events
PaymentVerified
event PaymentVerified(
bytes32 indexed intentHash,
bytes32 indexed method,
bytes32 indexed currency,
uint256 amount,
uint256 timestamp,
bytes32 paymentId,
bytes32 payeeId
);
Emitted when a payment is successfully verified. Used for off-chain reconciliation and monitoring.
PaymentMethodAdded
event PaymentMethodAdded(bytes32 indexed paymentMethod);
PaymentMethodRemoved
event PaymentMethodRemoved(bytes32 indexed paymentMethod);
AttestationVerifierUpdated
event AttestationVerifierUpdated(address indexed oldVerifier, address indexed newVerifier);