Skip to main content
This guide covers testing your ZKP2P integrations and understanding the protocol’s test suite.

Overview

The zkp2p-contracts project includes:
  • Hardhat tests: TypeScript-based integration and unit tests
  • Foundry tests: Solidity-based fuzz and invariant tests
  • Test utilities: Helpers for common testing patterns
  • Mock contracts: Simulated external dependencies

Running Protocol Tests

Hardhat Tests

# Run full test suite
yarn test

# Fast mode (skip slower tests)
yarn test:fast

Foundry Tests

# Run all Foundry tests
yarn test:forge

# Verbose output
yarn test:forge -vvv

# Test specific contract
forge test --match-contract EscrowTest

# Test specific function
forge test --match-test testCreateDeposit

Testing Your Integration

Local Development Setup

1

Clone and install

git clone https://github.com/zkp2p/zkp2p-v2-contracts.git
cd zkp2p-v2-contracts
yarn install
2

Configure environment

cp .env.default .env
# Edit .env with your API keys (optional for local testing)
3

Start local node

# Terminal 1: Start Hardhat node
yarn chain

# Terminal 2: Deploy contracts
yarn deploy:localhost
4

Get contract addresses

# Addresses saved in deployments/localhost/
cat deployments/localhost/Escrow.json | jq .address
cat deployments/localhost/Orchestrator.json | jq .address

Testing Deposit Creation

import { expect } from "chai";
import { ethers } from "hardhat";
import { Escrow, USDCMock } from "../typechain";

describe("Integration: Create Deposit", () => {
  let escrow: Escrow;
  let usdc: USDCMock;
  let maker: SignerWithAddress;

  before(async () => {
    [maker] = await ethers.getSigners();

    // Get deployed contracts
    const escrowAddress = "0x..."; // From deployments/
    escrow = await ethers.getContractAt("Escrow", escrowAddress);

    const usdcAddress = "0x...";
    usdc = await ethers.getContractAt("USDCMock", usdcAddress);

    // Mint test USDC
    await usdc.mint(maker.address, ethers.utils.parseUnits("10000", 6));
  });

  it("should create a deposit with multiple payment methods", async () => {
    // Approve USDC
    const amount = ethers.utils.parseUnits("1000", 6);
    await usdc.connect(maker).approve(escrow.address, amount);

    // Payment method hashes
    const venmo = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("venmo"));
    const paypal = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("paypal"));

    // Currency codes
    const USD = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("USD"));

    // Create deposit
    const tx = await escrow.connect(maker).createDeposit({
      token: usdc.address,
      amount: amount,
      intentAmountRange: {
        min: ethers.utils.parseUnits("10", 6),
        max: ethers.utils.parseUnits("500", 6)
      },
      paymentMethods: [venmo, paypal],
      paymentMethodData: [
        {
          intentGatingService: ethers.constants.AddressZero,
          payeeDetails: ethers.utils.keccak256(
            ethers.utils.toUtf8Bytes("maker-venmo")
          ),
          data: "0x"
        },
        {
          intentGatingService: ethers.constants.AddressZero,
          payeeDetails: ethers.utils.keccak256(
            ethers.utils.toUtf8Bytes("maker-paypal")
          ),
          data: "0x"
        }
      ],
      currencies: [
        [{ code: USD, minConversionRate: ethers.utils.parseEther("1.0") }],
        [{ code: USD, minConversionRate: ethers.utils.parseEther("1.0") }]
      ],
      delegate: ethers.constants.AddressZero,
      intentGuardian: ethers.constants.AddressZero,
      retainOnEmpty: true
    });

    const receipt = await tx.wait();
    const event = receipt.events?.find(e => e.event === "DepositReceived");
    const depositId = event?.args?.depositId;

    // Verify deposit
    const deposit = await escrow.getDeposit(depositId);
    expect(deposit.depositor).to.equal(maker.address);
    expect(deposit.remainingDeposits).to.equal(amount);
    expect(deposit.acceptingIntents).to.be.true;

    // Verify payment methods
    const methods = await escrow.getDepositPaymentMethods(depositId);
    expect(methods).to.have.lengthOf(2);
    expect(methods[0]).to.equal(venmo);
    expect(methods[1]).to.equal(paypal);

    console.log("✓ Deposit created successfully, ID:", depositId.toString());
  });
});

