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
SignatureGatingPreIntentHook is a pre-intent hook that validates taker eligibility using off-chain signatures. It enables depositors to control who can signal intents against their deposits without maintaining on-chain whitelists.
Contract Location: contracts/hooks/SignatureGatingPreIntentHook.sol
Interface: IPreIntentHook
Compatible With: OrchestratorV2 (pre-intent hooks only supported in V2)
Use Cases
Dynamic Access Control
Allow takers based on off-chain criteria:
- KYC/AML compliance checks
- Credit scoring
- Rate limiting
- Time-based access
- Custom business logic
Selective Liquidity Provision
Depositors can:
- Gate intents to verified users only
- Implement per-taker limits
- Control access without gas costs for whitelist updates
- Revoke access instantly by not signing new requests
Privacy-Preserving Gating
Off-chain signatures enable:
- Private eligibility criteria (not visible on-chain)
- Dynamic policy changes without transactions
- Reduced on-chain footprint
Architecture
Two-Phase Authorization
- Setup Phase: Depositor sets authorized signer for their deposit
- Signal Phase: Taker obtains signature from signer and includes it in
signalIntent
Signature Payload
The signature commits to:
- Orchestrator address (prevents cross-orchestrator replay)
- Escrow and deposit ID (binds to specific deposit)
- Intent amount (prevents amount manipulation)
- Taker address (binds signature to specific user)
- Recipient address (prevents redirection)
- Payment method and fiat currency
- Conversion rate (prevents rate manipulation)
- Referrer and fees
- Signature expiration (time-bound validity)
- Chain ID (prevents cross-chain replay)
Data Structures
PreIntentHookData
Passed in SignalIntentParams.preIntentHookData:
bytes memory preIntentHookData = abi.encode(
signature, // bytes - EIP-191 signature from authorized signer
signatureExpiration // uint256 - Unix timestamp when signature expires
);
Fields:
signature: EIP-191 signed message hash from the authorized signer
signatureExpiration: Timestamp after which signature is invalid
Storage
Deposit Signer Mapping
// escrow => depositId => authorized signer
mapping(address => mapping(uint256 => address)) public depositSigner;
Each deposit can have one authorized signer. Set to address(0) to disable gating.
Functions
setDepositSigner
Sets or clears the authorized signer for a deposit.
function setDepositSigner(
address _escrow,
uint256 _depositId,
address _signer
) external;
Parameters:
_escrow: Escrow contract address
_depositId: Deposit ID
_signer: Authorized signer address (use address(0) to remove)
Authorization: Only callable by deposit owner or delegate
Events:
event DepositSignerSet(
address indexed escrow,
uint256 indexed depositId,
address indexed signer,
address setter
);
Example:
// Set signer
signatureGatingHook.setDepositSigner(
escrowAddress,
depositId,
signerAddress
);
// Remove signer (disable gating)
signatureGatingHook.setDepositSigner(
escrowAddress,
depositId,
address(0)
);
getDepositSigner
Returns the authorized signer for a deposit.
function getDepositSigner(
address _escrow,
uint256 _depositId
) external view returns (address);
validateSignalIntent
Validates the signature when taker signals intent (called by orchestrator).
function validateSignalIntent(
PreIntentContext calldata _ctx
) external view override;
Validation Steps:
- Verify caller is authorized orchestrator
- Retrieve authorized signer for deposit
- Decode signature and expiration from
_ctx.preIntentHookData
- Verify signature has not expired
- Reconstruct signed message from context
- Verify signature is from authorized signer
Reverts:
UnauthorizedOrchestratorCaller: Caller is not an authorized orchestrator
SignerNotSet: No signer configured for this deposit
SignatureExpired: Signature timestamp has passed
InvalidSignature: Signature verification failed
Usage
1. Deploy Hook:
SignatureGatingPreIntentHook hook = new SignatureGatingPreIntentHook(
orchestratorRegistryAddress,
block.chainid
);
2. Set Signer for Deposit:
// As deposit owner
hook.setDepositSigner(escrowAddress, depositId, signerAddress);
3. Configure Deposit to Use Hook:
orchestratorV2.setDepositPreIntentHook(
escrowAddress,
depositId,
IPreIntentHook(address(hook))
);
Off-Chain: Generate Signature
The authorized signer generates signatures off-chain:
TypeScript/JavaScript Example:
import { ethers } from 'ethers';
// Signer's wallet
const signer = new ethers.Wallet(privateKey);
// Prepare message payload (matches contract's message construction)
const message = ethers.utils.solidityPack(
[
'address', // orchestrator
'address', // escrow
'uint256', // depositId
'uint256', // amount
'address', // taker
'address', // to
'bytes32', // paymentMethod
'bytes32', // fiatCurrency
'uint256', // conversionRate
'address', // referrer
'uint256', // referrerFee
'uint256', // signatureExpiration
'uint256' // chainId
],
[
orchestratorAddress,
escrowAddress,
depositId,
amount,
takerAddress,
recipientAddress,
paymentMethod,
fiatCurrency,
conversionRate,
referrerAddress,
referrerFee,
signatureExpiration, // e.g., Math.floor(Date.now() / 1000) + 3600 (1 hour)
chainId
]
);
// Sign with EIP-191 prefix
const messageHash = ethers.utils.keccak256(message);
const signature = await signer.signMessage(ethers.utils.arrayify(messageHash));
console.log('Signature:', signature);
console.log('Expiration:', signatureExpiration);
Python Example:
from eth_account import Account
from eth_account.messages import encode_defunct
from web3 import Web3
import time
# Signer's private key
account = Account.from_key(private_key)
# Prepare message payload
message = Web3.solidity_keccak(
[
'address', 'address', 'uint256', 'uint256', 'address', 'address',
'bytes32', 'bytes32', 'uint256', 'address', 'uint256', 'uint256', 'uint256'
],
[
orchestrator_address,
escrow_address,
deposit_id,
amount,
taker_address,
recipient_address,
payment_method,
fiat_currency,
conversion_rate,
referrer_address,
referrer_fee,
int(time.time()) + 3600, # expiration: 1 hour from now
chain_id
]
)
# Sign with EIP-191
signable_message = encode_defunct(message)
signed = account.sign_message(signable_message)
print(f"Signature: {signed.signature.hex()}")
print(f"Expiration: {expiration}")
On-Chain: Signal Intent with Signature
Taker calls signalIntent with signature:
// Encode hook data
bytes memory preIntentHookData = abi.encode(
signature, // From off-chain signer
signatureExpiration // From off-chain signer
);
// Signal intent
SignalIntentParams memory params = SignalIntentParams({
escrow: escrowAddress,
depositId: depositId,
amount: amount,
to: recipientAddress,
paymentMethod: paymentMethod,
fiatCurrency: fiatCurrency,
conversionRate: conversionRate,
payeeId: payeeId,
referrer: referrerAddress,
referrerFee: referrerFee,
postIntentHook: address(0), // No post-intent hook in this example
signalHookData: "", // No post-intent hook data
preIntentHookData: preIntentHookData // Signature gating data
});
orchestratorV2.signalIntent(params);
Execution Flow
Events
DepositSignerSet
Emitted when signer is set or updated:
event DepositSignerSet(
address indexed escrow,
uint256 indexed depositId,
address indexed signer,
address setter
);
Parameters:
escrow: Escrow contract address
depositId: Deposit ID
signer: New signer address (or address(0) if removed)
setter: Address that called setDepositSigner (owner or delegate)
Errors
error ZeroAddress();
error UnauthorizedCallerOrDelegate(address caller, address owner, address delegate);
error UnauthorizedOrchestratorCaller(address caller);
error SignerNotSet(address escrow, uint256 depositId);
error SignatureExpired(uint256 expiration, uint256 currentTime);
error InvalidSignature();
Security Considerations
Signature Expiration
Always set reasonable expiration times:
- Too short: Taker may not have time to submit transaction
- Too long: Increases replay window if conditions change
- Recommended: 5-60 minutes depending on use case
Example:
const expiration = Math.floor(Date.now() / 1000) + 1800; // 30 minutes
Replay Protection
Signature commits to:
- Chain ID: Prevents cross-chain replay
- Orchestrator address: Prevents cross-orchestrator replay
- Deposit ID: Binds to specific deposit
- Taker address: Binds to specific user
- All intent parameters: Prevents parameter manipulation
Signer Key Security
Signer private key should:
- Be stored securely (HSM, secure enclave, or encrypted storage)
- Not be reused for other purposes
- Have revocation mechanism (change signer via
setDepositSigner)
- Be rotated periodically
For production:
// Rotate signer
hook.setDepositSigner(escrowAddress, depositId, newSignerAddress);
Signature Verification
The hook uses OpenZeppelin’s SignatureChecker which supports:
- EOA signatures (ECDSA)
- Smart contract signatures (EIP-1271)
This allows both externally owned accounts and smart contracts to act as signers.
DoS Considerations
Preventing DoS:
- Signatures expire automatically (no need to revoke)
- Signer can be changed instantly by depositor
- No on-chain storage per signature (gas-efficient)
Attack mitigation:
- Rate limit signature generation off-chain
- Monitor for signature request abuse
- Implement IP-based or account-based throttling in signer service
Implementation Examples
Example 1: KYC Gating
Setup:
// Deploy hook
SignatureGatingPreIntentHook hook = new SignatureGatingPreIntentHook(
orchestratorRegistry,
1 // Ethereum mainnet
);
// Set KYC service as signer
hook.setDepositSigner(escrowAddress, depositId, kycServiceAddress);
orchestratorV2.setDepositPreIntentHook(escrowAddress, depositId, hook);
Off-chain KYC Service:
// API endpoint: POST /api/request-signature
app.post('/api/request-signature', async (req, res) => {
const { taker, depositId, amount, ... } = req.body;
// Verify KYC status
const kycStatus = await checkKYC(taker);
if (!kycStatus.approved) {
return res.status(403).json({ error: 'KYC not approved' });
}
// Generate signature
const expiration = Math.floor(Date.now() / 1000) + 1800; // 30 min
const signature = await generateSignature({ taker, depositId, amount, expiration });
res.json({ signature, expiration });
});
Example 2: Rate Limiting
Off-chain Rate Limiter:
const userLimits = new Map<string, number>();
app.post('/api/request-signature', async (req, res) => {
const { taker, amount } = req.body;
// Check daily limit
const dailyUsage = userLimits.get(taker) || 0;
const DAILY_LIMIT = 10000e6; // 10,000 USDC
if (dailyUsage + amount > DAILY_LIMIT) {
return res.status(429).json({ error: 'Daily limit exceeded' });
}
// Generate signature
const signature = await generateSignature(req.body);
// Update usage
userLimits.set(taker, dailyUsage + amount);
res.json({ signature });
});
Example 3: Time-Based Access
app.post('/api/request-signature', async (req, res) => {
const { taker } = req.body;
// Only allow during business hours (9 AM - 5 PM UTC)
const hour = new Date().getUTCHours();
if (hour < 9 || hour >= 17) {
return res.status(403).json({ error: 'Outside business hours' });
}
const signature = await generateSignature(req.body);
res.json({ signature });
});
Comparison with Whitelist Hook
| Feature | SignatureGatingHook | WhitelistHook |
|---|
| Gas Cost | No gas for eligibility changes | Gas required to add/remove addresses |
| Privacy | Criteria can be off-chain | All addresses visible on-chain |
| Flexibility | Dynamic, per-request validation | Static list |
| Revocation | Automatic via expiration | Requires transaction |
| Scalability | Unlimited users | Limited by gas costs |
| Complexity | Requires off-chain signer service | Pure on-chain |
| Best For | Dynamic criteria, KYC, rate limiting | Small, stable user sets |