Перейти к основному содержанию

Распределение доходности (Yield Distribution)

✅ Готово к разработке

Данная спецификация контракта прошла аудит бизнес-логики, техническое ревью, проверку кросс-программной интеграции (OT + RWT Engine), верификацию математики вестинга и стандартизацию именования. Разработчик может реализовать контракт по этому документу.
Распределяет доходность в RWT держателям OT пропорционально их балансу токенов. Каждый OT-проект имеет один перманентный дистрибьютор — пополняется ежемесячно в RWT, вестинг линейный на протяжении 365 дней. Держатели могут клеймить в любое время; UI показывает баланс, растущий в реальном времени.
Обновляемый контракт. Полномочия на обновление программы = Team Multisig (Squads). Отделены от config authority и publish authority.

Ключевые концепции

11 инструкций

Конфигурация, создание/закрытие дистрибьютора, конвертация, пополнение, публикация корня, клейм, управление полномочиями

4 аккаунта состояния

DistributionConfig, MerkleDistributor, Accumulator, ClaimStatus

4 PDA Seeds

dist_config, merkle_dist, accumulator, claim_status

Как это работает

1

Поступление дохода OT (ежемесячно)

Контракт OT через distribute_revenue отправляет 70% USDC в Accumulator PDA дистрибьютора (USDC ATA, принадлежащий контракту). Внешние кошельки не нужны.
2

Конвертация USDC → RWT + пополнение (атомарно)

Крэнк вызывает convert_to_rwt — контракт обменивает USDC на DEX по цене до NAV, минтит остаток по NAV, депонирует RWT в reward vault и обновляет состояние вестинга. Всё в одной атомарной инструкции. Ранее завестированная сумма фиксируется, новые RWT начинают вестинг с текущего момента.
3

Публикация корня Меркла (каждые 10 мин)

Publish authority (серверный кошелёк) сканирует держателей OT офф-чейн, строит дерево Меркла, публикует корень он-чейн через publish_root. Держатели с балансом менее $100 исключаются; их доля → ARL Treasury (доход протокола).
4

Держатели клеймят (в любое время)

Держатель вызывает claim с доказательством Меркла. Контракт вычисляет завестированную сумму для данного получателя и переводит RWT. Можно клеймить сколько угодно часто — вестинг начисляется каждую секунду.

Модель вестинга

Перманентный дистрибьютор с инкрементальным пополнением. И convert_to_rwt, и fund_distributor используют идентичную логику вестинга при добавлении RWT:
// All vesting calculations cast to u128 BEFORE multiply to prevent overflow

On fund (either convert_to_rwt or fund_distributor):
  elapsed = now - last_fund_ts
  locked_vested += (total_funded - locked_vested) * min(elapsed, period) / period  // u128
  total_funded += new_amount (net after protocol fee)
  last_fund_ts = now

On claim:
  new_portion = total_funded - locked_vested
  new_vested = new_portion * min(now - last_fund_ts, period) / period
  total_vested = min(locked_vested + new_vested, max_total_claim)
  
  my_share = total_vested * my_cumulative_amount / max_total_claim
  claimable = my_share - already_claimed
UX: UI вычисляет total_vested на стороне клиента каждую секунду, чтобы показывать баланс, растущий в реальном времени. Фактический перевод он-чейн происходит только когда держатель нажимает «Клеймить». Это стандартный паттерн в DeFi (Jito, Marinade и др.).
**Минимум 100впортфеле:Толькодержателиссуммарнымбалансом100 в портфеле:** Только держатели с суммарным балансом ≥ 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 минут
Горячий кошелёк, строящий деревья Меркла офф-чейн и публикующий корни. При компрометации config authority может его заменить. НЕ контролирует средства.

🌐 Без ограничений доступа

Любой кошелёк / Крэнк-бот
  • convert_to_rwt — конвертация USDC→RWT
  • fund_distributor — депозит RWT
  • claim — клейм доходности держателем

Инструкции

Инициализация

