Wiki

stake.one technical reference

Everything we know about the staking vault, in one page. For depositors, auditors, and integrators who want to verify protocol properties before trusting the contract. Pair this with the FAQ (plain-English) and the source.

1. Deployments

One immutable contract per chain. No proxy, no upgrade path. Each contract's code is the code at deploy and forever after.

ChainStatusContract
Harmonylive0x061A7e7e317AcE450940D5aD3372Db0b7C205dE4
Injectivelive0xeB756281D670666f2CDF26a1FA89697a08d14976
Seicontract ready
Soniccontract ready
ZetaChaincontract ready

Retired Harmony vault versions are still on-chain; their state is read-only at this point (deposits paused, draining via withdrawals). Full registry on the repo.

2. What the contract promises

  • 1% fee, hardcoded. Cannot be changed by anyone, including the deployer.
  • No proxy. No upgrade mechanism exists. The bytecode is final.
  • Non-transferable vault tokens. Internal bookkeeping only — not ERC-20. Cannot be sold, traded, or wrapped.
  • No admin keys for funds. Owner has governance powers (validator weights, pause, emergency mode) but cannot unilaterally move user funds.
  • Permissionless compounding. Any vault depositor can call compound() and earn the bounty.
  • Withdrawals always go to a configured receiver, not to msg.sender. Caller pays gas; funds flow to the request's pre-set recipient.
  • Two-step ownership. Vault owner uses OpenZeppelin's Ownable2Step — accidental hand-off impossible.

3. Fee math

uint256 constant FEE_BPS = 100;            // 1% of rewards
uint256 constant CALLER_BOUNTY_BPS = 9000; // 90% of the 1% fee → caller

For every compound() call:

  • totalFee = rewards × 1%
  • callerBounty = totalFee × 90% → paid in native token to the caller
  • protocolFee = totalFee × 10% → accrued inside the vault contract in the treasuryFeesAccrued storage slot (immutable on-chain field name); the balance is held by the contract, not by any operator
  • netAdded = rewards − totalFee → re-delegated to validators per current weights
  • Existing depositors see the redemption rate rise by netAdded / totalShares

Net effect: depositors keep 99% of staking rewards via the redemption rate. The 1% fee is split 90/10 between the call-trigger and the protocol fee balance held inside the immutable vault contract — the trigger is anyone who holds vault tokens.

4. Function reference

User-callable

FunctionEffectConstraints
deposit()send tokens, get sharesvalue ≥ minDelegationWei (100); not paused
withdraw(uint256)burn shares, queue requestshares > 0, ≤ caller's balance
compound(uint256)harvest rewards, redelegate, earn bountycaller must hold shares; not in emergency
claimWithdrawal(uint256)pay out a matured requestalways pays to req.receiver
claimMultiple(uint256)batch claim oldest claimablebounded by MAX_BATCH_CLAIMS
transferWithdrawalRequest(uint256, address)transfer ownership+receiver of pending requestonly req.owner can call
withdrawClaimable(uint256)pull credited tokens if push failedamount ≤ claimableONE[caller]
emergencyWithdraw()pro-rata exit when emergency mode is ononly when emergency mode

Owner-only — instant (no timelock)

Six functions exist as instant escape hatches. Each has a narrow purpose; none can move user funds to a non-user destination.

  • pauseDeposits(bool) — block new deposits; existing flow unaffected
  • setEmergencyMode(bool) — toggle emergency mode; enables emergencyWithdraw
  • emergencyPauseValidator(address) — set a validator's weight to 0 instantly (e.g., if it gets unelected)
  • removeValidator(address) — hard remove (only if delegation = 0)
  • triggerMigration(address, uint256) — start undelegating from a validator (7-day chain unbond)
  • cancelApproveValidator(uint256) / cancelTreasury / cancelProtocolParams / cancelChainParams — abort a queued timelock before it executes

Owner-only — 7-day timelocked

