Code review: How reflect contracts work (RFI, SafeMoon, DogeBonk & Co)


On your crypto journey you may stumbled upon tokens like SafeMoon or DogeBonk that offer reflections to their holders. Just by holding their tokens you will get rewarded with fees according to the amount of tokens you already have. Those fees are paid by sell or buy transactions from other addresses. Although SafeMoon made those contracts popular, it was reflect.finance that started it all. Therefore we will review the RFI contract to analyze how those reflections work.

At first glance this sound easy. Just hand out the fees by increasing the amount of tokens each address has. However, in the world of smart contracts this is not feasible. Operations should be lightweight and shouldn’t scale with the amount of address holders. A token can be distributed among millions of holders. If we need to iterate threw all of them during each transaction the gas limit would be reached in no time. This means that the fees have to be distributed without altering the amount of tokens each address holds.

Lets start with the contract members

mapping (address => uint256) private _rOwned;
mapping (address => uint256) private _tOwned;
mapping (address => mapping (address => uint256)) private _allowances;

mapping (address => bool) private _isExcluded;
address[] private _excluded;
   
uint256 private constant MAX = ~uint256(0);
uint256 private constant _tTotal = 10 * 10**6 * 10**9;
uint256 private _rTotal = (MAX - (MAX % _tTotal));
uint256 private _tFeeTotal;
...
uint8 private _decimals = 9;

Instead of one single mapping used for the amount of tokens an address holds we have two. _tOwned for the t(oken) amount an _rOwned for the amount of r(eflections). _isExcluded and _excluded hold data for addresses that are excluded from reflections. MAX is just a constant for the maximum value of uint256 values. It’s generated by creating 0 and flipping all of its bits. _tTotal is the true amount of tokens that exist.

(1)   \begin{gather*} 10 \cdot 10^6 = 10000000 \end{gather*}

The added 9 other zeros are there for the decimals. Because the amount is always represented in integer values we need to add those to the total amount and set _decimals to 9.

(2)   \begin{gather*} 10 \cdot 10^6 \cdot 10^9 = 10000000000000000 \end{gather*}

This amount is already a pretty high value. However, to be able the represent reflections to users that own just a tiny bit of that amount the reflections need to be calculated more precise than that. This is done by representing the total amount of reflections _rTotal with a much higher value. If we assume for example that the the reflection amount _rTotal is 100 times bigger than _tTotal we can calculate reflections with a much higher precision. If we want to represent the reflections as real tokens again we just need to take care to normalize them by dividing them by 100.

Again, we need to be as precise as possible. The highest possible value for _rTotal is of course MAX. The subtraction with the modulo is done to ensure no remainder after a division exists and we can convert the values to each other. Be aware that _rTotal is a really big number now.

(3)   \begin{gather*} 11579208923731619542357098500868790785326998466564056403945750000000000000000 \end{gather*}

_tFeeTotal represents the amount of fees already paid. This is default 0.

Let’s have a look at the constructor.

constructor () public {
    _rOwned[_msgSender()] = _rTotal;
    emit Transfer(address(0), _msgSender(), _tTotal);
}

The contract creator gets all the reflections, which also means that the creator has reflections worth all real tokens. The emitted event just tells everybody that the zero address has given the creator all available tokens.

To return the total supply of tokens is easy.

function totalSupply() public view override returns (uint256) {
    return _tTotal;
}

It gets a little bit tricky if we want to return the amount of tokens a single address has.

function balanceOf(address account) public view override returns (uint256) {
    if (_isExcluded[account]) return _tOwned[account];
    return tokenFromReflection(_rOwned[account]);
}

If the account is excluded from reflections the amount of owned tokens _tOwned gets returned. If this is not the case we have to return the owned reflections _rOwned. To calculate how much reflections are worth in real tokens we call tokenFromReflection().

function tokenFromReflection(uint256 rAmount) public view returns(uint256) {
    require(rAmount <= _rTotal, "Amount must be less than total reflections");
    uint256 currentRate =  _getRate();
    return rAmount.div(currentRate);
}

function _getRate() private view returns(uint256) {
    (uint256 rSupply, uint256 tSupply) = _getCurrentSupply();
    return rSupply.div(tSupply);
}

