Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-1973: Scalable Rewards

Authors Lee Raj (@lerajk), Qin Jian (@qinjian)
Created 2019-04-01

Simple Summary

A mintable token rewards interface that mints ‘n’ tokens per block which are distributed equally among the ‘m’ participants in the DAPP’s ecosystem.

Abstract

The mintable token rewards interface allows DApps to build a token economy where token rewards are distributed equally among the active participants. The tokens are minted based on per block basis that are configurable (E.g. 10.2356 tokens per block, 0.1 token per block, 1350 tokens per block) and the mint function can be initiated by any active participant. The token rewards distributed to each participant is dependent on the number of participants in the network. At the beginning, when the network has low volume, the tokens rewards per participant is high but as the network scales the token rewards decreases dynamically.

## Motivation

Distributing tokens through a push system to a large amount of participants fails due to block gas limit. As the number of participants in the network grow to tens of thousands, keeping track of the iterable registry of participants and their corresponding rewards in a push system becomes unmanagable. E.g. Looping through 5000 addresses to distribute 0.0000001 reward tokens is highly inefficient. Furthermore, the gas fees in these transactions are high and needs to be undertaken by the DApp developer or the respective company, leading to centralization concerns.

A pull system is required to keep the application completely decentralized and to avoid the block gas limit problem. However, no standard solution has been proposed to distribute scalable rewards to tens of thousands participants with a pull system. This is what we propose with this EIP through concepts like TPP, round mask, participant mask.

Specification

Definitions

token amount per participant in the ecosytem or TPP (token per participant): TPP = (token amount to mint / total active participants)

roundMask: the cumulative snapshot of TPP over time for the token contract. E.g. transactionOne = 10 tokens are minted with 100 available participants (TPP = 10 / 100) , transactionTwo = 12 tokens are minted with 95 participants (TPP = 12 / 95 )

roundMask = (10/100) + (12/95)

participantMask: is used to keep track of a msg.sender (participant) rewards over time. When a msg.sender joins or leaves the ecosystem, the player mask is updated

participantMask = previous roundMask OR (current roundMask - TPP)

rewards for msg.sender: roundMask - participantMask

E.g. Let’s assume a total of 6 transactions (smart contract triggers or functions calls) are in place with 10 existing participants (denominator) and 20 tokens (numerator) are minted per transaction. At 2nd transaction, the 11th participant joins the network and exits before 5th transaction, the 11th participant’s balance is as follows:

 t1 roundMask = (20/10)
 t2 roundMask = (20/10) + (20/11) 
 t3 roundMask = (20/10) + (20/11) + (20/11)
 t4 roundMask = (20/10) + (20/11) + (20/11) + (20/11)
 t5 roundMask = (20/10) + (20/11) + (20/11) + (20/11)+ (20/10)
 t6 roundMask = (20/10) + (20/11) + (20/11) + (20/11)+ (20/10) + (20/10)

Total tokens released in 6 transactions = 60 tokens

As the participant joins at t2 and leaves before t5, the participant deserves the rewards between t2 and t4. When the participant joins at t2, the ‘participantMask = (20/10)’, when the participant leaves before t5, the cumulative deserved reward tokens are :

rewards for msg.sender: [t4 roundMask = (20/10) + (20/11)+ (20/11) + (20/11)] - [participantMask = (20/10)] = [rewards = (20/11)+ (20/11) + (20/11)]

When the same participant joins the ecosystem at a later point (t27 or t35), a new ‘participantMask’ is given that is used to calculate the new deserved reward tokens when the participant exits. This process continues dynamically for each participant.

tokensPerBlock: the amount of tokens that will be released per block

blockFreezeInterval: the number of blocks that need to pass until the next mint. E.g. if set to 50 and ‘n’ tokens were minted at block ‘b’, the next ‘n’ tokens won’t be minted until ‘b + 50’ blocks have passed

lastMintedBlockNumber: the block number on which last ‘n’ tokens were minted

totalParticipants : the total number of participants in the DApp network

tokencontractAddress : the contract address to which tokens will be minted, default is address(this)


pragma solidity ^0.5.2;

import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol";
import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol";

