Разработка системы revenue sharing
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);
}
}
Плюс: масштабируется на любое число холдеров, gas платит получатель. Минус: требует 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 balances.
Защита от 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. Если нужна гибкость (разные токены, разные правила eligibility) — гибридная схема с off-chain snapshot и on-chain verification.
Сроки разработки: 3-5 недель на базовую систему, 6-9 недель с multi-reward, anti-flash-loan защитой и frontend dashboard.







