Skip to main content
This guide covers common issues you may encounter when integrating with or developing on zkp2p-v2-contracts, along with their solutions.

Deployment Issues

Contract Deployment Fails

Symptoms: Deployment script fails with error or timeout Common Causes:
  1. Missing Environment Variables
    # Check .env file has all required keys
    ALCHEMY_API_KEY=your_key_here
    BASE_DEPLOY_PRIVATE_KEY=your_private_key
    BASESCAN_API_KEY=your_basescan_key
    
  2. Insufficient Gas
    • Solution: Increase gas limit in hardhat.config.ts
    • Check current network gas prices
  3. Network Connection Issues
    • Verify RPC endpoint is accessible
    • Try alternative RPC provider (Alchemy, Infura)
  4. Nonce Mismatch
    # Reset nonce by deleting deployment cache
    rm -rf deployments/{network}/.migrations.json
    

Deployment Scripts Skip Unexpectedly

Symptoms: Some deployment scripts don’t execute Solution: Check the func.skip condition in deployment files. Many scripts are frozen in production:
func.skip = async (hre: HardhatRuntimeEnvironment): Promise<boolean> => {
  const network = hre.network.name;
  if (network === "localhost" || network === "hardhat") {
    return false;
  }
  return true; // Frozen for production networks
};
For production deployments, payment methods are now managed by 15_configure_v2_payment_methods.ts.

Contract Verification Fails

Symptoms: Etherscan/Basescan verification returns error Solutions:
  1. Check API Key:
    # Verify Basescan API key is set
    echo $BASESCAN_API_KEY
    
  2. Wait for Block Confirmations:
    # Wait 5-10 blocks before verifying
    yarn etherscan:base
    
  3. Manual Verification:
    • Use Basescan GUI with flattened contract
    • Ensure compiler version matches hardhat.config.ts

Payment Verification Issues

Payment Proof Verification Fails

Symptoms: fulfillIntent() reverts with verification error Common Errors:

“UPV: Invalid payment method”

Cause: Payment method not registered with UnifiedPaymentVerifier Solution:
// Check if payment method is registered
const isRegistered = await unifiedVerifier.isPaymentMethod(paymentMethodHash);

if (!isRegistered) {
  // Register payment method (owner only)
  await unifiedVerifier.addPaymentMethod(paymentMethodHash);
}
See contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:142

”UPV: Invalid attestation”

Cause: EIP-712 signature verification failed Solutions:
  1. Check Witness Signer:
    // Ensure correct witness signed the attestation
    const expectedWitness = await attestationVerifier.witness();
    
  2. Verify Domain Separator:
    // Domain must match verifier contract
    const domainSeparator = await unifiedVerifier.DOMAIN_SEPARATOR();
    
  3. Check Data Hash Integrity:
    // dataHash must match keccak256(data)
    const computedHash = ethers.utils.keccak256(attestation.data);
    assert(computedHash === attestation.dataHash);
    
See contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:183-214

”UPV: Snapshot hash mismatch”

Cause: Intent snapshot doesn’t match on-chain intent Solution: Ensure all intent fields match:
// Verify intent snapshot
const intent = await orchestrator.getIntent(intentHash);

// All these must match:
assert(snapshot.intentHash === intentHash);
assert(snapshot.payeeDetails === intent.payeeId);
assert(snapshot.amount === intent.amount);
assert(snapshot.paymentMethod === intent.paymentMethod);
assert(snapshot.fiatCurrency === intent.fiatCurrency);
assert(snapshot.conversionRate === intent.conversionRate);
assert(snapshot.signalTimestamp === intent.timestamp);
See contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:220-234

”UPV: Snapshot timestamp buffer exceeds maximum”

Cause: Timestamp buffer exceeds 48-hour maximum Solution:
const MAX_TIMESTAMP_BUFFER_MS = 48 * 60 * 60 * 1000; // 48 hours
assert(snapshot.timestampBuffer <= MAX_TIMESTAMP_BUFFER_MS);
See contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:31

”UPV: Data hash mismatch”

Cause: Signed data hash doesn’t match actual data Solution:
// Recompute data hash
const dataHash = ethers.utils.keccak256(attestation.data);

// Update attestation
attestation.dataHash = dataHash;
See contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:202-205

Nullifier Already Used

