Деплой

Протокол деплоится в двух сетях, в порядке, который не обсуждается.

Кошельки

Три разные роли, никогда один и тот же ключ.

Кошелёк Что он может
Владелец протокола Апгрейдить каждый модуль, кроме токенов, лока ликвидности и деплоера зеркальных волтов; подключать модули к фабрике; регистрировать корзины; задавать однократно задаваемые адреса; менять настройки протокола; задавать списки исключений аирдропа и регистрировать OFT каждой акции; немедленно перемещать активы в экстренной ситуации; выводить то, что модуль держит по ошибке (rescue); запускать или отменять режим завершения и выводить ликвидность, когда истекут его 30 дней
Кипер Запускать конвертации, батчи моста и аирдроп: открывать циклы, отправлять акции, размещать отложенные акции. С 2026-10-05 также выплачивать то, что хук и лок удерживают для получателя, и собирать LP-комиссии — это вызовы, открытые для любого
Деплоер Деплоить контракты. Он владеет фабрикой, пока владелец протокола не примет владение, и администрирует удалённый хаб, пока первый батч моста не назначит там владельца протокола

Скрипты берут подписанта из командной строки forge или из DEPLOYER_PRIVATE_KEY, заданного в открытом виде в переменных окружения. DeployEthereumRail, DeployProtocol, DeployRemote и DeployBridge принимают оба варианта; LaunchProtocol, RegisterBaskets и CreateMarket читают только DEPLOYER_PRIVATE_KEY.

С десятого цикла аудита каждая рассылка (broadcast) идёт с --slow --skip-simulation. Без них forge даёт каждой транзакции столько газа, сколько насчитала его собственная симуляция, по ценам до обновления Glamsterdam в Ethereum, а создание контракта после него требует в четыре-семь раз больше: каждому созданию не хватило бы газа. С ними forge берёт оценку узла для каждой транзакции, когда предыдущая уже смайнена. Поэтому цифры газа пробного прогона — тоже не бюджет: при Glamsterdam DeployProtocol требует около 240 миллионов газа, а запуск каждого рынка — от 15 до 24 миллионов.

Порядок

Порядок определяют три ограничения. Каждый модуль в Ethereum получает адрес фабрики при создании, поэтому фабрика идёт первой, а её владелец затем подключает к ней всё остальное. Роутер и оракул канала в Ethereum, как и хаб моста, привязаны к фабрике, поэтому они идут после протокола. Два хаба фиксируют адреса друг друга, предсказанные заранее.

  1. DeployProtocol: сначала фабрика, затем хук, намайненный под все 14 разрешений v4, лок, регистратор владения, деплоеры, имплементация волта, lens, своп-роутер и контракт аирдропа, AirdropDistributor, все подключённые к фабрике. Затем владение переходит к владельцу протокола, который должен его принять
  2. DeployEthereumRail с адресом фабрики: оракул и роутер ETH → USDC на Ethereum, которые владелец задаёт в фабрике
  3. DeployBridge --sig "predict()": выводит адреса, которые получат хаб моста и его адаптер
  4. DeployRemote на Robinhood Chain: сначала удалённый хаб, привязанный к этим предсказанным адресам, затем роутер акций, оракул и имплементация зеркального волта, которые администратор хаба подключает к нему, вместе с маршрутом аирдропа и адаптерами акций, если они заданы (см. ниже). С 2026-10-06 он при каждом запуске задаёт две политики газа удалённого хаба для доставок аирдропа на Ethereum, до маршрута аирдропа (см. ниже), и две защиты оракула, и отказывается запускаться без решения о проверке секвенсора: SEQUENCER_UPTIME_FEED — фид аптайма секвенсора L2 от Chainlink на Robinhood Chain — или SEQUENCER_CHECK_OFF=true — проверка, выключенная по выбору, — но никогда не оба сразу, а SEQUENCER_GRACE_PERIOD (по умолчанию 3 600 секунд) — только вместе с фидом. Chainlink не публикует такого фида для Robinhood Chain, поэтому запуск в мейннете сегодня задаёт SEQUENCER_CHECK_OFF=true, а владелец StockFun задаст фид позже (setSequencerUptimeFeed), если он будет опубликован. Затем скрипт включает паузу оракула каждой акции (setOraclePauseCheck) — после хаба, роутера и оракула, чтобы ни один предсказанный адрес не сдвинулся
  5. DeployBridge: хаб моста и его адаптер, по предсказанным адресам; владелец один раз назначает адаптер на хабе (setAdapter). С 2026-10-05 скрипт останавливается, ничего не задеплоив, если фабрика уже указывает хаб (с 2026-10-06 это его первая проверка, ещё до предсказанных адресов), а когда его подписант владеет фабрикой, сопоставляет акции до того, как назначить хаб
  6. setBridgeHub — до первого рынка. С 2026-10-05 он отклоняет хаб, который не может обслужить уже зарегистрированную корзину (UnmappedBridgeStock)
  7. addStockMapping на хабе моста, для каждой акции из корзин
  8. Зарегистрировать корзины: PlanBridgeBaskets выводит вызовы владельца. С 2026-10-06 корзина содержит не более пяти акций: см. Корзины
  9. LaunchProtocol: $STOCKFUN, весь саплай которого идёт в его заблокированную позицию, его волт, BuybackBurner и setBuybackWallet. Ему нужно, чтобы контракт аирдропа был назначен заранее, — это делает DeployProtocol; с 2026-10-05 сам скрипт вносит оператора запуска в список исключений $STOCKFUN до минта, для адреса, который займёт токен. С 2026-10-06 запуск, остановившийся до назначения рынка протокола, возобновляется с оставленными им токеном и волтом (--sig "resume(address,address)"), которые сначала проверяются, вместо минта второго $STOCKFUN; как только рынок протокола назначен, скрипт больше не запускается, а оставшиеся шаги выполняются вручную
  10. registerStockOft на контракте аирдропа, силами владельца, для OFT каждой акции на Ethereum

