Christian

發布於 2022-01-28到 Mirror 閱讀

Virtual Yield

Abstract

Global operations in the context of smart contracts tend to have costs that prohibit their effectiveness. Every mutation of a variable within a smart contract costs transaction senders native tokens. If we desire to construct a system in which the actions of one user affect the state of all other users, is it reasonable to expect that user to pay on behalf of all other users of the system? This expectation is generally unreasonable for most implementations of smart contracts. In order to minimize the impact of one transaction on the gas costs of individual users, which are already high on Ethereum L1, a solution that minimizes the overall number of read/writes to memory is needed. In this paper, I present my concept of virtual yield, which takes the idea of reflector tokens such as Safemoon and Goodbridging and minimizes their gas cost to users while simultaneously applying updates to user token balances globally. In this system, we can minimize the cost to individuals for effecting the global state of a token system. This has implications for a multitude of user cases, although I will consider the reflector token case only.

Introduction

Reflector tokens were introduced as a concept by reflect.finance in late 2020. The concept is simple: take a percentage fee from all transfers (or any other action involving the token) and apply the proceeds from this fee evenly across all holders of the token. This global tax generates a passive yield for all the participants in the ecosystem without them taking any particular action i.e. staking or adding liquidity to a pool. This system is optimal for low-tech or new users, who do not understand how to gain yield from active staking strategies. The concept was popularized by Safemoon, which forked the RFI codebase in March 2021 and became an early memecoin to generate large-scale returns in the 2021-22 bull market. In addition to Safemoon, Github lists RFI as having 104 forks of its repository, while Safemoon has more than 860.

Implementation - RFI

Let’s first look at the implementation done by Reflect.finance. To preface this investigation I would like to mention the following: 1) to the best of my knowledge, reflect.finance is no longer an active project, because there have been no github commits for about one year now. Additionally, there is a lack of general communication from the medium page, twitter and no updates on the website. Also, reflect provides no technical documentation so my understanding of the codebase is entirely from my own investigation. That said, I feel that reflect was a pioneer in a new type of DeFi primitive. I have the utmost respect for teams that pursue new economic experiments and understand why these types of approaches are not always viable in the long term. You probably shouldn’t attempt to purchase RFI tokens at this time.

