Destination Restriction
The DestinationRestriction contract is a reference policy that restricts which addresses can receive revealed data or tokens. It supports two modes: a single allowed recipient, or a Merkle allowlist of multiple permitted recipients.
Contract
contract DestinationRestriction is IRevealPolicy {
function validate(
bytes32, // commitment (unused)
bytes32, // nullifier (unused)
address recipient,
uint256, // amount (unused)
address, // token (unused)
bytes calldata policyParams
) external pure returns (bool valid) {
if (policyParams.length == 32) {
// Single address mode
address allowedRecipient = abi.decode(policyParams, (address));
return recipient == allowedRecipient;
} else {
// Merkle allowlist mode
(bytes32 allowlistRoot, bytes32[] memory merkleProof) =
abi.decode(policyParams, (bytes32, bytes32[]));
bytes32 leaf = keccak256(abi.encodePacked(recipient));
return _verifyMerkleProof(merkleProof, allowlistRoot, leaf);
}
}
}
The contract is stateless and pure -- it does not read any chain state (not even block.timestamp). Validation depends entirely on the submitted parameters and the recipient address from the reveal transaction.
Two Modes
Mode 1: Single Address
When policyParams is exactly 32 bytes, the contract decodes a single address and checks whether the reveal's recipient matches:
policyParams = abi.encode(allowedRecipient)
policyParamsHash = keccak256(abi.encode(allowedRecipient)) % BN254_SCALAR_FIELD
This is the simplest form: the committed data can only be revealed to exactly one address. Any other recipient will cause the validation to return false.
Mode 2: Merkle Allowlist
When policyParams is longer than 32 bytes, the contract decodes a bytes32 allowlistRoot and a bytes32[] merkleProof. The recipient's address is hashed as a Merkle leaf, and the proof is verified against the root:
leaf = keccak256(abi.encodePacked(recipient))
valid = verifyMerkleProof(proof, allowlistRoot, leaf)
This mode allows a set of permitted recipients to be defined at commit time. The Merkle root of the allowlist is embedded in the policyParamsHash, so the set of allowed recipients is fixed when the commitment is created. The actual proof is provided at reveal time.
Parameters
Single Address Mode
| Parameter | Type | Description |
|---|---|---|
allowedRecipient | address | The only address permitted to receive the revealed data/tokens |
Merkle Allowlist Mode
| Parameter | Type | Description |
|---|---|---|
allowlistRoot | bytes32 | Merkle root of the set of allowed recipient addresses |
merkleProof | bytes32[] | Merkle proof that the reveal's recipient is in the allowlist |
In both modes, the policyParamsHash is computed at commit time and bound to the commitment:
// Single address
policyParamsHash = keccak256(abi.encode(allowedRecipient)) % BN254_FIELD
// Merkle allowlist (only the root is in the committed params)
policyParamsHash = keccak256(abi.encode(allowlistRoot)) % BN254_FIELD
Important: For the Merkle allowlist mode, only the allowlistRoot is part of the committed policyParamsHash. The Merkle proof is provided dynamically at reveal time because it varies by recipient. The root is fixed; the proof is variable.
Merkle Proof Verification
The contract includes an internal Merkle proof verifier that follows the standard sorted-pair convention:
function _verifyMerkleProof(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf
) internal pure returns (bool) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
if (computedHash <= proof[i]) {
computedHash = keccak256(abi.encodePacked(computedHash, proof[i]));
} else {
computedHash = keccak256(abi.encodePacked(proof[i], computedHash));
}
}
return computedHash == root;
}
The sorted-pair approach ensures that the proof is deterministic regardless of left/right ordering -- the smaller hash always goes first.
Use Cases
DestinationRestriction applies to any data type in the Specter protocol.
Earmarked Payments
A grant funder commits GHOST tokens designated for a specific recipient (a researcher, a nonprofit). The tokens can only be revealed to that recipient's address. Even if the phantom key is compromised, an attacker cannot redirect the funds to a different address.
Payroll
A company commits monthly salary payments for each employee. Each commitment is bound to the employee's wallet address via DestinationRestriction. The employee can claim their salary at any time, but only to their own address. Combined with TimelockExpiry, the salary can be time-locked until the pay date.
Restricted Grants
A DAO approves a grant for a specific project. The grant tokens are committed with a Merkle allowlist of authorized team member addresses. Any team member can claim, but non-team addresses cannot.
Credential Delivery
A university commits a degree attestation hash. The credential is bound to the graduate's wallet address. Only the graduate can reveal and prove the credential -- no one else can extract or present it. This prevents credential theft even if the commitment is publicly visible.
Private Transfers to Known Recipients
A user wants to send tokens privately but ensure they arrive at the correct destination. By attaching a DestinationRestriction, the sender guarantees that even if the phantom key is intercepted in transit, the tokens can only be revealed by the intended recipient.
Multi-Recipient Distribution
An airdrop organizer commits tokens with a Merkle allowlist of eligible addresses. Each eligible address can claim by providing the Merkle proof of their inclusion. Ineligible addresses cannot claim even if they obtain the phantom key.
Single Address vs. Merkle Allowlist
| Property | Single Address | Merkle Allowlist |
|---|---|---|
| Permitted recipients | Exactly 1 | Any number (bounded by tree depth) |
| policyParams size | 32 bytes | 32 bytes + 32 bytes per proof element |
| Gas cost | Minimal (1 comparison) | Higher (Merkle proof verification) |
| Flexibility | Fixed at commit time | Set defined at commit time, membership proven at reveal time |
| Use case | 1:1 transfers, payroll | Airdrops, team grants, allowlists |