Files
proofs/lean/Agent/NFTOwnership.lean
zeekayandHanzo Dev 638fba4c90 Publish under the org, not a person
Bylines, paper authorship, correspondence addresses and the founder blurb under
each post named an individual. They name the organisation that published the
work instead: the brand on posts and pages, the research byline on papers, and
research@ in place of a personal address. Both are what these repositories
already used elsewhere — nothing new was coined.

Co-authors are untouched. Where a paper named someone else alongside, they stay
named; only the one byline changed. Bibliography entries, running heads,
PDF metadata and affiliations move with it, so a citation still resolves to
whoever the cited paper names.

Sample and fixture data uses the placeholder name these tests already use
elsewhere, so no assertion changed meaning.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 08:58:14 -07:00

182 lines
6.6 KiB
Lean4
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/-
Authors: Antje Worring, Zoo Labs Foundation Research
NFT-Bound Agent Ownership and Delegation
Formal proofs about the Agent NFT Standard's ownership model.
Agents are ERC-721 tokens that encapsulate autonomous AI systems
with wallets, governance rights, and configurable autonomy parameters.
Maps to:
- zoo/papers/zoo-agent-nft.tex: Agent NFT Standard specification
- Governance/AgentNFT.lean: basic transfer/revoke (extended here)
Key results:
- Transfer correctly updates the full capability set
- Delegated permissions are transitive
- Revoking owner propagates to all delegates
- No agent can exceed its granted capabilities (no escalation)
Author: Zoo Labs Foundation Research
-/
import Mathlib.Data.Nat.Defs
import Mathlib.Data.Finset.Basic
import Mathlib.Tactic
namespace Agent.NFTOwnership
/-! ## Core Structures -/
/-- Address (represented as Nat for simplicity). -/
abbrev Address := Nat
/-- Capability identifier (e.g., TRADE=1, GOVERN=2, STAKE=4, TRANSFER=8). -/
abbrev Capability := Nat
/-- A capability set is a finite set of capabilities. -/
abbrev CapSet := Finset Capability
/-- An Agent NFT with ownership, delegation, and capability tracking. -/
structure AgentNFT where
tokenId : Nat
owner : Address
/-- Capabilities granted to this agent by its owner -/
capabilities : CapSet
/-- Delegates: address -> subset of capabilities delegated to them -/
delegates : Address → CapSet
/-- Whether the agent is active (not revoked) -/
active : Bool
/-- The empty capability set. -/
def emptyCaps : CapSet := ∅
/-! ## Transfer -/
/-- Transfer ownership: new owner inherits the full capability set,
all existing delegations are cleared (security: new owner should
explicitly re-delegate). -/
def transfer (nft : AgentNFT) (newOwner : Address) : AgentNFT :=
{ nft with
owner := newOwner
delegates := fun _ => emptyCaps }
/-- Transfer correctly updates the capability set: the new owner
retains all capabilities that existed before transfer.
The agent itself doesn't lose capabilities, only delegations reset. -/
theorem ownership_transfer_updates_caps (nft : AgentNFT) (newOwner : Address) :
(transfer nft newOwner).capabilities = nft.capabilities := by
simp [transfer]
/-- Transfer changes the owner. -/
theorem transfer_changes_owner (nft : AgentNFT) (newOwner : Address) :
(transfer nft newOwner).owner = newOwner := by
simp [transfer]
/-- Transfer clears all delegations. -/
theorem transfer_clears_delegates (nft : AgentNFT) (newOwner : Address) (addr : Address) :
(transfer nft newOwner).delegates addr = emptyCaps := by
simp [transfer]
/-! ## Delegation -/
/-- Delegate a subset of capabilities to an address.
The delegated set must be a subset of the agent's capabilities. -/
def delegate (nft : AgentNFT) (to_ : Address) (caps : CapSet)
(_ : caps ⊆ nft.capabilities) : AgentNFT :=
{ nft with
delegates := fun addr =>
if addr = to_ then nft.delegates to_ ∪ caps
else nft.delegates addr }
/-- Transitive delegation: if A delegates to B, and B delegates a subset
of those capabilities to C, then C has those capabilities.
We model this as a chain of delegation records. -/
structure DelegationChain where
/-- The original agent -/
agent : AgentNFT
/-- Intermediate delegate -/
intermediate : Address
/-- Final delegate -/
final : Address
/-- Capabilities delegated to intermediate -/
intermediateCaps : CapSet
/-- Capabilities sub-delegated to final (must be subset of intermediate's) -/
finalCaps : CapSet
/-- Proof that intermediate actually has those capabilities -/
hIntermediate : intermediateCaps ⊆ agent.capabilities
/-- Proof that final's capabilities are subset of intermediate's -/
hFinal : finalCaps ⊆ intermediateCaps
/-- Delegation is transitive: if the agent grants caps to B and B
sub-delegates a subset to C, then C's capabilities are a subset
of the agent's original capabilities. -/
theorem delegation_transitive (chain : DelegationChain) :
chain.finalCaps ⊆ chain.agent.capabilities :=
Finset.Subset.trans chain.hFinal chain.hIntermediate
/-! ## Revocation -/
/-- Revoke the agent entirely: deactivates it and clears all delegations. -/
def revoke (nft : AgentNFT) : AgentNFT :=
{ nft with
active := false
delegates := fun _ => emptyCaps }
/-- Revoking the agent propagates to all delegates:
no delegate retains any capabilities after revocation. -/
theorem revocation_propagates (nft : AgentNFT) (addr : Address) :
(revoke nft).delegates addr = emptyCaps := by
simp [revoke]
/-- Revocation deactivates the agent. -/
theorem revocation_deactivates (nft : AgentNFT) :
(revoke nft).active = false := by
simp [revoke]
/-- Revocation preserves the token identity. -/
theorem revocation_preserves_id (nft : AgentNFT) :
(revoke nft).tokenId = nft.tokenId := by
simp [revoke]
/-! ## No Escalation -/
/-- A delegate's capabilities are always a subset of the agent's capabilities.
This is the fundamental no-escalation invariant: an agent can never
grant more capabilities than it possesses.
The `delegate` operation sets the delegate's caps to `old ∪ new` where
`new = caps ⊆ capabilities` by construction (`h`). For the union to stay
within `capabilities`, the pre-existing delegation `nft.delegates to_` must
already be bounded — exactly the no-escalation invariant maintained across
operations (transfer/revoke reset delegations to `∅`, which trivially
satisfies it). We therefore take that invariant as hypothesis `h_inv` and
prove the union is bounded honestly. -/
theorem no_escalation (nft : AgentNFT) (to_ : Address) (caps : CapSet)
(h : caps ⊆ nft.capabilities)
(h_inv : ∀ c ∈ nft.delegates to_, c ∈ nft.capabilities) :
∀ c ∈ (delegate nft to_ caps h).delegates to_, c ∈ nft.capabilities := by
intro c hc
have hc' : c ∈ nft.delegates to_ ∪ caps := by
simpa only [delegate, if_pos rfl] using hc
rw [Finset.mem_union] at hc'
rcases hc' with hc' | hc'
· exact h_inv c hc'
· exact h hc'
/-- Simpler no-escalation: delegated set is bounded by agent capabilities. -/
theorem no_escalation_set (_nft : AgentNFT) (_to_ : Address) (caps : CapSet)
(h : caps ⊆ _nft.capabilities) :
caps ⊆ _nft.capabilities := h
/-- An inactive agent cannot perform any actions.
This is enforced at the contract level: all action functions
check `require(agent.active)` before proceeding. -/
def canAct (nft : AgentNFT) : Prop := nft.active = true
theorem revoked_cannot_act (nft : AgentNFT) :
¬canAct (revoke nft) := by
simp [canAct, revoke]
end Agent.NFTOwnership