Visible at /timelocks from the moment they're queued. Public window before any change executes.

  • queueApproveValidator + executeApproveValidator — add validator OR change weight
  • queueTreasury + executeTreasury — protocol fee withdrawal (rotate the address authorized to withdraw the contract-held protocol fee balance; function names are immutable on-chain)
  • queueProtocolParams + executeProtocolParams — change minDelegation / minUndelegate
  • queueChainParams + executeChainParams — change epoch blocks / unbonding epochs
  • proposeSuccessor + activateSuccessor — propose a successor vault for major migration; codehash-pinned

5. Governance powers (bounded list)

The vault owner is set at deploy via Ownable2Step. Powers are explicit and bounded — every owner action falls into one of these buckets:

What owner can NEVER do

  • Change the 1% fee or the 90/10 split
  • Mint, burn, or transfer user vault tokens
  • Move user funds to a non-vault address
  • Upgrade the contract bytecode
  • Bypass the 7-day Harmony unbonding for withdrawals
  • Force a withdrawal on behalf of a user

What owner CAN do, instantly

  • Pause new deposits (pauseDeposits)
  • Toggle emergency mode (enables pro-rata exit for users)
  • Set a single validator's weight to 0 (emergencyPauseValidator)
  • Trigger migration of delegated stake from one validator (7-day chain-side unbond still applies)
  • Cancel a queued timelocked change

What owner CAN do, after a 7-day public timelock

  • Add a validator or raise an existing one's weight
  • Protocol Fee Withdrawal — rotate the address authorized to withdraw the accrued protocol fee balance held inside the vault contract
  • Change minDelegation / minUndelegate amounts
  • Change chain params (epoch blocks, unbonding epochs)
  • Propose a successor vault (with bytecode pinning)

Every queued change appears at /timelocks with executeAfter timestamp + cancellation status. You have at minimum 7 days to react before any change executes.

6. Rescue mechanisms

Three classes of incident the contract is built to handle without admin intervention.

Compromised depositor wallet

If your wallet is phished or your seed is leaked AFTER you've initiated a withdraw, the attacker holds your key and can claim the request post-unbond. Counter: anyone with the same key can call transferWithdrawalRequest(requestId, cleanAddr), which transfers BOTH req.owner and req.receiver to a fresh wallet. The contract enforces:

if (req.owner != msg.sender) revert InvalidShares();

First-mover wins. Once the transfer lands, the attacker's stolen key cannot touch that request — even though they still have it. This is the single most important post-deploy mechanism for active operators. It's production-tested.

Validator gets unelected or misbehaves

emergencyPauseValidator(losingValidator) sets the validator's weight to 0 instantly. New deposits and compound redelegations route to other validators. To move existing stake away requires triggerMigration + the chain's 7-day unbond — there's no contract-side bypass for the network unbond, but the future-flow stop is immediate.

Critical bug discovered

proposeSuccessor(newVault, codehash) proposes a migration target. The successor's bytecode is pinned at proposal time; even if redeployed at the same address with different code, activation reverts. After 7-day timelock, owner calls activateSuccessor(), the vault becomes deprecated, and users call migrate() to move their assets to the successor. You always have 7 days to opt-out before activation.

7. Withdrawal lifecycle

  1. User calls withdraw(N) — burns N shares, creates a request with owner = caller, receiver = caller, assetsOwed = N × pricePerShare.
  2. Same-epoch instant payment if vault has liquid balance: req.assetsPaid = min(owed, liquid). Remainder waits.
  3. 7-day unbonding via _undelegate calls to the staking precompile. unlockBlock = currentBlock + (unbondingEpochs × epochBlocks).
  4. After unlock, anyone calls claimWithdrawal(requestId). Vault pays remaining to req.receiver. Caller pays gas.
  5. Push-fallback: if the receiver rejects the push (e.g., contract that reverts on receive), funds are credited to claimableONE[receiver] for explicit pull via withdrawClaimable.

The FIFO queue (nextRequestId / nextClaimableRequestId) ensures requests are processed in submission order when the vault doesn't have full liquid coverage.

8. Emergency mode

Owner-toggled instant flag. When set:

  • compound() blocked (the whenNotEmergency modifier guards it)
  • triggerMigration blocked
  • emergencyWithdraw() enabled — users can pro-rata exit immediately, no 7-day wait
  • Existing pending withdrawal requests still claimable normally

