Разработка системы лояльности на токенах
Программа лояльности на токенах — это механика удержания пользователей через экономические стимулы, закреплённые в смарт-контрактах. Ключевое отличие от традиционных программ (кешбэк, баллы в приложении) — прозрачность начисления и возможность пользователей торговать или передавать накопленные активы. Пользователь видит свой баланс on-chain, а не в закрытой базе данных компании.
Для бизнеса это инструмент и retention, и community building: пользователи, которые накопили governance токены, становятся стейкхолдерами, а не просто покупателями.
Типы loyalty-токенов
Выбор типа токена определяет всю механику системы.
Non-transferable (soulbound) токены — не могут быть проданы или переданы. Ideal для: программ где ценность — статус («Gold member»), не актив. Реализуется через переопределение _beforeTokenTransfer в ERC-721 с revert для любого transfer кроме mint/burn. Примеры: Otterspace, Disco — используют SBT для репутационных систем.
Transferable ERC-20 points — накапливаемые баллы с рыночной ценой. Риск: пользователи могут купить статус не зарабатывая его. Плюс: liquid rewards привлекают больше участников. Подходит для протоколов с развитым secondary market.
Tiered NFT (ERC-1155) — различные уровни лояльности как токены. Tier 1 → Tier 2 → Tier 3 при достижении пороговых значений. Уровень может быть soulbound, а associated privileges — отдельными transferable токенами.
Hybrid: soulbound points-токен для подсчёта + отдельный claim-able reward токен. Points не продать, но ими можно claim-ить ценные rewards. Это наиболее распространённая production-модель (Blur использует похожую схему).
Архитектура контракта
Points-токен с ограниченным transfer
contract LoyaltyPoints is ERC20 {
address public immutable minter; // только авторизованный контракт
mapping(address => bool) public transferWhitelist;
modifier onlyMinter() {
require(msg.sender == minter, "Not minter");
_;
}
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override {
// Разрешаем: mint (from == 0), burn (to == 0),
// transfers в whitelist (reward контракт, staking)
if (from != address(0) && to != address(0)) {
require(transferWhitelist[to] || transferWhitelist[from], "Non-transferable");
}
}
function mint(address user, uint256 amount) external onlyMinter {
_mint(user, amount);
}
}
Whitelist включает адреса reward-контракта (для exchange points → rewards) и staking-контракта. Пользователь не может отправить points напрямую другому адресу.
Reward контракт
contract LoyaltyRewards {
LoyaltyPoints public immutable points;
IERC20 public immutable rewardToken;
// Расписание обмена: минимальный порог → reward amount
struct RewardTier {
uint256 pointsRequired;
uint256 rewardAmount;
uint256 cooldown; // между claim-ами
}
mapping(uint256 => RewardTier) public tiers;
mapping(address => uint256) public lastClaim;
function claimReward(uint256 tierId) external {
RewardTier memory tier = tiers[tierId];
require(points.balanceOf(msg.sender) >= tier.pointsRequired, "Insufficient points");
require(block.timestamp >= lastClaim[msg.sender] + tier.cooldown, "Cooldown active");
lastClaim[msg.sender] = block.timestamp;
// Сжигаем points (или нет — зависит от модели)
points.burnFrom(msg.sender, tier.pointsRequired);
rewardToken.safeTransfer(msg.sender, tier.rewardAmount);
}
}
Выбор: сжигать points при claim или нет — важное дизайн-решение. Сжигание создаёт дефляционную механику и стимулирует накопление для крупных rewards. Без сжигания — пользователи могут получать rewards бесконечно при стабильном накоплении.
Механики начисления баллов
On-chain триггеры
Наиболее прозрачный вариант: контракт протокола напрямую вызывает points.mint() при action пользователя.
// Внутри DEX контракта
function swap(...) external {
// ... логика swap ...
// Начисляем points пропорционально объёму
uint256 pointsToMint = (amountIn * POINTS_PER_DOLLAR) / tokenPrice;
loyaltyPoints.mint(msg.sender, pointsToMint);
}
Простота подхода: полная прозрачность, нет off-chain компонент. Ограничение: менять правила начисления требует upgrade основного контракта.
Off-chain расчёт + Merkle claim
Более гибкий подход: расчёт происходит off-chain (ежедневно или еженедельно), пользователь claim-ит accumulated points через Merkle proof. Позволяет менять правила без upgrade контракта, добавлять сложные расчёты (cross-protocol activity, streak бонусы).
Streak и multiplier механики
Daily streak: пользователь, взаимодействующий N дней подряд, получает множитель. Реализация: on-chain хранение lastActivityDay (block.timestamp / 86400), счётчик currentStreak. При gap > 1 день — streak сбрасывается.
Volume tiers: накопленный объём за период определяет множитель на следующий период. Аналог Silver/Gold/Platinum в авиапрограммах.
Governance интеграция
Если loyalty points должны давать voting power, стандарт — ERC-20Votes (OpenZeppelin). Он добавляет checkpoint-механизм: баланс votes фиксируется на каждом блоке изменения, что предотвращает flash loan voting attacks.
contract LoyaltyPoints is ERC20Votes {
// ERC20Votes добавляет delegate(), getPastVotes(), getPastTotalSupply()
// Пользователь должен делегировать себе или другому для активации voting power
}
Важный нюанс: пользователи должны явно вызвать delegate(address(this)) или делегировать кому-то, иначе их voting power равен нулю (даже при большом балансе). Это нужно объяснять в UI.
Антифрод
Rate limiting: максимальное количество points за транзакцию и за период (день/неделя). Защищает от единоразовой накрутки через большую транзакцию.
Activity verification: если начисление идёт через off-chain расчёт, добавить минимальные пороги: минимальный объём транзакции, минимальное время между транзакциями (чтобы bot-скрипт не мог генерировать тысячи мелких).
Sybil resistance: points должны начисляться только верифицированным кошелькам (Gitcoin Passport, World ID) если программа открытая. Для закрытых систем (KYC пользователи) — достаточно whitelist адресов.
Стек и интеграция
Solidity 0.8.x + OpenZeppelin 5.x (ERC20, ERC20Votes, AccessControl). Frontend: wagmi + viem + React для отображения баланса и claim UI. Для off-chain расчётов: TypeScript + The Graph subgraph + PostgreSQL.
| Компонент | Срок разработки |
|---|---|
| Points контракт (ERC-20 + SBT логика) | 1 неделя |
| Reward контракт с тирами | 1–2 недели |
| Off-chain расчётный сервис | 2–3 недели |
| Merkle claim система | 1 неделя |
| Frontend интеграция | 1–2 недели |
Итого MVP: 4–6 недель. Production-система с antifrog, аналитикой и governance интеграцией — 2–3 месяца.
Стоимость зависит от типа токена, механик начисления и наличия существующего протокола для интеграции.