Создание глобальной конфигурации распределения. Вызывается один раз при деплое протокола.Параметры:
ParameterTypeDescription
protocol_fee_bpsu16Комиссия на депозиты (по умолчанию: 25 = 0.25%)
min_distribution_amountu64Минимальная сумма пополнения (по умолчанию: эквивалент $100)
areal_fee_destinationPubkeyКуда направляются комиссии — RWT ATA Areal Finance (статичный, неизменяемый). Примечание: это RWT-аккаунт, не USDC — комиссия протокола YD удерживается в RWT после конвертации.
publish_authorityPubkeyСерверный кошелёк для publish_root
Вызывающий: Деплоер (однократно)Аккаунты:
  • deployer (signer, mut) — оплачивает создание аккаунта
  • config (init) — PDA seed: ["dist_config"]
  • system_program
Создаёт:
  • DistributionConfig PDA — глобальный синглтон
Начальное состояние:
  • authority = deployer (передаётся Team Multisig после бутстрапа)
  • publish_authority = publish_authority param
  • is_active = true

Жизненный цикл дистрибьютора

Создание перманентного дистрибьютора для OT-проекта. Один на OT mint, существует бессрочно. Также создаёт Accumulator PDA, куда поступает USDC-доход.
ParameterTypeDescription
vesting_period_secsi64Период вестинга (по умолчанию: 31 536 000 = 365 дней)
Вызывающий: Authority (Team Multisig)Аккаунты:
  • authority (signer) — должен совпадать с config.authority
  • config — проверка authority, is_active
  • ot_mint — OT, который обслуживает данный дистрибьютор
  • distributor (init) — PDA seed: ["merkle_dist", ot_mint]
  • reward_vault (init) — RWT-аккаунт, authority = distributor PDA
  • accumulator (init) — PDA seed: ["accumulator", ot_mint]
  • accumulator_usdc_ata (init) — USDC ATA, authority = accumulator PDA
  • rwt_mint — минт токена RWT
  • usdc_mint — минт USDC (для accumulator ATA)
  • token_program, system_program, associated_token_program
Создаёт:
  • MerkleDistributor PDA — состояние перманентного дистрибьютора
  • Reward Vault — RWT ATA, принадлежащий distributor PDA
  • Accumulator PDA — получает USDC от дохода OT
  • Accumulator USDC ATA — аккаунт хранения USDC
Начальное состояние:
  • total_funded = 0, total_claimed = 0
  • locked_vested = 0, last_fund_ts = now
  • merkle_root = [0; 32], epoch = 0
  • is_active = true
Валидация:
  • vesting_period_secs > 0
Окончательное закрытие дистрибьютора. Переводит все невостребованные RWT на указанный адрес.Вызывающий: Authority (Team Multisig)Аккаунты:
  • authority (signer) — должен совпадать с config.authority
  • config
  • distributor (mut)
  • reward_vault (mut) — RWT для перевода
  • unclaimed_destination (mut) — получает оставшиеся RWT (обычно ARL Treasury ATA)
  • token_program
Валидация:
  • distributor.is_active == true (нельзя закрыть повторно)
Эффект: Переводит все оставшиеся RWT → unclaimed_destination. Устанавливает distributor.is_active = false. Эмитирует DistributorClosed.
Закрытие дистрибьютора немедленно прекращает все клеймы. Держатели с незавестированными наградами теряют их. Authority должен убедиться, что весь вестинг завершён, или заранее предупредить о закрытии. Рекомендуется дождаться total_vested ≈ total_funded перед закрытием.

Пополнение

Конвертация накопленных USDC в RWT. Два шага: обмен на DEX по цене до NAV, минтинг остатка по NAV через RWT Engine. Accumulator PDA подписывает все переводы — внешние ключи не нужны.
ParameterTypeDescription
max_swap_amountu64Максимум USDC для обмена на DEX (остаток минтится по NAV)
min_rwt_outu64Минимум RWT к получению (защита от проскальзывания против сэндвич-атак при обмене на DEX)
Вызывающий: Permissionless (крэнк)Аккаунты:
  • crank (signer, mut)
  • config — чтение protocol_fee_bps, areal_fee_destination
  • distributor (mut) — обновление locked_vested, total_funded, last_fund_ts
  • accumulator — PDA, подписывает переводы USDC и RWT через seeds
  • fee_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"], получает депозит USDC
    • rwt_mint (mut) — минт токена RWT, authority = rwt_vault PDA
    • capital_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_ID
  • token_program
