Authorization and Access
Covered in this guide
Section titled “Covered in this guide”In this guide, you will:
- Understand the importance of authorization and access control in smart contract security.
- Learn how to define and apply access rules for components and their methods.
- Discover how to set permissions for resources, including minting, burning, and metadata access.
- Delegate a resource’s authorization to a component hook, and keep the ability to repair or retire it.
- Explore identity management and the different types of callers in the Tari Ootle environment.
- Follow best practices for securing templates and managing digital assets.
Authorization and access control are critical aspects of smart contract security. The Tari Ootle Template Library provides flexible mechanisms to define who can execute methods on your Components and interact with your Resources.
Access Rules
Section titled “Access Rules”Access rules determine which identities (Components, users, or other templates) are allowed to perform certain actions. They are typically defined when a Component is created or a Resource is minted.
Access rules are configured with ComponentAccessRules (for component methods) and ResourceAccessRules (for resource actions).
Component Access Rules
Section titled “Component Access Rules”When you create a Component, you can specify access rules for each of its methods. These rules decide which caller may invoke the method.
use tari_template_lib::prelude::*;
#[template]mod template { pub struct MyComponent { // Component state fields }
pub fn create_with_access_rules( admin_badge: ResourceAddress, ) -> Component<MyComponent> { Component::new(MyComponent { /* ... */ }) .with_access_rules(ComponentAccessRules::new() // Allow anyone to call `public_method`. .method("public_method", rule!(allow_all)) // Only a caller holding `admin_badge` may call `admin_method`. .method("admin_method", rule!(resource(admin_badge))) // Default rule: anyone can call methods not explicitly // listed. With no default rule, unlisted methods default to // `deny_all`, so only the owner may call them. .default(rule!(allow_all)) ) .create() }}ComponentAccessRules::new(): Creates a new set of component access rules with adeny_alldefault.ComponentAccessRules::allow_all(): Creates a set of rules where every method is callable by anyone by default..method("method_name", AccessRule): Defines an access rule for a specific method..default(AccessRule): Sets the default rule for any method not explicitly listed (defaults todeny_all).
Ownership is configured separately with .with_owner_rule(OwnerRule::...); the component owner may always
call any method. OwnerRule defaults to OwnedBySigner, meaning the transaction signer that created the
component owns it.
The rule! macro builds an AccessRule. The base rules are rule!(allow_all) and rule!(deny_all); a
restricted rule wraps one or more requirements:
rule!(resource(address))— the caller presents proof of a fungible resource.rule!(non_fungible(address))/rule!(public_key(pk))— the caller presents proof of an NFT, or a signature by the given public key.rule!(component(address))— execution is within the given component (the current frame is that component).rule!(template(address))— execution is within the given template (the current frame is that template).rule!(caller_component(address))— the immediate caller is the given component.rule!(direct_caller_template(address))— the immediate caller is code from the given template: any component instance of it, or any static function of it.rule!(any_caller_component)/rule!(any_caller_template)— there was a caller of that kind, whoever it was.rule!(any_of(...)),rule!(all_of(...)),rule!(m_of_n(n, ...))— combine requirements.
Caller vs. current component
Section titled “Caller vs. current component”There are two related but distinct ways a rule can reference a component or template:
-
component(address)/template(address)ask “is execution happening inside this component/template?”. On a method access rule the current (top) frame is the callee, so these describe the callee, not the caller. Use them when a method should only run while that component/template is executing, or when the same rule is evaluated for resource/ownership checks where the current frame is the actor. -
caller_component(address)/direct_caller_template(address)ask “who called into here?”. The caller is always the direct caller: the frame that was current immediately before this frame was pushed. If A calls B and B calls C, C’s frame sees B, never A.direct_caller_template(address)is a package identity, not an instance identity. It matches any component instance of that template, including instances anyone may create through a public constructor, and any static function of that template, which anyone may call from a transaction. It is therefore only as strong as the least careful outgoing call anywhere in that template: a method or static function that forwards a caller-chosen component and method lets anyone act as the template.
Caller identity is issued as a virtual badge, not evaluated once at the door. When the engine pushes a frame it stamps two badges into that frame’s authorization scope — one naming the caller’s component (when the caller has one) and one naming the caller’s template. Two things follow:
- The badges are checkable wherever any other badge is, and for the whole lifetime of the frame: a component method rule, an owner rule, a resource rule, or a stealth spend condition. A helper called deeper inside the same frame can still re-assert “we are acting for A”.
- They are re-derived at every call and never inherited. A frame cannot pass on the identity it was called
with, and there is no way to capture a caller badge as a
Proofand hand it onward.
A method or function invoked directly by a top-level transaction instruction has no caller frame — the caller
is a signer — so caller_component / direct_caller_template rules deny it. Use a badge, public key, or
other proof requirement to allow signer-driven calls.
Two further forms ask the weaker question “was there a caller at all?”, and take no address:
// Satisfied by any component caller, and by no top-level instruction.rule!(any_caller_component)// Satisfied by any frame below the top level, i.e. reached from template code// rather than directly from a transaction instruction.rule!(any_caller_template)You never need to name a badge or a reserved address to write a caller rule. If you do need the badge itself
— to combine it with other requirements by hand — NonFungibleAddress::caller_component_badge(addr) and
::direct_caller_template_badge(addr) build it, the same way NonFungibleAddress::from_public_key(pk) does
for a signer.
No resource exists at either badge address and none can be created, so these badges cannot be minted, held in a vault, or forged by a template.
Caution: caller_component / direct_caller_template match the immediate caller’s identity, which is only as
trustworthy as the code that makes the call. A method that forwards a caller-supplied component/template and
method (a “proxy” method) delegates that identity, so anyone who can call the proxy can act as the proxied
caller. caller_component(address) also trusts the owner of that component, not its current code: the
owner can migrate the component to any template unless it was created with OwnerRule::None.
The same applies to resource auth hooks: the engine invokes the hook with the acting component as the
caller, and any resource may bind any hook-shaped method. A caller gate on a hook method is therefore never
sufficient on its own; the hook must check AuthHookCaller::resource() against the resources it manages.
Caution, the other way round: calling out hands the callee your identity as a live badge for the whole of its frame, usable at every auth point it reaches — a resource rule, an ownership rule, a spend condition — not only the method it entered through. Weigh that before calling into code you do not control, and gate anything that must not be reachable that way on a proof the callee cannot obtain rather than on the caller badge alone.
A resource auth hook is the one call-out you do not choose: any resource may bind a hook, and anyone may deposit
that resource into an account. The engine therefore runs a hook in a sandboxed frame that may read, emit events
and update its own component state (when declared &mut self), but cannot write any other substate, act on any
vault or resource, or call another component. The acting component’s badge satisfies the hook’s own method
rule, and the sandbox is what stops the hook spending it on the acting component’s behalf.
Proofs reach a frame only as arguments
Section titled “Proofs reach a frame only as arguments”A Proof authorizes in the frame that holds it. A frame holds the proofs it creates itself and the proofs it
was passed as arguments — nothing else. In particular, a proof a transaction left on its workspace authorizes
the call boundary, where the callee’s method access rule is evaluated, and stops there: the callee’s frame
does not hold it, so it cannot use it for a resource action or forward it onward.
That makes the Proof parameter part of a method’s contract. A method that acts on a resource guarded by a
badge must take the badge as a Proof, and the caller must hand it in:
// Acts on `self.tokens`, whose withdraw rule names a badge. Only reachable with the badge.pub fn take_tokens(&mut self, auth: Proof, amount: Amount) -> Bucket { auth.authorize_with(|| self.tokens.withdraw(amount))}// In the transaction.call_method(account, "create_proof_for_resource", args![badge_resource]).put_last_instruction_output_on_workspace("proof").call_method(component, "take_tokens", args![Workspace("proof"), 100])The built-in account follows the same rule: withdraw, deposit and the create_proof_* methods each have a
_with_auth variant taking the proof, and a resource whose withdraw or deposit rule names a badge is reachable
only through those.
A proof handed in remains the caller’s — the callee may hold and forward it but never consumes it — and it stops
authorizing when the caller drops it. Signer badges are not proofs: they are stamped into every frame a
top-level instruction pushes, so a rule naming the transaction’s signer needs no Proof argument.
Resource Access Rules
Section titled “Resource Access Rules”Resources can also have access rules, typically governing who can mint, burn, recall, freeze, or update metadata. Each builder method takes two arguments:
- The [
AccessRule] that gates the action right now. - An [
UpdateRule] that gates who, if anyone, can later change that access rule. The updater is fixed at creation time and cannot itself be changed afterwards — this lets holders trust aLockedrule will stay locked forever.
use tari_template_lib::prelude::*;
#[template]mod template {
pub struct MyComponent { resource: ResourceManager, }
impl MyComponent { pub fn create_with_restricted_resource( minter_badge: NonFungibleAddress, ) -> ResourceAddress { let resource_address = ResourceBuilder::public_fungible() .with_token_symbol("RTK") .with_divisibility(10) .initial_supply(amount!["1000000000000000000000"]) // Only the minter badge can mint, and only the owner may // change who can mint later. .mintable(rule!(non_fungible(minter_badge)), OWNER) // Nobody can burn, and the rule is locked forever. .burnable(rule!(deny_all), LOCKED) .build();
MyComponent { resource: resource_address.into() }.create() } }}Each ResourceBuilder exposes one method per action. All accept (rule, updater):
.mintable(rule, updater)— who can mint new units of this resource..burnable(rule, updater)— who can burn units..recallable(rule, updater)— who can recall units from any external vault..freezable(rule, updater)— who can freeze a vault holding this resource..withdrawable(rule, updater)— who can withdraw this resource from vaults..depositable(rule, updater)— who can deposit this resource into vaults..update_metadata(rule, updater)— who can change the metadata (the token symbol always stays immutable)..update_non_fungible_data(rule, updater)— who can change the mutable data on individual NFTs (non-fungible only).
A resource can also delegate its decisions to a component method rather than a static rule — see Authorization hooks.
Gating a resource on the caller
Section titled “Gating a resource on the caller”Because caller identity is a badge, caller_component / direct_caller_template work on resource rules too.
That lets the gate live on the resource rather than on each component that holds it:
// These tokens may only be withdrawn — from any vault, in any component —// while the acting component is executing on behalf of `treasury`.let tokens = ResourceBuilder::public_fungible() .withdrawable(rule!(caller_component(treasury)), LOCKED) .initial_supply(1_000_000u64);Compare that with putting rule!(caller_component(treasury)) on a payout method of the one component that
holds the vault. The method gate covers only that holder: any other component holding a vault of the same
resource withdraws with no check, and the gate cannot be retrofitted onto holders you do not control. The
resource rule is evaluated on every withdrawal, whichever component performs it.
The rule is checked against the frame that acts on the resource, so it reads as “the component touching
these tokens was called by treasury”, not “the transaction was signed by treasury”.
UpdateRule
Section titled “UpdateRule”UpdateRule controls who can later change an access rule, and has three variants:
UpdateRule | Constant | Behaviour |
|---|---|---|
UpdateRule::Locked | LOCKED | Nobody — not even the resource owner — can change the rule. |
UpdateRule::Owner | OWNER | Only the resource owner (per the OwnerRule) can change the rule. |
UpdateRule::AccessRule | n/a | Anyone satisfying the inner AccessRule can change the rule. No implicit owner override. |
Why Locked matters — it is the only way to make a trust promise to holders that cannot be quietly retracted
later. If recall is DenyAll but the updater is Owner, the owner can flip recall back on after users have
already accepted the token; the same goes for freeze, mint (inflation), withdraw, and so on. Issuing a token
with the recall/freeze/mint rules Locked is what lets a user verify, by reading the substate once, that those
powers can never be granted to anyone — not even the issuer. Use LOCKED for any property you want holders to be
able to rely on indefinitely, and reserve OWNER (or a more permissive updater) for rules you genuinely expect
to govern over time.
Pass an AccessRule directly where an UpdateRule is expected and it is converted to
UpdateRule::AccessRule(rule):
let admin = rule!(non_fungible(admin_badge));let resource = ResourceBuilder::public_fungible() // Recall is denied today, but the admin badge holder can re-enable it later. .recallable(rule!(deny_all), admin.clone()) // Withdrawals are open, but only the owner can ever change that. .withdrawable(rule!(allow_all), OWNER) .build();Defaults
Section titled “Defaults”Calling ResourceAccessRules::new() (the default) yields safe baseline rules:
mint,burn,recall,freeze:DenyAll, updaterLocked.update_metadata,update_non_fungible_data:DenyAll, updaterOwner. The resource owner can always update metadata and NFT mutable data; anyone else needs an explicit rule.withdraw,deposit:AllowAll, updaterLocked.- The auth hook updater:
Locked(see Authorization hooks).
Locked-by-default means a resource issued with ResourceAccessRules::new() and no overrides can never have its
recall, mint, etc. silently re-enabled later. To leave room for future tightening or relaxation, pass OWNER
(or an explicit access rule) as the updater on the actions you want to keep editable.
Updating an access rule later
Section titled “Updating an access rule later”Once a resource exists, use ResourceManager::update_access_rule to change a single field. The engine consults
the field’s UpdateRule to decide whether the caller is allowed:
ResourceManager::get(my_resource) .update_access_rule(ResourceAuthAction::Withdraw, rule!(non_fungible(user_badge)));The action is one of ResourceAuthAction::{Mint, Burn, Recall, Withdraw, Deposit, UpdateNonFungibleData, UpdateMetadata, Freeze}.
If the field’s updater is Locked, this call fails even for the owner. If the updater is Owner, only the resource
owner can succeed. If the updater is AccessRule(rule), only a caller satisfying rule can succeed — the owner
has no implicit override.
Authorization hooks
Section titled “Authorization hooks”An access rule is a static predicate. When a decision needs to look at live state — a blocklist, a per-holder cap, a
paused flag — a resource can instead delegate to a hook: a method on a component that the engine calls on every
mint, burn, recall, withdraw, deposit, freeze, metadata update and NFT data update. The hook takes the action and an
AuthHookCaller, returns nothing, and denies by panicking.
let alloc = CallerContext::allocate_component_address(None);let tokens = ResourceBuilder::public_fungible() .with_authorization_hook(alloc.get_address(), "authorize") // Without this, the hook binds for the life of the resource. .with_authorization_hook_updater(OWNER) .initial_supply(1_000_000u64);The hook returns () and takes (ResourceAuthAction, AuthHookCaller); the engine rejects the resource at creation
otherwise. It is skipped when the acting component is the hook’s own component, and when the hook’s component does
not exist yet — which is what lets a template allocate the address, build the resource, and create the component
afterwards.
On stealth and confidential resources, every stealth transfer runs the hook as Withdraw, freezing or unfreezing
UTXOs or confidential outputs runs it as Freeze, and burning a UTXO runs it as Burn. A stealth transfer submitted
directly as a transaction instruction has no acting template, so AuthHookCaller::template() is None.
A hook frame is confined to its own component: it may take &mut self and update that component’s state, and the
engine refuses every other write, new substate, cross-template call, bucket and proof from inside it. That
confinement is what makes a hook safe to run with the acting component’s caller badges. The engine invokes the hook
on the acting component’s behalf without that component choosing to call it, so the badge satisfies the hook’s own
method rule but cannot be spent on any vault or resource from inside the hook.
Because the hook runs on nearly every action, a hook that panics, denies unconditionally, or fails to decode its
arguments takes the whole resource offline: no transfers, no burns, no metadata changes, and every balance in its
vaults becomes unspendable. .with_authorization_hook_updater(...) is what buys the ability to recover:
// Retire a broken hook entirely...ResourceManager::get(my_resource).set_auth_hook(None);// ...or point the resource at a repaired one.ResourceManager::get(my_resource) .set_auth_hook(Some(AuthHook::new(fixed_component, "authorize".try_into().unwrap())));The updater is Locked by default, so a hook stays permanently binding unless the issuer opts out — which is the
same trust promise LOCKED makes for the access rules, and for the same reason: a holder who reads the substate can
see that the hook governing their tokens can never change. Opting out has a real cost, so make it deliberately:
- Whoever satisfies the updater can retire a hook that holders were relying on, and can give a hook-free resource a hook. A resource inspected today as unhooked is not guaranteed to stay that way.
- The outgoing hook is not consulted when it is replaced. That is what makes a denying hook recoverable, and it means the hook cannot defend itself.
- A replacement is validated exactly like one supplied at creation, so a mistyped method name fails the transaction rather than bricking the resource.
caller_component(...) / direct_caller_template(...) are rejected in the updater’s access rule: they always
evaluate to false on a resource, so such an updater would be Locked wearing a disguise — the hook could never be
repaired.
Identity Management
Section titled “Identity Management”Callers are identified by various types, depending on the context:
ComponentAddress: When a component calls another component.ResourceAddress: When a resource grants specific permissions (e.g., holding a specific NFT grants voting rights).TemplateAddress: When a template has specific rights.- User identities: Through the transaction’s signers or proofs provided.
The auth module in tari_template_lib provides utilities for working with identities and proofs within your templates.
These identities appear in rule! requirements: component(...) / template(...) refer to the current
execution scope, while caller_component(...) / direct_caller_template(...) refer to the frame’s immediate
caller, carried as a virtual badge (see Caller vs. current component).
Best Practices
Section titled “Best Practices”- Principle of Least Privilege: Grant only the necessary permissions. Avoid
AllowAllunless it’s genuinely intended for public methods. - Clear Ownership: Define and manage component/resource ownership clearly.
- Test Thoroughly: Always write comprehensive tests to ensure your access rules behave as expected.
- Budget for a Broken Hook: An auth hook runs on nearly every resource action, so a bug in it strands every balance of that resource. Set an updater unless the hook’s permanence is itself the guarantee you are selling.
By effectively utilizing access rules, you can ensure the security and integrity of your Tari Ootle Templates and the assets they manage.