Розробка rebase-токена
Rebase-токен — це ERC-20, де кількість токенів на балансі кожного держателя автоматично змінюється залежно від зовнішної умови. Не через transfer, не через mint/burn — баланс змінюється у всіх одночасно. Перший відомий приклад — Ampleforth (AMPL), де supply коригується щодня залежно від відхилення ціни від target ($1 для AMPL). Механіка проста в описі, нетривіальна в реалізації та з серйозними наслідками для сумісності з DeFi протоколами.
Механіка rebase: як це працює
Ключове розрізнення: зовнішній баланс (те, що бачить користувач) та внутрішній баланс (те, що зберігає контракт).
Контракт зберігає _gonBalances — фіксовані "доліі" кожного держателя від загального пулу. Зовнішній баланс обчислюється як:
externalBalance = _gonBalances[account] / _gonsPerFragment
При rebase змінюється тільки _gonsPerFragment — і всі баланси "автоматично" змінюються без ітерації по держателям.
// Упрощена реалізація
uint256 private constant TOTAL_GONS = type(uint256).max / 2; // велике число
uint256 private _totalSupply;
uint256 private _gonsPerFragment;
mapping(address => uint256) private _gonBalances;
constructor(uint256 initialSupply) {
_totalSupply = initialSupply;
_gonsPerFragment = TOTAL_GONS / initialSupply;
_gonBalances[msg.sender] = TOTAL_GONS;
}
function balanceOf(address account) public view returns (uint256) {
return _gonBalances[account] / _gonsPerFragment;
}
function rebase(int256 supplyDelta) external onlyOracle returns (uint256) {
if (supplyDelta == 0) return _totalSupply;
if (supplyDelta < 0) {
_totalSupply -= uint256(-supplyDelta);
} else {
_totalSupply += uint256(supplyDelta);
}
// Clamp: не даємо totalSupply упасти нижче 1
if (_totalSupply > MAX_SUPPLY) _totalSupply = MAX_SUPPLY;
_gonsPerFragment = TOTAL_GONS / _totalSupply;
emit LogRebase(epoch, _totalSupply);
return _totalSupply;
}
function transfer(address to, uint256 value) public override returns (bool) {
// Конвертуємо зовнішній value в gons
uint256 gonValue = value * _gonsPerFragment;
_gonBalances[msg.sender] -= gonValue;
_gonBalances[to] += gonValue;
emit Transfer(msg.sender, to, value);
return true;
}
Три типи rebase механізмів
Elastic supply з price target
Класичний Ampleforth-style. Oracle повідомляє текущу ціну, контракт коригує supply щоб наблизити market cap до target.
Supply delta calculation:
function calculateSupplyDelta(uint256 currentPrice, uint256 targetPrice)
internal view returns (int256) {
// Відхилення від таргету
int256 priceDeviation = int256(currentPrice) - int256(targetPrice);
int256 deviationPercent = (priceDeviation * 1e18) / int256(targetPrice);
// Supply збільшується якщо ціна вище таргету, зменшується якщо нижче
// Dampening factor щоб уникнути overshooting
int256 supplyDelta = (int256(_totalSupply) * deviationPercent)
/ int256(REBASE_LAG * 1e18);
return supplyDelta;
}
REBASE_LAG — кількість періодів для повного equilibrium. Ampleforth використовував lag=10 (10 rebase'ів щоб «дійти» до таргету при постійному відхиленні).
Oracle для ціни — не spot price, а TWAP з DEX + агрегатор (Chainlink). Spot price можна маніпулювати flash loans.
Yield-bearing rebase (stETH-style)
Lido's stETH — ребейсящий токен, де баланс зростає пропорційно staking rewards. Якщо застейкав 1 ETH та отримав 1 stETH, через рік у тебе ~1.035 stETH (при 3.5% APR), не тому що тобі мінтнули токени, а тому що змінився _gonsPerFragment.
// stETH механіка: rebase пропорційно total pooled ETH
function rebase(uint256 totalPooledEther) external onlyBeaconChainOracle {
uint256 prevTotalShares = _totalShares; // еквівалент наших gons
// totalShares не змінюється, але totalPooledEther зростає
// => sharesToEth ratio зростає => всі баланси зростають
emit TokenRebased(
prevTotalShares,
_totalShares,
prevTotalPooledEther,
totalPooledEther,
sharesMintedAsFees
);
_totalPooledEther = totalPooledEther;
}
function getPooledEthByShares(uint256 sharesAmount) public view returns (uint256) {
return sharesAmount * _getTotalPooledEther() / _getTotalShares();
}
Це позитивний rebase — supply тільки зростає (або стагнує при нульовому yield). Значно простіше в розумінні користувачами, ніж elastic supply.
Inflationary з treasury
Supply зростає за розписанням (наприклад, 2% на рік), нові токени йдуть в treasury або stakers. Це більш скоро періодичний mint ніж справжній rebase, але реалізується подібною механікою з gons.
Проблема сумісності DeFi: головна точка болі
Rebase-токени ламають assumptions більшості DeFi протоколів. Це найважливіша частина, яку потрібно зрозуміти перед початком розробки.
AMM (Uniswap, Curve)
Uniswap зберігає reserves як абсолютні значення. Після rebase реальний баланс токена в пулі змінится, але reserves у Uniswap не обновляться до наступного свопу. Це створює arb можливість, але також означає що LP позиції "розъезджаються" з реальним балансом.
Рішення: для AMM інтеграції використовувати wrapped версію — wstETH замість stETH. Wrapped версія зберігає shares (gons), а не rebasing amount. Курс конвертації — окрема функція.
// Wrapped non-rebasing версія
contract WrappedRebaseToken is ERC20 {
IRebaseToken public immutable underlying;
function wrap(uint256 amount) external returns (uint256 sharesAmount) {
underlying.transferFrom(msg.sender, address(this), amount);
sharesAmount = underlying.getSharesByPooledTokens(amount);
_mint(msg.sender, sharesAmount);
}
function unwrap(uint256 sharesAmount) external returns (uint256 amount) {
_burn(msg.sender, sharesAmount);
amount = underlying.getPooledTokensByShares(sharesAmount);
underlying.transfer(msg.sender, amount);
}
// balanceOf повертає shares — стабільне число
}
Lido саме так вирішила: stETH — rebasing, wstETH для DeFi протоколів (Aave, Compound, Uniswap V3 liquidity).
Lending протоколи
Aave та Compound зберігають deposited amount. Після negative rebase collateral зменшується — це може створити неочікувану ліквідацію. Після positive rebase зайві токени застрянуть у протоколі, доступні тільки тому, хто їх «заберетьїх» arb транзакцією.
Рішення: у більшості lending протоколів використовується wrapped версія або сам протокол підтримує rebasing (Aave підтримує через aTokens — їхній власний rebasing механізм).
ERC-20 transfer assumptions
Деякі контракти роблять:
uint256 before = token.balanceOf(address(this));
token.transferFrom(msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - before;
// received може != amount для rebase токенів ІЗ-ЗА rebase між двома вызовами
Рідко, але трапляється. Потрібно тестувати інтеграції.
Oracle для rebase: надійність критична
Якщо oracle для price/yield rate скомпрометований або маніпульований — наслідки катастрофічні: злоумисник може вызвати rebase до нуля або до MAX_SUPPLY.
Захисти:
- TWAP замість spot price (мінімум 30-хвилинне вікно для elastic supply)
- Bounds check: максимальна зміна supply за один rebase (наприклад, ±10%)
- Timelock: між оновленням параметрів oracle та застосуванням — N годин
- Multi-oracle aggregation: використовувати Chainlink + власний TWAP, розхідність > X% — rebase не відбувається
function rebase() external {
uint256 chainlinkPrice = getChainlinkPrice();
uint256 twapPrice = getTWAPPrice();
// Якщо ціни розходяться більш ніж на 2% — пропускаємо rebase
require(
absDiff(chainlinkPrice, twapPrice) * 100 / chainlinkPrice < 2,
"Oracle mismatch"
);
// Максимальна зміна ±10% за один rebase
int256 supplyDelta = calculateSupplyDelta(twapPrice);
int256 maxDelta = int256(_totalSupply / 10);
supplyDelta = clamp(supplyDelta, -maxDelta, maxDelta);
_rebase(supplyDelta);
}
Gas та продуктивність
Rebase сам по собі — O(1) операція, не O(n) по держателям. Це ключове преимущество gons-підходу. Але:
-
balanceOfна один SLOAD дорожче ніж у звичайного ERC-20 (ділення + множення) -
transferаналогічно — трохи дорожче через конвертацію в gons
Для high-frequency DEX операцій різниця помітна. Benchmark: звичайний ERC-20 transfer ~51,000 gas, rebasing ERC-20 transfer ~57,000–65,000 gas (+10–25%).
Аудит та відомі вразливості
Integer precision loss — ділення у gons вичисленнях може створити dust accounts (баланси, які округляються до 0 при обратній конвертації). Тестувати граничні випадки: мінімальний депозит, мінімальний transfer.
Front-running rebase — якщо rebase передбачуваний (фіксоване время), арбітражери купують перед positive rebase, продають після. Частково вирішується рандомізацією часу rebase або використанням committed randomness.
Negative rebase до нуля — контракт повинен мати floor на мінімальний totalSupply.
Коли rebase оправданий
Rebase має сенс для:
- Yield-bearing tokens (stETH-style) — користувачу зручно бачити зростаючий баланс замість exchange rate
- Algorithmic stablecoin з elastic supply (високий ризик, складна механіка)
- Inflationary governance token де потрібне рівномірне розводнення всіх держателів
Rebase не потрібен для: стандартних utility токенів, токенів з emission розписанням, більшості governance токенів. У цих випадках простіше звичайний mint/burn.