Логика:
  1. Чтение баланса USDC аккумулятора. Если 0, возврат.
  2. Расчёт цены пула DEX vs NAV RWT
  3. Если цена пула < NAV: CPI-обмен min(max_swap_amount, balance) USDC → RWT на DEX
  4. Если USDC остались: CPI к rwt_engine::mint_rwt по NAV (accumulator PDA подписывает как user)
  5. Запасной путь: Если пул DEX не существует или имеет нулевую ликвидность: минтить ВСЁ по NAV через RWT Engine. В этом случае весь объём USDC обходит DEX — комиссии LP не генерируются, объёма свопов нет. Это ожидаемо при бутстрапе (до создания пулов), но не должно происходить в нормальном режиме.
  6. Расчёт комиссии протокола на полученные RWT: fee = rwt_acquired * protocol_fee_bps / 10,000
  7. Перевод fee RWT → fee_account (accumulator PDA подписывает)
  8. Перевод rwt_acquired - fee → reward_vault (accumulator PDA подписывает)
  9. Фиксация вестинга: locked_vested += (total_funded - locked_vested) * min(elapsed, period) / period (приведение к u128)
  10. total_funded += rwt_acquired - fee
  11. last_fund_ts = now
  12. Валидация: rwt_acquired >= min_rwt_out (проверка проскальзывания — откат при сэндвич-атаке с невыгодным обменом)
  13. Эмитирование StreamConverted
Эффект: USDC конвертированы в RWT и депонированы в reward vault в одной атомарной операции. Обновляет состояние вестинга дистрибьютора. Отдельный вызов fund_distributor для сконвертированных средств не нужен.
Эта инструкция объединяет конвертацию + пополнение в одну атомарную операцию. Accumulator PDA подписывает и обмен/минт USDC, и перевод RWT в reward vault. Нет промежуточного состояния, где RWT «висят» между аккаунтами.
Циклическая зависимость: YD вызывает rwt_engine::mint_rwt, а RWT Engine вызывает yd::claim (через claim_yield). Чтобы избежать циклических импортов крейтов Anchor, YD использует raw invoke_signed для CPI к RWT Engine. Дискриминатор: sha256("global:mint_rwt")[..8]. Layout аккаунтов должен точно соответствовать инструкции mint_rwt RWT Engine.
Депозит RWT в reward vault дистрибьютора. Фиксирует ранее завестированную сумму и начинает вестинг новой порции с текущего момента.
ParameterTypeDescription
amountu64Сумма RWT для депозита
Вызывающий: Permissionless — пополнить может любойАккаунты:
  • depositor (signer)
  • config — проверка is_active
  • distributor (mut) — обновление locked_vested, total_funded, last_fund_ts
  • reward_vault (mut) — получает RWT
  • depositor_token (mut) — исходный RWT ATA, constraint: owner == depositor
  • fee_account (mut) — constraint: key == config.areal_fee_destination
  • token_program
Валидация:
  • amount > 0
  • amount >= config.min_distribution_amount (по умолчанию эквивалент $100)
  • distributor.is_active == true
Логика:
  1. Расчёт комиссии протокола: fee = amount * protocol_fee_bps / 10,000
  2. Перевод fee → fee_account
  3. Перевод amount - fee → reward_vault
  4. Фиксация вестинга: locked_vested += (total_funded - locked_vested) * min(elapsed, period) / period (приведение к u128 перед умножением)
  5. total_funded += amount - fee
  6. last_fund_ts = now
  7. Эмитирование DistributorFunded
Расчёт locked_vested сохраняет уже заработанную доходность при поступлении новых средств. Без этого добавление средств разбавляло бы прогресс вестинга существующих депозитов.

Распределение

Публикация нового корня Меркла, представляющего текущие веса держателей OT. Вызывается каждые 10 минут publish authority (серверным кошельком).
ParameterTypeDescription
merkle_root[u8; 32]Хеш корня дерева Меркла
max_total_claimu64Должен равняться текущему total_funded
Вызывающий: Publish authority (серверный кошелёк)Аккаунты:
  • publish_authority (signer) — должен совпадать с config.publish_authority
  • config — проверка publish_authority, is_active
  • distributor (mut) — обновление merkle_root, epoch
