Розробка системи розподілу доходів для власників токенів
Revenue sharing — механізм розповсюження доходу протоколу між власниками токенів. Звучить просто, але реалізація містить кілька нетривіальних завдань: як честиво розраховувати долю кожного власника, як ефективно розповсюджувати між тисячами адрес, що робити з невиконаним доходом та як уникнути flash loan атак.
Моделі розповсюження
Continuous streaming (Superfluid/Sablier)
Дохід «течет» до власників постійно пропорційно балансу. Теоретично ідеально — практично складно: при кожному трансфері токена потрібно перераховувати потоки. На Ethereum це дорого при великій кількості власників.
Snapshot + Merkle Distribution
Найпоширеніша модель. Раз на період (тиждень/місяць) робиться snapshot балансів, розраховується кожна доля, будується Merkle tree. Власники самі забирають свою частку, надаючи Merkle proof.
contract RevenueDistributor {
IERC20 public immutable rewardToken;
bytes32 public merkleRoot;
uint256 public distributionId;
mapping(uint256 => mapping(address => bool)) public claimed;
function setDistribution(bytes32 _root) external onlyOwner {
distributionId++;
merkleRoot = _root;
emit DistributionSet(distributionId, _root);
}
function claim(
uint256 amount,
bytes32[] calldata proof
) external {
require(!claimed[distributionId][msg.sender], "Already claimed");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, amount));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
claimed[distributionId][msg.sender] = true;
rewardToken.transfer(msg.sender, amount);
emit Claimed(distributionId, msg.sender, amount);
}
}
Плюс: масштабується на будь-яку кількість власників, користувач платить газ. Мінус: потребує off-chain інфраструктури для snapshot та генерації Merkle tree.
Dividend-bearing token (редистрибуція через індекс)
Модель MasterChef / Synthetix Rewards: контракт зберігає rewardPerTokenStored. При кожному новому поступленні доходу індекс оновлюється. При claim користувач отримує різницю між поточним індексом та тим, що був при останньому claim.
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
function rewardPerToken() public view returns (uint256) {
if (totalStaked == 0) return rewardPerTokenStored;
return rewardPerTokenStored + (
(rewardRate * (block.timestamp - lastUpdateTime) * 1e18) / totalStaked
);
}
function earned(address account) public view returns (uint256) {
return (
(balanceOf(account) * (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
) + rewards[account];
}
Це операція O(1) per user — не потрібно робити snapshot усіх власників. Ідеально для staking контрактів. Не підходить, якщо токени не застейковані — потрібно враховувати non-staked балансі.
Захист від flash loan атак
Найкритичніша проблема: зловмисник бере flash loan на огромну суму токенів, snapshot попадає в той же блок, він забирає непропорційно велику частку.
Рішення 1: Time-weighted balance. Snapshot розраховує не поточний баланс, а time-weighted average за період. Навіть якщо атакуючий має великий баланс у момент snapshot — це не допоможе, якщо до цього баланс був нульовим.
Рішення 2: Minimum holding period. Право на revenue sharing отримують тільки адреси, що тримають токени довше N днів. Реалізується через timestamp останнього transfer.
Рішення 3: Snapshot через Chainlink або commit-reveal. Момент snapshot не відомий заздалегідь, визначається випадково або з затримкою. Атакуючий не може підготуватися.
// Запис timestamps останніх входящих переводів
mapping(address => uint256) public lastReceived;
function _afterTokenTransfer(address, address to, uint256) internal override {
lastReceived[to] = block.timestamp;
}
// При snapshot: включаємо тільки адреси з lastReceived > N днів назад
function isEligible(address holder) public view returns (bool) {
return lastReceived[holder] <= block.timestamp - MIN_HOLD_DURATION;
}
Мультитокенне розповсюження
Протокол часто генерує дохід у різних токенах: ETH від trading fees, USDC від стабілізаційного механізму, native токен з emission. Система повинна агрегувати всі це.
Варіант 1: все конвертується у единий reward token (наприклад, USDC) перед розповсюженням. Просто для користувачів, потребує on-chain swap через DEX.
Варіант 2: multi-reward контракт з окремим індексом для кожного токена. Користувач забирає кілька токенів за одну транзакцію.
// Multi-reward: окремий rewardPerToken для кожного reward токена
mapping(address => uint256) public rewardPerTokenStored; // rewardToken => value
mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid;
Off-chain інфраструктура для Merkle Distribution
Генерація Merkle tree — off-chain процес. Потрібен pipeline:
- Snapshot: запрос до archival node (Alchemy/Infura з archive access) для отримання балансів на конкретному block number
- Calculation: розрахунок доля кожної адреси пропорційно балансу
-
Tree building: бібліотека
@openzeppelin/merkle-tree - Publishing: Merkle root постить on-chain, повне дерево в IPFS або публічний API
- Proof service: API для отримання proof по адресі
Весь pipeline повинен бути відтворюваним та верифікованим: будь-який учасник може самостійно перерахувати root та переконатися у коректності.
Практичні параметри вибору
Якщо власників < 1000 та дохід розповсюджується часто — Synthetix-style staking rewards. Якщо власників > 10,000 та розповсюження періодичне — Merkle distribution. Якщо потрібна гнучкість (різні токени, різні правила) — гібридна схема з off-chain snapshot та on-chain verification.
Терміни розробки: 3-5 тижнів на базову систему, 6-9 тижнів з multi-reward, anti-flash-loan захистом та frontend dashboard.







