Skip to content

Commit

Permalink
Merge pull request #529 from NexusMutual/test/stakingPool-withdraw
Browse files Browse the repository at this point in the history
Test: withdraw unit tests
  • Loading branch information
shark0der committed Jan 5, 2023
2 parents 18a4b93 + 5d510a3 commit 2b4d436
Show file tree
Hide file tree
Showing 11 changed files with 760 additions and 60 deletions.
2 changes: 2 additions & 0 deletions contracts/interfaces/IStakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,6 @@ interface IStakingPool {
event PoolFeeChanged(address indexed manager, uint newFee);

event PoolDescriptionSet(uint poolId, string ipfsDescriptionHash);

event Withdraw(address indexed src, uint indexed tokenId, uint tranche, uint amountStakeWithdrawn, uint amountRewardsWithdrawn);
}
3 changes: 2 additions & 1 deletion contracts/mocks/TokenControllerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ contract TokenControllerMock is MasterAwareV2 {
token().operatorTransfer(from, amount);
}

function withdrawNXMStakeAndRewards(address /*to*/, uint stakeToWithdraw, uint rewardsToWithdraw, uint poolId) external {
function withdrawNXMStakeAndRewards(address to, uint stakeToWithdraw, uint rewardsToWithdraw, uint poolId) external {
stakingPoolNXMBalances[poolId].deposits -= uint128(stakeToWithdraw);
stakingPoolNXMBalances[poolId].rewards -= uint128(rewardsToWithdraw);
token().transfer(to, stakeToWithdraw + rewardsToWithdraw);
}

function burnStakedNXM(uint amount, uint poolId) external {
Expand Down
85 changes: 48 additions & 37 deletions contracts/modules/staking/StakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ contract StakingPool is IStakingPool, ERC721 {
uint elapsed = bucketStartTime - _lastAccNxmUpdate;

uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
? elapsed * _rewardPerSecond / _rewardsSharesSupply
? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
: 0;

_accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
Expand All @@ -300,7 +300,7 @@ contract StakingPool is IStakingPool, ERC721 {
uint trancheEndTime = (_firstActiveTrancheId + 1) * TRANCHE_DURATION;
uint elapsed = trancheEndTime - _lastAccNxmUpdate;
uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
? elapsed * _rewardPerSecond / _rewardsSharesSupply
? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
: 0;
_accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
_lastAccNxmUpdate = trancheEndTime;
Expand Down Expand Up @@ -334,7 +334,7 @@ contract StakingPool is IStakingPool, ERC721 {
if (updateUntilCurrentTimestamp) {
uint elapsed = block.timestamp - _lastAccNxmUpdate;
uint newAccNxmPerRewardsShare = _rewardsSharesSupply != 0
? elapsed * _rewardPerSecond / _rewardsSharesSupply
? elapsed * _rewardPerSecond * ONE_NXM / _rewardsSharesSupply
: 0;
_accNxmPerRewardsShare = _accNxmPerRewardsShare.uncheckedAdd(newAccNxmPerRewardsShare);
_lastAccNxmUpdate = block.timestamp;
Expand Down Expand Up @@ -429,7 +429,7 @@ contract StakingPool is IStakingPool, ERC721 {
// if we're increasing an existing deposit
if (deposit.rewardsShares != 0) {
uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(deposit.lastAccNxmPerRewardShare);
deposit.pendingRewards += newEarningsPerShare * deposit.rewardsShares;
deposit.pendingRewards += newEarningsPerShare * deposit.rewardsShares / ONE_NXM;
}

deposit.stakeShares += newStakeShares;
Expand All @@ -451,7 +451,7 @@ contract StakingPool is IStakingPool, ERC721 {

// calculate rewards until now
uint newRewardPerShare = _accNxmPerRewardsShare.uncheckedSub(feeDeposit.lastAccNxmPerRewardShare);
feeDeposit.pendingRewards += newRewardPerShare * feeDeposit.rewardsShares;
feeDeposit.pendingRewards += newRewardPerShare * feeDeposit.rewardsShares / ONE_NXM;
feeDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare;
feeDeposit.rewardsShares += newFeeRewardShares;
}
Expand Down Expand Up @@ -546,40 +546,49 @@ contract StakingPool is IStakingPool, ERC721 {

Deposit memory deposit = deposits[tokenId][trancheId];

// can withdraw stake only if the tranche is expired
if (withdrawStake && trancheId < _firstActiveTrancheId) {

// Deposit withdrawals are not permitted while the manager is locked in governance to
// prevent double voting.
require(
managerLockedInGovernanceUntil < block.timestamp,
"StakingPool: While the pool manager is locked for governance voting only rewards can be withdrawn"
);

// calculate the amount of nxm for this deposit
uint stake = expiredTranches[trancheId].stakeAmountAtExpiry;
uint stakeShareSupply = expiredTranches[trancheId].stakeShareSupplyAtExpiry;
withdrawnStake += stake * deposit.stakeShares / stakeShareSupply;
{
uint trancheRewardsToWithdraw;
uint trancheStakeToWithdraw;

// can withdraw stake only if the tranche is expired
if (withdrawStake && trancheId < _firstActiveTrancheId) {

// Deposit withdrawals are not permitted while the manager is locked in governance to
// prevent double voting.
require(
managerLockedInGovernanceUntil < block.timestamp,
"StakingPool: While the pool manager is locked for governance voting only rewards can be withdrawn"
);

// calculate the amount of nxm for this deposit
uint stake = expiredTranches[trancheId].stakeAmountAtExpiry;
uint stakeShareSupply = expiredTranches[trancheId].stakeShareSupplyAtExpiry;
trancheStakeToWithdraw = stake * deposit.stakeShares / stakeShareSupply;
withdrawnStake += trancheStakeToWithdraw;

// mark as withdrawn
deposit.stakeShares = 0;
}

// mark as withdrawn
deposit.stakeShares = 0;
}
if (withdrawRewards) {

if (withdrawRewards) {
// if the tranche is expired, use the accumulator value saved at expiration time
uint accNxmPerRewardShareToUse = trancheId < _firstActiveTrancheId
? expiredTranches[trancheId].accNxmPerRewardShareAtExpiry
: _accNxmPerRewardsShare;

// if the tranche is expired, use the accumulator value saved at expiration time
uint accNxmPerRewardShareToUse = trancheId < _firstActiveTrancheId
? expiredTranches[trancheId].accNxmPerRewardShareAtExpiry
: _accNxmPerRewardsShare;
// calculate reward since checkpoint
uint newRewardPerShare = accNxmPerRewardShareToUse.uncheckedSub(deposit.lastAccNxmPerRewardShare);
trancheRewardsToWithdraw = newRewardPerShare * deposit.rewardsShares / ONE_NXM + deposit.pendingRewards;
withdrawnRewards += trancheRewardsToWithdraw;

// calculate reward since checkpoint
uint newRewardPerShare = accNxmPerRewardShareToUse.uncheckedSub(deposit.lastAccNxmPerRewardShare);
withdrawnRewards += newRewardPerShare * deposit.rewardsShares + deposit.pendingRewards;
// save checkpoint
deposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare;
deposit.pendingRewards = 0;
deposit.rewardsShares = 0;
}

// save checkpoint
deposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare;
deposit.pendingRewards = 0;
deposit.rewardsShares = 0;
emit Withdraw(msg.sender, tokenId, trancheId, trancheStakeToWithdraw, trancheRewardsToWithdraw);
}

deposits[tokenId][trancheId] = deposit;
Expand All @@ -591,6 +600,8 @@ contract StakingPool is IStakingPool, ERC721 {
withdrawnRewards,
poolId
);

return (withdrawnStake, withdrawnRewards);
}

function requestAllocation(
Expand Down Expand Up @@ -1121,13 +1132,13 @@ contract StakingPool is IStakingPool, ERC721 {
// if there already is a deposit on the new tranche, calculate its pending rewards
if (updatedDeposit.lastAccNxmPerRewardShare != 0) {
uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(updatedDeposit.lastAccNxmPerRewardShare);
updatedDeposit.pendingRewards += newEarningsPerShare * updatedDeposit.rewardsShares;
updatedDeposit.pendingRewards += newEarningsPerShare * updatedDeposit.rewardsShares / ONE_NXM;
}

// calculate the rewards for the deposit being extended and move them to the new deposit
{
uint newEarningsPerShare = _accNxmPerRewardsShare.uncheckedSub(initialDeposit.lastAccNxmPerRewardShare);
updatedDeposit.pendingRewards += newEarningsPerShare * initialDeposit.rewardsShares;
updatedDeposit.pendingRewards += newEarningsPerShare * initialDeposit.rewardsShares / ONE_NXM;
updatedDeposit.pendingRewards += initialDeposit.pendingRewards;
}

Expand Down Expand Up @@ -1415,7 +1426,7 @@ contract StakingPool is IStakingPool, ERC721 {

// update pending reward and reward shares
uint newRewardPerRewardsShare = _accNxmPerRewardsShare.uncheckedSub(feeDeposit.lastAccNxmPerRewardShare);
feeDeposit.pendingRewards += newRewardPerRewardsShare * feeDeposit.rewardsShares;
feeDeposit.pendingRewards += newRewardPerRewardsShare * feeDeposit.rewardsShares / ONE_NXM;
feeDeposit.lastAccNxmPerRewardShare = _accNxmPerRewardsShare;
// TODO: would using tranche.rewardsShares give a better precision?
feeDeposit.rewardsShares = feeDeposit.rewardsShares * newFee / oldFee;
Expand Down
19 changes: 9 additions & 10 deletions test/unit/StakingPool/depositTo.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');

const { getTranches, getNewRewardShares, estimateStakeShares, TRANCHE_DURATION } = require('./helpers');
const { setEtherBalance, increaseTime, setNextBlockTime, mineNextBlock } = require('../utils').evm;
const { getTranches, getNewRewardShares, estimateStakeShares, setTime, TRANCHE_DURATION } = require('./helpers');
const { setEtherBalance, increaseTime } = require('../utils').evm;
const { daysToSeconds } = require('../utils').helpers;

const { BigNumber } = ethers;
Expand Down Expand Up @@ -56,8 +56,7 @@ describe('depositTo', function () {

// Move to the beginning of the next tranche
const { firstActiveTrancheId: trancheId } = await getTranches();
await setNextBlockTime((trancheId + 1) * TRANCHE_DURATION);
await mineNextBlock();
await setTime((trancheId + 1) * TRANCHE_DURATION);
});

it('reverts if caller is not cover contract or manager when pool is private', async function () {
Expand Down Expand Up @@ -258,9 +257,9 @@ describe('depositTo', function () {
expect(secondDepositData.lastAccNxmPerRewardShare).to.equal(secondAccNxmPerRewardsShare);
expect(secondDepositData.pendingRewards).to.not.equal(0);
expect(secondDepositData.pendingRewards).to.equal(
depositData.rewardsShares.mul(
secondDepositData.lastAccNxmPerRewardShare.sub(depositData.lastAccNxmPerRewardShare),
),
depositData.rewardsShares
.mul(secondDepositData.lastAccNxmPerRewardShare.sub(depositData.lastAccNxmPerRewardShare))
.div(parseEther('1')),
);

// Last deposit
Expand All @@ -273,9 +272,9 @@ describe('depositTo', function () {
expect(lastDepositData.pendingRewards).to.not.equal(0);
expect(lastDepositData.pendingRewards).to.equal(
secondDepositData.pendingRewards.add(
secondDepositData.rewardsShares.mul(
lastDepositData.lastAccNxmPerRewardShare.sub(secondDepositData.lastAccNxmPerRewardShare),
),
secondDepositData.rewardsShares
.mul(lastDepositData.lastAccNxmPerRewardShare.sub(secondDepositData.lastAccNxmPerRewardShare))
.div(parseEther('1')),
),
);
});
Expand Down
12 changes: 6 additions & 6 deletions test/unit/StakingPool/extendDeposit.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ describe('extendDeposit', function () {
expect(newTrancheDeposit.stakeShares).to.equal(initialDeposit.stakeShares);
expect(newTrancheDeposit.rewardsShares).to.equal(initialDeposit.rewardsShares.add(newRewardsIncrease));
expect(newTrancheDeposit.pendingRewards).to.equal(
initialDeposit.rewardsShares.mul(
newTrancheDeposit.lastAccNxmPerRewardShare.sub(initialDeposit.lastAccNxmPerRewardShare),
),
initialDeposit.rewardsShares
.mul(newTrancheDeposit.lastAccNxmPerRewardShare.sub(initialDeposit.lastAccNxmPerRewardShare))
.div(parseEther('1')),
);
expect(newTrancheDeposit.lastAccNxmPerRewardShare).to.equal(accNxmPerRewardsShare);
});
Expand Down Expand Up @@ -239,9 +239,9 @@ describe('extendDeposit', function () {
);
expect(updatedDeposit.rewardsShares).to.equal(initialDeposit.rewardsShares.add(newRewardsIncrease));
expect(updatedDeposit.pendingRewards).to.equal(
initialDeposit.rewardsShares.mul(
updatedDeposit.lastAccNxmPerRewardShare.sub(initialDeposit.lastAccNxmPerRewardShare),
),
initialDeposit.rewardsShares
.mul(updatedDeposit.lastAccNxmPerRewardShare.sub(initialDeposit.lastAccNxmPerRewardShare))
.div(parseEther('1')),
);
expect(updatedDeposit.lastAccNxmPerRewardShare).to.equal(accNxmPerRewardsShare);
});
Expand Down
23 changes: 21 additions & 2 deletions test/unit/StakingPool/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,13 @@ async function getNewRewardShares(params) {
);
}

async function generateRewards(stakingPool, signer, period = daysToSeconds(10), gracePeriod = daysToSeconds(10)) {
const amount = parseEther('1');
async function generateRewards(
stakingPool,
signer,
period = daysToSeconds(10),
gracePeriod = daysToSeconds(10),
amount = parseEther('1'),
) {
const previousPremium = 0;
const allocationRequest = {
productId: 0,
Expand All @@ -190,6 +195,19 @@ async function generateRewards(stakingPool, signer, period = daysToSeconds(10),
await stakingPool.connect(signer).requestAllocation(amount, previousPremium, allocationRequest);
}

async function calculateStakeAndRewardsWithdrawAmounts(stakingPool, deposit, trancheId) {
const { accNxmPerRewardShareAtExpiry, stakeAmountAtExpiry, stakeShareSupplyAtExpiry } =
await stakingPool.expiredTranches(trancheId);

return {
rewards: deposit.rewardsShares
.mul(accNxmPerRewardShareAtExpiry.sub(deposit.lastAccNxmPerRewardShare))
.div(parseEther('1'))
.add(deposit.pendingRewards),
stake: stakeAmountAtExpiry.mul(deposit.stakeShares).div(stakeShareSupplyAtExpiry),
};
}

module.exports = {
setTime,
calculateBasePrice,
Expand All @@ -206,6 +224,7 @@ module.exports = {
getNewRewardShares,
estimateStakeShares,
generateRewards,
calculateStakeAndRewardsWithdrawAmounts,
TRANCHE_DURATION,
BUCKET_DURATION,
MAX_ACTIVE_TRANCHES,
Expand Down
1 change: 1 addition & 0 deletions test/unit/StakingPool/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ describe('StakingPool unit tests', function () {
require('./setPoolFee');
require('./setPoolPrivacy');
require('./setProducts');
require('./withdraw');
});
12 changes: 9 additions & 3 deletions test/unit/StakingPool/processExpirations.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,13 @@ describe('processExpirations', function () {
const accFromBeforeToBucketExpiration = nextBucketStartTime
.sub(lastAccNxmUpdateBefore)
.mul(rewardPerSecondBefore)
.mul(parseEther('1'))
.div(rewardsSharesSupply);

const accFromBucketExpirationToTrancheExpiration = trancheEndTime
.sub(nextBucketStartTime)
.mul(rewardPerSecondBefore.sub(nextBucketRewardPerSecondCut))
.mul(parseEther('1'))
.div(rewardsSharesSupply);

expect(expiredTranche.accNxmPerRewardShareAtExpiry).to.equal(
Expand All @@ -314,11 +316,13 @@ describe('processExpirations', function () {
const accFromTrancheExpirationToSecondBucketExpiration = secondNextBucketStartTime
.sub(trancheEndTime)
.mul(rewardPerSecondBefore.sub(nextBucketRewardPerSecondCut))
.mul(parseEther('1'))
.div(rewardsSharesSupply.sub(tranche.rewardsShares));

const accFromSecondBucketExpirationToCurrentTime = BigNumber.from(timestamp)
.sub(secondNextBucketStartTime)
.mul(rewardPerSecondBefore.sub(nextBucketRewardPerSecondCut).sub(secondBucketRewardPerSecondCut))
.mul(parseEther('1'))
.div(rewardsSharesSupply.sub(tranche.rewardsShares));

expect(accNxmPerRewardsShareAfter).to.equal(
Expand Down Expand Up @@ -365,7 +369,7 @@ describe('processExpirations', function () {
expect(expiredBucketRewards).to.equal(rewardPerSecondBefore);
expect(rewardPerSecondAfter).to.equal(rewardPerSecondBefore.sub(expiredBucketRewards));
expect(accNxmPerRewardsShareAfter).to.equal(
accNxmPerRewardsShareBefore.add(elapsed.mul(rewardPerSecondBefore).div(rewardsSharesSupply)),
accNxmPerRewardsShareBefore.add(elapsed.mul(rewardPerSecondBefore).mul(parseEther('1')).div(rewardsSharesSupply)),
);
expect(lastAccNxmUpdateAfter).to.equal(bucketStartTime);
});
Expand Down Expand Up @@ -463,10 +467,12 @@ describe('processExpirations', function () {
const elapsedAfterBucket = BigNumber.from(lastBlock.timestamp).sub(lastAccNxmUpdateBefore);

const accNxmPerRewardsAtBucketEnd = accNxmPerRewardsShareBefore.add(
elapsedInBucket.mul(rewardPerSecondBefore).div(rewardsSharesSupply),
elapsedInBucket.mul(rewardPerSecondBefore).mul(parseEther('1')).div(rewardsSharesSupply),
);
expect(accNxmPerRewardsShareAfter).to.equal(
accNxmPerRewardsAtBucketEnd.add(elapsedAfterBucket.mul(rewardPerSecondAfter).div(rewardsSharesSupply)),
accNxmPerRewardsAtBucketEnd.add(
elapsedAfterBucket.mul(rewardPerSecondAfter).mul(parseEther('1')).div(rewardsSharesSupply),
),
);
expect(lastAccNxmUpdateAfter).to.equal(lastBlock.timestamp);
});
Expand Down
2 changes: 1 addition & 1 deletion test/unit/StakingPool/setPoolFee.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ describe('setPoolFee', function () {

expect(managerDepositAfter.lastAccNxmPerRewardShare).to.equal(newLastAccNxmPerRewardShare);
expect(managerDepositAfter.pendingRewards).to.equal(
managerDepositAfter.lastAccNxmPerRewardShare.mul(managerDepositBefore.rewardsShares),
managerDepositAfter.lastAccNxmPerRewardShare.mul(managerDepositBefore.rewardsShares).div(parseEther('1')),
);
expect(managerDepositAfter.rewardsShares).to.equal(
managerDepositBefore.rewardsShares.mul(newPoolFee).div(initialPoolFee),
Expand Down
7 changes: 7 additions & 0 deletions test/unit/StakingPool/setup.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { ethers } = require('hardhat');
const { parseEther } = ethers.utils;
const { getAccounts } = require('../../utils/accounts');
const { setEtherBalance } = require('../../utils/evm');
const { Role } = require('../utils').constants;
const { zeroPadRight } = require('../utils').helpers;

Expand Down Expand Up @@ -78,6 +79,8 @@ async function setup() {
await master.enrollGovernance(governanceContract.address);
}

await tokenController.changeMasterAddress(master.address);

const config = {
REWARD_BONUS_PER_TRANCHE_RATIO: await stakingPool.REWARD_BONUS_PER_TRANCHE_RATIO(),
REWARD_BONUS_PER_TRANCHE_DENOMINATOR: await stakingPool.REWARD_BONUS_PER_TRANCHE_DENOMINATOR(),
Expand All @@ -96,11 +99,15 @@ async function setup() {
GLOBAL_MIN_PRICE_RATIO: await cover.GLOBAL_MIN_PRICE_RATIO(),
};

const coverSigner = await ethers.getImpersonatedSigner(cover.address);
await setEtherBalance(coverSigner.address, ethers.utils.parseEther('1'));

this.tokenController = tokenController;
this.master = master;
this.nxm = nxm;
this.stakingPool = stakingPool;
this.cover = cover;
this.coverSigner = coverSigner;
this.dai = dai;
this.accounts = accounts;
this.config = config;
Expand Down
Loading

0 comments on commit 2b4d436

Please sign in to comment.