Валидация:
  • max_total_claim > 0 (предотвращает деление на ноль в claim — нельзя публиковать корень для непополненного дистрибьютора)
  • max_total_claim == distributor.total_funded
  • max_total_claim >= distributor.total_claimed
Формат листа Меркла: sha256(claimant_pubkey_bytes || cumulative_amount_le_bytes)Где cumulative_amount = пропорциональная доля держателя от total_funded:
cumulative_amount = total_funded * holder_ot_balance / total_ot_supply
Эффект: Инкрементирует epoch, сохраняет новый корень. Эмитирует RootPublished.Рекомендуемая частота: Каждые 10 минут (офф-чейн крэнк). Обеспечивает отражение изменений баланса в течение 10 мин. Также должен вызываться сразу после каждого fund_distributor, чтобы новые средства стали доступны для клейма. Стоимость: ~$2.60/месяц на OT-проект.
Клейм завестированных наград в RWT. Может вызываться в любое время, сколько угодно часто. Вестинг начисляется каждую секунду.
ParameterTypeDescription
cumulative_amountu64Кумулятивная доля держателя (из листа Меркла)
proofVec<[u8; 32]>Путь доказательства Меркла (макс. 20 узлов)
Вызывающий: Держатель (или PDA через CPI — напр., RWT Vault, ARL Treasury)Аккаунты:
  • claimant (signer) — кошелёк держателя или PDA
  • payer (signer, mut) — оплачивает ренту за инициализацию ClaimStatus при первом клейме
  • distributor — чтение состояния вестинга, merkle_root
  • claim_status (init_if_needed) — PDA seed: ["claim_status", distributor, claimant]
  • reward_vault (mut) — источник RWT
  • claimant_token (mut) — RWT ATA держателя, constraint: mint == reward_vault.mint
  • token_program, system_program
Валидация:
  • distributor.is_active == true (нельзя клеймить из закрытого дистрибьютора)
  • Доказательство Меркла: verify(proof, root, sha256(claimant || cumulative_amount))
  • proof.len() <= MAX_PROOF_LEN (20)
  • epoch > 0 (корень должен быть опубликован)
Логика:
  1. Верификация доказательства Меркла
  2. Инициализация ClaimStatus при первом клейме (claimant, distributor, claimed_amount=0)
  3. Расчёт total_vested (приведение к u128 до умножения, аналогично логике fund):
    new_portion = total_funded - locked_vested
    new_vested = (new_portion as u128) * (min(now - last_fund_ts, vesting_period) as u128) / (vesting_period as u128)
    total_vested = locked_vested + new_vested as u64
    
  4. Ограничение опубликованным корнем: total_vested = min(total_vested, max_total_claim) (предотвращает избыточный клейм, если fund произошёл после последнего publish_root)
  5. Минимальный вестинг: total_vested = max(total_vested, min(MIN_VESTED_AMOUNT, max_total_claim))
  6. Персональная доля: my_vested = (total_vested as u128) * cumulative_amount / max_total_claim
  7. claimable = my_vested - claim_status.claimed_amount
  8. Если claimable == 0, возврат
  9. Перевод claimable RWT: reward_vault → claimant_token (distributor PDA подписывает)
  10. claim_status.claimed_amount += claimable
  11. distributor.total_claimed += claimable
  12. Эмитирование RewardsClaimed

Конфигурация и полномочия

Обновление глобальной конфигурации распределения.
ParameterTypeDescription
protocol_fee_bpsu16Новая комиссия в BPS
min_distribution_amountu64Новый минимум
is_activeboolГлобальный флаг активности
Вызывающий: Authority (Team Multisig)Аккаунты:
  • authority (signer) — должен совпадать с config.authority
  • config (mut)
Эффект: Перезаписывает значения конфигурации. Эмитирует ConfigUpdated.
areal_fee_destination является неизменяемым — устанавливается при инициализации, изменить нельзя. Только обновление программы может его модифицировать.
Смена серверного кошелька, который может вызывать publish_root.
ParameterTypeDescription
new_publish_authorityPubkeyНовый серверный кошелёк
Вызывающий: Authority (Team Multisig)Аккаунты:
  • authority (signer) — должен совпадать с config.authority
  • config (mut)
