Warning: This code has not been audited. Use at your own risk.
A smart contract system that enforces eligibility rules for token sales conducted via Uniswap's Continuous Clearing Auction (CCA). It validates that bidders have completed KYC, are not sanctioned, and are within their allocation limits.
Permitter is a validation hook for CCA auctions that integrates with:
- Chainlink CCID Identity Registry - Maps wallet addresses to verified identities (CCIDs)
- Chainlink ACE Policy Engine - Enforces sanctions policies
- Merkle-based allowlists - Optional per-sale participant whitelisting
- Simple - Prefer out-of-the-box components and patterns
- Flexible - Easy to configure different policies per sale
- Modular - Stateless policies with state managed in the hook
- Immutable - No upgrade mechanisms; deploy new hooks for fixes
┌─────────────────────────────────────────────────────────────────┐
│ CCA Auction │
│ (Uniswap Contract) │
└─────────────────────┬───────────────────────────────────────────┘
│ calls validate()
▼
┌─────────────────────────────────────────────────────────────────┐
│ Permitter │
│ - Implements IValidationHook │
│ - Manages state: committed amounts per CCID, global total │
│ - Configurable sanctions, allowlist, and limit enforcement │
└─────────────────────┬───────────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────┐ ┌─────────────┐
│ Chainlink │ │ Chainlink │ │ Merkle │
│ Policy │ │ Identity │ │ Allowlist │
│ Engine │ │ Registry │ │ │
└───────────────┘ └─────────────┘ └─────────────┘
The main validation hook implementing Uniswap's IValidationHook interface.
Key Features:
- Token-denominated per-user and global purchase limits
- Optional sanctions checking via Chainlink ACE
- Optional Merkle-based allowlist verification
- Dual tracking by CCID (primary) and address (fallback)
- Pausable by owner
Configuration:
| Parameter | Description |
|---|---|
identityRegistry |
Chainlink CCID registry address |
policyEngine |
Chainlink ACE Policy Engine address |
merkleRoot |
Root hash for allowlist verification |
perUserLimit |
Maximum tokens per CCID (0 = unlimited) |
globalCap |
Maximum tokens for entire sale (0 = unlimited) |
requireSanctionsCheck |
Whether to enforce sanctions |
requireAllowlist |
Whether to require allowlist membership |
Factory contract for gas-efficient deployment using EIP-1167 minimal proxies.
Functions:
createPermitter(config)- Deploy a new PermittercreatePermitterDeterministic(config, salt)- Deploy at predictable addresspredictPermitterAddress(salt)- Predict deployment addressgetPermittersByCreator(address)- Query permitters by creator
When validate() is called by the CCA:
- Verify CCA is authorized and caller is the authorized CCA
- Check paused state
- Verify allowlist membership (if enabled) via Merkle proof
- Resolve bidder address to CCID via Identity Registry
- Check sanctions via Policy Engine (if enabled)
- Verify bid doesn't exceed per-user limit
- Verify bid doesn't exceed global cap
- Record commitment if all checks pass
If any check fails, the transaction reverts with a descriptive error.
forge installforge buildforge test1. Deploy the Factory (one-time)
forge script script/DeployFactory.s.sol --rpc-url $RPC_URL --broadcast2. Create a Permitter
FACTORY=0x... \
IDENTITY_REGISTRY=0x... \
POLICY_ENGINE=0x... \
PER_USER_LIMIT=1000000000000000000000 \
GLOBAL_CAP=100000000000000000000000 \
REQUIRE_SANCTIONS=true \
forge script script/DeployPermitter.s.sol --rpc-url $RPC_URL --broadcastEnvironment Variables:
| Variable | Required | Description |
|---|---|---|
FACTORY |
Yes | PermitterFactory address |
IDENTITY_REGISTRY |
No | Chainlink CCID registry |
POLICY_ENGINE |
No | Chainlink ACE Policy Engine |
MERKLE_ROOT |
No | Allowlist Merkle root |
PER_USER_LIMIT |
No | Per-user limit in wei (0 = unlimited) |
GLOBAL_CAP |
No | Global cap in wei (0 = unlimited) |
REQUIRE_SANCTIONS |
No | Enable sanctions check |
REQUIRE_ALLOWLIST |
No | Enable allowlist |
CCA |
No | CCA address to authorize immediately |
SALT |
No | Salt for deterministic deployment |
3. Authorize the CCA
If not authorized during deployment:
permitter.authorizeCCA(ccaAddress);For frontend integration:
checkEligibility(address)- Fast check if user can participategetRemainingUserCapacity(address)- Tokens remaining for usergetRemainingGlobalCapacity()- Tokens remaining globallygetUserCommitted(address)- Tokens already committed by user
Owner-only functions for managing the permitter:
setPerUserLimit(uint256)- Update per-user limitsetGlobalCap(uint256)- Update global capsetMerkleRoot(bytes32)- Update allowlistsetPaused(bool)- Pause/unpause validationtransferOwnership(address)- Transfer ownership
| Error | Description |
|---|---|
Unauthorized() |
Caller is not owner |
CCANotConfigured() |
No CCA authorized yet |
UnauthorizedCCA() |
Caller is not the authorized CCA |
CCAAlreadyAuthorized() |
CCA already set |
Paused() |
Contract is paused |
NoCCIDFound(address) |
Bidder has no CCID |
NotOnAllowlist(address) |
Bidder not on allowlist |
SanctionsFailed(address, bytes32) |
CCID is sanctioned |
IndividualLimitExceeded(bytes32, uint256, uint256) |
Per-user limit exceeded |
GlobalCapExceeded(uint256, uint256) |
Global cap exceeded |
- Single owner EOA with full control over configuration
- Authorized CCA is immutable once set
- Fail-closed on registry unavailability
totalCommitted <= globalCapcommittedByCCID[ccid] <= perUserLimitfor all CCIDs- Once
auctionis set, it cannot be changed - Limits follow CCID, allowing wallet migration
This project uses the ScopeLift Foundry Template.
default- Production settingslite- Optimizer off for faster compilationci- Deep fuzz/invariant testing
# Format code
scopelint fmt
# Check formatting and best practices
scopelint checkMIT