Testing Intent Flow

import { expect } from "chai";
import { ethers } from "hardhat";
import { Orchestrator, Escrow, PaymentVerifierMock } from "../typechain";

describe("Integration: Intent Lifecycle", () => {
  let orchestrator: Orchestrator;
  let escrow: Escrow;
  let verifier: PaymentVerifierMock;
  let maker: SignerWithAddress;
  let taker: SignerWithAddress;
  let depositId: number;
  let intentHash: string;

  before(async () => {
    [maker, taker] = await ethers.getSigners();
    
    // Setup contracts and create deposit
    // ... (similar to previous example)
  });

  it("should signal an intent", async () => {
    const venmo = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("venmo"));
    const USD = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("USD"));

    const tx = await orchestrator.connect(taker).signalIntent({
      escrow: escrow.address,
      depositId: depositId,
      amount: ethers.utils.parseUnits("50", 6),
      to: taker.address,
      paymentMethod: venmo,
      fiatCurrency: USD,
      conversionRate: ethers.utils.parseEther("1.0"),
      referrer: ethers.constants.AddressZero,
      referrerFee: 0,
      gatingServiceSignature: "0x",
      signatureExpiration: 0,
      postIntentHook: ethers.constants.AddressZero,
      data: "0x"
    });

    const receipt = await tx.wait();
    const event = receipt.events?.find(e => e.event === "IntentSignaled");
    intentHash = event?.args?.intentHash;

    // Verify intent created
    const intent = await orchestrator.getIntent(intentHash);
    expect(intent.owner).to.equal(taker.address);
    expect(intent.amount).to.equal(ethers.utils.parseUnits("50", 6));

    // Verify liquidity locked
    const deposit = await escrow.getDeposit(depositId);
    expect(deposit.outstandingIntentAmount).to.equal(
      ethers.utils.parseUnits("50", 6)
    );

    console.log("✓ Intent signaled, hash:", intentHash);
  });

  it("should fulfill the intent", async () => {
    // Mock payment proof (in real scenario, this comes from attestation service)
    const mockProof = "0x" + "00".repeat(100);

    // Configure mock verifier to return success
    await verifier.setVerificationResult({
      success: true,
      intentHash: intentHash,
      releaseAmount: ethers.utils.parseUnits("50", 6)
    });

    const balanceBefore = await usdc.balanceOf(taker.address);

    // Fulfill intent
    const tx = await orchestrator.fulfillIntent({
      intentHash: intentHash,
      paymentProof: mockProof,
      verificationData: "0x",
      postIntentHookData: "0x"
    });

    await tx.wait();

    const balanceAfter = await usdc.balanceOf(taker.address);
    const received = balanceAfter.sub(balanceBefore);

    // Should receive 50 USDC minus protocol fees
    expect(received).to.be.closeTo(
      ethers.utils.parseUnits("50", 6),
      ethers.utils.parseUnits("1", 6) // Allow 1 USDC variance for fees
    );

    // Verify intent removed
    const intent = await orchestrator.getIntent(intentHash);
    expect(intent.timestamp).to.equal(0); // Intent deleted

    console.log("✓ Intent fulfilled, received:", ethers.utils.formatUnits(received, 6), "USDC");
  });
});

Testing Custom Hooks

import { expect } from "chai";
import { ethers } from "hardhat";

