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
Clone and install
git clone https://github.com/zkp2p/zkp2p-v2-contracts.git
cd zkp2p-v2-contracts
yarn install
Configure environment
cp .env.default .env
# Edit .env with your API keys (optional for local testing)
Start local node
# Terminal 1: Start Hardhat node
yarn chain
# Terminal 2: Deploy contracts
yarn deploy:localhost
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