Каждый апгрейдируемый модуль деплоится как два контракта: сначала его имплементация, затем его прокси. predict() учитывает их: прокси хаба моста приходится на nonce + 1 деплоера, а прокси его адаптера — на nonce + 3.

StockFun не связывает ни одного пира LayerZero: пиры USDG OFT принадлежат его эмитенту, а префлайт лишь проверяет их.

С 2026-10-06 локальный скрипт и скрипт для тестнета, LocalRun и DeployTestnetBridge, записывают свой файл деплоя, только когда действительно отправляют транзакции (broadcast), а с девятого цикла аудита — и DeployTestnetRail: у адресов пробного прогона нет кода. В тестнете DeployTestnetRail принимает те же три входа для секвенсора, все необязательные (без фида проверка остаётся выключенной, поскольку Chainlink не указывает такого фида и для тестнета), и включает паузу оракула каждой акции; скрипты для Ethereum оставляют обе защиты выключенными. Кипер в тестнете работает с KEEPER_REQUIRE_MARKET_OPEN, KEEPER_AIRDROP_AFTER_SESSION и, с 2026-10-06, KEEPER_CONVERT_ONCE_PER_WINDOW со значением false, чтобы конвертировать на каждом проходе: см. Кипер. С седьмого цикла аудита, 2026-10-06, кипер перед запуском сверяет сеть каждого RPC со своей конфигурацией: кипер в тестнете задаёт KEEPER_CHAIN_ID=11155111 и, с мостом, KEEPER_REMOTE_CHAIN_ID=46630, а кипер в мейннете — KEEPER_CHAIN_ID=1 с 4663. С восьмого цикла аудита оба обязательны: кипер отказывается запускаться без KEEPER_CHAIN_ID или без KEEPER_REMOTE_CHAIN_ID при мосте. Worker данных приложения так же проверяет сеть своих эндпоинтов, а его публичные RPC следуют двум его идентификаторам сети, поэтому Worker в тестнете нуждается только в них.

Тестнетовый запуск LayerZero от 2026-10-06 задеплоил протокол в Sepolia и в тестнете Robinhood Chain (сеть 46630) продакшн-скриптами или тестнетовыми обёртками, сохраняющими их тело, со своими тестовыми токенами, площадками и адаптерами, в папке контрактов только для тестнета. С девятого цикла аудита префлайт проверяет и этот запуск, если ему даны два его файла, с SEPOLIA_RPC_URL и ROBINHOOD_TESTNET_RPC_URL. Что этот запуск доказал, а что нет, описано в разделе Тестирование и верификация.

Аирдроп

С 2026-10-04 контракт аирдропа деплоится вместе с протоколом. Его настройки берутся из переменных окружения:

Скрипт Переменная По умолчанию Роль
DeployProtocol AIRDROP_LZ_ENDPOINT Нет: только локальный канал Эндпоинт LayerZero на Ethereum, для акций, купленных на Robinhood Chain
DeployProtocol AIRDROP_REMOTE_EID 30416 при заданном эндпоинте Идентификатор эндпоинта LayerZero для Robinhood Chain, единственного источника доставки
DeployProtocol AIRDROP_CYCLE_LENGTH 86 400 (24 часа) Длина окна, в секундах, целое число часов
DeployProtocol AIRDROP_CYCLE_OFFSET 46 800 (13:00 UTC) Момент, когда заканчиваются окна, в секундах после 00:00 UTC, целое число часов: до открытия торгов в США круглый год
DeployRemote AIRDROP_DISTRIBUTOR Нет Контракт аирдропа на Ethereum, в который отправляют зеркальные волты
DeployRemote AIRDROP_RECEIVE_GAS, _MIN, _MAX 650 000, 200 000, 1 500 000 С 2026-10-06: газ lzReceive каждой доставки на Ethereum сверх того, что задаёт OFT акции, когда кипер запрашивает значение по умолчанию, а также нижний предел и потолок того, что он может запросить
DeployRemote AIRDROP_COMPOSE_GAS, _MIN, _MAX 1 250 000, 600 000, 4 000 000 Газ вызова каждой доставки на контракте аирдропа, lzCompose, по тому же принципу. До 2026-10-06 одна цифра, 600 000, задавала газ каждой доставки
DeployRemote STOCK_ADAPTERS Нет Список через запятую, по одному адаптеру LayerZero на каждый элемент STOCKS, ноль для акции без адаптера

Это начальные значения: позже владелец может изменить расписание (setCycleSchedule) и эндпоинт LayerZero (setLayerZero). В DeployRemote переменные AIRDROP_DISTRIBUTOR и STOCK_ADAPTERS необязательны: администратор удалённого хаба может задать их позже (setAirdrop, setStockAdapter). Две политики газа задаются при каждом запуске, из переменных окружения или из значений хаба по умолчанию, до дистрибьютора, газ compose которого должен укладываться в них; позже администратор может их изменить (setAirdropReceiveGas, setAirdropComposeGas). Значение больше uint128 останавливает запуск. Затем идут шаги владельца:

  • setAirdropDistributor на фабрике, это делает DeployProtocol. Позже владелец может назначить другой контракт; волты читают его в реальном времени, а заменённый дистрибьютор сохраняет каждый свой цикл доступным для клейма там же
  • registerStockOft на контракте аирдропа, для OFT каждой акции на Ethereum, который должен использовать собственный эндпоинт LayerZero этого контракта
  • setExclusions, только для токена, которому нужно исключить адреса помимо адреса сжигания: по умолчанию такого нет ни у одного токена рынка; список $STOCKFUN, с его оператором запуска, задаёт LaunchProtocol

Самих адаптеров акций, по одному на акцию, — lockbox-адаптера на Robinhood Chain и его OFT на Ethereum — в репозитории для мейннета нет: им нужен пакет LayerZero oft-evm. DeployRemote принимает их адреса. Тестнетовый запуск LayerZero от 2026-10-06 использовал тестовые адаптеры — OFTAdapter от LayerZero поверх тестовых акций и OFT от LayerZero для обёрнутых акций — в своей папке только для тестнета.

Каждая доставка с Robinhood Chain выполняется на Ethereum двумя вызовами: lzReceive OFT акции, который минтит обёрнутую акцию в контракт аирдропа, затем lzCompose контракта аирдропа, который её зачисляет. С 2026-10-06 кипер при каждой отправке указывает газ обоих, выбранный по симуляциям на Ethereum (см. Кипер), а удалённый хаб ограничивает каждое значение рамками своей политики, причём ноль означает значение по умолчанию. Значения по умолчанию покрывают с запасом в 35 % и 30 % самые тяжёлые случаи, измеренные в Sepolia после апгрейда Ethereum Glamsterdam: там lzReceive нужно 184 702 газа для зачисления на баланс, который контракт аирдропа уже держит, и 481 548 для самой первой доставки акции; compose — 105 075, когда цикл уже перечисляет акцию, 433 645, когда цикл, открытый кипером, её ещё не перечисляет, около 531 600, когда доставка к тому же первое зачисление цикла, и 962 154, когда она сама открывает цикл. Самой тяжёлой доставке, построенной в тестах, — открытию при шестнадцати исключённых холдерах с длинной историей, которое к тому же забирает четыре отложенные акции, — нужно около 2,8 млн по ценам Glamsterdam, ниже потолка compose. До Glamsterdam, по замерам «на холодную» в тестах, доставка в открытый цикл занимала около 85 000, доставка, которая открывает цикл при одном исключённом адресе, — около 275 000, а самая тяжёлая — около 1 016 000. Доставка, которой не хватило газа, не проходит, ничего не теряя: она остаётся сохранённой на эндпоинте LayerZero, обёрнутые акции уже находятся в контракте аирдропа, если не прошёл только compose, и любой может выполнить её повторно с большим газом. С 2026-10-06 это делает сам кипер, в пределах собственного ограничения, и подаёт оповещение о втором сбое.

