In this article, I will dig into the smart contracts of a few projects with such functionalities in order to understand how the rebase function in Solidity smart contract works, as well as some of the potential risks of these projects. 4 tokens - Ampleforth (AMPL), Antiample (XAMP), Tokens of Babel (TOB) and RMPL (RMPL) will be the subject of interest for this research.
Ampleforth (AMPL)
Ampleforth is first token that started it all and made rebase popular enough that new tokens are now copying and innovating on top of it.
Let's take a look in the Smart Contract of Ampleforth.
AMPL Contract: https://etherscan.io/address/0xd46ba6d942050d489dbd938a2c909a5d5039a161#code
Since the contract is a proxy, the real contract is here: https://etherscan.io/address/0xf9e6a96229140195777a1c3ab47c9bf16f055406#code
/**
* @dev Notifies Fragments contract about a new rebase cycle.
* @param supplyDelta The number of new fragment tokens to add into circulation via expansion.
* @return The total number of fragments after the supply adjustment.
*/
function rebase(uint256 epoch, int256 supplyDelta)
external
onlyMonetaryPolicy
returns (uint256)
{
if (supplyDelta == 0) {
emit LogRebase(epoch, _totalSupply);
return _totalSupply;
}
if (supplyDelta < 0) {
_totalSupply = _totalSupply.sub(uint256(supplyDelta.abs()));
} else {
_totalSupply = _totalSupply.add(uint256(supplyDelta));
}
if (_totalSupply > MAX_SUPPLY) {
_totalSupply = MAX_SUPPLY;
}
_gonsPerFragment = TOTAL_GONS.div(_totalSupply);
// From this point forward, _gonsPerFragment is taken as the source of truth.
// We recalculate a new _totalSupply to be in agreement with the _gonsPerFragment
// conversion rate.
// This means our applied supplyDelta can deviate from the requested supplyDelta,
// but this deviation is guaranteed to be < (_totalSupply^2)/(TOTAL_GONS - _totalSupply).
//
// In the case of _totalSupply <= MAX_UINT128 (our current supply cap), this
// deviation is guaranteed to be < 1, so we can omit this step. If the supply cap is
// ever increased, it must be re-included.
// _totalSupply = TOTAL_GONS.div(_gonsPerFragment)
emit LogRebase(epoch, _totalSupply);
return _totalSupply;
}
How rebase works?
Basically, Ampleforth's smart contract changes the code of balanceOf
method. They just need to make a contract call to rebase
in order to change the total ownership that you have in the token. In this case, to call the rebase
method, all you need is just epoch
which is date time for logging and supplyDelta
which is to add or remove supply of the token.
As you can see onlyMonetaryPolicy
role can call this rebase
function.
// Used for authentication
address public monetaryPolicy;
/**
* @param monetaryPolicy_ The address of the monetary policy contract to use for authentication.
*/
modifier onlyMonetaryPolicy() {
require(msg.sender == monetaryPolicy);
_;
}
In the contract data, it seems like the address is set to 0x0
which is not the case when you use readAsProxy
feature on Etherscan on the main proxy contract.
https://etherscan.io/address/0xd46ba6d942050d489dbd938a2c909a5d5039a161#readProxyContract
So the monetoryPolicy
address is set to https://etherscan.io/address/0x1b228a749077b8e307c5856ce62ef35d96dca2ea where in this case, it is an oracle smart contract called Rebaser.
By using an oracle like Chainlink, Ampleforth can create a trustless manner of rebasing. This article does not cover the part of oracle code.
AntiAmple (XAMP)
The AntiAmple token also rebases, but it only allow burning of the supply.
XAMP contract: https://etherscan.io/address/0xf911a7ec46a2c6fa49193212fe4a2a9b95851c27#code
function rebase(uint256 epoch, uint256 supplyDelta)
external
onlyOwner
returns (uint256)
{
if (supplyDelta == 0) {
emit LogRebase(epoch, _totalSupply);
return _totalSupply;
}
_totalSupply = _totalSupply.sub(supplyDelta);
if (_totalSupply > MAX_SUPPLY) {
_totalSupply = MAX_SUPPLY;
}
_gonsPerFragment = TOTAL_GONS.div(_totalSupply);
emit LogRebase(epoch, _totalSupply);
return _totalSupply;
}
For AntiAmple, the rebase part of the code is almost similar as Ampleforth's rebase.
How it works is that, it only has negative rebase. The line _totalSupply = _totalSupply.sub(supplyDelta);
shows that the supplyDelta
is an unsigned integer, the total supply can only be reduce, never increase.
This rebase
method is accessible via a modifier called onlyOwner.
address public _owner;
modifier onlyOwner() {
require(_owner == msg.sender, "Ownable: caller is not the owner");
_;
}
If you read the contract https://etherscan.io/address/0xf911a7ec46a2c6fa49193212fe4a2a9b95851c27#readContract
The contract owner is actually a Rebaser Oracle also https://etherscan.io/address/0x8ceb211a7567cf399e1ee01e6974bf4a13b64c04
Tokens of Babel (TOB)
Tokens of Babel was also created by the Antiample team.
Contract Code: https://etherscan.io/address/0x7777770f8a6632ff043c8833310e245eba9209e6#code
function rebase(uint256 epoch, uint256 supplyDelta)
external
onlyOwner
returns (uint256)
{
if (supplyDelta == 0) {
emit LogRebase(epoch, _totalSupply);
return _totalSupply;
}
_totalSupply = _totalSupply.sub(supplyDelta);
if (_totalSupply > MAX_SUPPLY) {
_totalSupply = MAX_SUPPLY;
}
_gonsPerFragment = TOTAL_GONS.div(_totalSupply);
emit LogRebase(epoch, _totalSupply);
return _totalSupply;
}
The code is similar to AntiAmple, similar onlyOwner
modifier, but here's a small difference.
At the point of writing, when you check the owner of the contract, it is actually still owned by the owner. https://etherscan.io/address/0xe41e5fa65d197afa059edce5225c1da2a01a361c
I asked in TOB telegram group about this issue.
Well, it make sense that building the oracle with logic require a lot of testing and development time, so in order to ship it first, it used an offchain mechanism (maybe a script) to do a rebase.
Refer to end of the article, where I share the possible vulnerability of off-chain Owner key.
RMPL
RMPL is a random rebase version of Ampleforth.
Contract address: https://etherscan.io/address/0xe17f017475a709de58e976081eb916081ff4c9d5#code
struct Transaction {
bool enabled;
address destination;
bytes data;
}
/**
* @dev Notifies Fragments contract about a new rebase cycle.
* @param supplyDelta The number of new fragment tokens to add into circulation via expansion.
* @return The total number of fragments after the supply adjustment.
*/
function rebase(int256 supplyDelta)
external
onlyOwner
returns (uint256)
{
_epoch = _epoch.add(1);
if (supplyDelta == 0) {
emit LogRebase(_epoch, _totalSupply);
return _totalSupply;
}
if (supplyDelta < 0) {
_totalSupply = _totalSupply.sub(uint256(supplyDelta.abs()));
} else {
_totalSupply = _totalSupply.add(uint256(supplyDelta));
}
if (_totalSupply > MAX_SUPPLY) {
_totalSupply = MAX_SUPPLY;
}
_gonsPerFragment = TOTAL_GONS.div(_totalSupply);
// From this point forward, _gonsPerFragment is taken as the source of truth.
// We recalculate a new _totalSupply to be in agreement with the _gonsPerFragment
// conversion rate.
// This means our applied supplyDelta can deviate from the requested supplyDelta,
// but this deviation is guaranteed to be < (_totalSupply^2)/(TOTAL_GONS - _totalSupply).
//
// In the case of _totalSupply <= MAX_UINT128 (our current supply cap), this
// deviation is guaranteed to be < 1, so we can omit this step. If the supply cap is
// ever increased, it must be re-included.
// _totalSupply = TOTAL_GONS.div(_gonsPerFragment)
emit LogRebase(_epoch, _totalSupply);
for (uint i = 0; i < transactions.length; i++) {
Transaction storage t = transactions[i];
if (t.enabled) {
bool result = externalCall(t.destination, t.data);
if (!result) {
emit TransactionFailed(t.destination, i, t.data);
revert("Transaction Failed");
}
}
}
return _totalSupply;
}
The code is similar to Ampleforth, and they implemented onlyOwner
modifier
address private _owner;
modifier onlyOwner() {
require(isOwner());
_;
}
function isOwner() public view returns(bool) {
return msg.sender == _owner;
}
According to their RMPL roadmap
Once onchain random rebasing solution has been finalized, contract owner will be locked to ensure no party has control and the implementation is completely self governed.
Similar to TOB, Currently the rebase
function are still being triggered off chain.
Owner key address: https://etherscan.io/address/0xd91cf98fdfc0f38c5b7c87510a414636fd3890b0
Final Thoughts
AntiAmple and Ampleforth had a rebase mechanism using an Oracle. Since the analysis of Oracle is out of scope of today's article, we will be focusing on the rebase
function
At the moment of writing, Holding TOB and RMPL will exposed risk to the Owner key, where the owner key can call rebase
with any amount of supplyDelta
. So instead of using an on-chain oracle to get the price, they use offchain method and update the contract via owner key.
Why is this important?
If this part of the owner address is not executed by a smart contract oracle (which can be verified), the owner address have the power to provide supplyDelta
value in whatever parameters that owner wants.
What would happen?
The owner can hold a certain amount of TOB, and then provide an arbitrary high value of supplyDelta
to the rebase function. And then, with that the owner can dump the token in the next block.
So until the Owner key is changed to Oracle based, this rebase
method are exposed to the risk.
At CoinGecko, we wanted inform all our users regarding on this kind of risk when investing to the coin. That is why we added a notice for these coin that are exposed to the risk of offchain rebase.
Subscribe to the CoinGecko Daily Newsletter!