-
Notifications
You must be signed in to change notification settings - Fork 453
Proxy System Overview
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)
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
Allows every call. Full control over the account. The only type that allows calling kill_pure and remove_proxies through a proxy.
Allows everything EXCEPT:
-
Balances::*(all TAO transfers) SubtensorModule::transfer_stakeSubtensorModule::schedule_swap_coldkeySubtensorModule::swap_coldkey
Use case: delegate can do almost anything but cannot move funds out.
Allows everything EXCEPT:
SubtensorModule::dissolve_networkSubtensorModule::root_registerSubtensorModule::burned_registerSudo::*
Use case: delegate can operate freely but cannot destroy subnets or use sudo.
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.
-
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.
Balances::transfer_keep_aliveBalances::transfer_allow_deathBalances::transfer_allSubtensorModule::transfer_stake
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
-
AdminUtils::*(all admin calls EXCEPTsudo_set_sn_owner_hotkey) SubtensorModule::set_subnet_identitySubtensorModule::update_symbol
Use case: delegate manages subnet settings without being able to reassign ownership.
SubtensorModule::burned_registerSubtensorModule::register
SubtensorModule::set_childrenSubtensorModule::set_childkey_take
SubtensorModule::swap_hotkey
SubtensorModule::claim_root
-
Sudo::sudo_unchecked_weightONLY when it wrapsSystem::set_code - Any other call inside
sudo_unchecked_weightis rejected
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
Senate, Triumvirate, Governance, RootWeights
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
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.
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.
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.
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.
| 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 |
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.
-
remove_proxyremoves the proxy relationship and returns the deposit. It also clears theset_real_pays_feesetting for that pair. -
It does NOT delete the delegate's pending announcements. Those announcements become harmless (
proxy_announcedwill fail withNotProxy), but the delegate's announcement deposit stays locked until the delegate callsremove_announcementor someone callsreject_announcement.
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.
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
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).
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.
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.
| 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 |