Reversible via setEmergencyMode(false). Designed for: critical bug response, coordinated user exit, governance-level threats. NOT for: regular ops, validator issues (emergencyPauseValidator handles those).

9. Per-chain notes

ChainDecimalsUnbondQuirk
Harmony18~7 daysstaking precompile at 0xfc
Injective1821 daysvalidator addresses are strings; gas estimation unreliable
Sei18 native, 6 usei21 daysdecimal bridge wei↔usei (1e12 ratio)
Sonic187 daysSFC contract (UUPS-upgradeable on Sonic side)
ZetaChain1814 daysdual precompile 0x800/0x801; non-payable delegate

Each chain has its own immutable contract. Same architecture, chain-specific bindings to native staking. Constants like fee/bounty/share semantics are identical across chains.

10. Verify it yourself

Read state directly from the contract. No backend, no API — just the chain. Install Foundry for cast:

VAULT=0x061A7e7e317AcE450940D5aD3372Db0b7C205dE4
RPC=https://api.harmony.one

# How much is staked, share economics
cast call $VAULT 'totalManagedAssets()(uint256)' --rpc-url $RPC
cast call $VAULT 'totalShares()(uint256)' --rpc-url $RPC
cast call $VAULT 'pricePerShare()(uint256)' --rpc-url $RPC

# Governance state — verify owner, no pending hand-off, not paused, not emergency
cast call $VAULT 'owner()(address)' --rpc-url $RPC
cast call $VAULT 'pendingOwner()(address)' --rpc-url $RPC
cast call $VAULT 'depositsPaused()(bool)' --rpc-url $RPC
cast call $VAULT 'emergencyMode()(bool)' --rpc-url $RPC
cast call $VAULT 'successorVault()(address)' --rpc-url $RPC

# Validator slate + weights
cast call $VAULT 'totalWeight()(uint32)' --rpc-url $RPC
cast call $VAULT 'getValidatorList()(address[])' --rpc-url $RPC
cast call $VAULT 'validators(address)(bool,uint16,uint256)' <validator-addr> --rpc-url $RPC

# Your specific position
cast call $VAULT 'sharesOf(address)(uint256)' <your-addr> --rpc-url $RPC
cast call $VAULT 'userBalance(address)(uint256)' <your-addr> --rpc-url $RPC
cast call $VAULT 'getUserRequestIds(address)(uint256[])' <your-addr> --rpc-url $RPC

# Read a withdrawal request
cast call $VAULT 'requests(uint256)(address,address,uint256,uint256,uint64,uint256,uint64,bool)' <id> --rpc-url $RPC

# Public timelock queue
cast call $VAULT 'nextTimelockId()(uint256)' --rpc-url $RPC
cast call $VAULT 'timelockQueue(uint256)(address,uint16,uint64,bool,bool)' <id> --rpc-url $RPC

The web app at /stats, /validators, and /timelocks are just thin wrappers around these calls — anything you see there, you can verify with cast.

11. What the contract cannot do

A reverse list — claims that are structurally false regardless of operator intent:

  • It cannot stop being non-custodial. Funds at all times either belong to a depositor (via their vault tokens) or are pending withdrawal to a configured receiver.
  • It cannot raise the fee. FEE_BPS = 100 is a Solidity constant — read-only, baked into bytecode.
  • It cannot pay the owner more than the 0.1% protocol fee share — there's no privileged compound-bounty path.
  • It cannot be upgraded to a different rule set. No proxy, no delegatecall-based admin slot, no UUPSUpgradeable.
  • It cannot be paused for withdrawals. Even if depositsPaused = true and emergencyMode = true, the withdrawal claim path remains open.
  • It cannot transfer your vault tokens to someone else. Vault tokens are non-transferable; the only way out is via withdraw from your own address (or transfer your withdrawal-request via your own signature).

This wiki is the technical truth. For user-facing FAQ language, see /faq. For the source code, see github.com/stake-one. For live state, see /stats.