Перейти к содержимому

Экономика

Десять долларов за слот Orbit и правила, по которым они делятся. Всё это лежит в контрактах, и любую цифру можно прочитать вызовом view-функции.

Слот Orbit стоит 10 sUSDC, Orbit действует 30 дней. Слотов в Orbit от 1 до 5, и Orbit на k слотов стоит k цен — без скидки за количество. Дальше все расчёты ведутся на один слот. Срок отсчитывается от активации, а не от покупки, так что купленный токен может спокойно лежать.

Срок считается во времени обслуживания: пока протокол на паузе, оно стоит, поэтому пауза сдвигает конец идущей подписки ровно на свою длину. Подписка, которая ждёт в очереди за другой, сдвигается вместе с ней, но длиннее не становится.

Платят в USDC. Оплата в ECHO в контракте уже есть, но выключена: echoMintingEnabled равен false, и попытка вернёт EchoMintingDisabled.

ETH для покупки не нужен. mintWithPermit принимает подпись намерения EIP-712 и permit на токен и проходит через GasStation. Газ этой покупки, активации и регистрации личностей кошелёк оплачивает в USDC вместе с ценой; сколько, говорит GasStation.gasFee. Эта плата идёт в GasStation и в сплит не входит.

Текущие значения: SubscriptionNFT.priceUSDC() и SUBSCRIPTION_DURATION. Что покупка стоит прямо сейчас, со скидкой, если она есть, — mintCost(slots) и addSlotsCost(tokenId, extra).

Сплит можно менять: владелец контракта переписывает его вызовом setSplitBps, и он уже переписывался. Сумма шести долей всегда 10 000 базисных пунктов. Текущие доли возвращает SubscriptionNFT.getSplit().

Сейчас в блокчейне так:

ДоляСколькоКуда
Узлы70% — $7.00RewardDistributor, пул узлов
DAO10% — $1.00UsdcTreasury
Ликвидность10% — $1.00LiquidityManager
Control Plane5% — $0.50RewardDistributor, пул CP
Основатель5% — $0.50кошелёк основателя
Выкуп0%—

UsdcTreasury — казна DAO в USDC. Тратить из неё может только её владелец, после передачи управления это timelock. Например, он может заранее оплатить места новым Control Plane: fundRewardReserve переводит деньги в пул CP, и места открываются с оплатой на весь срок. Доля выкупа понадобится после листинга ECHO.

Остаток от округления уходит в долю узлов. Сплит всегда считается от полной цены, в том числе при покупке со скидкой (см. промо).

flowchart LR
    sub["Слот Orbit<br/>10 sUSDC"]
    lp["LiquidityManager<br/>$1.00"]
    dao["UsdcTreasury<br/>$1.00"]
    fw["Основатель<br/>$0.50"]
    rd["RewardDistributor"]
    npool["Пул узлов $7.00"]
    cpool["Пул CP $0.50"]
    nodes["Узлы"]
    cps["Control Plane"]

    sub --> lp
    sub --> dao
    sub --> fw
    sub --> rd
    dao -.->|"fundRewardReserve"| cpool
    rd --> npool
    rd --> cpool
    npool -->|"claimNode · секунды × ставка"| nodes
    cpool -->|"claimCp · оплата места за эпоху"| cps

Узлу платят по фиксированной ставке за каждую засчитанную секунду:

ставка узла = priceUSDC × доля узлов / 10000 / T_SUB

При нынешних 70%:

10 sUSDC × 0.70 / 2 592 000 с ≈ 0.0000027 sUSDC в секунду ≈ 0.2333 в сутки

Один слот оплачивает одну секунду работы узла в секунду.

Ставка не зависит от того, сколько узлов работает и как загружена сеть. Её меняет только цена или доля узлов в сплите, и такую смену контракт пропустит, только если пул узлов сможет оплатить по новой ставке всё уже проданное время (см. смену цены).

