aavegotchi / aavegotchi-realm-diamond

23 stars 9 forks source link

Implementing Variable Rate Rewards with a Smart Contract #4

Closed mudgen closed 2 years ago

mudgen commented 2 years ago

Various reward smart contracts are used to calculate token rewards when users stake tokens or do other activities. Rewards are calculated automatically by using a base number multiplied by a rate number multiplied by the number of seconds rewards are given out. Various other numbers can be used in this calculation as well. The base number could be a fixed arbitrary number or the number of tokens staked to earn rewards.

This works fine but a problem arises when a project decides to change the rate of reward. You cannot just change the rate of rewards because the entire unclaimed reward amounts shift to using the new rate instead of only new rewards using the new rate.

An epoch implementation that specifies time ranges for different reward rates can be used. See here for an implementation of that: https://github.com/aavegotchi/ghst-staking/blob/master/contracts/facets/StakingFacet.sol

Here is another way to implement variable rate rewards in a smart contract. To call it something I call it the Reward Index method.

  1. You implement a rewardIndex function. This function returns the RewardIndex. The RewardIndex is a number that starts at 1.0 and increases a small amount every second. For example the RewardIndex could increase 5 percent over one year. After one year the RewardIndex is 1.05. At any time it is possible to change the rate of increase so for example the rate of increase becomes 7 percent or 3 percent etc.

  2. Have a base number that is used to calculate rewards with. This could be the number of tokens staked to earn rewards or could be an arbitrary number like 100.

  3. Rewards can be generated in this way: When rewards accumulation starts for someone or something (an entity) the current RewardIndex is stored and associated with the entity -- call it StoredRewardIndex. After that the rewards for the entity are calculated like this: uint256 rewards = (RewardIndex - StoredRewardIndex) * BaseNumber.

  4. Now it is possible to change the rate of rewards simply by increasing or decreasing the amount that RewardIndex is increased per second.

Here is an example:

Let's say that the base number is 100. And RewardIndex is increasing at 5 percent. Entity A starts getting rewards at the same time that RewardIndex starts increasing. At the beginning RewardIndex is 0 so the value of StoredRewardIndex is 0. After one year here is the calculation: 5 = (0.05 - 0.0) * 100

Let's say that Entity B starts getting rewards after the first 6 months. Here is the calculation of rewards for 6 months: 2.5 = (0.05 - 0.025) * 100

Now lets say that after the first year the interest rate changes from 5 percent to 10 percent. Here is the calculation for Entity A at year 2: 15 = (0.15 - 0.0) * 100.

Here is the calculation for Entity B at year 2: 12.5 = (0.15 - 0.025) * 100

cinnabarhorse commented 2 years ago

Thanks for the interesting solution @mudgen.

A few questions:

  1. Would the StoredRewardIndex need to be reset whenever the user increases/decreases the BaseNumber? (in our case, the amount they have staked)

  2. How would you recommend applying this technique to dozens of different NFTs, each with different rates? Would you have a single RewardIndex, or would each NFT (with its own unique rate) manage its own RewardIndex?

mudgen commented 2 years ago

Would the StoredRewardIndex need to be reset whenever the user increases/decreases the BaseNumber? (in our case, the amount they have staked)

Yes. When someone increases or decreases their stake then the current rewards can be calculated and stored in a state variable (like StoredRewards) that is associated with the user and the StoredRewardIndex number is reset to the current RewardIndex. The rewards calculation would then be: uint256 rewards = ((RewardIndex - StoredRewardIndex) * BaseNumber) + StoredRewards). When rewards were claimed the StoredRewards state variable would be set to 0.

Another idea is to automatically claim rewards when someone increases or decreases their stake.

How would you recommend applying this technique to dozens of different NFTs, each with different rates? Would you have a single RewardIndex, or would each NFT (with its own unique rate) manage its own RewardIndex?

NFTs that share the same interest rate could use the same RewardIndex. NFTs that use different interest rates would have or manage a different RewardIndex.

Another idea is to have a base rate that all NFTs use and share, and a custom rate that is specific to every NFT. Combine the two rates to one rate for each NFT. Doing this would make it possible to change the interest rate for all NFTs by changing the base rate.

mudgen commented 2 years ago

Here is a simple example implementation of a rewardIndex function that works with 9 levels and other related functions:


    struct NFTRewards {
        uint256 nftType;
        uint256 level;
        uint256 storedRewardIndex;
        uint256 storedReward;
    }

   struct RewardRate {
       uint256 ratePerSecond;
       uint256 storedRewardIndex;
       uint256 timeLastUpdated;
   }

    struct AppStorage {
        // nftType => RewardRate
        mapping(uint256 => RewardRate) rewardRates;
        // tokenId => NFTRewards
        mapping(uint256 => NFTRewards) rewards;
    }

    AppStorage s;

    // rate is per 10,000.  For example the rate 500 is 5 percent.  The rate 525 is 5.25 percent  
    // The rate 50000  is 500 percent
    function setRewardIndexRate(uint256 _nftType, uint256 _level, uint256 _rate) external onlyOwner {
        RewardRate storage rewardRate = s.rewardRates[nftType][level];
        rewardRate.storedRewardIndex = ((block.timestamp - rewardRate.timeLastUpdated) * rewardRate.ratePerSecond) + rewardRate.storedRewardIndex;
        rewardRate.timeLastUpdated = block.timestamp;
        // this sets the amount a rate increases per second
        rewardRate.ratePerSecond = (1e14 * _rate) / 365 days;
    }    

    function stakeNFT(uint256 _tokenId, uint256 _nftType, uint256 _level) external {
        rewards[_tokenId].nftType = _nftType;
        rewards[_tokenId].level = _level;
        rewards[_tokenId]].storedRewardIndex = rewardIndex(_tokenId);
    }

    function rewardIndex(uint256 _tokenId) public view returns (uint256) {
        uint256 nftType = s.rewards[_tokenId].nftType;
        uint256 level = s.rewards[tokenId].level;
        RewardRate storage rewardRate = s.rewardRates[nftType][level];
        return ((block.timestamp - rewardRate.timeLastUpdated) * rewardRate.ratePerSecond) + rewardRate.storedRewardIndex;
    }

    function calculateRewards(uint _tokenId) external view returns (uint256 reward_) {
        // 10000 is the arbitrary base number
        // reward_ is a number with 18 decimal places,  which could be used with an ERC20 token 
        reward_ = (rewardIndex(_tokenId) - rewards[_tokenId].storedRewardIndex) * 10000;
    }