Appearance
L1-6: Rebasing Token (Elastic Supply Token)
1. Problem
Create an elastic supply ERC20 token similar to AMPL (Ampleforth). The total supply can be adjusted via rebase, and all holder balances change proportionally.
For example: total supply 10M → rebase(-9M) → total supply 1M, each holder's balance becomes 1/10 of the original.
2. Why
AMPL aims to create a "purchasing-power-stable" currency. Supply adjustments are used to target a price:
- Price > $1 → positive rebase (increase supply, holders' balances increase)
- Price < $1 → negative rebase (decrease supply, holders' balances decrease)
It is technically impossible to iterate over all holders to modify balances (Gas = infinity), so an indirect approach is needed.
3. Solution
Core Mechanism: GONs + Scaling Factor
Internal representation (GONs): _gonBalances[addr] // unchanged by rebase
Scaling factor: _gonsPerFragment // changes during rebase
External representation (display): balanceOf = _gonBalances / _gonsPerFragment
Rebase Operation
solidity
function rebase(int256 amount) external onlyOwner {
uint256 currentTotal = totalSupply(); // GONs / _gonsPerFragment
uint256 newTotal = currentTotal + amount; // (positive rebase) or currentTotal - decreaseBy (negative rebase)
_gonsPerFragment = _gonTotalSupply / newTotal;
// No need to modify any _gonBalances!
}
Why It Works
- Positive rebase: total supply 10M → 11M,
_gonsPerFragmentgoes from 1e18 to_gonTotalSupply / 11M(decreases) balanceOf=_gonBalances / _gonsPerFragment(numerator unchanged, denominator smaller = result larger)
4. Pitfalls Encountered
4.1 transfer/transferFrom amounts must be converted
The amount passed in by users is the display value and must be converted to GONs internally:
solidity
function transfer(address to, uint256 amount) public override returns (bool) {
uint256 gonAmount = amount * _gonsPerFragment;
_transferGon(msg.sender, to, gonAmount);
emit Transfer(msg.sender, to, amount);
return true;
}
4.2 allowance is not affected by rebase
OZ ERC20's allowance stores display values, not GONs. transferFrom must handle this correctly:
solidity
_spendAllowance(from, msg.sender, amount); // deduct allowance in display value
_transferGon(from, to, gonAmount); // transfer in GONs
4.3 Integer division precision loss
After multiple rebases, _gonsPerFragment may accumulate errors. _gonTotalSupply / newTotal has rounding error.
5. Why the Pitfalls Happen
5.1
If transfer does not convert, a user transferring 100 tokens would actually transfer only 100 GONs. But _gonsPerFragment may be > 1 or < 1, causing the actual amount to significantly differ from what was expected.
5.2
Allowance semantics: what the user authorizes is the "display value", not GONs. If allowances were also scaled in GONs, the authorized amount would change with rebases (not expected).
5.3
Solidity integer division truncates. Cumulative rounding errors across rebases can cause the sum of balanceOf values to not equal totalSupply.
6. How to Resolve the Pitfalls
- All external interfaces (transfer/transferFrom/approve) use display values for parameters and events
- Internal storage and computation use GONs
- Allowance storage and spending use display values (pass display value when calling
_spendAllowance) - For precision loss: use high precision (1e18 as the initial _gonsPerFragment) to reduce rounding impact
7. Technical Highlights
| Point | Description |
|---|---|
| GONs pattern | Fixed internal unit + variable external ratio |
| AMPL principle | Elastic supply for purchasing-power stability |
| ERC20 overrides | balanceOf / totalSupply / transfer / transferFrom all rewritten |
| Allowance isolation | Allowances are not affected by rebase |
| Precision handling | Use 18 decimal places = 1e18 base |