1. Deployments
One immutable contract per chain. No proxy, no upgrade path. Each contract's code is the code at deploy and forever after.
| Chain | Status | Contract |
|---|---|---|
| Harmony | live | 0x061A7e7e317AcE450940D5aD3372Db0b7C205dE4 |
| Injective | live | 0xeB756281D670666f2CDF26a1FA89697a08d14976 |
| Sei | contract ready | — |
| Sonic | contract ready | — |
| ZetaChain | contract 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 → callerFor every compound() call:
totalFee = rewards × 1%callerBounty = totalFee × 90%→ paid in native token to the callerprotocolFee = totalFee × 10%→ accrued inside the vault contract in thetreasuryFeesAccruedstorage slot (immutable on-chain field name); the balance is held by the contract, not by any operatornetAdded = 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
| Function | Effect | Constraints |
|---|---|---|
| deposit() | send tokens, get shares | value ≥ minDelegationWei (100); not paused |
| withdraw(uint256) | burn shares, queue request | shares > 0, ≤ caller's balance |
| compound(uint256) | harvest rewards, redelegate, earn bounty | caller must hold shares; not in emergency |
| claimWithdrawal(uint256) | pay out a matured request | always pays to req.receiver |
| claimMultiple(uint256) | batch claim oldest claimable | bounded by MAX_BATCH_CLAIMS |
| transferWithdrawalRequest(uint256, address) | transfer ownership+receiver of pending request | only req.owner can call |
| withdrawClaimable(uint256) | pull credited tokens if push failed | amount ≤ claimableONE[caller] |
| emergencyWithdraw() | pro-rata exit when emergency mode is on | only 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 unaffectedsetEmergencyMode(bool)— toggle emergency mode; enablesemergencyWithdrawemergencyPauseValidator(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 weightqueueTreasury+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 / minUndelegatequeueChainParams+executeChainParams— change epoch blocks / unbonding epochsproposeSuccessor+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
- User calls
withdraw(N)— burns N shares, creates a request withowner = caller,receiver = caller,assetsOwed = N × pricePerShare. - Same-epoch instant payment if vault has liquid balance:
req.assetsPaid = min(owed, liquid). Remainder waits. - 7-day unbonding via
_undelegatecalls to the staking precompile.unlockBlock = currentBlock + (unbondingEpochs × epochBlocks). - After unlock, anyone calls
claimWithdrawal(requestId). Vault pays remaining toreq.receiver. Caller pays gas. - Push-fallback: if the receiver rejects the push (e.g., contract that reverts on receive), funds are credited to
claimableONE[receiver]for explicit pull viawithdrawClaimable.
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 (thewhenNotEmergencymodifier guards it)triggerMigrationblockedemergencyWithdraw()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
| Chain | Decimals | Unbond | Quirk |
|---|---|---|---|
| Harmony | 18 | ~7 days | staking precompile at 0xfc |
| Injective | 18 | 21 days | validator addresses are strings; gas estimation unreliable |
| Sei | 18 native, 6 usei | 21 days | decimal bridge wei↔usei (1e12 ratio) |
| Sonic | 18 | 7 days | SFC contract (UUPS-upgradeable on Sonic side) |
| ZetaChain | 18 | 14 days | dual 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 $RPCThe 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 = 100is a Solidityconstant— 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, noUUPSUpgradeable. - It cannot be paused for withdrawals. Even if
depositsPaused = trueandemergencyMode = 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
withdrawfrom 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.
