Skip to main content

Overview

WhitelistPreIntentHook is a pre-intent hook that restricts intent signaling to a depositor-managed whitelist of taker addresses. It enables “private orderbook” functionality where deposits are visible on-chain but only whitelisted addresses can lock liquidity. Contract Location: contracts/hooks/WhitelistPreIntentHook.sol Interface: IPreIntentHook Compatible With: OrchestratorV2 (pre-intent hooks only supported in V2)

Use Cases

Private Liquidity Pools

Control access to deposit liquidity:
  • Restrict intents to trusted partners
  • Create private OTC markets
  • Enable institutional-only deposits
  • Prevent public order sniping

Relationship-Based Trading

Depositors can:
  • Whitelist only known counterparties
  • Build trust-based trading networks
  • Enforce business relationships on-chain
  • Create invite-only markets

Risk Management

Limit exposure by:
  • Whitelisting only KYC’d addresses
  • Restricting to verified entities
  • Controlling counterparty risk
  • Preventing unknown takers

Architecture

Per-Deposit Whitelists

Each deposit maintains its own independent whitelist:
// escrow => depositId => taker => whitelisted
mapping(address => mapping(uint256 => mapping(address => bool))) public whitelist;
This allows:
  • Different whitelists per deposit
  • Fine-grained access control
  • Independent management by different depositors

Authorization Model

Only the deposit owner or delegate can manage the whitelist for their deposit.

Functions

addToWhitelist

Adds one or more taker addresses to the whitelist.
function addToWhitelist(
    address _escrow,
    uint256 _depositId,
    address[] calldata _takers
) external;
Parameters:
  • _escrow: Escrow contract address
  • _depositId: Deposit ID
  • _takers: Array of taker addresses to whitelist
Authorization: Only deposit owner or delegate Reverts:
  • ZeroAddress(): Escrow or taker address is zero
  • EmptyArray(): Takers array is empty
  • UnauthorizedCallerOrDelegate(): Caller is not owner or delegate
Events:
event TakerWhitelisted(
    address indexed escrow,
    uint256 indexed depositId,
    address indexed taker
);
Example:
address[] memory takers = new address[](3);
takers[0] = 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb;
takers[1] = 0x1234567890123456789012345678901234567890;
takers[2] = 0xABCDEF123456789012345678901234567890ABCD;

whitelistHook.addToWhitelist(escrowAddress, depositId, takers);

removeFromWhitelist

Removes one or more taker addresses from the whitelist.
function removeFromWhitelist(
    address _escrow,
    uint256 _depositId,
    address[] calldata _takers
) external;
Parameters:
  • _escrow: Escrow contract address
  • _depositId: Deposit ID
  • _takers: Array of taker addresses to remove
Authorization: Only deposit owner or delegate Reverts:
  • ZeroAddress(): Escrow address is zero
  • EmptyArray(): Takers array is empty
  • UnauthorizedCallerOrDelegate(): Caller is not owner or delegate
  • TakerNotInWhitelist(): Taker was not whitelisted (reverts to prevent mistakes)
Events:
event TakerRemovedFromWhitelist(
    address indexed escrow,
    uint256 indexed depositId,
    address indexed taker
);
Example:
address[] memory takers = new address[](1);
takers[0] = 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb;

whitelistHook.removeFromWhitelist(escrowAddress, depositId, takers);

isWhitelisted

Checks if a taker is whitelisted for a specific deposit.
function isWhitelisted(
    address _escrow,
    uint256 _depositId,
    address _taker
) external view returns (bool);
Returns: true if taker is whitelisted, false otherwise Example:
bool allowed = whitelistHook.isWhitelisted(
    escrowAddress,
    depositId,
    takerAddress
);

if (allowed) {
    // Taker can signal intent
}

validateSignalIntent

Validates taker is whitelisted when intent is signaled (called by orchestrator).
function validateSignalIntent(
    PreIntentContext calldata _ctx
) external view override;
Validation:
  1. Verify caller is authorized orchestrator
  2. Check if _ctx.taker is whitelisted for _ctx.escrow and _ctx.depositId
  3. Revert if not whitelisted
Reverts:
  • UnauthorizedOrchestratorCaller(): Caller is not an authorized orchestrator
  • TakerNotWhitelisted(): Taker is not in the whitelist

Usage

Setup: Deploy and Configure