Symptoms: Transaction reverts with nullifier error Cause: Payment ID has already been used (double-spend prevention) Explanation: Nullifiers are created as: keccak256(abi.encodePacked(paymentMethod, paymentId)) Each payment can only be used once across the entire protocol. Solution:
  • If legitimate retry: Use a different payment (new payment ID)
  • If testing: Deploy fresh contracts or use different payment IDs
  • In production: This is expected behavior - find the original intent that used this payment
See contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:242-245

Payment Method Not Found in Registry

Symptoms: signalIntent() reverts with payment method error Cause: Payment method not registered in PaymentVerifierRegistry Solution:
// Add payment method to registry (owner only)
await paymentVerifierRegistry.addPaymentMethod(
  paymentMethodHash,
  unifiedVerifierAddress,
  [Currency.USD, Currency.EUR] // Supported currencies
);

Intent Lifecycle Issues

Cannot Signal Intent

Symptoms: signalIntent() reverts Common Causes:

Insufficient Deposit Liquidity

// Check available deposit amount
const deposit = await escrow.getDeposit(depositId);
const available = deposit.availableAmount;

if (intentAmount > available) {
  // Reduce intent amount or choose different deposit
}

Amount Below Minimum

// Check minimum amount for payment method
const minAmount = await escrow.getMinAmount(depositId, paymentMethodHash);

if (intentAmount < minAmount) {
  // Increase intent amount
}

Deposit Expired

// Check deposit expiry
const deposit = await escrow.getDeposit(depositId);
const currentTime = Math.floor(Date.now() / 1000);

if (currentTime > deposit.expiryTimestamp) {
  // Deposit has expired, choose another
}

Invalid Gating Signature

// If orchestrator requires gating signature
const gatingSignature = await generateGatingServiceSignature(
  gatingServiceWallet,
  intentOwner,
  escrowAddress,
  depositId,
  // ... other params
);

await orchestrator.signalIntent({
  // ... intent params
}, gatingSignature); // Include signature

Cannot Fulfill Intent

Symptoms: fulfillIntent() reverts Common Causes:

Intent Expired

// Check intent expiry
const intent = await orchestrator.getIntent(intentHash);
const currentTime = Math.floor(Date.now() / 1000);

if (currentTime > intent.expiryTimestamp) {
  // Intent has expired, must cancel instead
  await orchestrator.cancelIntent(intentHash);
}

Wrong Intent State

// Intent must be in SIGNALED state
const intent = await orchestrator.getIntent(intentHash);
const IntentState = {
  NONE: 0,
  SIGNALED: 1,
  FULFILLED: 2,
  CANCELLED: 3
};

assert(intent.state === IntentState.SIGNALED);

Payment Verification Failed

See Payment Verification Issues section above.

Cannot Cancel Intent

Symptoms: cancelIntent() reverts Cause: Intent not yet expired or already fulfilled Solution:
const intent = await orchestrator.getIntent(intentHash);
const currentTime = Math.floor(Date.now() / 1000);

if (currentTime <= intent.expiryTimestamp) {
  // Wait until expiry before canceling
  const waitTime = intent.expiryTimestamp - currentTime;
  console.log(`Wait ${waitTime} seconds before canceling`);
}

Testing Issues

Tests Fail with Timeout

Symptoms: Tests hang or timeout Solutions:
  1. Increase Timeout:
    // In test file
    this.timeout(120000); // 2 minutes
    
  2. Use Fast Tests:
    yarn test:fast  # Skips slow integration tests
    
  3. Run Specific Suite:
    yarn test test/unifiedVerifier/unifiedPaymentVerifier.spec.ts
    

Mock Contract Issues

Symptoms: Mock contracts behave unexpectedly Solution: Check mock configuration in test setup
// Example: Configure mock USDC
usdcToken = await deployer.deployUSDCMock(
  ethers.utils.parseUnits("1000000", 6),
  "USDC",
  "USDC"
);

// Transfer to test accounts
await usdcToken.transfer(maker.address, depositAmount);
await usdcToken.connect(maker.wallet).approve(escrow.address, depositAmount);

Foundry Tests Fail

Symptoms: yarn test:forge fails Solutions:
  1. Update Foundry:
    foundryup
    
  2. Clean Cache:
    forge clean
    forge build
    
  3. Check Remappings:
    # Verify remappings in foundry.toml
    cat foundry.toml
    

Integration Issues

TypeScript Type Errors

Symptoms: TypeScript compilation errors with contract types Solution: Regenerate Typechain bindings
# Full rebuild
yarn build

# Or just regenerate types
yarn compile
yarn typechain
Types are in typechain/ directory.

Contract Address Not Found

