Policy examples
Worked examples of complete policies, from simple address rules to smart contract data validation. Each one shows what the policy does, the rules that make it up, the final configuration, and how to confirm it in the simulator.
If you want a policy that isn't covered here, talk to engineering and we'll help you build it.
Every example uses real transaction structure, so before you deploy, confirm the current contract addresses from the protocol's own deployment page.
Uniswap: liquidity operations only
This policy restricts a wallet so it can only be used with Uniswap, and only for managing liquidity positions. It combines two rules with an AND:
- A whitelist so the wallet can only interact with known Uniswap contracts, stored in an address book.
- A data comparison so the only call the wallet can make is
modifyLiquidities, Uniswap V4's single entrypoint for liquidity actions.
Together, a key cannot send funds to an arbitrary address, cannot interact with any non-Uniswap contract, and cannot do anything on Uniswap other than manage liquidity.
How it works
On Uniswap V4, every liquidity action goes through one function on the PositionManager:
modifyLiquidities(bytes unlockData, uint256 deadline) // selector 0xdd46508f
The specific action (mint, burn, increase, decrease) is encoded as an action byte inside unlockData. This policy pins the wallet to that entrypoint, so it can only ever manage liquidity, never swap, approve, or call an unrelated contract. It does not distinguish between the individual liquidity actions; to allow only opening positions, see Go further below.
1. Whitelist the Uniswap addresses
Create an address book (for example Uniswap Addresses) containing the Uniswap contracts the wallet is allowed to reach on this chain: the V4 PositionManager, plus Permit2 and the Universal Router if your flow uses them. Confirm the current addresses from Uniswap's deployments page. On Arbitrum, for instance, the V4 PositionManager is 0xd88F38F930b7952f2DB2432Cb002E7abbF3dD869.
Add a Whitelist rule pointing at that address book. Because policies are created per chain, use the address book for the chain you are configuring.
2. Restrict to the liquidity entrypoint
Add a Data Comparison rule that checks the function selector at the start of the transaction data:
| Parameter | Value |
|---|---|
| Byte string | dd46508f |
| Start index | 0 |
| Address | * |
The selector dd46508f is modifyLiquidities. Start index 0 matches it at the very first byte of the data field, which is where the function selector always sits. The address * applies the check regardless of contract, since the whitelist rule already limits which contracts the wallet can reach.
The final policy
Combine both rules under an AND group, so a transaction must satisfy every rule to be signed:
The result: the wallet can only call modifyLiquidities on a whitelisted Uniswap contract. Anything else, a swap, a token approval, a transfer, or a call to any other contract, is rejected before a signature is ever created.
Go further: allow only opening positions
To restrict the wallet to only opening new positions (MINT_POSITION) rather than all liquidity actions, add a second Data Comparison rule for the action byte inside unlockData:
| Parameter | Value |
|---|---|
| Byte string | 02 |
| Start index | 196 |
| Address | * |
The action byte 02 is MINT_POSITION, and it sits at offset 196 in a standard modifyLiquidities call (offset 904 in the multicall with permitBatch variant).
Hyperliquid: trading and funding on one wallet
Hyperliquid uses two very different signing surfaces. Trading and money movement both happen as EIP-712 signed actions on the Hyperliquid L1, while deposits are a plain USDC transfer on Arbitrum. This policy confines a wallet to Hyperliquid, and it also separates duties: automated bots may only trade, while human traders hold the sensitive money-movement actions. It uses a top-level OR of three branches, and a transaction is signed only if it fully satisfies one branch.
How it works
- Trading (
order,cancel,modify) is an EIP-712 action signed under Hyperliquid'sexchangedomain. - Withdrawals and transfers (
withdraw,usdSend,spotSend,usdClassTransfer) are EIP-712 actions signed under thehyperliquidsigntransactiondomain. These move funds, so they are the sensitive ones. - Deposits are a regular USDC transfer on Arbitrum to the Hyperliquid bridge contract.
The branches split on both transaction type and group. On an EIP-712 branch the whitelist matches the signature's domain name; on the transaction branch it matches the destination address. The Bots group gets trading only, while the Traders group (humans) gets the sensitive actions and deposits.
Branch 1: bots can trade
| Enforce | Match |
|---|---|
| Transaction Type | Equals Evm EIP712 |
| Group Access | Equals Bots |
| Whitelist | Equals exchange |
exchange is Hyperliquid's domain for L1 trading actions. Restricting the Bots group to this domain lets automated strategies place, cancel, and modify orders, but a bot can never sign a hyperliquidsigntransaction action, so a compromised bot key cannot withdraw or transfer funds.
Branch 2: traders manage funds
| Enforce | Match |
|---|---|
| Transaction Type | Equals Evm EIP712 |
| Group Access | Equals Traders |
| Whitelist | Equals hyperliquidsigntransaction |
hyperliquidsigntransaction is Hyperliquid's domain for user-signed actions such as withdrawals and transfers. Because these move money, they are limited to the human Traders group. Bots are excluded from this branch by group, so the sensitive actions sit only with people.
Branch 3: traders deposit
| Enforce | Match |
|---|---|
| Transaction Type | Equals Transaction |
| Group Access | Equals Traders |
| Whitelist | Equals 0x2df1c51e09aecf9cacb7bc98cb1742757f163df7, 0xaf88d065e77c8cc2239327c5edb3a432268e5831 |
For a regular transaction, the whitelist checks the destination address. The two entries are the Hyperliquid bridge contract and native USDC on Arbitrum, so a human trader can fund the account by depositing USDC to the bridge, but the wallet cannot send a regular transaction anywhere else.
The final policy
A signature is produced only if it matches one full branch: a bot trading action on exchange, a trader money-movement action on hyperliquidsigntransaction, or a trader USDC deposit to the bridge. Everything else is rejected before any signature exists. Because the two EIP-712 branches are separated by group, trading authority (bots) and fund-movement authority (humans) never overlap.
Go further
- Restrict withdrawal destinations. Branch 2 allows the withdraw action but does not limit where funds go. Add a field comparison on the withdrawal
destination, or split withdrawals into their own branch with a destination whitelist, to pin payouts to known treasury addresses.
Confirm the Hyperliquid bridge address against Hyperliquid's current documentation before deploying.
XRPL: DEX trading and treasury payments
On XRPL this policy keeps a wallet to three things: trading on the DEX, paying out only to your own treasury wallets, and managing trust lines. Because XRPL has native transaction types, the whole policy is built from the Transaction Type rule with a single whitelist on payments. You can do DEX transactions, and you can make payments only to your known treasury wallets.
How it works
XRPL transactions each carry a type, and only the Payment type has a Destination. So the policy sorts by type:
- DEX swaps are
OfferCreate(andOfferCancelto clear resting offers). They trade against the ledger's order book and have no external destination to whitelist. - Payments are the
Paymenttype, whoseDestinationis checked against an address book. - Trust lines are
TrustSet, needed to hold issued tokens (IOUs).
The policy is a top-level OR: a transaction is signed if it is a payment to a whitelisted wallet, or one of the allowed DEX and trust-line types.
Branch 1: payments to treasury
| Enforce | Match |
|---|---|
| Transaction Type | Equals Payment |
| Whitelist | Equals Fortkey wallets |
The whitelist checks the payment's destination against the Fortkey wallets address book, so payments can only reach your known treasury wallets. A payment to any other address is rejected.
Branch 2: DEX and trust lines
| Enforce | Match |
|---|---|
| Transaction Type | Equals OfferCreate, OfferCancel, TrustSet |
These types have no external recipient, so no whitelist is needed. The wallet can place and cancel DEX offers and set trust lines, but nothing else, no AccountSet, no key changes, no escrow.
The final policy
A signature is produced only if the transaction is a Payment to a Fortkey wallets address, or an OfferCreate, OfferCancel, or TrustSet. Anything else, a payment elsewhere or any other transaction type, is rejected.
Polymarket: bots trade and redeem, humans hold the money
Polymarket runs on Polygon. Its collateral is pUSD (Polymarket USD), Polymarket's own stablecoin backed 1:1 by USDC, which replaced USDC.e in the April 2026 exchange upgrade. Placing an order is an EIP-712 signed message, while redeeming winnings and moving funds are regular on-chain transactions. This policy lets automated bots place orders and redeem, while only human traders can move money, using a top-level OR of three branches. A transaction is signed only if it fully satisfies one branch.
How it works
- Orders are EIP-712 messages signed under Polymarket's
polymarket ctf exchangedomain. Settlement on-chain is done by Polymarket's operator, not your wallet. - Redeeming winnings is an on-chain transaction,
redeemPositionson the Conditional Tokens (CTF) contract. - Moving money and approvals are on-chain transactions against pUSD and the CTF contract.
The branches split on transaction type and group. The Bots group can place orders and redeem, but has no path to move funds; the Traders group (humans) can move pUSD and manage approvals.
Branch 1: bots place orders
| Enforce | Match |
|---|---|
| Transaction Type | Equals Evm EIP712 |
| Group Access | Equals Bots |
| Whitelist | Equals polymarket ctf exchange |
For an EIP-712 signature, the whitelist matches the domain name. polymarket ctf exchange is Polymarket's order domain. Bots can sign Polymarket orders, but the wallet cannot sign an EIP-712 message for any other protocol, such as a permit or a transferWithAuthorization that could move funds.
Branch 2: bots redeem winnings
| Enforce | Match |
|---|---|
| Transaction Type | Equals Transaction |
| Group Access | Equals Bots |
| Whitelist | Equals 0x4d97dcd97ec945f40cf65f87097ace5ea0476045 |
| Data Comparison | Byte string 01b7037c at index 0, for the CTF address |
The whitelist pins the destination to the Conditional Tokens contract, and the data comparison pins the function selector to 01b7037c, which is redeemPositions(address,bytes32,bytes32,uint256[]). The CTF contract is an ERC-1155, so without the selector pin a bot could safeTransferFrom your outcome tokens away. With it, the only on-chain action a bot can take is redeeming.
Branch 3: humans move money
| Enforce | Match |
|---|---|
| Transaction Type | Equals Transaction |
| Group Access | Equals Traders |
| Whitelist | Equals 0x4d97dcd97ec945f40cf65f87097ace5ea0476045, 0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb |
| Data Comparison | Byte string a9059cbb000000000000000000000000 f8cbc761d489284a7251fcc92f055b0bec6b408c at index 0, for the pUSD address |
The whitelist allows the CTF and pUSD contracts. The data comparison is scoped to pUSD, so it only fires on pUSD transactions and forces them to be a transfer to the treasury wallet:
a9059cbbis thetransfer(address,uint256)selector,- followed by 12 bytes of zero padding,
- then the 20-byte treasury address
f8cbc761d489284a7251fcc92f055b0bec6b408c.
The amount is left unconstrained, so any size transfer to treasury passes. Because the data comparison is scoped to pUSD, CTF transactions are unaffected, so humans can still setApprovalForAll the exchange on the CTF contract.
The final policy
A signature is produced only if it matches one full branch: a bot order on the polymarket ctf exchange domain, a bot redeemPositions call on the CTF contract, or a human pUSD transfer to the treasury wallet. Everything else is rejected. Bots can trade and redeem but have no branch that moves money, and humans can only send pUSD to treasury, so a compromised bot key cannot exfiltrate funds.
Go further
- Neg-risk markets. This policy covers standard CTF markets. Multi-outcome (neg-risk) markets use a different order domain (
Polymarket Neg Risk CTF Exchange) and redeem through the Neg Risk Adapter. Add that domain to Branch 1 and the adapter contract plus its redeem selector to Branch 2 if you trade them. - pUSD approvals. Because Branch 3 pins every pUSD transaction to a transfer to treasury, a pUSD
approveis blocked. Grant the exchange's pUSD allowance once during setup, or widen the pUSD check to anORof transfer-to-treasury and approve-to-exchange. - Deposits and wrapping. Funding the account means acquiring pUSD by wrapping USDC through Polymarket's CollateralOnramp. To let humans do that here, whitelist the CollateralOnramp and USDC contracts in Branch 3 as well.
Confirm the current addresses against Polymarket's documentation before deploying: Conditional Tokens 0x4D97…0476045, pUSD 0xC011…82DFB, and the V2 CTF Exchange 0xE111…996B (the contract you approve for pUSD spending).
Canton: restrict a wallet to one Daml app
On Canton, transactions are Daml ledger actions, and a policy can match both the action type and the specific template involved. This policy restricts a wallet to the quickstart-licensing app from the Canton Network quickstart: it only signs Create and Rollback actions, and only for that app's templates.
How it works
- Transaction Type matches the Daml node type. Here it allows
Create(creating a contract) andRollback. - Canton Field matches a field inside the transaction. Here it checks
v1.create.template_id, the template of the contract being created, against a list of allowed templates.
The two combine with AND, so a signature is produced only for a Create (or Rollback) whose created contract is one of the licensing app's templates.
The rules
The transaction type is limited to creating contracts and rolling back:
| Enforce | Match |
|---|---|
| Transaction Type | Equals Create, Rollback |
v1.create.template_id | quickstart-licensing:Licensing.AppInstall:AppInstall, quickstart-licensing:Licensing.License:License, ... |
The Canton Field rule then pins the created template to the four templates of the quickstart-licensing package. Those are the install request and install (Licensing.AppInstall) and the license and its renewal request (Licensing.License), the full template set of the quickstart licensing app.
The values are written with the package name here for readability, but the policy needs the package's ID, the hash. You have to replace quickstart-licensing in each value with the actual hash of your deployed Daml package before saving, because the package name does not resolve. See "Get the package ID" under Go further below.
The final policy
The wallet can only create contracts belonging to the quickstart-licensing app (and roll back), nothing else. A Create of any other template, or any other action type, is rejected before a signature exists.
Go further
- Allow choices. To let the wallet run the app's workflows (accept an install, renew a license), add
Exerciseto the transaction types and a matching field check on the exercised template or choice, not justCreate. - Get the package ID. The template ID must use the package's ID, the hash, not its name, so each value looks like
<package-id-hash>:Licensing.License:License. You have to replace the value with the actual hash of your deployed Daml package. Read it from your deployed DAR (quickstart-licensing-0.0.1.darin the quickstart). The hash changes every time the package is recompiled, so update the policy whenever you redeploy a new build.