The RFI implementation centers around the modification of the `(address => uint256) private _balances; mapping that is standard in any ERC20 contract. Changing the way that balances are stored and accessed also implies a need to modify functions including balanceOf(address) and transferFrom because of nonstandard behavior at both transfer and balance check events. RFI splits this into two mappings defined as

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

To the best of my understanding rOwned is a measure of the fees being extracted from senders and given to everyone else in addition to the actual user balances, as it is updated at each reflection. As for the tOwned mapping, it appears to be a measure of the balances of users without reflections, presumably to deal with scenarios when tokens are transferred under conditions that do not cause reflection.

function reflect(uint256 tAmount) public {
        address sender = _msgSender();
        require(!_isExcluded[sender], "Excluded addresses cannot call this function");
        (uint256 rAmount,,,,) = _getValues(tAmount);
        _rOwned[sender] = _rOwned[sender].sub(rAmount);
        _rTotal = _rTotal.sub(rAmount);
        _tFeeTotal = _tFeeTotal.add(tAmount);
  }

To calculate the actual balance of any user the following function is used inside of balanceOf:

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);
  }

The value currentRate represents the ratio of tokens that will be applied to a user’s balance due to reflections and is calculated as rSupply / tSupply. Then, the balance is taken from dividing a user’s balance by this rate. In this setup, multiple division operations are needed in order to facilitate reflection.

Implementation - My Approach

The motivation behind my implementation of a reflector was out an attempt to make a naive implementation of a fee-based ecosystem. At first, I explored the possibility of using for-loops and iterating over the entire mapping of balances in order to update them at each reflection event. This approach, of course, would be infeasible even on cheaper networks at scale due to cost increases. For-loops are generally frowned upon in solidity, and iterating over a mapping’s keys is not as simple as it is in many other programming languages. This is because normally mappings are considered objects, in languages like Python or Nodejs, which allows the user to access the keys as a list within the object. In solidity, such constructs do not exist and even if they did, would be wildly expensive. Thus, to use a for loop effectively, we would need to both track the balances as a mapping and the set of holders as a list. Then, as holders enter and exit the system, periodically rebalance the list and mapping to keep them synchronized. The gas costs of all of these operations would fall on the sender, unfairly taxing them for the benefit of everyone else’s gains. Clearly, although relatively simple to implement, this solution does not work.

While consulting Jake Brukhman on this issue, he suggested that there is a simpler way of updating balances for each reflection: using a global multiplier to track the total number of reflections, then simply multiplying each user’s underlying balance by this value when balanceOf is called. Together we iterated the concept to a workable state after which I began to implement it in solidity.

The system achieves cost savings in several ways: no need for extra data structures to track information, no need for costly global operations, and only one more costly operation (multiplication) is needed. One could implement this concept in a variety of ways, but I chose a stake-unstake system, in which all tx of unstaked tokens increase the multiplier, but only staked tokens benefit from this multiplier increase. When unstaking, the yields from reflection are accrued to the underlying balance of the user. At all other times, the virtual balance (meaning the balance including gained rewards/yield) is calculated as:

function stakedVirtualBalanceOf(address act) public view returns (uint256) {
	uint aTruncated = a.div(10**18);
	return _stakedBalances[act].mul(aTruncated);
}

A user’s balance is virtual because the _balances mapping is not yet updated. When they chose to roll over their yields into true memory, that virtual yield, represented by the multiplier, is applied to their underlying balance. I chose to set the following rule for multiplier increase (this could be anything): at each valid tx, the multiplier should increase all eligible balances by 1%. In order to do that the calculation would be balance = balance*a where a = a^x where a is 0.01 and x are the number of events causing multiplier increase. I realized quickly that there is a problem with this approach. Namely, that solidity does not natively support decimals. So, we must use numbers with lots of zeroes (18) instead. This quickly caused overflows when working with exponents. So to solve this issue, I Taylor-expanded the 1% increase function, then played with the coefficients to get: f(x) = 1.01 + 0.0011x^2 + 0.00000011x^3 and used this function instead. This also saves gas by setting the number of multiplications to update a at a fixed amount. In solidity this is:

uint t1 = 11000000000000000;
uint t2 = 110000000000;
t1 = t1.mul(eventCount**2);
t2 = t2.mul(eventCount**3);
a = baseA.add(t1).add(t2);

Now, we have achieved a global operation that does not require loops or additional mappings to function (my exact final implementation does use two mappings, but this is because of the additional conditions I wanted to impose, not the underlying design). Thus we have achieved a type of reflection in cost efficient manner. Importantly, the cost associated with updating balances across the entire set of holders is not forced upon the user as would happen with the naive approach, and the cost is not increased by using multiple mappings as would happen with the RFI approach. The multiplier system creates tokens that exist in theory, but not in memory. They are conjured into existence via multiplication, but are not written into the storage onchain until a user decides to compound their rewards. Thus, a ‘virtual yield’ is achieved, one that only exists in terms of function calls.

Conclusion

Reflection is an interesting topic that should be given further consideration within DeFi. By making use of concepts from reflectors, the idea of virtual yield emerges; allowing for many token balances to be tracked and updated using only a simple number or function. The apparent failure of other reflector tokens to gain adoption does not necessarily indicate that the concept itself, or the concept of virtual yield, is not useful. Reflection could have potential use cases in social taxation and DAO membership; essentially extracting passive fees to retain users via constant reward streaming. Although many different implementations are possible, I believe that the virtual yield approach is superior as a means of achieving the desired outcome while being flexible to more forms of modification for particular use cases.

Disclaimer