General Description
Link: https://github.com/tonwhales/ton-nominators\\ License:GNU v3 Language:FunC A decentralized smart contract launched by the Ton Whales team on March 14, 2021, which stores all staking records and allows nominators to pool together to send funds for staking as a single block.Architecture
The contract consists of two main parts- Proxy contract - serves to relay signals from the shard chain to the elector. It is needed to save storage costs. This contract is located in the masterchain and allows not storing the main contract in it (thus saving funds).
- Main contract - allows users and the staker to interact (see below). Located in a cheaper shard chain. Each user command is processed by the staker and after verification can be converted into a command for the main contract.
Masterchain is the main part of the TON blockchain, which ensures coordination and management of all other chains (shardchains). It houses the most important contracts, but transaction costs in it are also the highest.
Shardchain is a subchain that allows for parallel transaction processing. Shardchains divide the network load, improving its scalability.
Elector is a specialized smart contract or mechanism used to manage the validator selection process in the network.
Validator is a network node (a remote high-powered computer) that is responsible for verifying and confirming transactions.
Shardchain is a subchain that allows for parallel transaction processing. Shardchains divide the network load, improving its scalability.
Elector is a specialized smart contract or mechanism used to manage the validator selection process in the network.
Validator is a network node (a remote high-powered computer) that is responsible for verifying and confirming transactions.
What is the Staker for
The Staker is a contract that implements staking logic and manages the main contract. It processes “occurring events”, understands when to send funds to the elector and when to withdraw them (the analogy of a staker in computer terminology is a driver).The Staker by itself can only send control signals. It cannot compromise anything, as all its commands are re-verified by the main contract before execution.
Staker is not part of the Ton nominators contract and is not an open source solution
Staking Entities
- Elector — a special system smart contract that conducts validator elections, accepts stakes from nodes/validators, determines candidates with the highest stake for inclusion in the next cycle, and manages stake storage and distribution, reward payments, and stake returns.
- Owner — an arbitrary wallet or DAO that can make critical changes to the contract: change fees, start/stop the pool, and even update the code. The owner also receives all profit from fees.
- Controller — an arbitrary wallet or DAO that sends staking commands for validation and/or voting. The controller pays for these operations and can withdraw “unowned” balances to restore its balance and continue operating. It’s recommended to keep around ~1000 TON for uninterrupted operation.
- Proxy (proxy contract) — the only contract located in the masterchain that represents the validation pool there. It performs simple proxying between the pool and Elector.
- Ton nominators (pool) — the main contract that stores all records of all stakes and directly performs staking.
Contract Roles
owner— pool owner:- performs deployment, updates, and pool configuration;
- participates in staking on equal terms with regular users and receives pool commission;
- manages assignment of
controllerandownerroles; - has the right to withdraw unowned tokens.
- Controller (management contract) — executive component of the system:
- directs applications to Elector for stake, recover, and finalize operations;
- processes deposits and withdrawals in batch mode through
accept_stakesandaccept_withdrawsfunctions; - has the right to forcibly exclude participants through
force_kick; - can initiate
withdraw_unownedin unsafe mode to withdraw excess balance.
- Nominators (regular participants) — external wallets of the base workchain:
- deposit funds into staking;
- withdraw staked funds;
- initiate
recoverprocedure to return stake from Elector upon completion of validation cycle; - make voluntary donations to the pool through donate function.
Stake Lifecycle
Deposit
Operation initiators:- pool owner (through
op_owner) - any nominator (through
op_nominators)
Text method — sending a message with body
"Deposit" (recognized by parse_text_command function)Binary method — sending a message containing:
op = op::stake_deposit()query_id(value greater than 0)gas_limit- sufficient amount of TON in
valuefield
In both cases, the
op_deposit(member, value) function is called, which performs the following actions:
- Checks pool activity (
enabled) - Calculates stake size:
stake = value - (receipt_price + deposit_fee) - Verifies compliance with minimum stake size (
stake >= min_stake) - Loads participant data (
load_member(member)) - Executes
member_stake_deposit(stake):- recalculates participant profit (
member_update_balance) - resets pending withdrawal
- increases
ctx_member_pending_depositandctx_balance_pending_deposits
- recalculates participant profit (
- Sends confirmation:
- when
ctx_query_id == 0(text command): text message “Stake <amount> accepted” - in other cases: standard message with
op::stake_deposit::response()andquery_id
- when
- Saves updated participant data and base state
Important note: the deposit at this stage is in pending status (
pending_deposit) and is not included in the pool’s total balance until confirmed.Deposit Acceptance
Operation initiator:- pool controller (via
op_controller)
Binary method — sending a message containing:
op = op::accept_stakes()dict members(dictionary of participants, where key is participant ID, value is not important)- sufficient amount of TON in the
valuefield
The
op_controller_accept_stakes() function is called, which performs the following actions:
- Checks fund sufficiency (
value >= params::pending_op()) - Checks that the pool is not locked (
!ctx_locked) - For each participant in the
membersdictionary:- Loads participant data (
load_member(member)) - Executes
member_accept_stake():- checks for pending deposit (
ctx_member_pending_deposit > 0) - checks that the pool is not locked (
!ctx_locked) - updates participant’s profit (
member_update_balance()) - transfers
ctx_member_pending_deposittoctx_member_balance - increases total pool balance (
ctx_balance)
- checks for pending deposit (
- Saves participant data (
store_member())
- Loads participant data (
- Saves pool base state (
store_base_data())
Important note: from this point forward, the deposit is officially recorded in the pool’s general stake and participates in validation.
Withdrawal Request
Operation initiators:- pool owner (via
op_owner) - any nominator (via
op_nominators)
Text method — sending a message with body
"Withdraw" or "Withdraw all"Binary method — sending a message containing:
op = op::stake_withdraw()query_id(value greater than 0)stake(amount to withdraw, 0 means “withdraw all”)- sufficient TON amount in
valuefield to cover fees
In both cases, the
op_withdraw(member, value, stake) function is called, which performs the following actions:
- Validates fee correctness:
value == receipt_price + withdraw_fee - Loads member data (
load_member(member)) - Executes
member_stake_withdraw(stake):- When
stake == 0, sets full withdrawal flag (withdraw_all = true) - Calculates total amount to withdraw from all sources
- Attempts to withdraw funds in the following order:
- from pending deposits (
ctx_member_pending_deposit) - from previously requested withdrawals (
ctx_member_withdraw) - from active stake (
ctx_member_balance), if pool is not locked
- from pending deposits (
- Places remainder in withdrawal queue (
ctx_member_pending_withdraw) - Returns immediate withdrawal amount and operation completion status
- When
- Determines recipient address:
- for owner —
ctx_owner - for nominator —
serialize_work_addr(member)
- for owner —
- Sends confirmation:
- when
ctx_query_id == 0(text command): text message with amount and status- “Withdraw completed” — for full withdrawal
- “Withdraw requested…” — for partial withdrawal
- in other cases: standard message with
op::stake_withdraw::response()orop::stake_withdraw::delayed()
- when
- Saves updated member data and base state
Important note: funds placed in the withdrawal queue (
pending_withdraw) will be withdrawn after the pool is unlocked by the controller.Accepting Withdrawals
Operation initiator:- pool controller (via
op_controller)
Binary method — sending a message containing:
op = op::accept_withdraws()dict members(dictionary of participants, where key is participant ID, value is not important)- sufficient amount of TON in the
valuefield
The
op_controller_accept_withdraws() function is called, which performs the following actions:
- Checks fund sufficiency (
value >= params::pending_op()) - Checks that the pool is not locked (
!ctx_locked) - For each participant in the
membersdictionary:- Loads participant data (
load_member(member)) - Executes
member_accept_withdraw():- checks for pending withdrawal (
ctx_member_pending_withdraw > 0) - checks that the pool is not locked (
!ctx_locked) - updates participant’s profit (
member_update_balance()) - takes
amount = ctx_member_pending_withdrawand transfers it:- decreases
ctx_member_balancebyamount - increases
ctx_member_withdrawbyamount
- decreases
- at pool level:
- decreases
ctx_balancebyamount - increases
ctx_balance_withdrawbyamount - decreases
ctx_balance_pending_withdrawbyamount
- decreases
- resets
ctx_member_pending_withdrawandctx_member_pending_withdraw_allflag
- checks for pending withdrawal (
- Saves participant data (
store_member())
- Loads participant data (
- Saves pool base state (
store_base_data())
Important note: after accepting a withdrawal, the funds transition to a ready-to-receive status (
ctx_member_withdraw). External code (script/controller/owner) can send the accumulated amount to the user via regular transfer, since the contract does not initiate transfers itself but only maintains records.Gas
Nominators and owner:- Pay gas fees for deposit and withdrawal operations
- Cover the cost of receipts according to the receipt_price parameter
- When executing staking, adds fees::stake_fees() commission on top of the base amount
- Uses pool balance exclusively for stake formation
- When returning stake, covers gas expenses for processing the response message from the returned amount (value)
- Maintains a minimum reserve of 1 TON to ensure data storage
The withdrawal cost for a user is 0.2 TON, with unused remainder returned to the user’s wallet balance after sending. If two transactions are required to complete the withdrawal, the costs will double.
The cost of sending the total stake to the elector is 6 TON, with unused remainder returned to the controller’s balance after sending.
Audits
Quantstamp
What was checked
- Staking/nominator pool on TON (nominators.fc contract + op-, store-, proxy modules, TypeScript wrapper).
- Deposit/withdrawal logic, stake delegation to validator, reward and loss distribution.
- Access control (owner, controller, proxy), contract upgrades and parameter changes.
- Error handling and bounce messages, balance accounting and profit-per-coin model.
- Storage limits (nominators dictionary), fee economics and general best practices.
- Tests (11 test suites, 41 tests, monkey/large/fuzz).
Result
Minor issues were identified and have been resolved.ToB
What was checked
- TON nominator pool (nominators-v2, FunC).
- Roles and privileges (owner, controller, nominators).
- Main flows: deposits, withdrawals, lock/unlock, sending stake to validator and its return.
- Profit/loss distribution among nominators.
- Bounce message handling and gas/message values.
- Possible race conditions (message order, elector responses).
- Documentation and test quality.