Staking Contracts
Panda ships a first-class staking primitive. Two contracts anchor it:
| Contract | Role |
|---|---|
| StakingRegistry | The canonical system contract. Owns validators, delegations, the reward pool, and the reward accumulator. |
| StakingVault | An example application contract that pools user deposits and delegates them to a single validator. |
Both are exercised from the Staking SDK flow and the Staking Vault / Staking Registry playground templates. This page is the on-chain contract reference.
Contract code lives in the playground template picker under Staking. Open the playground, pick a template, and click Deploy to try it live.
StakingVault
A StakingVault lets many users pool tokens and delegate them to one validator, sharing rewards proportionally. It is a thin wrapper over the panda.staking module.
State
| Field | Type | Meaning |
|---|---|---|
validator | str | The validator this vault delegates to |
owner | str | Deployer address |
depositors | dict | address -> deposited amount |
total_deposited | int | Sum of all deposits |
Methods
| Method | Kind | Description |
|---|---|---|
deploy(validator) | constructor | Targets a validator. Reverts unless staking.validator(validator).status == "active". Emits VaultDeployed. |
deposit(amount) | call | Delegates amount via staking.delegate(...), tracks the depositor. Emits Deposited. |
claim() | call | Claims rewards via staking.claim_rewards(...). Returns the claimed amount. Emits RewardsClaimed. |
withdraw(amount) | call | Begins unbonding via staking.undelegate(...). Returns an unbonding record_id. Emits WithdrawalStarted. |
complete_withdrawal(record_id) | call | Finalizes an unbonding after the waiting period via staking.complete_unbonding(...). Returns released amount. Emits WithdrawalCompleted. |
get_validator_info() | query | Live validator info (moniker, commission, total delegated, uptime, status). |
get_depositor(address) | query | How much an address has deposited. |
get_staking_stats() | query | Unbonding period, slashing bps, reward pool, and total deposited. |
The staking lifecycle
deposit -> staking.delegate -> stake is live, rewards accrue
claim -> staking.claim_rewards -> pull rewards (no waiting period)
withdraw -> staking.undelegate -> start unbonding timer, get record_id
complete_withdrawal -> staking.complete_unbonding -> tokens released
Claiming is independent of unbonding, so it is safe to call at any time.
StakingRegistry (system contract)
The StakingRegistry is the contract panda.staking actually talks to. In production it is seeded at the well-known address staking-registry on chain boot; the playground template deploys a standalone instance so you can see the mechanics.
State highlights
| Field | Meaning |
|---|---|
validators | address -> validator record |
delegations | `"{delegator} |
authorized_validators | Admin allow-list gating registration |
reward_pool | Undistributed rewards |
acc_rewards_per_share | MasterChef-style accumulator, scaled by REWARD_SCALE = 10**18 |
total_staked | Global staked amount |
params | unbonding_period_blocks, commission_max_bps, min_self_delegation, slashing_bps_dishonesty |
admin | The only account that can authorize validators, accrue rewards, slash, and set params |
Key methods
| Method | Kind | Description |
|---|---|---|
deploy(admin, unbonding_period_blocks, commission_max_bps, min_self_delegation, slashing_bps_dishonesty, initial_authorized_validators) | constructor | Initializes params and the admin. |
set_authorized_validator(addr, authorized) | call | Admin adds/removes an address from the validator allow-list. Emits ValidatorAuthorized. |
register_validator(commission_bps, moniker, self_delegation) | call | Allow-listed caller registers; self-delegation is staked. Emits ValidatorRegistered. |
delegate(validator, amount) | call | Delegates to a validator; returns the delegation key. Emits Delegated. |
undelegate(validator, amount) | call | Begins unbonding; returns a record_id. Emits Undelegated. |
complete_unbonding(record_id) | call | Anyone-can-call; releases funds to the recorded delegator. Emits UnbondingCompleted. |
claim_rewards(validator) | call | Pull-based reward claim, capped at the reward pool. Emits RewardsClaimed. |
accrue_block_reward(amount, block_height) | call | Admin/consensus only; bumps the accumulator. Emits BlockRewardAccrued. |
slash_validator(validator, bps, reason, block_height) | call | Admin only; burns stake (self first, then delegators pro-rata) and jails the validator. Emits ValidatorSlashed. |
set_param(name, value) | call | Admin only; restricted to the mutable-param allow-list. Emits ParamUpdated. |
pause() / unpause() | call | Admin only. |
Read methods: get_validator(address), get_delegation(delegator, validator) (includes pending_rewards), get_delegations_for(delegator), list_validators(active_only), get_params(), get_unbonding_record(record_id), stats().
Why registration is gated
Without the allow-list, any address could self-register as a "validator" and start earning a pro-rata slice of block rewards without producing any blocks. set_authorized_validator is the cheapest gate that closes that sybil/grief vector. The admin (typically a Safe) controls membership, and register_validator refuses callers who are not on the list.
Reward accounting
Rewards use a pull-based MasterChef accumulator. Each block, accrue_block_reward increases acc_rewards_per_share by amount * REWARD_SCALE // total_staked. A delegator's pending reward is derived from the difference between the current accumulator and the snapshot taken when their stake last changed. This makes both accrual and claiming O(1) regardless of the number of delegators, and multiplication always happens before division to preserve precision.
Try it
- Playground: open the Playground, choose the Staking Vault or Staking Registry template, and follow the in-editor tutorial.
- SDK flow: see the Staking guide for the off-chain client view.
- SDK types: the
panda.stakingmodule reference is in SDK Types.