Skip to main content

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:
  1. Constructs the struct hash from attestation fields
  2. Creates the EIP-712 digest: keccak256("\x19\x01" || DOMAIN_SEPARATOR || structHash)
  3. Verifies data integrity by checking keccak256(attestation.data) == attestation.dataHash
  4. 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);

Build docs developers (and LLMs) love