Skip to content

Proxy System Overview

Roman edited this page Mar 31, 2026 · 2 revisions

Bittensor Proxy System

1. Key Terms

The proxy system has two participants:

Delegator (also called real) -- the account owner. The one whose funds and permissions are being used. Pays the deposit for creating proxy relationships.

Delegate -- the authorized representative. Signs transactions on behalf of the delegator, but can only do what the proxy_type allows.

The relationship between them is stored on-chain as a ProxyDefinition:

Field Meaning
delegate The authorized account
proxy_type What calls this delegate is allowed to make (see Section 2)
delay How many blocks the delegate must wait before a call executes. If 0, calls are immediate. If > 0, delegate must announce first, then wait.

Limits:

  • Max 20 proxy relationships per account (MaxProxies)
  • Max 75 pending announcements per delegate (MaxPending)

2. Proxy Types -- What Each One Can Do

Each proxy type defines a filter: which on-chain calls the delegate is allowed to make on behalf of the delegator.

Source: runtime/src/lib.rs L612-L828

Allow-all

Any

Allows every call. Full control over the account. The only type that allows calling kill_pure and remove_proxies through a proxy.

Deny-list types (allow everything EXCEPT listed calls)

NonTransfer

Allows everything EXCEPT:

  • Balances::* (all TAO transfers)
  • SubtensorModule::transfer_stake
  • SubtensorModule::schedule_swap_coldkey
  • SubtensorModule::swap_coldkey

Use case: delegate can do almost anything but cannot move funds out.

NonCritical

Allows everything EXCEPT:

  • SubtensorModule::dissolve_network
  • SubtensorModule::root_register
  • SubtensorModule::burned_register
  • Sudo::*

Use case: delegate can operate freely but cannot destroy subnets or use sudo.

NonFungible

Allows everything EXCEPT anything that moves TAO/Alpha or changes key ownership:

  • Balances::*
  • All staking calls (add_stake, remove_stake, unstake_all, unstake_all_alpha, swap_stake, swap_stake_limit, move_stake, add_stake_limit, remove_stake_limit, remove_stake_full_limit)
  • transfer_stake, burned_register, root_register
  • schedule_swap_coldkey, swap_coldkey, swap_hotkey

Use case: delegate can manage non-financial aspects of the account.

Allow-list types (ONLY listed calls are allowed)

Staking

  • add_stake, remove_stake
  • unstake_all, unstake_all_alpha
  • swap_stake, swap_stake_limit
  • move_stake
  • add_stake_limit, remove_stake_limit, remove_stake_full_limit
  • set_root_claim_type

Note: transfer_stake is NOT included. A leaked Staking proxy cannot move funds to another account.

Transfer

  • Balances::transfer_keep_alive
  • Balances::transfer_allow_death
  • Balances::transfer_all
  • SubtensorModule::transfer_stake

SmallTransfer

Same calls as Transfer, but each call is capped:

  • TAO transfers: < 0.5 TAO per call
  • Alpha transfers (transfer_stake): < 0.5 Alpha per call

Owner

  • AdminUtils::* (all admin calls EXCEPT sudo_set_sn_owner_hotkey)
  • SubtensorModule::set_subnet_identity
  • SubtensorModule::update_symbol

Use case: delegate manages subnet settings without being able to reassign ownership.

Registration

  • SubtensorModule::burned_register
  • SubtensorModule::register

ChildKeys

  • SubtensorModule::set_children
  • SubtensorModule::set_childkey_take

SwapHotkey

  • SubtensorModule::swap_hotkey

RootClaim

  • SubtensorModule::claim_root

SudoUncheckedSetCode

  • Sudo::sudo_unchecked_weight ONLY when it wraps System::set_code
  • Any other call inside sudo_unchecked_weight is rejected

SubnetLeaseBeneficiary

Allows SubtensorModule::start_call and a fixed set of AdminUtils calls for operating a leased subnet:

sudo_set_serving_rate_limit, sudo_set_min_difficulty, sudo_set_max_difficulty, sudo_set_weights_version_key, sudo_set_adjustment_alpha, sudo_set_immunity_period, sudo_set_min_allowed_weights, sudo_set_kappa, sudo_set_rho, sudo_set_activity_cutoff, sudo_set_network_registration_allowed, sudo_set_network_pow_registration_allowed, sudo_set_max_burn, sudo_set_bonds_moving_average, sudo_set_bonds_penalty, sudo_set_commit_reveal_weights_enabled, sudo_set_liquid_alpha_enabled, sudo_set_alpha_values, sudo_set_commit_reveal_weights_interval, sudo_set_toggle_transfer

