Распределение доходности (Yield Distribution)
✅ Готово к разработке
Данная спецификация контракта прошла аудит бизнес-логики, техническое ревью, проверку кросс-программной интеграции (OT + RWT Engine), верификацию математики вестинга и стандартизацию именования. Разработчик может реализовать контракт по этому документу.
Ключевые концепции
11 инструкций
Конфигурация, создание/закрытие дистрибьютора, конвертация, пополнение, публикация корня, клейм, управление полномочиями
4 аккаунта состояния
DistributionConfig, MerkleDistributor, Accumulator, ClaimStatus
4 PDA Seeds
dist_config, merkle_dist, accumulator, claim_status
Как это работает
Поступление дохода OT (ежемесячно)
Контракт OT через
distribute_revenue отправляет 70% USDC в Accumulator PDA дистрибьютора (USDC ATA, принадлежащий контракту). Внешние кошельки не нужны.Конвертация USDC → RWT + пополнение (атомарно)
Крэнк вызывает
convert_to_rwt — контракт обменивает USDC на DEX по цене до NAV, минтит остаток по NAV, депонирует RWT в reward vault и обновляет состояние вестинга. Всё в одной атомарной инструкции. Ранее завестированная сумма фиксируется, новые RWT начинают вестинг с текущего момента.Публикация корня Меркла (каждые 10 мин)
Publish authority (серверный кошелёк) сканирует держателей OT офф-чейн, строит дерево Меркла, публикует корень он-чейн через
publish_root. Держатели с балансом менее $100 исключаются; их доля → ARL Treasury (доход протокола).Модель вестинга
Перманентный дистрибьютор с инкрементальным пополнением. Иconvert_to_rwt, и fund_distributor используют идентичную логику вестинга при добавлении RWT:
UX: UI вычисляет
total_vested на стороне клиента каждую секунду, чтобы показывать баланс, растущий в реальном времени. Фактический перевод он-чейн происходит только когда держатель нажимает «Клеймить». Это стандартный паттерн в DeFi (Jito, Marinade и др.).**Минимум 100 по всем позициям OT + RWT имеют право на доходность. Доля неподходящих держателей направляется в ARL Treasury (= ARL OtTreasury PDA
["ot_treasury", arl_ot_mint]) как лист дерева Меркла. Это доход протокола — Areal Finance получает доходность, на которую мелкие держатели не имеют права. Клеймится через OT claim_yd_for_treasury на инстансе ARL OT.Три роли
🏛️ Config Authority
Team Multisig (после бутстрапа)
create_distributor— новый OT-проектclose_distributor— закрытие проектаupdate_config— изменение комиссий, лимитовupdate_publish_authority— смена серверного кошелькаpropose_authority_transfer
📡 Publish Authority
Серверный кошелёк (крэнк-бот на VPS)
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
Создание перманентного дистрибьютора для OT-проекта. Один на OT mint, существует бессрочно. Также создаёт Accumulator PDA, куда поступает USDC-доход.
Вызывающий: Authority (Team Multisig)Аккаунты:
| 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
Окончательное закрытие дистрибьютора. Переводит все невостребованные RWT на указанный адрес.Вызывающий: Authority (Team Multisig)Аккаунты:
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
Конвертация накопленных USDC в RWT. Два шага: обмен на DEX по цене до NAV, минтинг остатка по NAV через RWT Engine. Accumulator PDA подписывает все переводы — внешние ключи не нужны.
Вызывающий: Permissionless (крэнк)Аккаунты:
| 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 для сконвертированных средств не нужен.Эта инструкция объединяет конвертацию + пополнение в одну атомарную операцию. Accumulator PDA подписывает и обмен/минт USDC, и перевод RWT в reward vault. Нет промежуточного состояния, где RWT «висят» между аккаунтами.
fund_distributor
fund_distributor
Депозит RWT в reward vault дистрибьютора. Фиксирует ранее завестированную сумму и начинает вестинг новой порции с текущего момента.
Вызывающий: Permissionless — пополнить может любойАккаунты:
| 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
Расчёт locked_vested сохраняет уже заработанную доходность при поступлении новых средств. Без этого добавление средств разбавляло бы прогресс вестинга существующих депозитов.
Распределение
publish_root
publish_root
Публикация нового корня Меркла, представляющего текущие веса держателей OT. Вызывается каждые 10 минут publish authority (серверным кошельком).
Вызывающий: Publish authority (серверный кошелёк)Аккаунты:Эффект: Инкрементирует
| 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 = пропорциональная доля держателя от total_funded:epoch, сохраняет новый корень. Эмитирует RootPublished.Рекомендуемая частота: Каждые 10 минут (офф-чейн крэнк). Обеспечивает отражение изменений баланса в течение 10 мин. Также должен вызываться сразу после каждого fund_distributor, чтобы новые средства стали доступны для клейма. Стоимость: ~$2.60/месяц на OT-проект.claim
claim
Клейм завестированных наград в RWT. Может вызываться в любое время, сколько угодно часто. Вестинг начисляется каждую секунду.
Вызывающий: Держатель (или PDA через CPI — напр., RWT Vault, ARL Treasury)Аккаунты:
| 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
Конфигурация и полномочия
update_config
update_config
Обновление глобальной конфигурации распределения.
Вызывающий: Authority (Team Multisig)Аккаунты:
| 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]
Accumulator не хранит баланс USDC в состоянии. Баланс считывается из USDC ATA, принадлежащего этому PDA. PDA подписывает переводы USDC в
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]
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 | Трекинг клеймов для каждого держателя |
Константы
| 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 | Дистрибьютор закрыт |
Коды ошибок
| 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)
Модель полномочий
🔧 Обновление программы
Team Multisig (Squads)Может деплоить новые версии контракта.
🏛️ Config Authority
Team Multisig (после бутстрапа)
create_distributor/close_distributorupdate_configupdate_publish_authoritypropose_authority_transfer
📡 Publish Authority
Серверный кошелёк (крэнк-бот на VPS)
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 |
Построение дерева Меркла (офф-чейн)
Дерево Меркла строится офф-чейн сервером publish authority каждые 10 минут:- Сканирование всех токен-аккаунтов для OT mint (
getParsedProgramAccounts). Включает обычные кошельки И он-чейн PDA, которые держат OT: RWT Vault PDA (держит OT как обеспечение портфеля), PDA пулов DEX (vault-ы пулов держат OT с PDA пула как authority), и OtTreasury PDA (может держать OT от управления). - Фильтрация: только держатели с суммарным балансом ≥ $100 по протоколу
- Расчёт cumulative_amount для каждого держателя:
total_funded * holder_balance / total_eligible_supply - Доля неподходящих → кошелёк ARL Treasury как лист (доход протокола — Areal Finance получает доходность от держателей ниже порога $100)
- Построение дерева: лист =
sha256(address_bytes || cumulative_amount_le_bytes) - Публикация корня он-чейн через
publish_root
Контракт только верифицирует доказательства — он не сканирует держателей и не вычисляет веса. Всё построение дерева происходит офф-чейн.