SHA256
Добавить спецификации Solana программ и вынести формат PDA
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
# Программа `shine_payments`
|
||||
|
||||
Документ описывает текущее целевое поведение программы `shine_payments`.
|
||||
|
||||
Назначение программы:
|
||||
|
||||
- хранить общие настройки экономической модели платежей и выплат;
|
||||
- принимать покупку тикетов очереди выплат;
|
||||
- хранить очереди и отдельные ticket PDA;
|
||||
- выдавать лимиты менеджерам на ручное добавление тикетов;
|
||||
- выполнять пошаговые выплаты из inflow-вольта.
|
||||
|
||||
Если код и этот документ расходятся, нужно в том же изменении синхронизировать либо код, либо документ.
|
||||
|
||||
## 1. Program ID
|
||||
|
||||
Текущий program id:
|
||||
|
||||
- `m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR`
|
||||
|
||||
## 2. Основная модель
|
||||
|
||||
`shine_payments` разделяет две вещи:
|
||||
|
||||
1. регистрацию долга/обязательств через тикеты;
|
||||
2. фактическую выплату через отдельный inflow-вольт.
|
||||
|
||||
Это важно:
|
||||
|
||||
- покупка тикета не означает немедленную выплату из вольта;
|
||||
- payout выполняется отдельной инструкцией `step_payout`;
|
||||
- inflow-вольт должен быть заранее или отдельно пополнен.
|
||||
|
||||
## 3. PDA и seed-правила
|
||||
|
||||
### 3.1. Single PDA
|
||||
|
||||
- config: `shine_payments_config`
|
||||
- coef/limit: `shine_payments_coef_limit`
|
||||
- queues: `shine_payments_queues`
|
||||
- inflow vault: `shine_payments_inflow_vault`
|
||||
|
||||
### 3.2. Ticket PDA
|
||||
|
||||
- queue 1 seed prefix: `shine_payments_q1_ticket`
|
||||
- queue 2 seed prefix: `shine_payments_q2_ticket`
|
||||
- второй seed: `ticket_index` в little-endian `u64`
|
||||
|
||||
### 3.3. Manager allowance PDA
|
||||
|
||||
- seed prefix: `shine_p_manager_allow`
|
||||
- второй seed: `manager_wallet.as_ref()`
|
||||
|
||||
## 4. Состояния программы
|
||||
|
||||
### 4.1. `ConfigState`
|
||||
|
||||
Поля:
|
||||
|
||||
- `version: u8`
|
||||
- `dao_wallet: Pubkey`
|
||||
- `inflow_vault: Pubkey`
|
||||
|
||||
### 4.2. `CoefLimitState`
|
||||
|
||||
Поля:
|
||||
|
||||
- `version: u8`
|
||||
- `coef_ppm: u64`
|
||||
- `limit_usd_cents: u64`
|
||||
- `call_reward_lamports: u64`
|
||||
|
||||
Смысл:
|
||||
|
||||
- `coef_ppm` — множитель payout в ppm;
|
||||
- `limit_usd_cents` — лимит суммы очереди Q1;
|
||||
- `call_reward_lamports` — награда вызывающему `step_payout`.
|
||||
|
||||
### 4.3. `QueuesState`
|
||||
|
||||
Поля агрегатов:
|
||||
|
||||
- `q1_tickets_total`
|
||||
- `q1_tickets_paid`
|
||||
- `q1_sum_total_usd_cents`
|
||||
- `q1_sum_paid_usd_cents`
|
||||
- `q2_tickets_total`
|
||||
- `q2_tickets_paid`
|
||||
- `q2_sum_total_usd_cents`
|
||||
- `q2_sum_paid_usd_cents`
|
||||
|
||||
### 4.4. `TicketState`
|
||||
|
||||
Поля:
|
||||
|
||||
- `version: u8`
|
||||
- `queue_id: u8`
|
||||
- `index: u64`
|
||||
- `is_paid: bool`
|
||||
- `recipient_wallet: Pubkey`
|
||||
- `payout_usd_cents: u64`
|
||||
- `debt_before_usd_cents: u64`
|
||||
|
||||
### 4.5. `ManagerAllowanceState`
|
||||
|
||||
Поля:
|
||||
|
||||
- `version: u8`
|
||||
- `manager_wallet: Pubkey`
|
||||
- `q1_available_usd_cents: u64`
|
||||
- `q2_available_usd_cents: u64`
|
||||
|
||||
### 4.6. `VaultState`
|
||||
|
||||
Поля:
|
||||
|
||||
- `version: u8`
|
||||
|
||||
Это техническая метка существования inflow PDA. Лампорты лежат на самом PDA-аккаунте.
|
||||
|
||||
## 5. Константы и экономика
|
||||
|
||||
Текущие базовые значения:
|
||||
|
||||
- `COEF_SCALE_PPM = 1_000_000`
|
||||
- `START_COEF_PPM = 5_000_000` (5.0x)
|
||||
- `START_LIMIT_USD_CENTS = 1_000_000` (10_000 USD)
|
||||
- `START_CALL_REWARD_LAMPORTS = 8_000_000`
|
||||
- `MAX_CALL_REWARD_LAMPORTS = 10_000_000`
|
||||
- `ORACLE_MAX_AGE_SECS = 120`
|
||||
- цена SOL/USD берётся из Pyth.
|
||||
|
||||
## 6. Инструкция `init`
|
||||
|
||||
### Назначение
|
||||
|
||||
Инициализировать все базовые PDA программы.
|
||||
|
||||
### Создаваемые PDA
|
||||
|
||||
- `config_pda`
|
||||
- `coef_limit_pda`
|
||||
- `queues_pda`
|
||||
- `inflow_vault_pda`
|
||||
|
||||
### Кто может вызвать
|
||||
|
||||
Технически любой signer, если система ещё не инициализирована.
|
||||
|
||||
### Что записывается
|
||||
|
||||
- `dao_wallet` берётся из settings/deploy config;
|
||||
- `inflow_vault` указывается как PDA самой программы;
|
||||
- коэффициенты и лимиты пишутся стартовыми значениями;
|
||||
- очереди стартуют с нулями.
|
||||
|
||||
## 7. Инструкция `update_coef_limit`
|
||||
|
||||
### Назначение
|
||||
|
||||
Изменить коэффициент выплат, лимит очереди Q1 и награду за шаг выплат.
|
||||
|
||||
### Авторизация
|
||||
|
||||
Только `dao_wallet` из `ConfigState`.
|
||||
|
||||
### Проверки
|
||||
|
||||
- signer = `config.dao_wallet`
|
||||
- `coef_ppm > 0`
|
||||
- `limit_usd_cents > 0`
|
||||
- `call_reward_lamports <= MAX_CALL_REWARD_LAMPORTS`
|
||||
|
||||
## 8. Инструкция `grant_manager_limits`
|
||||
|
||||
### Назначение
|
||||
|
||||
Выдать менеджеру квоты на добавление тикетов вручную.
|
||||
|
||||
### Авторизация
|
||||
|
||||
Только DAO.
|
||||
|
||||
### Поведение
|
||||
|
||||
- создаёт `manager_allowance_pda`, если её ещё нет;
|
||||
- увеличивает доступные лимиты Q1/Q2 для указанного менеджера;
|
||||
- не добавляет тикеты сама.
|
||||
|
||||
## 9. Инструкции покупки тикета
|
||||
|
||||
Есть три входных варианта:
|
||||
|
||||
- `buy_ticket`
|
||||
- `buy_ticket_usd`
|
||||
- `buy_ticket_sol`
|
||||
|
||||
Все они в итоге сводятся к одному действию:
|
||||
|
||||
- рассчитывается `purchase_usd_cents`;
|
||||
- создаётся ticket в очереди `Q1`;
|
||||
- обновляются агрегаты очереди `Q1`.
|
||||
|
||||
### 9.1. Общие правила
|
||||
|
||||
- ticket создаётся только в `Q1`;
|
||||
- очередь `Q1` временно блокируется, если достигнут её суммарный лимит;
|
||||
- `payout_usd_cents = purchase_usd_cents * coef_ppm / COEF_SCALE_PPM`;
|
||||
- `recipient_wallet` записывается в ticket.
|
||||
|
||||
### 9.2. Важная деталь денежного потока
|
||||
|
||||
В текущей реализации при покупке тикета лампорты переводятся:
|
||||
|
||||
- не во inflow vault;
|
||||
- а напрямую в `dao_wallet`.
|
||||
|
||||
То есть:
|
||||
|
||||
- ticket фиксирует долг на будущую выплату;
|
||||
- а inflow vault — отдельный источник средств для исполнения payouts.
|
||||
|
||||
Эта деталь обязательно должна быть осознана при переписи: либо её сохраняют как сознательную модель, либо отдельно меняют и тогда обновляют этот документ.
|
||||
|
||||
## 10. Инструкция `manager_add_ticket`
|
||||
|
||||
### Назначение
|
||||
|
||||
Дать менеджеру возможность вручную создать ticket в `Q1` или `Q2`.
|
||||
|
||||
### Авторизация
|
||||
|
||||
Signer должен совпадать с `manager_wallet` в `manager_allowance_pda`.
|
||||
|
||||
### Проверки
|
||||
|
||||
- `queue_id` только `1` или `2`;
|
||||
- `payout_usd_cents > 0`;
|
||||
- доступный allowance по очереди не меньше суммы тикета;
|
||||
- ticket PDA ещё не существует.
|
||||
|
||||
### Эффект
|
||||
|
||||
- создаётся ticket;
|
||||
- allowance уменьшается;
|
||||
- агрегаты соответствующей очереди увеличиваются.
|
||||
|
||||
## 11. Инструкция `step_payout`
|
||||
|
||||
### Назначение
|
||||
|
||||
Сделать один шаг исполнения очереди выплат.
|
||||
|
||||
### Выбор очереди
|
||||
|
||||
- если в `Q1` есть pending ticket, сначала обслуживается `Q1`;
|
||||
- если `Q1` пустая, обслуживается `Q2`.
|
||||
|
||||
### Что считается pending
|
||||
|
||||
```text
|
||||
pending = tickets_total - tickets_paid
|
||||
```
|
||||
|
||||
### Как выбирается ticket
|
||||
|
||||
Берётся следующий индекс:
|
||||
|
||||
```text
|
||||
next_index = tickets_paid + 1
|
||||
```
|
||||
|
||||
### Проверки
|
||||
|
||||
- `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda` валидны;
|
||||
- `dao_wallet` совпадает с `config.dao_wallet`;
|
||||
- `next_ticket_pda` совпадает с PDA следующего тикета;
|
||||
- тикет ещё не оплачен;
|
||||
- `ticket_recipient_wallet` совпадает с `ticket.recipient_wallet`;
|
||||
- в inflow vault достаточно средств на весь шаг.
|
||||
|
||||
### Сколько переводится
|
||||
|
||||
Для target queue:
|
||||
|
||||
- получателю тикета переводится `ticket_lamports`;
|
||||
- DAO переводится `dao_lamports`;
|
||||
- вызвавшему шаг переводится `call_reward_lamports`.
|
||||
|
||||
`dao_multiplier`:
|
||||
|
||||
- для `Q1` = `1`
|
||||
- для `Q2` = `2`
|
||||
|
||||
То есть DAO получает:
|
||||
|
||||
- `1x payout_usd` для Q1;
|
||||
- `2x payout_usd` для Q2.
|
||||
|
||||
### Источник денег
|
||||
|
||||
Все три перевода идут из inflow vault PDA.
|
||||
|
||||
### Если pending нет
|
||||
|
||||
Если обе очереди пусты:
|
||||
|
||||
- весь доступный остаток inflow vault переводится в DAO wallet.
|
||||
|
||||
## 12. Инструкция `change_ticket_recipient`
|
||||
|
||||
### Назначение
|
||||
|
||||
Позволить текущему получателю тикета поменять адрес получения, пока ticket ещё не исполнен.
|
||||
|
||||
### Авторизация
|
||||
|
||||
Только текущий `ticket.recipient_wallet`.
|
||||
|
||||
### Ограничения
|
||||
|
||||
Нельзя менять получателя у следующего тикета на выплату в активной очереди.
|
||||
|
||||
Логика:
|
||||
|
||||
- если в `Q1` есть pending — следующий ticket определяется в `Q1`;
|
||||
- иначе берётся следующий ticket в `Q2`;
|
||||
- если текущий ticket и есть этот ближайший ticket, смена recipient запрещена.
|
||||
|
||||
## 13. Pyth oracle и конвертация
|
||||
|
||||
Программа использует Pyth SOL/USD.
|
||||
|
||||
Проверки oracle:
|
||||
|
||||
- передан именно тот oracle account, что указан в settings;
|
||||
- feed id совпадает с ожидаемым;
|
||||
- цена не старше `ORACLE_MAX_AGE_SECS`;
|
||||
- цена положительная и корректно переводима в ratio.
|
||||
|
||||
Внутренние преобразования:
|
||||
|
||||
- `lamports -> usd_cents` делаются с округлением вниз;
|
||||
- `usd_cents -> lamports` делаются с округлением вверх.
|
||||
|
||||
Это важно, чтобы не недоплачивать обязательства при payout.
|
||||
|
||||
## 14. Ошибки и классы отказа
|
||||
|
||||
Программа должна различать как минимум:
|
||||
|
||||
- неверный inflow vault;
|
||||
- неверный DAO wallet;
|
||||
- неавторизованный DAO;
|
||||
- неавторизованный manager;
|
||||
- неверный manager allowance PDA;
|
||||
- неверный queue id;
|
||||
- тикет уже выплачен;
|
||||
- неверный recipient;
|
||||
- нельзя сменить recipient ближайшего тикета;
|
||||
- недостаточно средств inflow vault для step payout;
|
||||
- queue temporarily paused из-за лимита;
|
||||
- oracle account/feed/price invalid;
|
||||
- slippage exceeded.
|
||||
|
||||
## 15. Что должно сохраниться при переписи без Anchor
|
||||
|
||||
Обязательно сохранить:
|
||||
|
||||
- те же PDA seed-правила;
|
||||
- те же состояния и поля;
|
||||
- ту же модель очередей Q1/Q2;
|
||||
- ту же приоритетность `Q1` над `Q2` в `step_payout`;
|
||||
- ту же логику allowance менеджера;
|
||||
- те же oracle-ограничения и округления;
|
||||
- ту же текущую модель денежных потоков, если она не меняется отдельным решением.
|
||||
|
||||
Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ.
|
||||
Reference in New Issue
Block a user