Skip to main content

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

  1. Setup Phase: Depositor sets authorized signer for their deposit
  2. 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:
  1. Verify caller is authorized orchestrator
  2. Retrieve authorized signer for deposit
  3. Decode signature and expiration from _ctx.preIntentHookData
  4. Verify signature has not expired
  5. Reconstruct signed message from context
  6. 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

Setup: Deploy and Configure

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

FeatureSignatureGatingHookWhitelistHook
Gas CostNo gas for eligibility changesGas required to add/remove addresses
PrivacyCriteria can be off-chainAll addresses visible on-chain
FlexibilityDynamic, per-request validationStatic list
RevocationAutomatic via expirationRequires transaction
ScalabilityUnlimited usersLimited by gas costs
ComplexityRequires off-chain signer servicePure on-chain
Best ForDynamic criteria, KYC, rate limitingSmall, stable user sets

Build docs developers (and LLMs) love