This method gets the currentRate which tells us how much reflections a single token is worth. Afterwards it returns the amount of reflections divided by the currentRate. To get the current rate _getRate() is called. This does nothing more than dividing the reflection supply with the token supply. Now, how is the current supply calculated?

function _getCurrentSupply() private view returns(uint256, uint256) {
    uint256 rSupply = _rTotal;
    uint256 tSupply = _tTotal;      
    for (uint256 i = 0; i < _excluded.length; i++) {
        if (_rOwned[_excluded[i]] > rSupply || _tOwned[_excluded[i]] > tSupply) return (_rTotal, _tTotal);
        rSupply = rSupply.sub(_rOwned[_excluded[i]]);
        tSupply = tSupply.sub(_tOwned[_excluded[i]]);
    }
    if (rSupply < _rTotal.div(_tTotal)) return (_rTotal, _tTotal);
    return (rSupply, tSupply);
}

Without excluded addresses this would be super simple. We would just return _rTotal and _tTotal as the current supply. Unfortunately we also have to consider addresses that are excluded from reflections. This is done by iterating over all excluded adresses _excluded and subtracting the amount of tokens and reflections from the total supply.

There are also two if statements. Those check if the rate would be smaller than the amount of available reflections or even negative. If that would be the case the calculations wouldn’t work anymore. If the reflections amount is too big the rate gets too big to divide the amount via integer division. tokenFromReflection() would return 0 and small holders would loose all their tokens all of a sudden. Therefore the default values _rTotal and _tTotal are returned.

Now, lets see what happens if we make a transaction.

function _transfer(address sender, address recipient, uint256 amount) private {
    require(sender != address(0), "ERC20: transfer from the zero address");
    require(recipient != address(0), "ERC20: transfer to the zero address");
    require(amount > 0, "Transfer amount must be greater than zero");
    if (_isExcluded[sender] && !_isExcluded[recipient]) {
        _transferFromExcluded(sender, recipient, amount);
    } else if (!_isExcluded[sender] && _isExcluded[recipient]) {
        _transferToExcluded(sender, recipient, amount);
    } else if (!_isExcluded[sender] && !_isExcluded[recipient]) {
        _transferStandard(sender, recipient, amount);
    } else if (_isExcluded[sender] && _isExcluded[recipient]) {
        _transferBothExcluded(sender, recipient, amount);
    } else {
        _transferStandard(sender, recipient, amount);
    }
}

At the beginning we make sure that only non zero amounts are send to real addresses. After that several if-else statements are executed. For each possible outcome of sender/receiver pairs a different method is called, depending on the map of reflection excluded addresses. Lets start with the easiest one _transferStandard().

function _transferStandard(address sender, address recipient, uint256 tAmount) private {
    (uint256 rAmount, uint256 rTransferAmount, uint256 rFee, uint256 tTransferAmount, uint256 tFee) = _getValues(tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);       
    _reflectFee(rFee, tFee);
    emit Transfer(sender, recipient, tTransferAmount);
}

The method starts with a call to _getValues() with the desired amount of token tAmount. This method returns the amount represented in reflections rAmount and also the amount rTransferAmount, which is rAmount minus rFee. Same goes for tTransferAmount and tFee. We will have a look into those later. After we got the values we substract the reflection amount rAmount from the sender and add the value without the fees rTransferAmount to the recipient. At the end we have to distribute the fees to all other token holders. This is done by calling _reflectFee().

The method _getValues() is pretty boring.

function _getValues(uint256 tAmount) private view returns (uint256, uint256, uint256, uint256, uint256) {
    (uint256 tTransferAmount, uint256 tFee) = _getTValues(tAmount);
    uint256 currentRate =  _getRate();
    (uint256 rAmount, uint256 rTransferAmount, uint256 rFee) = _getRValues(tAmount, tFee, currentRate);
    return (rAmount, rTransferAmount, rFee, tTransferAmount, tFee);
}

function _getTValues(uint256 tAmount) private pure returns (uint256, uint256) {
    uint256 tFee = tAmount.div(100);
    uint256 tTransferAmount = tAmount.sub(tFee);
    return (tTransferAmount, tFee);
}

function _getRValues(uint256 tAmount, uint256 tFee, uint256 currentRate) private pure returns (uint256, uint256, uint256) {
    uint256 rAmount = tAmount.mul(currentRate);
    uint256 rFee = tFee.mul(currentRate);
    uint256 rTransferAmount = rAmount.sub(rFee);
    return (rAmount, rTransferAmount, rFee);
}

