Распределение доходности (Yield Distribution)
✅ Готово к разработке
Ключевые концепции
11 инструкций
4 аккаунта состояния
4 PDA Seeds
Как это работает
Поступление дохода OT (ежемесячно)
distribute_revenue отправляет 70% USDC в Accumulator PDA дистрибьютора (USDC ATA, принадлежащий контракту). Внешние кошельки не нужны.Конвертация USDC → RWT + пополнение (атомарно)
convert_to_rwt — контракт обменивает USDC на DEX по цене до NAV, минтит остаток по NAV, депонирует RWT в reward vault и обновляет состояние вестинга. Всё в одной атомарной инструкции. Ранее завестированная сумма фиксируется, новые RWT начинают вестинг с текущего момента.Публикация корня Меркла (каждые 10 мин)
publish_root. Держатели с балансом менее $100 исключаются; их доля → ARL Treasury (доход протокола).Модель вестинга
Перманентный дистрибьютор с инкрементальным пополнением. Иconvert_to_rwt, и fund_distributor используют идентичную логику вестинга при добавлении RWT:
total_vested на стороне клиента каждую секунду, чтобы показывать баланс, растущий в реальном времени. Фактический перевод он-чейн происходит только когда держатель нажимает «Клеймить». Это стандартный паттерн в DeFi (Jito, Marinade и др.).["ot_treasury", arl_ot_mint]) как лист дерева Меркла. Это доход протокола — Areal Finance получает доходность, на которую мелкие держатели не имеют права. Клеймится через OT claim_yd_for_treasury на инстансе ARL OT.Три роли
🏛️ Config Authority
create_distributor— новый OT-проектclose_distributor— закрытие проектаupdate_config— изменение комиссий, лимитовupdate_publish_authority— смена серверного кошелькаpropose_authority_transfer
📡 Publish Authority
publish_root— каждые 10 минут
🌐 Без ограничений доступа
convert_to_rwt— конвертация USDC→RWTfund_distributor— депозит RWTclaim— клейм доходности держателем
Инструкции
Инициализация
initialize_config
initialize_config
| Parameter | Type | Description |
|---|---|---|
protocol_fee_bps | u16 | Комиссия на депозиты (по умолчанию: 25 = 0.25%) |
min_distribution_amount | u64 | Минимальная сумма пополнения (по умолчанию: эквивалент $100) |
areal_fee_destination | Pubkey | Куда направляются комиссии — RWT ATA Areal Finance (статичный, неизменяемый). Примечание: это RWT-аккаунт, не USDC — комиссия протокола YD удерживается в RWT после конвертации. |
publish_authority | Pubkey | Серверный кошелёк для publish_root |
deployer(signer, mut) — оплачивает создание аккаунтаconfig(init) — PDA seed:["dist_config"]system_program
DistributionConfigPDA — глобальный синглтон
authority = deployer(передаётся Team Multisig после бутстрапа)publish_authority = publish_authority paramis_active = true
Жизненный цикл дистрибьютора
create_distributor
create_distributor
| Parameter | Type | Description |
|---|---|---|
vesting_period_secs | i64 | Период вестинга (по умолчанию: 31 536 000 = 365 дней) |
authority(signer) — должен совпадать сconfig.authorityconfig— проверка authority, is_activeot_mint— OT, который обслуживает данный дистрибьюторdistributor(init) — PDA seed:["merkle_dist", ot_mint]reward_vault(init) — RWT-аккаунт, authority = distributor PDAaccumulator(init) — PDA seed:["accumulator", ot_mint]accumulator_usdc_ata(init) — USDC ATA, authority = accumulator PDArwt_mint— минт токена RWTusdc_mint— минт USDC (для accumulator ATA)token_program,system_program,associated_token_program
MerkleDistributorPDA — состояние перманентного дистрибьютора- Reward Vault — RWT ATA, принадлежащий distributor PDA
AccumulatorPDA — получает USDC от дохода OT- Accumulator USDC ATA — аккаунт хранения USDC
total_funded = 0,total_claimed = 0locked_vested = 0,last_fund_ts = nowmerkle_root = [0; 32],epoch = 0is_active = true
vesting_period_secs > 0
close_distributor
close_distributor
authority(signer) — должен совпадать сconfig.authorityconfigdistributor(mut)reward_vault(mut) — RWT для переводаunclaimed_destination(mut) — получает оставшиеся RWT (обычно ARL Treasury ATA)token_program
distributor.is_active == true(нельзя закрыть повторно)
distributor.is_active = false. Эмитирует DistributorClosed.Пополнение
convert_to_rwt
convert_to_rwt
| Parameter | Type | Description |
|---|---|---|
max_swap_amount | u64 | Максимум USDC для обмена на DEX (остаток минтится по NAV) |
min_rwt_out | u64 | Минимум RWT к получению (защита от проскальзывания против сэндвич-атак при обмене на DEX) |
crank(signer, mut)config— чтение protocol_fee_bps, areal_fee_destinationdistributor(mut) — обновление locked_vested, total_funded, last_fund_tsaccumulator— PDA, подписывает переводы USDC и RWT через seedsfee_account(mut) — constraint:key == config.areal_fee_destination(получает комиссию протокола в RWT)accumulator_usdc_ata(mut) — источник USDC, constraint:owner == accumulator.key()accumulator_rwt_ata(mut) — промежуточный RWT ATA, constraint:owner == accumulator.key(),mint == rwt_mint(получает RWT от обмена на DEX/минтинга через RWT Engine, затем переводит в reward_vault). Создаётся крэнком при необходимости черезinit_if_needed.reward_vault(mut) — конечное назначение для RWT- Аккаунты DEX:
pool_state,dex_config,vault_in,vault_out,dao_fee_account dex_program— constraint:key == DEX_PROGRAM_ID- Аккаунты CPI к RWT Engine (должны точно соответствовать layout инструкции
mint_rwt):rwt_vault(mut) — RWT Engine vault PDA["rwt_vault"], получает депозит USDCrwt_mint(mut) — минт токена RWT, authority = rwt_vault PDAcapital_acc(mut) — USDC ATA, принадлежащий rwt_vault (vault.capital_accumulator_ata)rwt_engine_fee_account(mut) —vault.areal_fee_destination(получает комиссию 0.5% за минт)- Accumulator PDA подписывает как
user(депозитор) в CPI mint_rwt
rwt_engine_program— constraint:key == RWT_ENGINE_PROGRAM_IDtoken_program
- Чтение баланса USDC аккумулятора. Если 0, возврат.
- Расчёт цены пула DEX vs NAV RWT
- Если цена пула < NAV: CPI-обмен
min(max_swap_amount, balance)USDC → RWT на DEX - Если USDC остались: CPI к
rwt_engine::mint_rwtпо NAV (accumulator PDA подписывает как user) - Запасной путь: Если пул DEX не существует или имеет нулевую ликвидность: минтить ВСЁ по NAV через RWT Engine. В этом случае весь объём USDC обходит DEX — комиссии LP не генерируются, объёма свопов нет. Это ожидаемо при бутстрапе (до создания пулов), но не должно происходить в нормальном режиме.
- Расчёт комиссии протокола на полученные RWT:
fee = rwt_acquired * protocol_fee_bps / 10,000 - Перевод
feeRWT → fee_account (accumulator PDA подписывает) - Перевод
rwt_acquired - fee→ reward_vault (accumulator PDA подписывает) - Фиксация вестинга:
locked_vested += (total_funded - locked_vested) * min(elapsed, period) / period(приведение к u128) total_funded += rwt_acquired - feelast_fund_ts = now- Валидация:
rwt_acquired >= min_rwt_out(проверка проскальзывания — откат при сэндвич-атаке с невыгодным обменом) - Эмитирование
StreamConverted
fund_distributor для сконвертированных средств не нужен.fund_distributor
fund_distributor
| Parameter | Type | Description |
|---|---|---|
amount | u64 | Сумма RWT для депозита |
depositor(signer)config— проверка is_activedistributor(mut) — обновление locked_vested, total_funded, last_fund_tsreward_vault(mut) — получает RWTdepositor_token(mut) — исходный RWT ATA, constraint:owner == depositorfee_account(mut) — constraint:key == config.areal_fee_destinationtoken_program
amount > 0amount >= config.min_distribution_amount(по умолчанию эквивалент $100)distributor.is_active == true
- Расчёт комиссии протокола:
fee = amount * protocol_fee_bps / 10,000 - Перевод fee → fee_account
- Перевод
amount - fee→ reward_vault - Фиксация вестинга:
locked_vested += (total_funded - locked_vested) * min(elapsed, period) / period(приведение к u128 перед умножением) total_funded += amount - feelast_fund_ts = now- Эмитирование
DistributorFunded
Распределение
publish_root
publish_root
| Parameter | Type | Description |
|---|---|---|
merkle_root | [u8; 32] | Хеш корня дерева Меркла |
max_total_claim | u64 | Должен равняться текущему total_funded |
publish_authority(signer) — должен совпадать сconfig.publish_authorityconfig— проверка publish_authority, is_activedistributor(mut) — обновление merkle_root, epoch
max_total_claim > 0(предотвращает деление на ноль вclaim— нельзя публиковать корень для непополненного дистрибьютора)max_total_claim == distributor.total_fundedmax_total_claim >= distributor.total_claimed
sha256(claimant_pubkey_bytes || cumulative_amount_le_bytes)cumulative_amount вычисляется офф-чейн методом per-deposit snapshot aggregation (см. Построение дерева Меркла):snapshot_i, дают 0 за deposit_i. Инвариант: Σ cumulative_amount[h] == total_funded.Эффект: Инкрементирует epoch, сохраняет новый корень. Эмитирует RootPublished.Рекомендуемая частота: Каждые 10 минут (офф-чейн крэнк). Обеспечивает отражение изменений баланса в течение 10 мин. Также должен вызываться сразу после каждого fund_distributor, чтобы новые средства стали доступны для клейма. Стоимость: ~$2.60/месяц на OT-проект.claim
claim
| Parameter | Type | Description |
|---|---|---|
cumulative_amount | u64 | Кумулятивная доля держателя (из листа Меркла) |
proof | Vec<[u8; 32]> | Путь доказательства Меркла (макс. 20 узлов) |
claimant(signer) — кошелёк держателя или PDApayer(signer, mut) — оплачивает ренту за инициализацию ClaimStatus при первом клеймеdistributor— чтение состояния вестинга, merkle_rootclaim_status(init_if_needed) — PDA seed:["claim_status", distributor, claimant]reward_vault(mut) — источник RWTclaimant_token(mut) — RWT ATA держателя, constraint:mint == reward_vault.minttoken_program,system_program
distributor.is_active == true(нельзя клеймить из закрытого дистрибьютора)- Доказательство Меркла:
verify(proof, root, sha256(claimant || cumulative_amount)) proof.len() <= MAX_PROOF_LEN(20)epoch > 0(корень должен быть опубликован)
- Верификация доказательства Меркла
- Инициализация ClaimStatus при первом клейме (claimant, distributor, claimed_amount=0)
- Расчёт total_vested (приведение к u128 до умножения, аналогично логике fund):
- Ограничение опубликованным корнем:
total_vested = min(total_vested, max_total_claim)(предотвращает избыточный клейм, если fund произошёл после последнего publish_root) - Минимальный вестинг:
total_vested = max(total_vested, min(MIN_VESTED_AMOUNT, max_total_claim)) - Персональная доля:
my_vested = (total_vested as u128) * cumulative_amount / max_total_claim claimable = my_vested - claim_status.claimed_amount- Если claimable == 0, возврат
- Перевод
claimableRWT: reward_vault → claimant_token (distributor PDA подписывает) claim_status.claimed_amount += claimabledistributor.total_claimed += claimable- Эмитирование
RewardsClaimed
Маршрутизация ликвидности
15% доля RWT, формируемаяrwt_engine::claim_yield, стейджится в принадлежащей контракту PDA LiquidityHolding, прежде чем атомарно дренироваться в токен-аккаунт DEX LiquidityNexus. Yield Distribution предоставляет одну новую инструкцию для этого потока.
| Инструкция | Вызывающий | Назначение |
|---|---|---|
withdraw_liquidity_holding | Authority (Team Multisig) | Атомарный drain — переводит RWT из RWT ATA LiquidityHolding в RWT ATA Nexus с CPI на native_dex::nexus_record_deposit, обновляющим principal floor Nexus в той же инструкции |
ownership_token::distribute_revenue— отправляет USDC выручки OT в per-OT Accumulator YDyield_distribution::convert_to_rwt— конвертирует USDC из Accumulator в RWT, депонирует RWT в reward vault, обновляет состояние вестинга (документировано в секции Funding выше)rwt_engine::claim_yield— RWT Engine клеймит свою долю через стандартный flow YDclaim; 15% доля ликвидности приземляется в RWT ATALiquidityHolding(см. контракт RWT Engine)ownership_token::claim_yd_for_treasury— PDA OT Treasury клеймит свою долю любого distributor (cross-project yield)
withdraw_liquidity_holding
withdraw_liquidity_holding
LiquidityHolding в RWT ATA Nexus DEX, с обновлением principal floor через CPI на native_dex::nexus_record_deposit. Authority-gated.| Параметр | Тип | Описание |
|---|---|---|
amount | u64 | RWT для drain (должен быть ≤ баланса holding) |
authority(signer) — должен совпадать сconfig.authorityconfig— гейт authority и паузыliquidity_holding(mut) — инкрементируетtotal_drainedliquidity_holding_rwt_ata(mut) — source ATA, владелец = PDALiquidityHoldingnexus_token_ata(mut) — destination ATA, владелец = PDALiquidityNexusна стороне DEXliquidity_nexus(mut) — PDA Nexus DEX, principal floor обновляется через CPIdex_program— аккаунт программы DEX, валидируетсяexecutable()token_programsystem_program
amount > 0amount ≤ liquidity_holding_rwt_ata.balancenexus_token_ata.mint == RWT_MINT(defence-in-depth)nexus_token_ata.owner == liquidity_nexusPDAdex_program.address() == DEX_PROGRAM_ID
- SPL Transfer
amountизliquidity_holding_rwt_ata→nexus_token_ata(PDALiquidityHoldingподписывает). - CPI на
native_dex::nexus_record_deposit(amount, token_kind=RWT)(PDALiquidityHoldingподписывает). - Инкремент
liquidity_holding.total_drainedнаamount. - Эмитирование
LiquidityHoldingWithdrawn { amount, total_drained, timestamp }.
total_deposited_rwt могли бы расходиться на этом канале.Конфигурация и полномочия
update_config
update_config
| Parameter | Type | Description |
|---|---|---|
protocol_fee_bps | u16 | Новая комиссия в BPS |
min_distribution_amount | u64 | Новый минимум |
is_active | bool | Глобальный флаг активности |
authority(signer) — должен совпадать сconfig.authorityconfig(mut)
ConfigUpdated.areal_fee_destination является неизменяемым — устанавливается при инициализации, изменить нельзя. Только обновление программы может его модифицировать.update_publish_authority
update_publish_authority
propose_authority_transfer
propose_authority_transfer
accept_authority_transfer
accept_authority_transfer
Аккаунты состояния
DistributionConfig
Глобальный синглтон. Один на деплой протокола.| Field | Type | Description |
|---|---|---|
authority | Pubkey | Config authority (Team Multisig после бутстрапа) |
pending_authority | Option<Pubkey> | Целевой адрес ожидающей передачи полномочий |
publish_authority | Pubkey | Серверный кошелёк для publish_root (изменяемый authority) |
protocol_fee_bps | u16 | Комиссия на fund_distributor (по умолчанию: 25 = 0.25%) |
min_distribution_amount | u64 | Минимальная сумма пополнения |
areal_fee_destination | Pubkey | RWT ATA Areal Finance — получает комиссию протокола в RWT (статичный, неизменяемый) |
is_active | bool | Глобальный аварийный выключатель |
bump | u8 | PDA bump seed |
["dist_config"]
MerkleDistributor
Один на OT-проект, перманентный (никогда не пересоздаётся).| Field | Type | Description |
|---|---|---|
ot_mint | Pubkey | OT, который обслуживает данный дистрибьютор |
reward_vault | Pubkey | RWT-аккаунт (authority = distributor PDA) |
accumulator | Pubkey | Accumulator PDA для данного OT (USDC поступают сюда) |
merkle_root | [u8; 32] | Текущий корень Меркла весов держателей |
max_total_claim | u64 | Равен total_funded (предохранительный лимит) |
total_claimed | u64 | Кумулятивно заклеймленные RWT по всем держателям |
total_funded | u64 | Кумулятивно депонированные RWT (растёт с каждым пополнением) |
locked_vested | u64 | RWT с гарантированным вестингом (фиксируется при каждом пополнении) |
last_fund_ts | i64 | Временная метка последнего вызова fund_distributor |
vesting_period_secs | i64 | Длительность вестинга (по умолчанию: 365 дней) |
epoch | u64 | Счётчик publish_root |
is_active | bool | Флаг активности (false после close_distributor) |
bump | u8 | PDA bump seed |
["merkle_dist", ot_mint]
stream_id в seed — один дистрибьютор на OT mint. Это ключевое упрощение по сравнению с архитектурой множественных потоков.Accumulator
Аккаунт приёма USDC для каждого OT. Принадлежит контракту — внешние ключи не нужны. OTdistribute_revenue отправляет USDC сюда. convert_to_rwt тратит отсюда.
| Field | Type | Description |
|---|---|---|
ot_mint | Pubkey | OT, который обслуживает данный аккумулятор |
bump | u8 | PDA bump seed |
["accumulator", ot_mint]
convert_to_rwt через seeds.ClaimStatus
Трекинг для каждой пары (distributor, claimant). Создаётся при первом клейме черезinit_if_needed.
| Field | Type | Description |
|---|---|---|
claimant | Pubkey | Кошелёк держателя или PDA |
distributor | Pubkey | К какому дистрибьютору относится |
claimed_amount | u64 | Кумулятивно заклеймленные RWT |
bump | u8 | PDA bump seed |
["claim_status", distributor, claimant]
LiquidityHolding
Singleton-PDA, стейджит RWT, полученный от 15% доли ликвидности изrwt_engine::claim_yield, перед тем как он будет атомарно дренирован в DEX LiquidityNexus. PDA владеет одним RWT ATA (liquidity_holding_rwt_ata), который и есть стейджинговый аккаунт; инструкция drain переводит RWT в Nexus и обновляет principal floor Nexus через CPI в одной транзакции.
| Поле | Тип | Описание |
|---|---|---|
total_received | u64 | Lifetime RWT, полученные от claim_yield (running counter) |
total_drained | u64 | Lifetime RWT, дренированные в Nexus |
bump | u8 | Bump-сид PDA |
["liq_holding"]
Owned ATA: PDA LiquidityHolding владеет одним RWT ATA (liquidity_holding_rwt_ata) — per-epoch стейджинговый аккаунт.
nexus_deposit. С LiquidityHolding весь поток — это одна инструкция (withdraw_liquidity_holding), объединяющая SPL-трансфер и обновление principal floor через CPI.PDA Seeds
| Account | Seeds | Description |
|---|---|---|
| DistributionConfig | "dist_config" | Глобальный синглтон конфигурации |
| MerkleDistributor | "merkle_dist", ot_mint | Перманентный дистрибьютор для каждого OT |
| Accumulator | "accumulator", ot_mint | Приёмник USDC для каждого OT (принадлежит контракту) |
| ClaimStatus | "claim_status", distributor, claimant | Трекинг клеймов для каждого держателя |
| LiquidityHolding | "liq_holding" | Singleton — стейджинг RWT для drain в Nexus |
Константы
| Constant | Value | Description |
|---|---|---|
BPS_DENOMINATOR | 10,000 | 100% в базисных пунктах |
DEFAULT_PROTOCOL_FEE_BPS | 25 | Комиссия 0.25% на fund_distributor |
DEFAULT_MIN_DISTRIBUTION | 100,000,000 | Эквивалент $100 (6 десятичных знаков) |
DEFAULT_VESTING_PERIOD | 31,536,000 | 365 дней в секундах |
MAX_PROOF_LEN | 20 | Максимальная глубина доказательства Меркла (~1 млн держателей) |
MIN_VESTED_AMOUNT | 1,000,000 | Минимум 1 RWT завестирован (предотвращает зависание на нуле) |
RWT_ENGINE_PROGRAM_ID | hardcoded | Валидируется в convert_to_rwt |
DEX_PROGRAM_ID | hardcoded | Валидируется в convert_to_rwt |
События
| Event | Fields | When |
|---|---|---|
ConfigInitialized | authority, publish_authority, protocol_fee_bps, timestamp | Конфигурация создана |
DistributorCreated | ot_mint, reward_vault, accumulator, vesting_period_secs, timestamp | Дистрибьютор создан |
DistributorFunded | ot_mint, amount, protocol_fee, total_funded, locked_vested, timestamp | RWT депонированы |
StreamConverted | ot_mint, usdc_swapped, rwt_minted, total_rwt, timestamp | Конвертация USDC→RWT |
RootPublished | ot_mint, epoch, merkle_root, max_total_claim, timestamp | Новый корень опубликован |
RewardsClaimed | claimant, ot_mint, amount, cumulative_claimed, timestamp | Держатель заклеймил RWT |
ConfigUpdated | protocol_fee_bps, min_distribution_amount, is_active, timestamp | Конфигурация изменена |
PublishAuthorityUpdated | old_publish_authority, new_publish_authority, timestamp | Серверный кошелёк изменён |
AuthorityTransferProposed | current_authority, pending_authority, timestamp | Передача предложена |
AuthorityTransferAccepted | old_authority, new_authority, timestamp | Передача принята |
DistributorClosed | ot_mint, unclaimed_swept, timestamp | Дистрибьютор закрыт |
LiquidityHoldingWithdrawn | amount, total_drained, timestamp | RWT дренирован из LiquidityHolding в RWT ATA Nexus DEX через withdraw_liquidity_holding |
Коды ошибок
| Error | Description |
|---|---|
Unauthorized | Подписант не является требуемым authority |
SystemPaused | Глобальная конфигурация is_active = false |
DistributorNotActive | Дистрибьютор закрыт |
ZeroAmount | Сумма должна быть > 0 |
BelowMinDistribution | Сумма пополнения ниже min_distribution_amount |
RootNotPublished | Нельзя клеймить до первого publish_root (epoch = 0) |
InvalidProof | Верификация доказательства Меркла не пройдена |
NothingToClaim | Доступная для клейма сумма = 0 |
ProofTooLong | Доказательство превышает MAX_PROOF_LEN (20) |
ExceedsMaxClaim | total_claimed превысит max_total_claim |
SlippageExceeded | rwt_acquired < min_rwt_out (защита от сэндвич-атак) |
MathOverflow | Арифметическое переполнение |
InvalidVestingPeriod | vesting_period_secs должен быть > 0 |
SelfTransfer | Нельзя передать полномочия самому себе |
NoPendingAuthority | Нет ожидающей передачи для принятия |
InvalidPendingAuthority | Подписант ≠ pending_authority |
Архитектура и руководство по интеграции
Кросс-программная интеграция
← Ownership Token (получает 70% дохода в USDC)
← Ownership Token (получает 70% дохода в USDC)
- OT
distribute_revenueотправляет 70% USDC на USDC ATA Accumulator PDA - Адрес Accumulator PDA вычисляется:
["accumulator", ot_mint] - Конфигурация назначения OT указывает на этот ATA
- Поток: Доход OT → Accumulator USDC ATA →
convert_to_rwt(атомарно: конвертация USDC→RWT + депозит в vault)
→ RWT Engine (конвертация USDC → RWT)
→ RWT Engine (конвертация USDC → RWT)
convert_to_rwtвызывает CPI кnative_dex::swap(покупка RWT ниже NAV)- Если USDC остались: CPI к
rwt_engine::mint_rwt(минт по NAV) - Accumulator PDA подписывает как user/depositor
- Использует raw
invoke_signedдля CPI к RWT Engine (обход циклической зависимости)
→ RWT Engine (vault клеймит доходность)
→ RWT Engine (vault клеймит доходность)
- RWT Vault является держателем OT → имеет лист в дереве Меркла
- RWT Engine
claim_yieldвызывает CPI кyield_distribution::claim - Vault PDA подписывает как claimant
→ Ownership Token (ARL Treasury клеймит долю неподходящих держателей)
→ Ownership Token (ARL Treasury клеймит долю неподходящих держателей)
- Доля неподходящих держателей со ВСЕХ OT-проектов → лист ARL OtTreasury PDA в дереве Меркла (доход протокола)
- OT
claim_yd_for_treasury(вызывается на инстансе ARL OT) делает CPI кyield_distribution::claim - ARL OtTreasury PDA подписывает как claimant (контракт OT подписывает через PDA seeds)
- ARL Treasury =
["ot_treasury", arl_ot_mint]— тот же PDA, что и у treasury любого другого OT, только для проекта ARL
→ Team Multisig (config authority)
→ Team Multisig (config authority)
Модель полномочий
🔧 Обновление программы
🏛️ Config Authority
create_distributor/close_distributorupdate_configupdate_publish_authoritypropose_authority_transfer
📡 Publish Authority
publish_root(каждые 10 мин)
Чек-лист деплоя
Предварительные требования: RWT Engine и Native DEX должны быть задеплоены (необходимы для CPI convert_to_rwt).- Вызвать
initialize_configс areal_fee_destination, publish_authority (серверный кошелёк) - Создать дистрибьютор для каждого OT-проекта через
create_distributor(также создаёт Accumulator PDA) - Настроить назначение дохода OT на USDC ATA Accumulator
- Передать config authority Team Multisig через
propose_authority_transfer+accept_authority_transfer - Запустить крэнк-бот — publish_root каждые 10 минут, convert_to_rwt после каждого распределения дохода OT
Сводка потоков токенов
| From | To | Mechanism | Who triggers |
|---|---|---|---|
| Доход OT (USDC) | Accumulator USDC ATA | OT distribute_revenue | Крэнк |
| Accumulator USDC | Reward vault RWT (через DEX + минт) | convert_to_rwt (атомарно: конвертация + пополнение) | Крэнк |
| Внешние RWT | Reward vault | fund_distributor (прямой депозит RWT) | Любой |
| Reward vault | RWT ATA держателя | claim (вестинг для каждого получателя) | Держатель |
| Reward vault | RWT Vault ATA | claim (через CPI RWT Engine) | Крэнк |
| Reward vault | ARL OtTreasury RWT ATA | claim (через OT claim_yd_for_treasury на инстансе ARL) | Крэнк |
| Reward vault | Назначение (при закрытии) | close_distributor | Authority |
| RWT ATA LiquidityHolding | RWT ATA Nexus | withdraw_liquidity_holding (атомарно: SPL transfer + CPI обновление floor) | Authority |
Построение дерева Меркла (офф-чейн)
Дерево Меркла строится офф-чейн сервером publish authority. Алгоритм использует per-deposit snapshots, чтобы обеспечить справедливое распределение доходности — каждый депозит распределяется только между теми держателями, которые держали OT на момент этого депозита. Это предотвращает захват исторической доходности поздними покупателями.Алгоритм per-deposit snapshot
На каждое fund-событие (эмитируетсяfund_distributor или convert_to_rwt):
- Бот слушает события
DistributorFunded/StreamConvertedон-чейн. - На слоте fund-события бот делает snapshot балансов всех держателей OT через
getProgramAccounts. Это требует архивного RPC-тарифа (например, Helius / Triton), так как public RPC бесплатного тарифа не хранит старые program accounts. - Snapshot включает: обычные кошельки, RWT Vault PDA (держит OT как обеспечение портфеля), PDA пулов DEX (vault-ы пулов держат OT с PDA пула как authority), OtTreasury PDA (может держать OT, полученные от governance).
- Фильтр на каждый snapshot: только держатели с суммарным балансом ≥ $100 по протоколу на момент слота этого snapshot.
- Доля неподходящих (< $100) для этого snapshot распределяется в лист ARL OtTreasury PDA — доход протокола.
- Snapshot сохраняется:
{distributor, deposit_epoch, slot, balances: {holder → balance}, total_eligible}.
publish_root (каждые 10 минут):
- Для каждого держателя, появившегося в каком-либо snapshot, рассчитывается:
Держатели, отсутствующие в
snapshot_i, дают 0 заdeposit_i. - Проверка инварианта
Σ cumulative_amount[h] == total_funded. Остаток от целочисленного деления (ограниченN_deposits) добавляется в лист ARL OtTreasury. - Построение дерева Меркла: лист =
sha256(address_bytes || cumulative_amount_le_bytes)с каноническим порядком (меньший хеш первым). - Публикация корня он-чейн через
publish_rootсmax_total_claim = total_funded.
max_total_claim == total_funded и total_claimed ≤ max_total_claim держатся независимо от того, как cumulative_amount вычислен офф-чейн — контракт верифицирует только доказательство, не способ вычисления cumulative. Смена алгоритма (например, naive → per-deposit → TWAB) — офф-чейн вопрос; редеплой контракта не нужен.Инфраструктура publisher
- Ключ publisher. Хранится в HSM или managed KMS (AWS KMS / Google Cloud KMS / аппаратный кошелёк). Бот подписывает через KMS API; приватный ключ никогда не загружается на диск или в память процесса. Файлы keypair на диске недопустимы для mainnet.
- Архивный RPC. Требуется для snapshot балансов на исторических слотах. Стоимость: ~$50 / месяц на OT-проект (тариф Helius / Triton).
- Хранилище snapshot. ~40 МБ на snapshot × 12 депозитов / год ≈ 500 МБ / год на distributor. SQLite / RocksDB / JSONL — все приемлемы.
- Независимые верификаторы (настоятельно рекомендуется). Два или более отдельных сервиса — командных и/или коммьюнити — независимо вычисляют корень и постят его хеш в Discord, Telegram или он-чейн лог. Несовпадение → алерт → ротация
publish_authorityчерезupdate_publish_authority.
Операционные параметры
- Частота публикации: каждые 10 минут. Стоимость mainnet TX: ~$2.60 / месяц на OT-проект.
- Compute: линейно по держателям × депозитам. Примерно 1 секунда CPU на 1М держателей × 12 депозитов на commodity-железе.
- Потолок масштабирования: линейный; до 100 депозитов / год на distributor без алгоритмической переработки.
См. также
- Liquidity Nexus — обзор подсистемы LP протокола и инвариант principal-lock
- Контракт RWT Engine —
claim_yieldи дележ 70 / 15 / 15; 15% доля ликвидности попадает в RWT ATALiquidityHolding, определённой здесь