Symptoms: Cannot find deployed contract address Solution: Check deployment artifacts
import { getDeployedContractAddress } from "./deployments/helpers";

const orchestratorAddress = getDeployedContractAddress(
  network,
  "Orchestrator"
);
Deployment artifacts are in deployments/{network}/.

Event Parsing Fails

Symptoms: Cannot parse contract events Solution: Use correct event filter and parsing
// Listen for IntentSignaled event
const filter = orchestrator.filters.IntentSignaled();
const events = await orchestrator.queryFilter(filter, fromBlock, toBlock);

events.forEach(event => {
  const { intentHash, escrow, depositId } = event.args;
  console.log(`Intent ${intentHash} signaled`);
});

Gas Estimation Fails

Symptoms: Transaction reverts during gas estimation Solutions:
  1. Simulate Transaction:
    // Use callStatic to simulate
    try {
      await orchestrator.callStatic.fulfillIntent({
        intentHash,
        paymentProof,
        data
      });
    } catch (error) {
      console.error("Simulation failed:", error);
    }
    
  2. Manual Gas Limit:
    await orchestrator.fulfillIntent(
      { intentHash, paymentProof, data },
      { gasLimit: 500000 }
    );
    

Registry Permission Issues

”Caller is not owner”

Symptoms: Registry modification reverts with ownership error Cause: Only owner can modify registries Solution:
// Check current owner
const owner = await paymentVerifierRegistry.owner();

// Transfer ownership if needed (current owner only)
await paymentVerifierRegistry.transferOwnership(newOwner);

“Caller does not have write permission”

Symptoms: Cannot write to NullifierRegistry Cause: UnifiedPaymentVerifier needs write permission Solution:
// Grant write permission (owner only)
await nullifierRegistry.addWritePermission(unifiedVerifierAddress);
See deployment script 01_deploy_unified_verifier.ts for setup example.

Network-Specific Issues

Base Network Issues

High Gas Prices

Solution: Wait for lower gas prices or increase gas budget
// Check current gas price
const gasPrice = await ethers.provider.getGasPrice();
console.log(`Current gas price: ${ethers.utils.formatUnits(gasPrice, "gwei")} gwei`);

RPC Rate Limiting

Solution: Use dedicated RPC provider or implement retry logic
// Exponential backoff retry
const retry = async (fn, retries = 3) => {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === retries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
    }
  }
};

Base Sepolia (Testnet) Issues

Insufficient Test ETH

Solution: Get test ETH from Base Sepolia faucet

Test USDC Not Available

Solution: Deploy mock USDC for testing
const usdcMock = await deployer.deployUSDCMock(
  ethers.utils.parseUnits("1000000", 6),
  "USDC",
  "USDC"
);

Build & Compilation Issues

Compilation Fails

Symptoms: yarn compile fails with Solidity errors Solutions:
  1. Clean Build:
    yarn clean
    yarn compile
    
  2. Check Solidity Version:
    • Contracts require Solidity ^0.8.18
    • Verify hardhat.config.ts compiler version
  3. Missing Dependencies:
    # Reinstall dependencies
    rm -rf node_modules
    yarn install
    

Module Not Found

Symptoms: Import errors in TypeScript files Solution: Check module aliases in tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@utils/*": ["./utils/*"],
      "@typechain/*": ["./typechain/*"]
    }
  }
}

Performance Issues

Slow RPC Responses

Solutions:
  1. Use Protocol Viewer: Batch queries instead of individual calls
    // Instead of multiple calls
    const deposit1 = await escrow.getDeposit(id1);
    const deposit2 = await escrow.getDeposit(id2);
    
    // Use Protocol Viewer batching
    const deposits = await protocolViewer.getDeposits(
      escrowAddress,
      [id1, id2]
    );
    
  2. Cache Results: Store frequently accessed data locally
  3. Use Multicall: Batch multiple read operations

High Transaction Costs

Solutions:
  1. Optimize Data: Minimize data in transactions
  2. Use Relayers: For gasless transactions (configure RelayerRegistry)
  3. Batch Operations: Combine multiple actions when possible

Getting More Help

If you’re still experiencing issues:
  1. Check Test Files: See test/ for working examples
  2. Review Deployment Scripts: See deploy/ for configuration examples
  3. Read Contract Comments: Contracts have extensive NatSpec documentation
  4. Check GitHub Issues: zkp2p-v2-contracts issues
  5. Ask the Community: Visit zkp2p.xyz for community links

Useful References

Build docs developers (and LLMs) love