1. Deploy Hook:
WhitelistPreIntentHook hook = new WhitelistPreIntentHook(
    orchestratorRegistryAddress
);
2. Configure Deposit to Use Hook: Depositor uses the dedicated whitelist hook slot:
orchestratorV2.setDepositWhitelistHook(
    escrowAddress,
    depositId,
    IPreIntentHook(address(hook))
);
OrchestratorV2 has a dedicated whitelist hook slot separate from the generic pre-intent hook slot. This allows using whitelist functionality without occupying the general-purpose hook slot.
3. Add Takers to Whitelist:
address[] memory takers = new address[](2);
takers[0] = trustedTaker1;
takers[1] = trustedTaker2;

hook.addToWhitelist(escrowAddress, depositId, takers);

Signal Intent (Taker)

Whitelisted takers can signal intents normally:
SignalIntentParams memory params = SignalIntentParams({
    escrow: escrowAddress,
    depositId: depositId,
    amount: 1000e6,
    to: recipientAddress,
    paymentMethod: keccak256("Venmo"),
    fiatCurrency: keccak256("USD"),
    conversionRate: 1e18,
    payeeId: keccak256("[email protected]"),
    referrer: address(0),
    referrerFee: 0,
    postIntentHook: address(0),
    signalHookData: "",
    preIntentHookData: ""  // No data needed for whitelist hook
});

orchestratorV2.signalIntent(params);
If taker is not whitelisted, transaction reverts with TakerNotWhitelisted.

Manage Whitelist (Depositor)

Add Takers:
address[] memory newTakers = new address[](1);
newTakers[0] = newTrustedTaker;

hook.addToWhitelist(escrowAddress, depositId, newTakers);
Remove Takers:
address[] memory removedTakers = new address[](1);
removedTakers[0] = untrustedTaker;

hook.removeFromWhitelist(escrowAddress, depositId, removedTakers);
Check Status:
bool isAllowed = hook.isWhitelisted(escrowAddress, depositId, takerAddress);

Execution Flow

Events

TakerWhitelisted

Emitted when a taker is added to whitelist:
event TakerWhitelisted(
    address indexed escrow,
    uint256 indexed depositId,
    address indexed taker
);
Emitted once per taker when addToWhitelist is called with multiple addresses.

TakerRemovedFromWhitelist

Emitted when a taker is removed from whitelist:
event TakerRemovedFromWhitelist(
    address indexed escrow,
    uint256 indexed depositId,
    address indexed taker
);
Emitted once per taker when removeFromWhitelist is called with multiple addresses.

Errors

error ZeroAddress();
error EmptyArray();
error UnauthorizedCallerOrDelegate(address caller, address owner, address delegate);
error UnauthorizedOrchestratorCaller(address caller);
error TakerNotWhitelisted(address taker, address escrow, uint256 depositId);
error TakerNotInWhitelist(address taker, address escrow, uint256 depositId);
Error Distinction:
  • TakerNotWhitelisted: Thrown during validateSignalIntent when taker tries to signal
  • TakerNotInWhitelist: Thrown during removeFromWhitelist when trying to remove non-existent entry

Gas Optimization

Batch Operations

Always use batch functions when adding/removing multiple takers: Efficient:
address[] memory takers = new address[](100);
// ... populate array
hook.addToWhitelist(escrowAddress, depositId, takers);  // Single transaction
Inefficient:
for (uint i = 0; i < 100; i++) {
    address[] memory singleTaker = new address[](1);
    singleTaker[0] = takers[i];
    hook.addToWhitelist(escrowAddress, depositId, singleTaker);  // 100 transactions!
}

Storage Costs

Each whitelist entry costs ~20,000 gas (cold SSTORE). For large whitelists:
  • Adding 100 addresses: ~2M gas
  • Adding 1000 addresses: ~20M gas (may hit block gas limit)
Consider alternative approaches for large whitelists:
  • Use SignatureGatingHook for dynamic/large user sets
  • Implement Merkle tree-based whitelist hook for very large sets

Security Considerations

Authorization

Only deposit owner or delegate can modify whitelist:
function _validateDepositorOrDelegate(address _escrow, uint256 _depositId) internal view {
    IEscrow.Deposit memory deposit = IEscrow(_escrow).getDeposit(_depositId);
    bool isDepositorOrDelegate = msg.sender == deposit.depositor
        || (deposit.delegate != address(0) && msg.sender == deposit.delegate);
    if (!isDepositorOrDelegate) {
        revert UnauthorizedCallerOrDelegate(msg.sender, deposit.depositor, deposit.delegate);
    }
}