Эффект: Устанавливает config.publish_authority = new_publish_authority. Эмитирует PublishAuthorityUpdated.
Шаг 1: Текущий authority предлагает нового config authority.
ParameterTypeDescription
new_authorityPubkeyПредлагаемый новый authority
Вызывающий: Текущий authorityАккаунты:
  • authority (signer) — должен совпадать с config.authority
  • config (mut)
Валидация: new_authority ≠ current authority (нельзя передать самому себе)Эффект: Устанавливает config.pending_authority = Some(new_authority). Эмитирует AuthorityTransferProposed.
Повторный вызов перезаписывает существующий pending_authority. Предыдущий предложенный authority теряет возможность принять передачу.
Шаг 2: Предложенный authority принимает передачу полномочий.Вызывающий: Предложенный новый authority (должен подписать)Аккаунты:
  • new_authority (signer) — должен совпадать с config.pending_authority
  • config (mut)
Валидация: signer == config.pending_authorityЭффект: Устанавливает authority = new_authority, очищает pending_authority. Эмитирует AuthorityTransferAccepted.

Аккаунты состояния

DistributionConfig

Глобальный синглтон. Один на деплой протокола.
FieldTypeDescription
authorityPubkeyConfig authority (Team Multisig после бутстрапа)
pending_authorityOption<Pubkey>Целевой адрес ожидающей передачи полномочий
publish_authorityPubkeyСерверный кошелёк для publish_root (изменяемый authority)
protocol_fee_bpsu16Комиссия на fund_distributor (по умолчанию: 25 = 0.25%)
min_distribution_amountu64Минимальная сумма пополнения
areal_fee_destinationPubkeyRWT ATA Areal Finance — получает комиссию протокола в RWT (статичный, неизменяемый)
is_activeboolГлобальный аварийный выключатель
bumpu8PDA bump seed
PDA Seed: ["dist_config"]

MerkleDistributor

Один на OT-проект, перманентный (никогда не пересоздаётся).
FieldTypeDescription
ot_mintPubkeyOT, который обслуживает данный дистрибьютор
reward_vaultPubkeyRWT-аккаунт (authority = distributor PDA)
accumulatorPubkeyAccumulator PDA для данного OT (USDC поступают сюда)
merkle_root[u8; 32]Текущий корень Меркла весов держателей
max_total_claimu64Равен total_funded (предохранительный лимит)
total_claimedu64Кумулятивно заклеймленные RWT по всем держателям
total_fundedu64Кумулятивно депонированные RWT (растёт с каждым пополнением)
locked_vestedu64RWT с гарантированным вестингом (фиксируется при каждом пополнении)
last_fund_tsi64Временная метка последнего вызова fund_distributor
vesting_period_secsi64Длительность вестинга (по умолчанию: 365 дней)
epochu64Счётчик publish_root
is_activeboolФлаг активности (false после close_distributor)
bumpu8PDA bump seed
PDA Seed: ["merkle_dist", ot_mint]
Нет stream_id в seed — один дистрибьютор на OT mint. Это ключевое упрощение по сравнению с архитектурой множественных потоков.

Accumulator

Аккаунт приёма USDC для каждого OT. Принадлежит контракту — внешние ключи не нужны. OT distribute_revenue отправляет USDC сюда. convert_to_rwt тратит отсюда.
FieldTypeDescription
ot_mintPubkeyOT, который обслуживает данный аккумулятор
bumpu8PDA bump seed
PDA Seed: ["accumulator", ot_mint]
Accumulator не хранит баланс USDC в состоянии. Баланс считывается из USDC ATA, принадлежащего этому PDA. PDA подписывает переводы USDC в convert_to_rwt через seeds.

ClaimStatus

Трекинг для каждой пары (distributor, claimant). Создаётся при первом клейме через init_if_needed.
FieldTypeDescription
claimantPubkeyКошелёк держателя или PDA
distributorPubkeyК какому дистрибьютору относится
claimed_amountu64Кумулятивно заклеймленные RWT
bumpu8PDA bump seed
PDA Seed: ["claim_status", distributor, claimant]