Подключение модулей к фабрике

Фабрика инициализируется только с владельцем и тремя кошельками; её настройки начинают со значений по умолчанию. Всё остальное владелец назначает потом:

  • setLaunchModules: хук и лок, раз и навсегда; оба должны указывать на эту фабрику, а лок ещё и на этот хук
  • setDeployers: два деплоера, которые можно заменить
  • setVaultImplementation: имплементация, стоящая за волтами рынков, созданных после этого
  • setHoldingRecorder: регистратор, которому сообщают о переводах новые токены рынков
  • setAirdropDistributor: контракт аирдропа, которому волты передают свои акции; он должен указывать на эту фабрику
  • setTreasuryRouter, setTreasuryOracle, setSwapRouter, setBridgeHub и setProtocolMarket

Ни один рынок нельзя создать, пока не назначены лок, имплементация волта и регистратор владения.

Хаб моста и своп-роутер

setBridgeHub можно задать только один раз. Если его пропустить, это уже не исправить. С 2026-10-05 он отклоняет хаб, который не может сопоставить каждую акцию уже зарегистрированных корзин: сначала сопоставьте их на хабе.

setBridgeHub нужно вызвать до создания первого рынка. Каждый TreasuryVault фиксирует адрес хаба моста при создании. Волт, созданный, пока адрес нулевой, навсегда остаётся на локальном канале и никогда ничего не отправляет в Robinhood Chain.

setSwapRouter не относится к настройкам с однократной записью: владелец может изменить его в любой момент. Он больше ни на один волт не влияет: волты перестали его читать, когда байбэк создателя был удалён из кода, 2026-09-28. Он записывает адрес официального своп-роутера, который проверяет префлайт.

Хук и его намайненный адрес

Адрес хука кодирует его разрешения v4 в младших битах: его находят перебором соли CREATE2. С 2026-10-02 майнится адрес прокси хука, со всеми 14 выставленными битами разрешений. Он зависит от байт-кода прокси и аргументов его конструктора, которые содержат адрес имплементации. Новый деплой придётся майнить заново; апгрейд сохраняет адрес.

foundry.toml должен содержать bytecode_hash = "none" и evm_version = "cancun", иначе намайненный адрес не совпадёт с задеплоенным контрактом.

Апгрейды

Апгрейд — это вызов владельца протокола на прокси модуля с указанием новой имплементации; он вступает в силу сразу. Перед каждым апгрейдом contracts/script/check-storage-layouts.sh сравнивает новую раскладку хранилища с той, что записана в contracts/storage-layouts/, и завершается с ошибкой при любом изменении, кроме дописывания в конец. С 2026-10-05 он сравнивает каждую структуру на всех уровнях вложенности, включая размеры, и отклоняет любое изменение структуры, которая является элементом массива в хранилище: расти, в своём конце, может только структура, являющаяся значением маппинга, или последняя переменная состояния. --write обновляет записи после намеренного изменения.

Политики газа удалённого хаба появились с девятым циклом аудита, 2026-10-06. Хаб, задеплоенный до них, после апгрейда читает обе политики как ноль, а зеркальный волт после апгрейда до соответствующего кода отклоняет каждую отправку в аирдроп и её котировку (AirdropGasNotSet), пока обе не заданы. Поэтому порядок такой: апгрейд удалённого хаба, задание обеих политик (setAirdropReceiveGas, setAirdropComposeGas), затем назначение новой имплементации зеркального волта и апгрейд каждого зеркального волта, и только потом запуск кипера девятого цикла, который запрашивает у хаба политики и отправляет с тремя аргументами. Кипер прежней версии тем временем продолжает отправлять на значениях хаба по умолчанию. Хаб, задеплоенный с новым кодом, задаёт значения по умолчанию при инициализации.

