NFT-holder policy executors
Make a data policy’s spend authority transferable: name an ERC-721 (collection, tokenId) as the executor and whoever holds the NFT may move funds within the policy’s bounds. Transfer the NFT = transfer the right to act — the property-management “agency credential” pattern, with mandatory amount bounds and live ownerOf authorisation.
A data policy lets a group account G authorise a bounded movement of funds — a confidential send (or deposit/withdraw) out of G — without ever handing over G's keys. An NFT-holder executor makes that authority transferable: the policy names an ERC-721 (collection, tokenId), and whoever currently holds that NFT may execute the policy. Transfer the NFT and you transfer the right to act — atomically, with no policy edit and no re-issuance.
The motivating case is property management. A property is owned by a group account that collects rent and holds a float. The owner wants a managing agent to pay bills out of that float — but only within a mandate (a per-payment cap, a balance floor) — and wants to be able to change agents without touching the account. Mint an "agency" credential NFT, attach a send policy whose executor is that NFT, and hand the NFT to the agent. Firing the agent is a transfer to the next one: the mandate moves with the token, and the previous holder is revoked the instant they no longer hold it.
Executors vs approvers
A policy has two distinct roles. Keep them separate:
required_signers + caller_idsexecutor_accounts + executor_idsexecuteUnderPolicy and move the fundsownerOfexecutor_ids[i] selects the executor kind, per slot:
"0"(or omitted) → address executor:executor_accounts[i]is a wallet/account address, matched by equality.- non-zero → NFT-holder executor:
executor_accounts[i]is the ERC-721 collection address, andownerOf(executor_ids[i])— resolved live at every call — is the executor.
Omit executor_accounts entirely and execution defaults to the approvers (the common "the approvers also execute" case).
Amount bounds are mandatory for a bearer executor
An NFT-holder executor is a bearer credential over money movement — whoever holds the token can spend within the policy's scope. So if a policy allows a fund-moving op (send/deposit/withdraw) and names any NFT executor, it must declare at least one amount bound. Registration fails otherwise — with a clear pre-submit error, and as a hard on-chain revert. Address-only executors (a fixed, known party) keep the historic opt-in behaviour; only the transferable-bearer case is forced.
Blast radius. A bound caps each individual send, not the lifetime total. One M-of-N approval authorises up to
max_useseparate bounded sends, so the true exposure of a bearer credential ismax_use × hi. Sizehiandmax_useto the mandate.
Lifecycle
- Mint the executor NFT. An obligation NFT in a transferable class works well (
createObligationorcreateComposedContract). Hold it wherever the right should start — e.g. the property group mints to itself. - Register the policy on G with
addDataPolicy: setexecutorAccounts = [<collection address>],executorIds = [<token id>], at least oneamountBound, theallowedOperations, therequiredSigners(your approvers), andexpiry/maxUse. - Collect approvals with
approveDataPolicyuntilminSignatoriesis met. (If the registrant is the sole required signer, approval is auto-collected at registration.) - Execute. The current NFT holder calls
executeUnderPolicyacting as the group — the platform mints them a short-lived, narrowly-scoped group-delegation JWT. On-chain, the policy readsownerOflive, in the same transaction as the send, and authorises only the current holder. - Transfer = re-delegate. Move the NFT to a new holder and the execute right moves with it. The former holder is denied on their next call (live
ownerOfno longer matches) — no policy edit, no re-issuance.
# 1) Register a send policy whose executor is an "agency" NFT.
mutation Register($input: AddDataPolicyInput!) {
pipelineGate { addDataPolicy(input: $input) { success policyId messageId } }
}
# $input:
# {
# account: "<group account>",
# walletId: "<group wallet id>",
# policyId: "1",
# expiry: "<unix seconds or ISO>",
# maxUse: "12",
# minSignatories: 1,
# requiredSigners: ["<approver wallet>"], # the M-of-N approvers (NOT the executor)
# allowedOperations: ["send"],
# amountBounds: [{ token: "<asset>", lo: "0", hi: "500000000000000000000" }],
# requirements: [{ source: 0, denomination: "<asset>", lo: "0", hi: "..." }],
# executorAccounts: ["<agency collection address>"], # the ERC-721 collection
# executorIds: ["<agency token id>"] # non-zero ⇒ NFT-holder executor
# }
# 2) The current NFT holder spends — submitted with a group-delegation JWT (acting as G).
mutation Execute($input: ExecuteUnderPolicyInput!) {
pipelineGate { executeUnderPolicy(input: $input) { success messageId } }
}
# $input: {
# account: "<group account>", policyId: "1", operationType: "Send",
# operationData: "{\"denomination\":\"<asset>\",\"destination\":\"<payee>\",\"amount\":\"100\"}"
# }
Authoring it in a deal (DMS)
The deal composer wires this end-to-end. A rental/agency deal plan mints the agency NFT, transfers it to the agent, and registers the send policy — threading the minted NFT's identifiers into the policy via $step references, so nothing is copied by hand:
create_composed_contract(the mint) exposestoken_idandobligation_address(the class) as step outputs.add_data_policybindsexecutor_accounts: ["$step.<mint>.obligation_address"]andexecutor_ids: ["$step.<mint>.token_id"].
The policy's executor is then exactly the NFT the deal just minted. See DMS for the deal-authoring model and Collateralisation for the obligation/contract primitives the mint builds on.
Security model (what a deployer must accept)
- On-chain is authoritative.
executeUnderPolicyre-readsownerOflive in the spend transaction; the off-chain delegation gate is only a UX accelerator and fails closed on every error path (a DB/RPC/JWT failure denies). A former holder reusing a still-valid delegation JWT can submit but the on-chain gate rejects the send — it wastes gas and leaks nothing. - Bearer semantics. A stolen or lent NFT carries the spend right within scope. Custody of the NFT is the control surface — secure it like a signing key.
- The owner picks the collection. Only G's owner can register a policy or choose the executor collection. A hostile ERC-721 can grief (deny) but cannot move funds — the policy re-checks the same collection on-chain.
The full threat model, the three authorization gates, and the test→guarantee mapping are in the security review nft-executor-security.md (smart-contracts docs). End-to-end behaviour is exercised by rental_nft_executor_transfer_suite.yaml (direct) and rental_nft_executor_dms_suite.yaml (through the deal flow): mint → register → holder executes → transfer → new holder executes → former holder denied.