contract Rewards is ERC20Mintable, ERC20Detailed {

using SafeMath for uint256;

uint256 public roundMask;
uint256 public lastMintedBlockNumber;
uint256 public totalParticipants = 0;
uint256 public tokensPerBlock; 
uint256 public blockFreezeInterval; 
address public tokencontractAddress = address(this);
mapping(address => uint256) public participantMask; 

/**
 * @dev constructor, initializes variables.
 * @param _tokensPerBlock The amount of token that will be released per block, entered in wei format (E.g. 1000000000000000000)
 * @param _blockFreezeInterval The amount of blocks that need to pass (E.g. 1, 10, 100) before more tokens are brought into the ecosystem.
 */
 constructor(uint256 _tokensPerBlock, uint256 _blockFreezeInterval) public ERC20Detailed("Simple Token", "SIM", 18){ 
lastMintedBlockNumber = block.number;
tokensPerBlock = _tokensPerBlock;
blockFreezeInterval = _blockFreezeInterval;
}

/**
 * @dev Modifier to check if msg.sender is whitelisted as a minter. 
 */
modifier isAuthorized() {
require(isMinter(msg.sender));
_;
}

/**
 * @dev Function to add participants in the network. 
 * @param _minter The address that will be able to mint tokens.
 * @return A boolean that indicates if the operation was successful.
 */
function addMinters(address _minter) external returns (bool) {
_addMinter(_minter);
totalParticipants = totalParticipants.add(1);
updateParticipantMask(_minter);
return true;
}


/**
 * @dev Function to remove participants in the network. 
 * @param _minter The address that will be unable to mint tokens.
 * @return A boolean that indicates if the operation was successful.
 */
function removeMinters(address _minter) external returns (bool) {
totalParticipants = totalParticipants.sub(1);
_removeMinter(_minter); 
return true;
}


/**
 * @dev Function to introduce new tokens in the network. 
 * @return A boolean that indicates if the operation was successful.
 */
function trigger() external isAuthorized returns (bool) {
bool res = readyToMint();
if(res == false) {
return false;
} else {
mintTokens();
return true;
}
}

/**
 * @dev Function to withdraw rewarded tokens by a participant. 
 * @return A boolean that indicates if the operation was successful.
 */
function withdraw() external isAuthorized returns (bool) {
uint256 amount = calculateRewards();
require(amount >0);
ERC20(tokencontractAddress).transfer(msg.sender, amount);
}

/**
 * @dev Function to check if new tokens are ready to be minted. 
 * @return A boolean that indicates if the operation was successful.
 */
function readyToMint() public view returns (bool) {
uint256 currentBlockNumber = block.number;
uint256 lastBlockNumber = lastMintedBlockNumber;
if(currentBlockNumber > lastBlockNumber + blockFreezeInterval) { 
return true;
} else {
return false;
}
}

/**
 * @dev Function to calculate current rewards for a participant. 
 * @return A uint that returns the calculated rewards amount.
 */
function calculateRewards() private returns (uint256) {
uint256 playerMask = participantMask[msg.sender];
uint256 rewards = roundMask.sub(playerMask);
updateParticipantMask(msg.sender);
return rewards;
}

/**
 * @dev Function to mint new tokens into the economy. 
 * @return A boolean that indicates if the operation was successful.
 */
function mintTokens() private returns (bool) {
uint256 currentBlockNumber = block.number;
uint256 tokenReleaseAmount = (currentBlockNumber.sub(lastMintedBlockNumber)).mul(tokensPerBlock);
lastMintedBlockNumber = currentBlockNumber;
mint(tokencontractAddress, tokenReleaseAmount);
calculateTPP(tokenReleaseAmount);
return true;
}

 /**
* @dev Function to calculate TPP (token amount per participant).
* @return A boolean that indicates if the operation was successful.
*/
function calculateTPP(uint256 tokens) private returns (bool) {
uint256 tpp = tokens.div(totalParticipants);
updateRoundMask(tpp);
return true;
}

 /**
* @dev Function to update round mask. 
* @return A boolean that indicates if the operation was successful.
*/
function updateRoundMask(uint256 tpp) private returns (bool) {
roundMask = roundMask.add(tpp);
return true;
}

 /**
* @dev Function to update participant mask (store the previous round mask)
* @return A boolean that indicates if the operation was successful.
*/
function updateParticipantMask(address participant) private returns (bool) {
uint256 previousRoundMask = roundMask;
participantMask[participant] = previousRoundMask;
return true;
}

}

Rationale

Currently, there is no standard for a scalable reward distribution mechanism. In order to create a sustainable cryptoeconomic environment within DAPPs, incentives play a large role. However, without a scalable way to distribute rewards to tens of thousands of participants, most DAPPs lack a good incentive structure. The ones with a sustainable cryptoeconomic environment depend heavily on centralized servers or a group of selective nodes to trigger the smart contracts. But, in order to keep an application truly decentralized, the reward distribution mechanism must depend on the active participants itself and scale as the number of participants grow. This is what this EIP intends to accomplish.

Backwards Compatibility

Not Applicable.

Test Cases

WIP, will be added.

Implementation

WIP, a proper implementation will be added later.A sample example is below:

etherscan rewards contract : https://ropsten.etherscan.io/address/0x8b0abfc541ab7558857816a67e186221adf887bc#tokentxns

Step 1 : deploy Rewards contract with the following parameters_tokensPerBlock = 1e18, _blockFreezeInterval = 1

Step 2 : add Alice(0x123) and Bob(0x456) as minters, addMinters(address _minter)

Step 3 : call trigger() from Alice / Bob’s account. 65 blocks are passed, hence 65 SIM tokens are minted. The RM is 32500000000000000000

Step 4 : Alice withdraws and receives 32.5 SIM tokens (65 tokens / 2 participants) and her PM = 32500000000000000000

Step 5 : add Satoshi(0x321) and Vitalik(0x654) as minters, addMinters(address _minter)

Step 6 : call trigger() from Alice / Bob’s / Satoshi / Vitalik account. 101 blocks are passed, hence 101 SIM tokens are minted. The RM is 57750000000000000000

Step 7 : Alice withdraws and receives 25.25 SIM tokens (101 tokens / 4 participants) and her PM = 57750000000000000000

Step 8 : Bob withdraws and receives 57.75 SIM tokens ((65 tokens / 2 participants) + (101 tokens / 4 participants)). Bob’s PM = 57750000000000000000

Copyright and related rights waived via CC0.

References

  1. Scalable Reward Distribution on the Ethereum Blockchain by Bogdan Batog, Lucian Boca and Nick Johnson

  2. Fomo3d DApp, https://fomo3d.hostedwiki.co/

Citation

Please cite this document as:

Lee Raj (@lerajk), Qin Jian (@qinjian), "ERC-1973: Scalable Rewards [DRAFT]," Ethereum Improvement Proposals, no. 1973, April 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1973.