Общее описание
Ссылка: https://github.com/tonwhales/ton-nominators\\ Лицензия: GNU v3 Язык: FunC Децентрализованный смарт-контракт, запущенный командой Ton Whales 14 марта 2021 года, который хранит все записи о стейкинге и позволяет номинаторам объединять средства для отправки в стейкинг единым блоком.Архитектура
Контракт состоит из двух основных частей:- Proxy contract — служит для ретрансляции сигналов из shardchain в elector. Он нужен для снижения затрат на хранение. Этот контракт расположен в masterchain и позволяет не хранить там основной контракт, что экономит средства.
- Main contract — позволяет пользователям и стейкеру взаимодействовать друг с другом. Находится в более дешевой shardchain. Каждая пользовательская команда обрабатывается стейкером и после проверки может быть преобразована в команду для основного контракта.
Masterchain — основная часть блокчейна TON, которая обеспечивает координацию и управление всеми остальными цепочками, то есть shardchains. В ней размещаются самые важные контракты, но и стоимость транзакций там выше всего.
Shardchain — подцепочка, которая позволяет обрабатывать транзакции параллельно. Shardchains распределяют нагрузку сети и повышают ее масштабируемость.
Elector — специализированный смарт-контракт или механизм, используемый для управления процессом выбора валидаторов в сети.
Validator — узел сети, то есть удаленный высокопроизводительный компьютер, отвечающий за проверку и подтверждение транзакций.
Shardchain — подцепочка, которая позволяет обрабатывать транзакции параллельно. Shardchains распределяют нагрузку сети и повышают ее масштабируемость.
Elector — специализированный смарт-контракт или механизм, используемый для управления процессом выбора валидаторов в сети.
Validator — узел сети, то есть удаленный высокопроизводительный компьютер, отвечающий за проверку и подтверждение транзакций.
Зачем нужен Staker
Staker — это контракт, который реализует логику стейкинга и управляет основным контрактом. Он обрабатывает возникающие события, понимает, когда нужно отправить средства в elector и когда их нужно вывести. Если проводить аналогию из компьютерной терминологии, staker похож на драйвер.Сам по себе staker может отправлять только управляющие сигналы. Он не может скомпрометировать систему, так как все его команды повторно проверяются основным контрактом перед исполнением.
Staker не является частью контракта Ton nominators и не является open source-решением.
Сущности стейкинга
- Elector — специальный системный смарт-контракт, который проводит выборы валидаторов, принимает стейки от нод и валидаторов, определяет кандидатов с наибольшим стейком для включения в следующий цикл, а также управляет хранением стейка, распределением наград и возвратом средств.
- Owner — произвольный кошелек или DAO, который может вносить критически важные изменения в контракт: менять комиссии, запускать и останавливать пул и даже обновлять код. Owner также получает всю прибыль от комиссий.
- Controller — произвольный кошелек или DAO, который отправляет команды для стейкинга, валидации и голосования. Он оплачивает эти операции и может выводить “ничейные” балансы, чтобы восстанавливать свой баланс и продолжать работу. Для бесперебойной работы рекомендуется держать около 1000 TON.
- Proxy — единственный контракт, расположенный в masterchain и представляющий там пул валидации. Он выполняет простое проксирование между пулом и Elector.
- Ton nominators (pool) — основной контракт, который хранит все записи о стейках и непосредственно выполняет стейкинг.
Роли контракта
owner— владелец пула:- выполняет деплой, обновления и настройку пула;
- участвует в стейкинге на тех же условиях, что и обычные пользователи, и получает комиссию пула;
- управляет назначением ролей
controllerиowner; - имеет право выводить ничейные токены.
- Controller (управляющий контракт) — исполнительный компонент системы:
- направляет заявки в Elector для операций stake, recover и finalize;
- обрабатывает пополнения и выводы пакетно через функции
accept_stakesиaccept_withdraws; - имеет право принудительно исключать участников через
force_kick; - может инициировать
withdraw_unownedв unsafe-режиме для вывода избыточного баланса.
- Nominators (обычные участники) — внешние кошельки base workchain:
- вносят средства в стейкинг;
- выводят застейканные средства;
- инициируют процедуру
recover, чтобы вернуть стейк из Elector после завершения цикла валидации; - делают добровольные пожертвования в пул через функцию donate.
Жизненный цикл стейка
Пополнение
Инициаторы операции:- владелец пула, через
op_owner - любой номинатор, через
op_nominators
Текстовый метод — отправка сообщения с телом
"Deposit", которое распознается функцией parse_text_command.Бинарный метод — отправка сообщения, содержащего:
op = op::stake_deposit()query_id, значение больше 0gas_limit- достаточную сумму TON в поле
value
В обоих случаях вызывается функция
op_deposit(member, value), которая выполняет следующие действия:
- Проверяет активность пула, то есть
enabled. - Вычисляет размер стейка:
stake = value - (receipt_price + deposit_fee). - Проверяет соответствие минимальному размеру стейка:
stake >= min_stake. - Загружает данные участника через
load_member(member). - Выполняет
member_stake_deposit(stake):- пересчитывает прибыль участника через
member_update_balance, - сбрасывает ожидающий вывод,
- увеличивает
ctx_member_pending_depositиctx_balance_pending_deposits.
- пересчитывает прибыль участника через
- Отправляет подтверждение:
- если
ctx_query_id == 0, то есть текстовая команда, отправляется текстовое сообщение"Stake <amount> accepted", - в остальных случаях отправляется стандартное сообщение с
op::stake_deposit::response()иquery_id.
- если
- Сохраняет обновленные данные участника и базовое состояние.
Важно: на этом этапе депозит находится в статусе pending, то есть
pending_deposit, и не включается в общий баланс пула до подтверждения.Принятие депозита
Инициатор операции:- controller пула через
op_controller
Бинарный метод — отправка сообщения, содержащего:
op = op::accept_stakes()dict members, то есть словарь участников, где ключ — идентификатор участника, а значение несущественно- достаточную сумму TON в поле
value
Вызывается функция
op_controller_accept_stakes(), которая выполняет следующие действия:
- Проверяет достаточность средств:
value >= params::pending_op(). - Проверяет, что пул не заблокирован:
!ctx_locked. - Для каждого участника в словаре
members:- загружает данные участника через
load_member(member), - выполняет
member_accept_stake():- проверяет наличие ожидающего депозита:
ctx_member_pending_deposit > 0, - проверяет, что пул не заблокирован:
!ctx_locked, - обновляет прибыль участника через
member_update_balance(), - переносит
ctx_member_pending_depositвctx_member_balance, - увеличивает общий баланс пула
ctx_balance.
- проверяет наличие ожидающего депозита:
- сохраняет данные участника через
store_member().
- загружает данные участника через
- Сохраняет базовое состояние пула через
store_base_data().
Важно: с этого момента депозит официально зафиксирован в общем стейке пула и участвует в валидации.
Запрос на вывод
Инициаторы операции:- владелец пула через
op_owner - любой номинатор через
op_nominators
Текстовый метод — отправка сообщения с телом
"Withdraw" или "Withdraw all".Бинарный метод — отправка сообщения, содержащего:
op = op::stake_withdraw()query_id, значение больше 0stake, то есть сумма к выводу, где 0 означает “вывести все”- достаточную сумму TON в поле
valueдля покрытия комиссий
В обоих случаях вызывается функция
op_withdraw(member, value, stake), которая выполняет следующие действия:
- Проверяет корректность комиссии:
value == receipt_price + withdraw_fee. - Загружает данные участника через
load_member(member). - Выполняет
member_stake_withdraw(stake):- если
stake == 0, устанавливает флаг полного выводаwithdraw_all = true, - рассчитывает общую сумму, доступную к выводу из всех источников,
- пытается вывести средства в следующем порядке:
- из ожидающих депозитов, то есть
ctx_member_pending_deposit, - из ранее запрошенных выводов, то есть
ctx_member_withdraw, - из активного стейка, то есть
ctx_member_balance, если пул не заблокирован.
- из ожидающих депозитов, то есть
- помещает остаток в очередь на вывод
ctx_member_pending_withdraw, - возвращает сумму мгновенного вывода и статус завершения операции.
- если
- Определяет адрес получателя:
- для owner это
ctx_owner, - для номинатора это
serialize_work_addr(member).
- для owner это
- Отправляет подтверждение:
- если
ctx_query_id == 0, отправляет текстовое сообщение с суммой и статусом:"Withdraw completed"— для полного вывода,"Withdraw requested..."— для частичного вывода;
- в остальных случаях отправляет стандартное сообщение с
op::stake_withdraw::response()илиop::stake_withdraw::delayed().
- если
- Сохраняет обновленные данные участника и базовое состояние.
Важно: средства, помещенные в очередь на вывод, то есть
pending_withdraw, будут выведены после разблокировки пула контроллером.Принятие вывода
Инициатор операции:- controller пула через
op_controller
Бинарный метод — отправка сообщения, содержащего:
op = op::accept_withdraws()dict members, то есть словарь участников, где ключ — идентификатор участника, а значение несущественно- достаточную сумму TON в поле
value
Вызывается функция
op_controller_accept_withdraws(), которая выполняет следующие действия:
- Проверяет достаточность средств:
value >= params::pending_op(). - Проверяет, что пул не заблокирован:
!ctx_locked. - Для каждого участника в словаре
members:- загружает данные участника через
load_member(member), - выполняет
member_accept_withdraw():- проверяет наличие ожидающего вывода:
ctx_member_pending_withdraw > 0, - проверяет, что пул не заблокирован:
!ctx_locked, - обновляет прибыль участника через
member_update_balance(), - берет
amount = ctx_member_pending_withdrawи переносит его:- уменьшает
ctx_member_balanceнаamount, - увеличивает
ctx_member_withdrawнаamount;
- уменьшает
- на уровне пула:
- уменьшает
ctx_balanceнаamount, - увеличивает
ctx_balance_withdrawнаamount, - уменьшает
ctx_balance_pending_withdrawнаamount;
- уменьшает
- сбрасывает
ctx_member_pending_withdrawи флагctx_member_pending_withdraw_all.
- проверяет наличие ожидающего вывода:
- сохраняет данные участника через
store_member().
- загружает данные участника через
- Сохраняет базовое состояние пула через
store_base_data().
Важно: после принятия вывода средства переходят в статус готовности к получению, то есть
ctx_member_withdraw. Внешний код, например скрипт, controller или owner, может отправить накопленную сумму пользователю обычным переводом, так как сам контракт не инициирует переводы, а только ведет учет.Gas
Номинаторы и owner:- оплачивают gas за операции пополнения и вывода,
- покрывают стоимость receipt согласно параметру
receipt_price.
- при выполнении стейкинга добавляет комиссию
fees::stake_fees()поверх базовой суммы, - использует баланс пула исключительно для формирования стейка.
- при возврате стейка покрывает расходы на gas за обработку ответного сообщения из возвращаемой суммы, то есть
value.
- поддерживает минимальный резерв в 1 TON для хранения данных.
Стоимость вывода для пользователя также составляет 0.2 TON, а неиспользованный остаток возвращается после отправки. Если для завершения вывода требуется две транзакции, стоимость удваивается.
Стоимость отправки всего стейка в elector составляет 6 TON, а неиспользованный остаток возвращается на баланс controller после отправки.
Аудиты
Quantstamp
Что проверялось
- стейкинг-пул номинаторов в TON, включая контракт
nominators.fc, модулиop-_,store-_, proxy-модули и TypeScript-обертку; - логика пополнения и вывода, делегирование стейка валидатору, распределение прибыли и убытков;
- контроль доступа для owner, controller и proxy, обновления контрактов и изменение параметров;
- обработка ошибок и bounce-сообщений, учет баланса и модель profit-per-coin;
- лимиты хранения, включая словарь nominators, экономику комиссий и общие best practices;
- тесты, включая 11 test suites, 41 test и monkey, large, fuzz-сценарии.