It starts by calling _getTValues() which calculates the fee tFee as 1% of tAmount. Same is done for the reflections within the method _getRValues(). This method uses the rate between tokens and reflections. We already discussed how _getRate() works.

We finally made it. We reached the interesting part. Lets have a look into _reflectFee() and figure out how fees are actually given out to other token holders.

function _reflectFee(uint256 rFee, uint256 tFee) private {
    _rTotal = _rTotal.sub(rFee);
    _tFeeTotal = _tFeeTotal.add(tFee);
}

Well, thats it. The total amount of existing reflections _rTotal is reduced. It has to be. Remember, we subtracted the amount from the sender but added the amount without the fees to the receiver. The total amount of all reflections has to be distributed between the token holders. If all holders now have lesser reflections the total amount _rTotal has to be reduced too. We didn’t touch other reflection holders but reduced the amount of total reflections. This means that each holder has now a bigger amount of reflections in relation to the total amount _rTotal, which also means more real tokens (everyone except the receiver of course).

At the end the amount of fees is added to _tFeeTotal which is just a member variable to see how many fees got payed during the lifetime of the contract.

But wait. We still have to look into what would happen if an address is excluded from the reflections. Lets start with the method that creates those excluded addresses.

function excludeAccount(address account) external onlyOwner() {
    require(!_isExcluded[account], "Account is already excluded");
    if(_rOwned[account] > 0) {
        _tOwned[account] = tokenFromReflection(_rOwned[account]);
    }
    _isExcluded[account] = true;
    _excluded.push(account);
}

function includeAccount(address account) external onlyOwner() {
    require(_isExcluded[account], "Account is already excluded");
    for (uint256 i = 0; i < _excluded.length; i++) {
        if (_excluded[i] == account) {
            _excluded[i] = _excluded[_excluded.length - 1];
            _tOwned[account] = 0;
            _isExcluded[account] = false;
            _excluded.pop();
            break;
        }
    }
}

During the exclusion of an account the if statement checks if the account owns reflections _rOwned. If so, the amount of real tokens is calculated and added to the amount of tokens for that address _tOwned. Afterwards the account is added as excluded to _isExcluded and _excluded. The same happens if an account is included but in reverse. The amount of real tokens _tOwned is set to 0 again.

You may noticed that the amount of reflections _rOwned has not changed. In fact all reflections are still calculated for excluded addresses. We can see that for example during the transfer between excluded accounts.

function _transferBothExcluded(address sender, address recipient, uint256 tAmount) private {
    (uint256 rAmount, uint256 rTransferAmount, uint256 rFee, uint256 tTransferAmount, uint256 tFee) = _getValues(tAmount);
    _tOwned[sender] = _tOwned[sender].sub(tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _tOwned[recipient] = _tOwned[recipient].add(tTransferAmount);
    _rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);        
    _reflectFee(rFee, tFee);
    emit Transfer(sender, recipient, tTransferAmount);
}

As you can see, even if the addresses are excluded from reflections, they still have to pay their fee. At first this seems to work. As long as we just exclude accounts it does. But be careful, reflection holders can be screwed by excluded accounts.

Suppose we have 400 real tokens _tTotal and 4000 reflections _rTotal evenly distributed between 4 addresses. Each address holds 1000 reflections and therefore 100 tokens (Picture a).

Now suppose we exclude a from the reflections. tAmount and rAmount are shrinking but everyone has still the same share of the available tokens (Picture b). For example b holds 1/3 of all 3000 reflections. This means b also holds 1/3 of the 300 available tokens, which means b owns 100 tokens.

Now c and d are trading with each other a couple of times. This means they loose their fees. The total amount of reflections will decrease to only 2000 (Picture c). Good news. b now owns 1/2 of all reflections just by doing nothing. This means he owns 150 of the available 300 tokens.

But now a gets removed from the excluded addresses. This means his 1000 token will be added to the overall amount rAmount. Now b, c and d are loosing tokens while a is gaining some. It appears that a was always part of the addresses that received reflections although a never did (Picture d).

a, b and c didn’t loose more tokens than they initially had. However they have to share the reflections now with a, which was unexpected. Only the account owner can add or remove addresses from the reflections. As always a renounced ownership is the way to go.