Orchestrator Validation

Hook verifies caller is authorized orchestrator:
if (!orchestratorRegistry.isOrchestrator(msg.sender)) {
    revert UnauthorizedOrchestratorCaller(msg.sender);
}

Removal Safety

removeFromWhitelist reverts if taker is not whitelisted. This prevents:
  • Accidental double-removal
  • Typos in removal addresses
  • Unclear whitelist state

Privacy Considerations

Whitelist addresses are public on-chain:
  • Anyone can query isWhitelisted
  • Events reveal all whitelisted addresses
  • Consider SignatureGatingHook for private eligibility criteria

Use Case Examples

Example 1: Private OTC Market

// Deploy hook
WhitelistPreIntentHook otcHook = new WhitelistPreIntentHook(orchestratorRegistry);

// Create deposit with whitelist
escrow.deposit(1000000e6, paymentToken, delegate);
orchestratorV2.setDepositWhitelistHook(escrow, depositId, otcHook);

// Whitelist institutional partners
address[] memory institutions = new address[](5);
institutions[0] = jumpTrading;
institutions[1] = galaxyDigital;
institutions[2] = cumberland;
institutions[3] = wintermute;
institutions[4] = flowTraders;

otcHook.addToWhitelist(escrow, depositId, institutions);

Example 2: KYC-Required Deposits

// Hook for KYC'd users only
WhitelistPreIntentHook kycHook = new WhitelistPreIntentHook(orchestratorRegistry);

// Set hook on deposit
orchestratorV2.setDepositWhitelistHook(escrow, depositId, kycHook);

// As users complete KYC, add them
function onKYCComplete(address user) external onlyKYCProvider {
    address[] memory newUser = new address[](1);
    newUser[0] = user;
    kycHook.addToWhitelist(escrow, depositId, newUser);
}

// If KYC expires or is revoked
function onKYCRevoked(address user) external onlyKYCProvider {
    address[] memory revokedUser = new address[](1);
    revokedUser[0] = user;
    kycHook.removeFromWhitelist(escrow, depositId, revokedUser);
}

Example 3: Invite-Based System

// Depositor can invite specific users
function inviteUser(address invitee) external {
    // Verify caller is depositor
    require(msg.sender == depositor, "Not depositor");
    
    // Add to whitelist
    address[] memory invitees = new address[](1);
    invitees[0] = invitee;
    whitelistHook.addToWhitelist(escrow, depositId, invitees);
    
    // Notify invitee off-chain
    emit UserInvited(depositId, invitee);
}

Dedicated Whitelist Hook Slot

OrchestratorV2 provides two pre-intent hook slots per deposit:
  1. Generic Pre-Intent Hook: depositPreIntentHooks[escrow][depositId]
    • Set via setDepositPreIntentHook()
    • For general-purpose validation
  2. Dedicated Whitelist Hook: depositWhitelistHooks[escrow][depositId]
    • Set via setDepositWhitelistHook()
    • Specifically for whitelist functionality
    • Allows using whitelist + another hook simultaneously
Both hooks are executed during signalIntent if set:
// From OrchestratorV2.sol
_executeHookIfSet(depositWhitelistHooks[_params.escrow][_params.depositId], _params);
_executeHookIfSet(depositPreIntentHooks[_params.escrow][_params.depositId], _params);
Example using both:
// Set whitelist hook
orchestratorV2.setDepositWhitelistHook(escrow, depositId, whitelistHook);

// Also set signature gating hook
orchestratorV2.setDepositPreIntentHook(escrow, depositId, signatureGatingHook);

// Now intents must pass BOTH validations:
// 1. Taker must be whitelisted
// 2. Taker must provide valid signature

Comparison with Signature Gating Hook

FeatureWhitelistHookSignatureGatingHook
StorageOn-chain whitelistOff-chain eligibility
Gas CostHigh (per-address SSTORE)Low (no state changes)
PrivacyAll addresses publicCriteria can be private
ScalabilityLimited by gasUnlimited
RevocationRequires transactionAutomatic (signature expiration)
ComplexitySimple, pure on-chainRequires off-chain signer
Best ForSmall, stable setsDynamic criteria, large sets
ManagementOn-chain transactionsOff-chain API

Build docs developers (and LLMs) love