describe("Custom Hook Integration", () => {
  let hook: MyCustomHook;
  let orchestrator: Orchestrator;
  let usdc: IERC20;

  beforeEach(async () => {
    // Deploy contracts
    const Hook = await ethers.getContractFactory("MyCustomHook");
    hook = await Hook.deploy(orchestrator.address, usdc.address);

    // Register hook
    const registry = await orchestrator.postIntentHookRegistry();
    await registry.addPostIntentHook(hook.address);
  });

  it("should execute hook on fulfillment", async () => {
    // Create deposit and signal intent with hook
    // ... setup code ...

    // Encode hook data
    const hookData = ethers.utils.defaultAbiCoder.encode(
      ["address", "uint256"],
      [destination.address, minAmount]
    );

    // Fulfill with hook
    await orchestrator.fulfillIntent({
      intentHash: intentHash,
      paymentProof: proof,
      verificationData: "0x",
      postIntentHookData: hookData
    });

    // Verify hook execution
    // Check events, balances, state changes
    expect(await hook.executionCount()).to.equal(1);
  });

  it("should handle hook failures gracefully", async () => {
    // Configure hook to fail
    await hook.setShouldFail(true);

    // Fulfillment should still succeed with fallback
    await expect(
      orchestrator.fulfillIntent(params)
    ).to.not.be.reverted;

    // Verify funds went to fallback recipient
    // ...
  });
});

Test Utilities

The protocol provides helpful test utilities:

Intent Hash Calculation

import { calculateIntentHash } from "@utils/protocolUtils";

const intentHash = calculateIntentHash(
  orchestrator.address,
  intentCounter // Current intent counter value
);

Mock Signatures

import { generateGatingServiceSignature } from "@utils/test/helpers";

const signature = await generateGatingServiceSignature(
  gatingServiceSigner,
  orchestrator.address,
  escrow.address,
  depositId,
  amount,
  recipient,
  paymentMethod,
  currency,
  conversionRate,
  chainId,
  expiration
);

Signal Intent Helper

import { createSignalIntentParams } from "@utils/test/helpers";

const params = await createSignalIntentParams(
  orchestrator.address,
  escrow.address,
  depositId,
  amount,
  recipient,
  paymentMethod,
  currency,
  conversionRate,
  referrer,
  referrerFee,
  gatingService,
  chainId,
  hook,
  hookData,
  signatureExpiration
);

Common Test Patterns

Time Manipulation

import { ethers } from "hardhat";

// Advance time by 1 day
await ethers.provider.send("evm_increaseTime", [86400]);
await ethers.provider.send("evm_mine", []);

// Get current timestamp
const block = await ethers.provider.getBlock("latest");
const timestamp = block.timestamp;

Event Testing

import { expect } from "chai";

// Expect specific event
await expect(tx)
  .to.emit(contract, "EventName")
  .withArgs(arg1, arg2, arg3);

// Multiple events
await expect(tx)
  .to.emit(contract, "Event1")
  .and.to.emit(contract, "Event2");

// Extract event data
const receipt = await tx.wait();
const event = receipt.events?.find(e => e.event === "EventName");
const value = event?.args?.paramName;

Revert Testing

// Expect revert with custom error
await expect(
  contract.functionCall()
).to.be.revertedWithCustomError(contract, "CustomError");

// With error arguments
await expect(
  contract.functionCall()
).to.be.revertedWithCustomError(contract, "CustomError")
  .withArgs(expectedArg1, expectedArg2);

// Generic revert
await expect(contract.functionCall()).to.be.reverted;

Continuous Integration

Tests run automatically on GitHub Actions:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  hardhat-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: yarn install
      - run: yarn test
      - run: yarn coverage

  foundry-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: foundry-rs/foundry-toolchain@v1
      - run: forge test

Coverage Reports

View coverage at codecov.io:
  • Overall coverage: >95%
  • Escrow.sol: 100%
  • Orchestrator.sol: 100%
  • UnifiedPaymentVerifier.sol: 100%

Best Practices

Test Reverts

Always test failure cases and revert conditions, not just happy paths.

Use Beforehooks

Setup common state in beforeEach to keep tests isolated and maintainable.

Check Events

Verify events are emitted with correct parameters for all state changes.

Fuzz Important Functions

Use Foundry fuzz tests for functions with complex input validation.

Resources

Hardhat Docs

Complete Hardhat documentation and guides

Foundry Book

Comprehensive Foundry testing guide

Chai Matchers

Waffle/Chai assertion matchers for Ethereum

Test Repository

Browse the full test suite on GitHub

Build docs developers (and LLMs) love