RewardDistributor.nodeRate() возвращает текущую ставку в минимальных единицах USDC за секунду, умноженную на 10¹⁸: при USDC с шестью знаками секунда стоит меньше минимальной единицы.

Эпоха длится 24 часа. Когда она закрывается, её ставка фиксируется (epochRate(epoch)), и выплата узлу равна его секундам за эпоху, умноженным на эту ставку. Ставка эпохи ниже текущей только в одном случае: если за эпоху засчитали больше секунд, чем к её концу прошло оплаченного времени всех подписок. Тогда все узлы этой эпохи получают одинаковую долю от ставки, и контракт порождает CapacityExceeded.

Control Plane получает фиксированную сумму за каждую эпоху, пока держит место. Сколько стоит эпоха места, возвращает seatPay().

Место открывается на срок в эпохах, и вся оплата за этот срок резервируется в пуле CP сразу. Место нельзя открыть, если в пуле нет свободных денег на весь срок. Поэтому выплаты Control Plane не зависят от того, сколько подписок продано после открытия места.

  • seats(cpStarId) — первая эпоха места, эпоха после последней и оплата за эпоху.
  • claimCp(cpStarId, epoch) платит оплату места за закрытую эпоху его срока, если у звезды роль CP и её подписант не помечен слэшингом.
  • После конца срока у владельца есть 7 эпох, чтобы забрать незабранное. Потом любой может вызвать settleSeat, и неоплаченное возвращается в свободные деньги пула.

Места открывает, продлевает и закрывает адрес seatManager(). На стадии теста это ключ деплоера; дальше это будет контракт мест, который сам допускает и выводит Control Plane.

Подписчик оплачивает слот на 30 дней, но держит сессию малую часть суток. Деньги за время, которое прошло без сессий, никому не причитаются. Это и есть свободный излишек.

RewardDistributor ведёт книги каждого пула:

ВызовЧто возвращает
nodeAssets()всё, что держит пул узлов
nodeLiabilities()что он должен: оплаченное время, которое ещё впереди, и засчитанное, но не забранное
nodeAccrued()засчитанное, но не забранное
freeNode()разница между ними, со знаком
cpAssets()всё, что держит пул CP
seatReserved()зарезервировано под оставшиеся сроки мест
spendableCp()свободное в пуле CP

Время подписки считается прошедшим с опозданием на эпоху: Control Plane присылают отработанное пачками, и секунды конца эпохи засчитываются уже в следующей. EpochAttestations не засчитывает время старше начала предыдущей эпохи, поэтому через эпоху после закрытия всё, что за неё могло быть засчитано, уже засчитано. Излишек, который контракт показывает, никогда не больше реально неиспользованного времени.

Свободный излишек расходуется только тремя путями:

  • на промо — автоматически, см. ниже;
  • вывод — releaseSurplus(to, nodeAmount, cpAmount) вызывает владелец, не больше свободного;
  • места CP — резервируются из свободных денег пула CP.

Пополнить пулы может кто угодно вызовом fundReserve(nodeAmount, cpAmount) после approve на USDC. Эти деньги сразу становятся свободными. Обычный перевод USDC на адрес контракта ни в один пул не попадает.

Часть свободного излишка возвращается подписчикам скидкой. Это происходит само, без голосований и ручных решений.

Кампания длится promoEpochs() эпох, сейчас 30. Когда она заканчивается, первая финализация эпохи открывает следующую:

  1. Неистраченное прошлой кампанией возвращается в пулы, из которых пришло.
  2. Бюджет — доля promoAlphaBps() (сейчас 15%) свободного излишка каждого пула. Каждый пул даёт пропорционально своему свободному излишку.
  3. Скидка — бюджет, делённый на цену слота и прогноз продаж. Прогноз — скользящее среднее проданных слотов за прошлые кампании с весом promoEwmaBps() (50%), но не меньше promoMinForecastSlots() (10 слотов).
  4. Скидка не больше потолка discountCeilingBps(). Если она меньше promoMinDiscountBps() (5%), кампания идёт без скидки, и деньги остаются в пулах.