Deprecated (always reject all calls)

Senate, Triumvirate, Governance, RootWeights


3. Two Kinds of Proxy

Regular Proxy

The delegator adds a delegate to their account. The delegate can then sign certain calls on the delegator's behalf.

sequenceDiagram
    participant D as Delegator (real)
    participant Chain as On-chain
    participant P as Delegate

    Note over D: Signs with own key
    D->>Chain: add_proxy(delegate, proxy_type, delay)
    Note over Chain: Store ProxyDefinition in Proxies[delegator]<br/>Reserve deposit from delegator

    alt delay = 0 -- immediate execution
        Note over P: Signs with delegate key
        P->>Chain: proxy(real=delegator, call)
        Chain->>Chain: Verify proxy exists, check proxy_type allows the call, execute
    else delay > 0 -- deferred execution (see Section 4)
        Note over P: Must announce first, wait, then execute
    end

    Note over D: When no longer needed:
    D->>Chain: remove_proxy(delegate, proxy_type, delay)
    Note over Chain: Remove ProxyDefinition<br/>Return deposit to delegator
Loading

Pure Proxy

A pure proxy is a new account with no private key. It does not exist before create_pure and becomes permanently inaccessible after kill_pure.

The spawner (who creates it) becomes its proxy -- the only way to control it.

sequenceDiagram
    participant S as Spawner
    participant Chain as On-chain
    participant Pure as Pure Account (no private key)

    S->>Chain: create_pure(proxy_type, delay, index)
    Note over Chain: Derive pure address deterministically<br/>from (spawner, block_height, ext_index, proxy_type, index).<br/>Store Proxies[pure] = [{delegate: spawner}]<br/>Reserve deposit from spawner.

    Note over S: Control pure via proxy call:
    S->>Chain: proxy(real=pure, call=...)
    Chain->>Chain: Execute call as if pure account signed it

    Note over S: To destroy the pure account:
    S->>Chain: proxy(real=pure, call=kill_pure(spawner, ...))
    Note over Chain: Remove Proxies[pure], return deposit to spawner
    Note over Pure: Permanently inaccessible.<br/>Any funds remaining on this account are lost forever.
Loading

Important about kill_pure: the proxy relationship used to call kill_pure must be of type Any. If the pure was created with create_pure(Staking, ...), the spawner has a Staking proxy to the pure account, and kill_pure will be blocked through that relationship. To kill such a pure account, an Any-type proxy must be added to it first.


4. Delayed Proxy (delay > 0)

When a proxy relationship has delay > 0, the delegate cannot call proxy() directly -- it will fail with Unannounced. Instead, a three-step process is required:

sequenceDiagram
    participant D as Delegator (real)
    participant Chain as On-chain
    participant P as Delegate

    Note over P: Step 1: Announce
    P->>Chain: announce(real=delegator, call_hash)
    Note over Chain: Record announcement with current block height.<br/>Reserve announcement deposit from delegate.

    rect rgb(40, 40, 60)
        Note over D,Chain: Waiting period: delay blocks must pass.<br/>During this time the delegator can veto:
        D-->>Chain: reject_announcement(delegate, call_hash)
        Note over Chain: Remove announcement, return deposit to delegate.
    end

    Note over P: Step 2: Execute (after delay blocks)
    P->>Chain: proxy_announced(delegate, real, call)
    Note over Chain: Verify announcement exists and delay has elapsed.<br/>Remove announcement, return deposit, execute call.

    Note over P: Alternative: delegate cancels own announcement
    P-->>Chain: remove_announcement(real, call_hash)
    Note over Chain: Remove announcement, return deposit to delegate.
Loading

Who can call proxy_announced? Anyone. The authorization was already verified at announce time (delegate must be a valid proxy). The delay period is the security window, not an authentication step.


5. Who Pays What

Deposits (reserved from your balance, returned when removed)

What Who pays When reserved When returned Amount
Proxy deposit Delegator add_proxy remove_proxy / remove_proxies Base: 0.06 TAO + 0.033 TAO per proxy
Pure proxy deposit Spawner create_pure kill_pure 0.093 TAO (base + 1 proxy)
Announcement deposit Delegate announce proxy_announced / remove_announcement / reject_announcement Base: 0.036 TAO + 0.068 TAO per announcement