PDA Seeds

AccountSeedsDescription
DistributionConfig"dist_config"Глобальный синглтон конфигурации
MerkleDistributor"merkle_dist", ot_mintПерманентный дистрибьютор для каждого OT
Accumulator"accumulator", ot_mintПриёмник USDC для каждого OT (принадлежит контракту)
ClaimStatus"claim_status", distributor, claimantТрекинг клеймов для каждого держателя

Константы

ConstantValueDescription
BPS_DENOMINATOR10,000100% в базисных пунктах
DEFAULT_PROTOCOL_FEE_BPS25Комиссия 0.25% на fund_distributor
DEFAULT_MIN_DISTRIBUTION100,000,000Эквивалент $100 (6 десятичных знаков)
DEFAULT_VESTING_PERIOD31,536,000365 дней в секундах
MAX_PROOF_LEN20Максимальная глубина доказательства Меркла (~1 млн держателей)
MIN_VESTED_AMOUNT1,000,000Минимум 1 RWT завестирован (предотвращает зависание на нуле)
RWT_ENGINE_PROGRAM_IDhardcodedВалидируется в convert_to_rwt
DEX_PROGRAM_IDhardcodedВалидируется в convert_to_rwt

События

EventFieldsWhen
ConfigInitializedauthority, publish_authority, protocol_fee_bps, timestampКонфигурация создана
DistributorCreatedot_mint, reward_vault, accumulator, vesting_period_secs, timestampДистрибьютор создан
DistributorFundedot_mint, amount, protocol_fee, total_funded, locked_vested, timestampRWT депонированы
StreamConvertedot_mint, usdc_swapped, rwt_minted, total_rwt, timestampКонвертация USDC→RWT
RootPublishedot_mint, epoch, merkle_root, max_total_claim, timestampНовый корень опубликован
RewardsClaimedclaimant, ot_mint, amount, cumulative_claimed, timestampДержатель заклеймил RWT
ConfigUpdatedprotocol_fee_bps, min_distribution_amount, is_active, timestampКонфигурация изменена
PublishAuthorityUpdatedold_publish_authority, new_publish_authority, timestampСерверный кошелёк изменён
AuthorityTransferProposedcurrent_authority, pending_authority, timestampПередача предложена
AuthorityTransferAcceptedold_authority, new_authority, timestampПередача принята
DistributorClosedot_mint, unclaimed_swept, timestampДистрибьютор закрыт

Коды ошибок

ErrorDescription
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)
ExceedsMaxClaimtotal_claimed превысит max_total_claim
SlippageExceededrwt_acquired < min_rwt_out (защита от сэндвич-атак)
MathOverflowАрифметическое переполнение
InvalidVestingPeriodvesting_period_secs должен быть > 0
SelfTransferНельзя передать полномочия самому себе
NoPendingAuthorityНет ожидающей передачи для принятия
InvalidPendingAuthorityПодписант ≠ pending_authority

Архитектура и руководство по интеграции

Кросс-программная интеграция

  • 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)
  • 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 Vault является держателем OT → имеет лист в дереве Меркла
  • RWT Engine claim_yield вызывает CPI к yield_distribution::claim
  • Vault PDA подписывает как claimant
  • Доля неподходящих держателей со ВСЕХ 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
  • Config authority = Team Multisig после бутстрапа
  • create/close distributor, update_config, update_publish_authority контролируются командой

Модель полномочий

🔧 Обновление программы

Team Multisig (Squads)Может деплоить новые версии контракта.

🏛️ Config Authority

Team Multisig (после бутстрапа)
  • create_distributor / close_distributor
  • update_config
  • update_publish_authority
  • propose_authority_transfer

📡 Publish Authority

Серверный кошелёк (крэнк-бот на VPS)
  • publish_root (каждые 10 мин)