Настройки батча адаптера USDG появились с десятым циклом аудита, 2026-10-06: газ compose, который добавляет каждый рынок батча моста, и наибольшее число рынков, которое несёт один батч. Адаптер, задеплоенный до них, после апгрейда читает обе как ноль и отклоняет каждый батч и каждую котировку (BatchGasNotSet), пока они не заданы. Поэтому его апгрейд задаёт их в той же транзакции (upgradeToAndCall с setBatchGas(400000, 17)), затем владелец снижает его прежний газ compose, 1 200 000, до базы, которая теперь нужна каждому батчу (setSettings, 200 000), и только потом запускает кипера десятого цикла, который читает потолок при каждом батче. Кипер прежней версии продолжает работать с адаптером после апгрейда, пока одновременно готовы не более 17 рынков. Адаптер тестнета получил апгрейд таким образом 2026-10-06, и его следующий батч прошёл с его новым газом compose.

Апгрейд, который меняет то, что читает сервис данных приложения, выкатывается раньше этого сервиса. С 2026-10-06 Lens содержит состояние каждого волта и то, что хук и лок ему должны, а Worker и приложение, которые читают эти поля, не могут читать более старый Lens: сначала выполните апгрейд Lens. Седьмой цикл аудита не меняет ни Lens, ни форму того, что публикует Worker (схема 7): его Worker и приложение деплоятся в любом порядке. Восьмой меняет форму (схема 8: каждая цена акции сообщает, почему она отсутствует, когда оракул Robinhood Chain её придерживает), но не Lens, и его Worker и приложение по-прежнему деплоятся в любом порядке: более старое приложение игнорирует причину, а это приложение читает более старый Worker без неё. Девятый сохраняет схему 8.

Оракул канала Robinhood, как и любой TreasuryOracle, начинает с двумя выключенными защитами. Поэтому оракул-замена, назначенный на удалённом хабе (setOracle), тоже начинает с выключенными защитами, и администратор хаба снова включает их для него, как это делает скрипт деплоя: паузу оракула каждой акции и фид секвенсора, если он был задан. Уже инициализированные зеркальные волты сохраняют оракул, с которым были инициализированы.

Настройки

Настройка — это вызов владельца протокола на модуле, который её хранит, а на Robinhood Chain — вызов администратора удалённого хаба; она вступает в силу сразу и генерирует событие. Каждый модуль начинает со значений по умолчанию, перечисленных в разделе Модель доверия. Адаптеры моста начинают с газа, заданного при деплое: на канале USDG — COMPOSE_GAS, часть последнего шага батча, нужная каждому батчу, по умолчанию 200 000 в DeployBridge с десятого цикла аудита (до того 1 200 000 на весь шаг), плюс 400 000 на каждый рынок батча и не более 17 рынков на батч (setBatchGas; газ самого большого батча — не более 24 000 000), рядом с границей Curve в 30 базисных пунктов; на каноническом канале — газ обоих тикетов и, с 2026-10-05, число байтов, по которому рассчитывается стоимость тикета депозита (DEPOSIT_CALLDATA_LENGTH в DeployTestnetBridge; ноль означает значение по умолчанию, 1 024). Удалённый хаб начинает с двумя политиками газа для доставок аирдропа (см. выше).

До мейннета

  • Полная репетиция аварийного режима: перемещение, пауза, снятие с паузы
  • Первый небольшой аирдроп на проверочном волте, до любого публичного открытия. Тестнетовый запуск LayerZero от 2026-10-06 провёл код StockFun от начала до конца через тестнетовые эндпоинты, DVN и исполнитель LayerZero; он не доказывает ни пару USDG от Paxos, ни акции Robinhood и их адаптеры, ни реальные фиды и ликвидность, ни газ, комиссии и финальность мейннета
  • Актуальный обзор ценовых фидов в удалённой сети
  • Проверка пиров LayerZero у USDG OFT и состояния паузы USDG через префлайт; с восьмого цикла аудита префлайт также сверяет защиты оракула с его манифестом, который должен указывать состояние проверки секвенсора, сегодня выключенной
  • Лимит размера пути, по которому USDG от Paxos идёт на Robinhood Chain, прочитанный из библиотеки отправки LayerZero (getExecutorConfig), и, если этот лимит не равен 10 000 байт, соответствующий ему потолок рынков в батче моста: не более (размер − 392) ÷ 544 рынков (с десятого цикла аудита)
  • Внешнее ревью — существующие формальные доказательства не покрывают кросс-чейн канал