Rebase Token Development
Rebase token is ERC-20 where number of tokens on each holder's balance automatically changes based on external condition. Not through transfer, not through mint/burn — balance changes for everyone simultaneously. First known example — Ampleforth (AMPL), where supply adjusts daily depending on price deviation from target ($1 for AMPL). Mechanics simple in description, non-trivial to implement and with serious DeFi protocol compatibility implications.
Rebase mechanics: how it works
Key distinction: external balance (what user sees) and internal balance (what contract stores).
Contract stores _gonBalances — fixed "shares" of each holder from total pool. External balance computed as:
externalBalance = _gonBalances[account] / _gonsPerFragment
On rebase only _gonsPerFragment changes — and all balances "automatically" change without iterating through holders.
// Simplified implementation
uint256 private constant TOTAL_GONS = type(uint256).max / 2; // large number
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: don't let totalSupply go below 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) {
// Convert external value to gons
uint256 gonValue = value * _gonsPerFragment;
_gonBalances[msg.sender] -= gonValue;
_gonBalances[to] += gonValue;
emit Transfer(msg.sender, to, value);
return true;
}
Three types of rebase mechanisms
Elastic supply with price target
Classic Ampleforth-style. Oracle reports current price, contract adjusts supply to approach market cap to target.
Supply delta calculation:
function calculateSupplyDelta(uint256 currentPrice, uint256 targetPrice)
internal view returns (int256) {
// Deviation from target
int256 priceDeviation = int256(currentPrice) - int256(targetPrice);
int256 deviationPercent = (priceDeviation * 1e18) / int256(targetPrice);
// Supply increases if price above target, decreases if below
// Dampening factor to avoid overshooting
int256 supplyDelta = (int256(_totalSupply) * deviationPercent)
/ int256(REBASE_LAG * 1e18);
return supplyDelta;
}
REBASE_LAG — number of periods for full equilibrium. Ampleforth used lag=10 (10 rebases to "reach" target at constant deviation).
Oracle for price — not spot price, but TWAP from DEX + aggregator (Chainlink). Spot price can be manipulated by flash loans.
Yield-bearing rebase (stETH-style)
Lido's stETH — rebasing token where balance grows proportionally to staking rewards. If staked 1 ETH and got 1 stETH, after year you have ~1.035 stETH (at 3.5% APR), not because tokens were minted for you, but because _gonsPerFragment changed.
// stETH mechanics: rebase proportional to total pooled ETH
function rebase(uint256 totalPooledEther) external onlyBeaconChainOracle {
uint256 prevTotalShares = _totalShares; // equivalent of our gons
// totalShares doesn't change, but totalPooledEther grows
// => sharesToEth ratio grows => all balances grow
emit TokenRebased(
prevTotalShares,
_totalShares,
prevTotalPooledEther,
totalPooledEther,
sharesMintedAsFees
);
_totalPooledEther = totalPooledEther;
}
function getPooledEthByShares(uint256 sharesAmount) public view returns (uint256) {
return sharesAmount * _getTotalPooledEther() / _getTotalShares();
}
This is positive rebase — supply only grows (or stagnates at zero yield). Much simpler for users than elastic supply.
Inflationary with treasury
Supply grows on schedule (e.g., 2% per year), new tokens go to treasury or stakers. More like periodic mint than true rebase, but implemented with gons mechanics.
DeFi compatibility problem: main pain point
Rebase tokens break assumptions of most DeFi protocols. This is most important part to understand before starting development.
AMM (Uniswap, Curve)
Uniswap stores reserves as absolute values. After rebase actual token balance in pool changes, but reserves in Uniswap won't update until next swap. This creates arb opportunity, but also means LP positions "diverge" from real balance.
Solution: for AMM integration use wrapped version — wstETH instead of stETH. Wrapped version stores shares (gons), not rebasing amount. Conversion rate is separate function.
// Wrapped non-rebasing version
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 returns shares — stable number
}
Lido solved exactly this way: stETH is rebasing, wstETH for DeFi protocols (Aave, Compound, Uniswap V3 liquidity).
Lending protocols
Aave and Compound store deposited amount. After negative rebase collateral decreases — can create unexpected liquidation. After positive rebase extra tokens get stuck in protocol, available only to whoever "takes" them via arb transaction.
Solution: in most lending protocols wrapped version is used or protocol itself supports rebasing (Aave supports through aTokens — their own rebasing mechanics).
ERC-20 transfer assumptions
Some contracts do:
uint256 before = token.balanceOf(address(this));
token.transferFrom(msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - before;
// received may != amount for rebase tokens BECAUSE rebase between two calls
Rare, but happens. Need to test integrations.
Oracle for rebase: reliability critical
If oracle for price/yield rate is compromised or manipulated — consequences are catastrophic: attacker can cause rebase to zero or to MAX_SUPPLY.
Protections:
- TWAP instead of spot price (minimum 30-minute window for elastic supply)
- Bounds check: maximum supply change per rebase (e.g., ±10%)
- Timelock: between oracle parameter update and application — N hours
- Multi-oracle aggregation: use Chainlink + own TWAP, divergence > X% — rebase doesn't happen
function rebase() external {
uint256 chainlinkPrice = getChainlinkPrice();
uint256 twapPrice = getTWAPPrice();
// If prices diverge more than 2% — skip rebase
require(
absDiff(chainlinkPrice, twapPrice) * 100 / chainlinkPrice < 2,
"Oracle mismatch"
);
// Maximum change ±10% per rebase
int256 supplyDelta = calculateSupplyDelta(twapPrice);
int256 maxDelta = int256(_totalSupply / 10);
supplyDelta = clamp(supplyDelta, -maxDelta, maxDelta);
_rebase(supplyDelta);
}
Gas and performance
Rebase itself is O(1) operation, not O(n) by holders. This is key advantage of gons approach. But:
-
balanceOfis one SLOAD more expensive than regular ERC-20 (division + multiplication) -
transfersimilarly — slightly more expensive due to gons conversion
For high-frequency DEX operations difference is noticeable. Benchmark: regular ERC-20 transfer ~51,000 gas, rebasing ERC-20 transfer ~57,000–65,000 gas (+10–25%).
Audit and known vulnerabilities
Integer precision loss — division in gons calculations can create dust accounts (balances rounding to 0 on reverse conversion). Test edge cases: minimum deposit, minimum transfer.
Front-running rebase — if rebase is predictable (fixed time), arbitrageurs buy before positive rebase, sell after. Partially solved by randomizing rebase time or using committed randomness.
Negative rebase to zero — contract should have floor on minimum totalSupply.
When rebase is justified
Rebase makes sense for:
- Yield-bearing tokens (stETH-style) — user conveniently sees growing balance instead of exchange rate
- Algorithmic stablecoin with elastic supply (high risk, complex mechanics)
- Inflationary governance token where uniform dilution of all holders needed
Rebase is not needed for: standard utility tokens, tokens with emission schedule, most governance tokens. In these cases simple mint/burn is simpler.







