CrypDefi is now Fortkey. read the announcement

    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:

    ParameterValue
    Byte stringdd46508f
    Start index0
    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:

    Policy Rules
    And
    EnforceWhitelistEqualsUniswap Addresses
    EnforceData ComparisonEqualsByte stringdd46508fAtStart index0ForAddress*

    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:

    ParameterValue
    Byte string02
    Start index196
    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's exchange domain.
    • Withdrawals and transfers (withdraw, usdSend, spotSend, usdClassTransfer) are EIP-712 actions signed under the hyperliquidsigntransaction domain. 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

    EnforceMatch
    Transaction TypeEquals Evm EIP712
    Group AccessEquals Bots
    WhitelistEquals 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

    EnforceMatch
    Transaction TypeEquals Evm EIP712
    Group AccessEquals Traders
    WhitelistEquals 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

    EnforceMatch
    Transaction TypeEquals Transaction
    Group AccessEquals Traders
    WhitelistEquals 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

    Policy Rules
    Or
    And
    EnforceTransaction TypeEqualsEvm EIP712
    EnforceGroup AccessEqualsBots
    EnforceWhitelistEqualsexchange
    And
    EnforceTransaction TypeEqualsEvm EIP712
    EnforceGroup AccessEqualsTraders
    EnforceWhitelistEqualshyperliquidsigntransaction
    And
    EnforceTransaction TypeEqualsTransaction
    EnforceGroup AccessEqualsTraders
    EnforceWhitelistEquals0x2df1c51e09aecf9cacb7bc98cb1742757f163df70xaf88d065e77c8cc2239327c5edb3a432268e5831

    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 (and OfferCancel to clear resting offers). They trade against the ledger's order book and have no external destination to whitelist.
    • Payments are the Payment type, whose Destination is 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

    EnforceMatch
    Transaction TypeEquals Payment
    WhitelistEquals 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

    EnforceMatch
    Transaction TypeEquals 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

    Policy Rules
    Or
    And
    EnforceTransaction TypeEqualsPayment
    EnforceWhitelistEqualsFortkey wallets
    And
    EnforceTransaction TypeEqualsOfferCreateOfferCancelTrustSet

    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 exchange domain. Settlement on-chain is done by Polymarket's operator, not your wallet.
    • Redeeming winnings is an on-chain transaction, redeemPositions on 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

    EnforceMatch
    Transaction TypeEquals Evm EIP712
    Group AccessEquals Bots
    WhitelistEquals 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

    EnforceMatch
    Transaction TypeEquals Transaction
    Group AccessEquals Bots
    WhitelistEquals 0x4d97dcd97ec945f40cf65f87097ace5ea0476045
    Data ComparisonByte 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

    EnforceMatch
    Transaction TypeEquals Transaction
    Group AccessEquals Traders
    WhitelistEquals 0x4d97dcd97ec945f40cf65f87097ace5ea0476045, 0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb
    Data ComparisonByte 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:

    • a9059cbb is the transfer(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

    Policy Rules
    Or
    And
    EnforceTransaction TypeEqualsEvm EIP712
    EnforceGroup AccessEqualsBots
    EnforceWhitelistEqualspolymarket ctf exchange
    And
    EnforceTransaction TypeEqualsTransaction
    EnforceGroup AccessEqualsBots
    EnforceWhitelistEquals0x4d97dcd97ec945f40cf65f87097ace5ea0476045
    EnforceData ComparisonEqualsByte string01b7037cAtStart index0ForAddress*
    And
    EnforceTransaction TypeEqualsTransaction
    EnforceGroup AccessEqualsTraders
    EnforceWhitelistEquals0x4d97dcd97ec945f40cf65f87097ace5ea04760450xc011a7e12a19f7b1f670d46f03b03f3342e82dfb
    EnforceData ComparisonEqualsByte stringa9059cbb000000000000000000000000f8cbc761d489284a7251fcc92f055b0bec6b408cAtStart index0ForAddress0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb

    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 approve is blocked. Grant the exchange's pUSD allowance once during setup, or widen the pUSD check to an OR of 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) and Rollback.
    • 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:

    EnforceMatch
    Transaction TypeEquals Create, Rollback
    v1.create.template_idquickstart-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

    Policy Rules
    And
    EnforceTransaction TypeEqualsCreateRollback
    EnforceCanton FieldEqualsv1.create.template_id
    quickstart-licensing:Licensing.AppInstall:AppInstallRequestquickstart-licensing:Licensing.AppInstall:AppInstallquickstart-licensing:Licensing.License:Licensequickstart-licensing:Licensing.License:LicenseRenewalRequest

    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 Exercise to the transaction types and a matching field check on the exercised template or choice, not just Create.
    • 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.dar in the quickstart). The hash changes every time the package is recompiled, so update the policy whenever you redeploy a new build.
    Questions or a chain we should add?Talk to engineeringTry the sandboxpolicy-examples