Create a Custom Policy
Policies are smart contracts that enforce rules at reveal time in the Ghost Protocol commit/reveal flow. When a user commits tokens with a policy attached, the policy contract is invoked during the reveal to validate that the reveal meets the policy's constraints — without breaking the zero-knowledge privacy guarantee for compliant users.
How Policies Work
- During commit, the user specifies a
policyId(the policy contract address) andpolicyParams(ABI-encoded parameters). These are hashed into the commitment. - During reveal, the zero-knowledge proof demonstrates that the commitment included a specific
policyIdandpolicyParamsHash. TheCommitRevealVaultthen calls the policy contract'svalidateReveal()function. - The policy contract performs pure validation — it checks conditions and either allows or reverts the reveal. Policies cannot write state.
The IRevealPolicy Interface
Every policy contract must implement the IRevealPolicy interface:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IRevealPolicy {
/// @notice Validates whether a reveal operation is permitted.
/// @param revealer The address submitting the reveal transaction.
/// @param recipient The address that will receive the revealed tokens.
/// @param token The token contract address being revealed.
/// @param amount The amount of tokens being revealed.
/// @param policyParams ABI-encoded parameters specific to this policy instance.
/// @return valid True if the reveal is permitted, false otherwise.
function validateReveal(
address revealer,
address recipient,
address token,
uint256 amount,
bytes calldata policyParams
) external view returns (bool valid);
}
Constraints
- View-only:
validateRevealmust be aviewfunction. It cannot modify state. TheCommitRevealVaultcalls it withstaticcall. - Gas limit: Policy validation is subject to a 100,000 gas limit. If your policy exceeds this, the reveal reverts. Keep logic simple and avoid loops over unbounded data.
- Deterministic: The function must return the same result for the same inputs. Do not rely on block-dependent values like
block.timestampfor anything other than time-based policies where temporal variance is expected.
Step 1: Implement the Policy
Here is a complete example of a policy that restricts reveals to a maximum amount per transaction:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./IRevealPolicy.sol";
/// @title MaxAmountPolicy
/// @notice Restricts reveals to a maximum token amount per transaction.
contract MaxAmountPolicy is IRevealPolicy {
/// @notice Validates that the reveal amount does not exceed the maximum.
/// @param policyParams ABI-encoded (uint256 maxAmount).
function validateReveal(
address /* revealer */,
address /* recipient */,
address /* token */,
uint256 amount,
bytes calldata policyParams
) external pure returns (bool valid) {
uint256 maxAmount = abi.decode(policyParams, (uint256));
return amount <= maxAmount;
}
}
Encoding policyParams
Policy parameters are encoded using Solidity's abi.encode. The encoding must match what the policy contract expects to decode:
import { ethers } from "ethers";
// For MaxAmountPolicy: encode a single uint256
const maxAmount = ethers.parseEther("1000"); // 1000 tokens max
const policyParams = ethers.AbiCoder.defaultAbiCoder().encode(
["uint256"],
[maxAmount]
);
For more complex policies with multiple parameters:
// For a policy that takes (address allowedRecipient, uint256 minAmount, uint256 maxAmount)
const policyParams = ethers.AbiCoder.defaultAbiCoder().encode(
["address", "uint256", "uint256"],
["0xRecipientAddress", ethers.parseEther("10"), ethers.parseEther("1000")]
);
The policyParamsHash stored in the commitment is computed as:
const policyParamsHash = ethers.keccak256(policyParams);
This hash is included in the Poseidon commitment, binding the policy parameters to the commitment without revealing them until reveal time.
Step 2: Deploy to Specter
Deploy your policy contract to the Specter chain (Chain ID 5446):
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://testnet.specterchain.com");
const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const MaxAmountPolicy = new ethers.ContractFactory(abi, bytecode, signer);
const policy = await MaxAmountPolicy.deploy();
await policy.waitForDeployment();
const policyAddress = await policy.getAddress();
console.log("Policy deployed at:", policyAddress);
Step 3: Register in PolicyRegistry (Optional)
The PolicyRegistry (0x2DC1641d5A32D6788264690D42710edC843Cb1db) is an on-chain directory of verified policy contracts. Registration is optional but recommended for discoverability and trust:
const registryAbi = [
"function registerPolicy(address policy, string name, string description) external"
];
const registry = new ethers.Contract(
"0x2DC1641d5A32D6788264690D42710edC843Cb1db",
registryAbi,
signer
);
const tx = await registry.registerPolicy(
policyAddress,
"MaxAmountPolicy",
"Restricts reveals to a maximum token amount per transaction."
);
await tx.wait();
Registered policies can be queried by anyone:
const queryAbi = [
"function getPolicy(address policy) view returns (string name, string description, address deployer, bool active)"
];
const registryReader = new ethers.Contract(
"0x2DC1641d5A32D6788264690D42710edC843Cb1db",
queryAbi,
provider
);
const info = await registryReader.getPolicy(policyAddress);
console.log(info.name, info.description);
Step 4: Use the Policy with commitWithPolicy
When committing tokens, specify the policy contract and its parameters:
const vaultAbi = [
"function commitWithPolicy(bytes32 commitment, address token, uint256 amount, address policyId, bytes policyParams) external"
];
const vault = new ethers.Contract(
"0x908aA11Dc9F2e2C3F69892acaDE112e831c0a14a",
vaultAbi,
signer
);
// Encode the policy params
const policyParams = ethers.AbiCoder.defaultAbiCoder().encode(
["uint256"],
[ethers.parseEther("1000")]
);
// The commitment must include the policyId and policyParamsHash
// (computed using Poseidon7 with all fields including policyId and keccak256(policyParams))
const commitment = computeCommitment({
secret,
nullifierSecret,
blinding,
tokenIdHash,
amount,
policyId: policyAddress,
policyParamsHash: ethers.keccak256(policyParams),
});
const tx = await vault.commitWithPolicy(
commitment,
tokenAddress,
amount,
policyAddress,
policyParams
);
await tx.wait();
At reveal time, the CommitRevealVault automatically calls validateReveal() on the specified policy contract. If the policy returns false or reverts, the entire reveal transaction reverts.
Built-In Policies
Specter ships with three built-in policies:
| Policy | Address | Description |
|---|---|---|
| TimelockExpiry | 0xd84D534E94f1eacE9BC5e9Bd90338d574d02B95c | Enforces a minimum time delay and optional expiry on reveals. policyParams: (uint256 unlockTime, uint256 expiryTime). |
| DestinationRestriction | 0x584F2c7F6da6f25a7bF6A1F3D7F422683Ac52Ef1 | Restricts reveals to a specific recipient address. policyParams: (address allowedRecipient). |
| ThresholdWitness | 0x5814e4755C0D98218ddb752D26dD03feba428c80 | Requires M-of-N signatures from designated witnesses. policyParams: (uint256 threshold, address[] witnesses). |
Complete Example: Time-Locked Commit
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./IRevealPolicy.sol";
/// @title WeekdayOnlyPolicy
/// @notice Only allows reveals on weekdays (Monday through Friday, UTC).
contract WeekdayOnlyPolicy is IRevealPolicy {
function validateReveal(
address,
address,
address,
uint256,
bytes calldata
) external view returns (bool valid) {
// day: 0 = Sunday, 1 = Monday, ..., 6 = Saturday
uint256 day = (block.timestamp / 1 days + 4) % 7;
return day >= 1 && day <= 5;
}
}
Security Considerations
- Immutability: Once a commitment includes a
policyId, it cannot be changed. Choose your policy carefully before committing. - Gas awareness: The 100,000 gas limit is strictly enforced. Test your policy's gas consumption with
estimateGasbefore deployment. - No external calls: Avoid making external calls from within
validateReveal. Reentrancy is not possible (it is astaticcall), but external calls consume gas and may fail unpredictably. - Parameter validation: Always validate that
policyParamsdecodes correctly. A malformed encoding will causeabi.decodeto revert, which reverts the reveal.