Строит дерево Меркла офф-чейн, публикует корень. Заменяемый config authority.
Неизменяемый после инициализации: areal_fee_destination нельзя изменить. Только обновление программы может его модифицировать.
Доверие к publish authority: Если серверный кошелёк скомпрометирован, злоумышленник может опубликовать поддельный корень Меркла и заклеймить доходность на свои кошельки.Он-чейн защита:
  • ClaimStatus отслеживает cumulative_claimed для каждого claimant — после клейма держатель не может повторно заклеймить ту же сумму даже под новым корнем. Злоумышленник не может украсть уже заклеймленные средства.
  • max_total_claim == total_funded — ограничивает общую сумму клеймов фактическим количеством RWT в vault, предотвращая атаки инфляции.
  • config.is_active аварийный выключатель — Team Multisig может немедленно приостановить все клеймы через update_config(is_active: false).
  • update_publish_authority — Team Multisig мгновенно заменяет скомпрометированный кошелёк.
Офф-чейн защита:
  • Все публикации корней видны он-чейн через события RootPublished — мониторинг неожиданных корней.
  • Автоматическое оповещение при публикации корня неожиданным кошельком или с необычной частотой.
Ограничение: Нет механизма истории или инвалидации корней. Поддельный корень действителен до перезаписи следующим publish_root. Окно уязвимости = время между обнаружением компрометации и update_publish_authority + новый publish_root.

Чек-лист деплоя

Предварительные требования: RWT Engine и Native DEX должны быть задеплоены (необходимы для CPI convert_to_rwt).
  1. Вызвать initialize_config с areal_fee_destination, publish_authority (серверный кошелёк)
  2. Создать дистрибьютор для каждого OT-проекта через create_distributor (также создаёт Accumulator PDA)
  3. Настроить назначение дохода OT на USDC ATA Accumulator
  4. Передать config authority Team Multisig через propose_authority_transfer + accept_authority_transfer
  5. Запустить крэнк-бот — publish_root каждые 10 минут, convert_to_rwt после каждого распределения дохода OT
Шаги 2 и 3 должны быть выполнены до первого распределения дохода OT. Иначе USDC некуда будет отправить.

Сводка потоков токенов

FromToMechanismWho triggers
Доход OT (USDC)Accumulator USDC ATAOT distribute_revenueКрэнк
Accumulator USDCReward vault RWT (через DEX + минт)convert_to_rwt (атомарно: конвертация + пополнение)Крэнк
Внешние RWTReward vaultfund_distributor (прямой депозит RWT)Любой
Reward vaultRWT ATA держателяclaim (вестинг для каждого получателя)Держатель
Reward vaultRWT Vault ATAclaim (через CPI RWT Engine)Крэнк
Reward vaultARL OtTreasury RWT ATAclaim (через OT claim_yd_for_treasury на инстансе ARL)Крэнк
Reward vaultНазначение (при закрытии)close_distributorAuthority

Построение дерева Меркла (офф-чейн)

Дерево Меркла строится офф-чейн сервером publish authority каждые 10 минут:
  1. Сканирование всех токен-аккаунтов для OT mint (getParsedProgramAccounts). Включает обычные кошельки И он-чейн PDA, которые держат OT: RWT Vault PDA (держит OT как обеспечение портфеля), PDA пулов DEX (vault-ы пулов держат OT с PDA пула как authority), и OtTreasury PDA (может держать OT от управления).
  2. Фильтрация: только держатели с суммарным балансом ≥ $100 по протоколу
  3. Расчёт cumulative_amount для каждого держателя: total_funded * holder_balance / total_eligible_supply
  4. Доля неподходящих → кошелёк ARL Treasury как лист (доход протокола — Areal Finance получает доходность от держателей ниже порога $100)
  5. Построение дерева: лист = sha256(address_bytes || cumulative_amount_le_bytes)
  6. Публикация корня он-чейн через publish_root
Рекомендуемая частота: Каждые 10 минут. Стоимость: ~2.60/месяцнаOTпроект.Оффчейнвычисления: 300мснаOT(10Кдержателей).Инфраструктура:VPS(2.60/месяц на OT-проект. Офф-чейн вычисления: ~300 мс на OT (10К держателей). Инфраструктура: VPS (10/месяц) + бесплатный RPC-тариф.
Контракт только верифицирует доказательства — он не сканирует держателей и не вычисляет веса. Всё построение дерева происходит офф-чейн.