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:
- Verify caller is authorized orchestrator
- Check if
_ctx.taker is whitelisted for _ctx.escrow and _ctx.depositId
- Revert if not whitelisted
Reverts:
UnauthorizedOrchestratorCaller(): Caller is not an authorized orchestrator
TakerNotWhitelisted(): Taker is not in the whitelist
Usage
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:
-
Generic Pre-Intent Hook:
depositPreIntentHooks[escrow][depositId]
- Set via
setDepositPreIntentHook()
- For general-purpose validation
-
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
| Feature | WhitelistHook | SignatureGatingHook |
|---|
| Storage | On-chain whitelist | Off-chain eligibility |
| Gas Cost | High (per-address SSTORE) | Low (no state changes) |
| Privacy | All addresses public | Criteria can be private |
| Scalability | Limited by gas | Unlimited |
| Revocation | Requires transaction | Automatic (signature expiration) |
| Complexity | Simple, pure on-chain | Requires off-chain signer |
| Best For | Small, stable sets | Dynamic criteria, large sets |
| Management | On-chain transactions | Off-chain API |