Разработка rebase-токена
Rebase-токен — это ERC-20, где количество токенов на балансе каждого держателя автоматически изменяется в зависимости от внешнего условия. Не через transfer, не через mint/burn — баланс меняется у всех одновременно. Первый известный пример — Ampleforth (AMPL), где supply корректируется daily в зависимости от отклонения цены от target ($1 в случае AMPL). Механика простая в описании, нетривиальная в реализации и с серьёзными implikациями для совместимости с 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) {
// Конвертируем external 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 чтобы приблизить рыночную cap к target.
Supply delta calculation:
function calculateSupplyDelta(uint256 currentPrice, uint256 targetPrice)
internal view returns (int256) {
// Deviation от таргета
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), не потому что тебе mint'нули токены, а потому что изменился _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: главный pain point
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 protocols
Aave и Compound хранят deposited amount. После negative rebase залог уменьшается — это может создать неожиданную ликвидацию. После 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 для rebasing токенов ИЗ-ЗА 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"
);
// Максимальное изменение за один rebase ±10%
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.