Потолок — меньшее из promoMaxDiscountBps() (30%) и 1 − доля узлов − promoMarginBps() (запас 5%), то есть сейчас 25%. Он читается из текущего сплита при каждой покупке. Покупатель, который сам обслуживает свой слот своим узлом, получает обратно не больше доли узлов от полной цены, поэтому скидка ниже потолка всегда меньше того, что слот приносит пулу.

Покупка со скидкой — это обычная полная покупка с двумя плательщиками: покупатель платит цену со скидкой, кампания доплачивает остальное, и полная цена делится по сплиту как всегда. Ставка узлов, места CP и доля ликвидности от скидки не меняются.

Бюджет тратится равномерно: к k-й эпохе кампании можно потратить не больше k/30 бюджета. Если остатка не хватает на скидку для покупки, покупка идёт по полной цене. Кошелёк подписывает потолок цены (maxCost), поэтому цена, которую он видел, не может молча вырасти: если скидка закончилась, покупка откатится, и клиент покажет новую цену.

Текущее состояние: campaign() (первая эпоха, эпоха после последней, скидка, бюджет, потрачено), currentDiscountBps() — скидка, которую получит покупка одного слота прямо сейчас, promoFromNode() и promoFromCp() — остаток бюджета по источникам.

Ставка одна для всего проданного времени. Поэтому, когда владелец меняет цену (setPriceUSDC) или долю узлов (setSplitBps), обязательства пула узлов пересчитываются по новой ставке. SubscriptionNFT перед сменой спрашивает RewardDistributor.checkRate, и смена откатывается с RateUnfunded, если пул узлов не может её покрыть. Снижение цены проходит всегда.

Цепочка из двух хопов — это две сессии на двух узлах, и каждая отчитывается о своём времени. Каждая занимает свой слот Orbit, а каждый слот оплачен отдельно, поэтому маршрут из двух хопов оплачен дважды.

  • Одновременные сессии на Orbit. По одной на слот, на всю сеть. На кошельковом уровне перед выдачей новой сессии каждый Control Plane прибавляет то, что по этому Orbit сообщают соседи. На анонимном уровне у Orbit по одной идентичности на слот, и каждая получает одно разрешение на окно.
  • Один источник на сессию. Одна сессия — одно устройство. Если через одни учётные данные три проверки подряд ходят с разных адресов, сессия закрывается.

Ни то ни другое оператор не настраивает. Число слотов записано в самом Orbit (SubscriptionNFT.slotsOf). Общий потолок сети — параметр директории maxSessionsPerSub, он читается вызовом SingulaProtocol.param; Control Plane перечитывают его раз в десять минут и, если он не задан, берут 5. Правило одного источника — константа в программе.

claimNode(node, epoch) и claimCp(cpStarId, epoch) может вызвать кто угодно. Получатель берётся из StarRegistry, так что посторонний, вызвав функцию, потратит свой газ на выплату вашему кошельку.

Для узла это не удобство, а необходимость: его подписант транзакций не отправляет и ETH не держит, а владелец должен оставаться холодным.

Забрать можно любую закрытую эпоху. Если её ещё никто не финализировал, claimNode сначала финализирует её сам. pendingNode(node, epoch) и pendingCp(cpStarId, epoch) показывают, сколько придёт; для закрытой, но не финализированной эпохи pendingNode считает по текущей ставке.

На текущей стадии всё идёт в USDC: в нём покупают Orbit и в нём платят звёздам. ECHO подключится на следующих стадиях роадмапа — для покупки Orbit и для выплат звёздам. Покупка за ECHO уже заложена в SubscriptionNFT и пока выключена, а доля выкупа в сплите стоит на нуле.

  • Оплаты за трафик. Объём никак не оценивается, платят за время.
  • Срока на получение награды узла. Незабранные награды узлов прошлых эпох остаются в контракте сколько угодно и учитываются как долг пула. Срок есть только у оплаты места CP: 7 эпох после конца срока места.