Skip to content

Commit

Permalink
Version 2.0.1 (#7)
Browse files Browse the repository at this point in the history
* 2.0.1

Co-authored-by: Jan-Paul Azucena <[email protected]>
Co-authored-by: Nathan Sala <[email protected]>
  • Loading branch information
3 people authored Jun 20, 2020
1 parent f4f73c2 commit 0cf3ffb
Show file tree
Hide file tree
Showing 15 changed files with 172 additions and 423 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 2.0.1 (20/06/2020)

### New features
* The module now exports a `constants` and a `utils` object.
* Added a function to calculate a total rewards based on a rewards schedule.

### Bugfixes
* Fixed a wrong import in the migration and restructured the script

## 2.0.0 (17/06/2020)

### Breaking changes
Expand Down Expand Up @@ -36,4 +45,4 @@
* Better abstraction of core staking features.

## 0.0.1 (15/04/2020)
* Initial commit.
* Initial commit.
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,33 +58,32 @@ Please see the mock contracts used for the tests in `contracts/mocks/staking/` f

_Staking_ is the mechanism by-which an NFT is transferred to the `NftStaking` staking contract, to be held for a period of time, in exchange for a claimable ERC20-based token payout (rewards). While staked, the staking contract maintains ownership of the NFT on behalf of the original owner until such time as that owner decides to withdraw, or _unstake_, the NFT from the staking contract. The unstaked NFT is then transferred back to the original owner.

Upon the initial stake of an NFT to the staking contract, the NFT will be "frozen" for a fixed duration (as specified by the `freezeLengthInCycles_` constructor argument) before being allowed to be unstaked from the staking contract.

Before any staker can stake or unstake their NFTs from the staking contract, all outstanding claimable rewards must first be claimed.
Upon the initial stake of an NFT to the staking contract, the NFT will be "frozen" for a fixed duration (1 cycle) before being allowed to be unstaked from the staking contract.


### Periods and Cycles

Discrete units of time in staking are expressed in terms of either _periods_ or _cycles_. A cycle is defined as some number of seconds (as specified by the `cycleLengthInSeconds_` constructor argument), and a period is defined as some number of cycles (as specified by the `periodLengthInCycles_` constructor argument). While cycles are used to calculate a staker's entitlement to claimable rewards, based on a fixed reward pool allotment schedule, periods are used for claiming rewards based on a payout schedule.
Discrete units of time in staking are expressed in terms of either _periods_ or _cycles_. A cycle is defined as some number of seconds (as specified by the `cycleLengthInSeconds_` constructor argument), and a period is defined as some number of cycles (as specified by the `periodLengthInCycles_` constructor argument). Cycles are used to calculate a staker's entitlement to claimable rewards, based on a fixed reward pool allotment schedule, while periods are used for claiming rewards based on a payout schedule.


### Claiming

As mentioned in the [Staking](#staking) section above, claiming outstanding claimable rewards is a requirement prior to staking and unstaking any additional NFTs from the staking contract. While entitled rewards for staking accumulate every cycle, those rewards can only be claimed according to a payout schedule; that schedule is defined as one (1) period. This means that at least one period must elapse before the accumulated rewards for staking an NFT in any given period can be claimed. Or in other words, a staker can claim rewards once per payout period.
Entitled rewards accumulate for every cycle that passes since staking. Those rewards can only be claimed according to a payout schedule; that schedule is defined as one (1) period. This means that at least one period must elapse before the accumulated rewards for staking an NFT, in any given period, can be claimed. Or in other words, a staker can claim rewards once per payout period.

The only period that cannot be claimed for by a staker is the current period, as it has not completed its full elapsed duration to be claimable.


### Snapshots

Snapshots are a historical record of changes in total staked weight over time. For every cycle in which an NFT is staked or unstaked, a new snapshot is created. This provides a means for calculating a staker's entitled proportion of rewards for every cycle of a period that they are claiming.
Snapshots are historical records of changes in total staked weight, over time. For every cycle in which an NFT is staked or unstaked, a new snapshot is created. This provides a means for calculating a staker's entitled proportion of rewards for every cycle of a period that they are claiming. There is a global snapshot history that tracks aggregate stake changes for all stakers, as well as a snapshot history for each staker to track their own stake changes.

Snapshots have the following properties:

- Span at least one cycle, and at most one period.
- The span of one snapshot will never overlap with another.
- Spans at least one cycle.
- Can span multiple cycles over multiple periods.
- The span of one snapshot will never overlap with another (for any given staker).
- Are arranged consecutively in sequence without skipping over cycles (i.e. there will never be a cycle in between two snapshots).
- Snapshots do not span multiple periods (i.e. a snapshot will not start in one period and end in a different period).
- Are removed from a staker's snapshot history as soon as a reward claim is made for the periods that cover the span of the snapshot.


## Testing
Expand Down
107 changes: 20 additions & 87 deletions migrations/1_NftStaking.js
Original file line number Diff line number Diff line change
@@ -1,109 +1,42 @@
const program = require('commander');
const { NFCollectionMaskLength } = require('../src').constants;
const { BN } = require('@openzeppelin/test-helpers');
const { fromWei, toWei } = require('web3-utils');
const { fromWei } = require('web3-utils');
const { DefaultNFMaskLength } = require('@animoca/ethereum-contracts-assets_inventory').constants;
const { DefaultCycleLengthInSeconds, DefaultPeriodLengthInCycles, ExamplePayoutSchedule, ExampleWeightsByRarity } = require('../src/constants');
const { rewardsPoolFromSchedule } = require('../src/utils');

const NftStaking = artifacts.require("NftStakingMock");
const AssetsInventory = artifacts.require("AssetsInventoryMock");
const ERC20 = artifacts.require("ERC20WithOperatorsMock");

const DayInSeconds = 86400;
const CycleLengthInSeconds = new BN(DayInSeconds);
const PeriodLengthInCycles = new BN(7);

const RewardsTokenInitialBalance = toWei('320000000');
const PayoutSchedule = [ // payouts are expressed in decimal form and need to be converted to wei
{ startPeriod: 1, endPeriod: 4, payoutPerCycle: '2700000' },
{ startPeriod: 5, endPeriod: 5, payoutPerCycle: '2200000' },
{ startPeriod: 6, endPeriod: 6, payoutPerCycle: '2150000' },
{ startPeriod: 7, endPeriod: 7, payoutPerCycle: '2100000' },
{ startPeriod: 8, endPeriod: 8, payoutPerCycle: '2050000' },
{ startPeriod: 9, endPeriod: 9, payoutPerCycle: '2000000' },
{ startPeriod: 10, endPeriod: 10, payoutPerCycle: '1950000' },
{ startPeriod: 11, endPeriod: 11, payoutPerCycle: '1900000' },
{ startPeriod: 12, endPeriod: 12, payoutPerCycle: '1850000' },
{ startPeriod: 13, endPeriod: 13, payoutPerCycle: '1800000' },
{ startPeriod: 14, endPeriod: 14, payoutPerCycle: '1750000' },
{ startPeriod: 15, endPeriod: 15, payoutPerCycle: '1700000' },
{ startPeriod: 16, endPeriod: 16, payoutPerCycle: '1650000' },
{ startPeriod: 17, endPeriod: 17, payoutPerCycle: '1600000' },
{ startPeriod: 18, endPeriod: 18, payoutPerCycle: '1550000' },
{ startPeriod: 19, endPeriod: 19, payoutPerCycle: '1500000' },
{ startPeriod: 20, endPeriod: 20, payoutPerCycle: '1475000' },
{ startPeriod: 21, endPeriod: 21, payoutPerCycle: '1450000' },
{ startPeriod: 22, endPeriod: 22, payoutPerCycle: '1425000' },
{ startPeriod: 23, endPeriod: 23, payoutPerCycle: '1400000' },
{ startPeriod: 24, endPeriod: 24, payoutPerCycle: '1375000' },
]; // total ~ 320,000,000

const RarityToWeightsMap = {
0: 500,// Apex,
1: 100,// Legendary,
2: 50,// Epic,
3: 50,// Epic,
4: 10,// Rare,
5: 10,// Rare,
6: 10,// Rare,
7: 1,// Common,
8: 1,// Common,
9: 1// Common,
};
const RewardsPool = rewardsPoolFromSchedule(ExamplePayoutSchedule, DefaultPeriodLengthInCycles);

module.exports = async (deployer, network, accounts) => {

switch (network) {
case "ganache":
await deployer.deploy(AssetsInventory, NFCollectionMaskLength);
this.nftContract = await AssetsInventory.deployed();
await deployer.deploy(ERC20, RewardsTokenInitialBalance);
this.rewardsTokenContract = await ERC20.deployed();
break;
case "rinkeby":
const nftContractAddressRinkeby = program.nftContractAddressRinkeby;
const ERC20BaseAddressRinkeby = program.ERC20BaseAddressRinkeby;

this.nftContract =
nftContractAddressRinkeby ?
await AssetsInventory.at(nftContractAddressRinkeby) :
await AssetsInventory.new(NFCollectionMaskLength);

this.rewardsTokenContract =
ERC20BaseAddressRinkeby ?
await ERC20.at(ERC20BaseAddressRinkeby) :
await ERC20.new(RewardsTokenInitialBalance);

break;
case "mainnet":

break;
default:
console.log(`Unknown network '${network}', stopping...`);
return;

}

this.inventoryContract = await AssetsInventory.new(DefaultNFMaskLength);
this.erc20Contract = await ERC20.new(RewardsPool);

await deployer.deploy(NftStaking,
CycleLengthInSeconds,
PeriodLengthInCycles,
this.nftContract.address,
this.rewardsTokenContract.address,
Object.keys(RarityToWeightsMap),
Object.values(RarityToWeightsMap),
DefaultCycleLengthInSeconds,
DefaultPeriodLengthInCycles,
this.inventoryContract.address,
this.erc20Contract.address,
Object.keys(ExampleWeightsByRarity),
Object.values(ExampleWeightsByRarity),
);

this.stakingContract = await NftStaking.deployed();

// Enough to cover the whole payout schedule needs to be approved and will be transferred to the contract at start
await this.rewardsTokenContract.approve(this.stakingContract.address, RewardsTokenInitialBalance);

for (schedule of PayoutSchedule) {
for (schedule of ExamplePayoutSchedule) {
console.log(`Setting schedule: ${fromWei(schedule.payoutPerCycle)} ERC20s per-cycle for periods ${schedule.startPeriod} to ${schedule.endPeriod}`);
await this.stakingContract.setRewardsForPeriods(
schedule.startPeriod,
schedule.endPeriod,
toWei(schedule.payoutPerCycle)
schedule.payoutPerCycle
);
}

console.log(`Approving ${fromWei(RewardsPool)} ERC20s to the staking contract for the reward pool before starting`);
await this.erc20Contract.approve(this.stakingContract.address, RewardsPool);

console.log('Starting the staking schedule');
await this.stakingContract.start();
}
Loading

0 comments on commit 0cf3ffb

Please sign in to comment.