Transaction fees

Default: the account that signs the transaction pays the fee. For proxy() calls, this is the delegate.

Override with set_real_pays_fee: the delegator can call set_real_pays_fee(delegate, true) to opt in to paying fees for that delegate's proxy calls. This works for:

  • Simple proxy() calls
  • Nested proxy calls (up to 2 levels deep)
  • Batch calls containing multiple proxy() calls with the same real account

Exception: set_real_pays_fee does NOT apply to proxy_announced. The signer of proxy_announced always pays.

What happens when you remove a proxy

  • remove_proxy removes the proxy relationship and returns the deposit. It also clears the set_real_pays_fee setting for that pair.
  • It does NOT delete the delegate's pending announcements. Those announcements become harmless (proxy_announced will fail with NotProxy), but the delegate's announcement deposit stays locked until the delegate calls remove_announcement or someone calls reject_announcement.

6. Checking If a Proxied Call Succeeded

The proxy() extrinsic itself always returns Ok even if the inner call failed. To check whether the inner call actually succeeded, read the LastCallResult storage:

LastCallResult[real_account] -> Ok(()) or Err(error)

This is updated after every proxied call execution.


7. Escalation Protection

When a delegate executes a call through proxy(), the runtime prevents privilege escalation:

Scenario Result
Delegate tries add_proxy with a broader type than their own Blocked
Delegate tries remove_proxy with a broader type than their own Blocked
Delegate tries remove_proxies or kill_pure Blocked unless delegate's proxy_type is Any

"Broader type" is determined by is_superset():

graph TD
    Any["Any -- superset of all types"]
    NonTransfer["NonTransfer -- superset of all<br/>EXCEPT Transfer and SmallTransfer"]
    Transfer["Transfer -- superset of SmallTransfer"]
    SmallTransfer["SmallTransfer"]
    Others["All other types -- superset of themselves only"]

    Any --> NonTransfer
    Any --> Transfer
    Any --> Others
    NonTransfer --> Others
    Transfer --> SmallTransfer
Loading

What this means in practice: a delegate with Staking proxy cannot add a new proxy of type Any, Transfer, NonTransfer, or anything other than Staking itself. A delegate with NonTransfer can add/remove proxies of any type except Transfer and SmallTransfer.

Important: is_superset only governs proxy management (adding/removing proxies through a proxy call). It does NOT mean that NonTransfer allows all the same calls as Staking -- each type has its own independent filter (see Section 2).


8. The force_proxy_type Parameter

When calling proxy(real, force_proxy_type, call), the force_proxy_type is optional:

  • If omitted (None): the runtime picks the first matching proxy relationship for this delegate/delegator pair.
  • If provided: the runtime checks that the delegate has a proxy relationship of exactly that type. Useful when a delegate has multiple proxy relationships with different types to the same delegator.

9. Deposit Adjustment After Runtime Upgrade

If deposit amounts change in a runtime upgrade, existing deposits may be too high or too low. The poke_deposit call lets any account recalculate their proxy and announcement deposits based on current rates. If the deposit amount actually changed, the transaction fee for poke_deposit is waived.


10. Extrinsic Reference

Extrinsic Signed by What it does
add_proxy(delegate, type, delay) Delegator Add a delegate to your account
remove_proxy(delegate, type, delay) Delegator Remove a specific delegate (must match exact params)
remove_proxies() Delegator Remove all delegates at once
proxy(real, force_type, call) Delegate Execute a call as the delegator (delay must be 0)
announce(real, call_hash) Delegate Announce a future call (required when delay > 0)
proxy_announced(delegate, real, type, call) Anyone Execute an announced call after the delay has passed
reject_announcement(delegate, call_hash) Delegator Veto a pending announcement from a delegate
remove_announcement(real, call_hash) Delegate Cancel your own pending announcement
create_pure(type, delay, index) Spawner Create a new keyless account controlled by you
kill_pure(spawner, type, index, height, ext_index) Pure (via proxy) Destroy the pure account permanently
set_real_pays_fee(delegate, pays_fee) Delegator Toggle whether you pay fees for a delegate's proxy calls
poke_deposit() Anyone (for their own account) Recalculate deposits after a runtime upgrade