@0x/contracts-staking
: Well, it almost worked.
This commit is contained in:
parent
38b94ec5f8
commit
7ef3c12722
@ -42,6 +42,7 @@ library LibStakingRichErrors {
|
||||
InvalidCobbDouglasAlpha,
|
||||
InvalidRewardDelegatedStakeWeight,
|
||||
InvalidMaximumMakersInPool,
|
||||
InvalidMinimumPoolStake,
|
||||
InvalidWethProxyAddress,
|
||||
InvalidEthVaultAddress,
|
||||
InvalidRewardVaultAddress,
|
||||
|
@ -187,11 +187,15 @@ contract MixinStakingPoolRewards is
|
||||
amountOfDelegatedStake
|
||||
);
|
||||
|
||||
// Normalize fraction components by dividing by the min token value
|
||||
// (10^18)
|
||||
// Normalize fraction components by dividing by the minimum denominator.
|
||||
uint256 minDenominator =
|
||||
mostRecentCumulativeRewards.denominator <= amountOfDelegatedStake ?
|
||||
mostRecentCumulativeRewards.denominator :
|
||||
amountOfDelegatedStake;
|
||||
minDenominator = minDenominator == 0 ? 1 : minDenominator;
|
||||
(uint256 numeratorNormalized, uint256 denominatorNormalized) = (
|
||||
numerator.safeDiv(MIN_TOKEN_VALUE),
|
||||
denominator.safeDiv(MIN_TOKEN_VALUE)
|
||||
numerator.safeDiv(minDenominator),
|
||||
denominator.safeDiv(minDenominator)
|
||||
);
|
||||
|
||||
// store cumulative rewards and set most recent
|
||||
|
@ -19,11 +19,173 @@
|
||||
pragma solidity ^0.5.9;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../src/Staking.sol";
|
||||
import "../src/interfaces/IStructs.sol";
|
||||
import "./TestStaking.sol";
|
||||
|
||||
|
||||
contract TestDelegatorRewards is
|
||||
Staking
|
||||
TestStaking
|
||||
{
|
||||
// TODO
|
||||
event Deposit(
|
||||
bytes32 poolId,
|
||||
address member,
|
||||
uint256 balance
|
||||
);
|
||||
|
||||
event FinalizePool(
|
||||
bytes32 poolId,
|
||||
uint256 reward,
|
||||
uint256 stake
|
||||
);
|
||||
|
||||
struct UnfinalizedMembersReward {
|
||||
uint256 reward;
|
||||
uint256 stake;
|
||||
}
|
||||
|
||||
constructor() public {
|
||||
init();
|
||||
}
|
||||
|
||||
mapping (uint256 => mapping (bytes32 => UnfinalizedMembersReward)) private
|
||||
unfinalizedMembersRewardByPoolByEpoch;
|
||||
|
||||
/// @dev Expose _finalizePool
|
||||
function internalFinalizePool(bytes32 poolId) external {
|
||||
_finalizePool(poolId);
|
||||
}
|
||||
|
||||
/// @dev Set unfinalized members reward for a pool in the current epoch.
|
||||
function setUnfinalizedMembersRewards(
|
||||
bytes32 poolId,
|
||||
uint256 membersReward,
|
||||
uint256 membersStake
|
||||
)
|
||||
external
|
||||
{
|
||||
unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId] =
|
||||
UnfinalizedMembersReward({
|
||||
reward: membersReward,
|
||||
stake: membersStake
|
||||
});
|
||||
}
|
||||
|
||||
/// @dev Advance the epoch.
|
||||
function advanceEpoch() external {
|
||||
currentEpoch += 1;
|
||||
}
|
||||
|
||||
/// @dev Create and delegate stake that is active in the current epoch.
|
||||
/// Only used to test purportedly unreachable states.
|
||||
/// Also withdraws pending rewards to the eth vault.
|
||||
function delegateStakeNow(
|
||||
address delegator,
|
||||
bytes32 poolId,
|
||||
uint256 stake
|
||||
)
|
||||
external
|
||||
{
|
||||
_transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator);
|
||||
_syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch);
|
||||
IStructs.StoredBalance storage _stake =
|
||||
delegatedStakeToPoolByOwner[delegator][poolId];
|
||||
_stake.currentEpochBalance += uint96(stake);
|
||||
_stake.nextEpochBalance += uint96(stake);
|
||||
_stake.currentEpoch = uint64(currentEpoch);
|
||||
}
|
||||
|
||||
/// @dev Create and delegate stake that will occur in the next epoch
|
||||
/// (normal behavior).
|
||||
/// Also withdraws pending rewards to the eth vault.
|
||||
function delegateStake(
|
||||
address delegator,
|
||||
bytes32 poolId,
|
||||
uint256 stake
|
||||
)
|
||||
external
|
||||
{
|
||||
_transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator);
|
||||
_syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch);
|
||||
IStructs.StoredBalance storage _stake =
|
||||
delegatedStakeToPoolByOwner[delegator][poolId];
|
||||
if (_stake.currentEpoch < currentEpoch) {
|
||||
_stake.currentEpochBalance = _stake.nextEpochBalance;
|
||||
}
|
||||
_stake.nextEpochBalance += uint96(stake);
|
||||
_stake.currentEpoch = uint64(currentEpoch);
|
||||
}
|
||||
|
||||
/// @dev Clear stake that will occur in the next epoch
|
||||
/// (normal behavior).
|
||||
/// Also withdraws pending rewards to the eth vault.
|
||||
function undelegateStake(
|
||||
address delegator,
|
||||
bytes32 poolId,
|
||||
uint256 stake
|
||||
)
|
||||
external
|
||||
{
|
||||
_transferDelegatorsAccumulatedRewardsToEthVault(poolId, delegator);
|
||||
_syncCumulativeRewardsNeededByDelegator(poolId, currentEpoch);
|
||||
IStructs.StoredBalance storage _stake =
|
||||
delegatedStakeToPoolByOwner[delegator][poolId];
|
||||
if (_stake.currentEpoch < currentEpoch) {
|
||||
_stake.currentEpochBalance = _stake.nextEpochBalance;
|
||||
}
|
||||
_stake.nextEpochBalance -= uint96(stake);
|
||||
_stake.currentEpoch = uint64(currentEpoch);
|
||||
}
|
||||
|
||||
/// @dev Expose `_recordDepositInRewardVaultFor`.
|
||||
function recordRewardForDelegators(
|
||||
bytes32 poolId,
|
||||
uint256 reward,
|
||||
uint256 amountOfDelegatedStake
|
||||
)
|
||||
external
|
||||
{
|
||||
_recordRewardForDelegators(poolId, reward, amountOfDelegatedStake);
|
||||
}
|
||||
|
||||
/// @dev Overridden to just emit events.
|
||||
function _transferMemberBalanceToEthVault(
|
||||
bytes32 poolId,
|
||||
address member,
|
||||
uint256 balance
|
||||
)
|
||||
internal
|
||||
{
|
||||
emit Deposit(
|
||||
poolId,
|
||||
member,
|
||||
balance
|
||||
);
|
||||
}
|
||||
|
||||
/// @dev Overridden to realize unfinalizedMembersRewardByPoolByEpoch in
|
||||
/// the current epoch and eit a event,
|
||||
function _finalizePool(bytes32 poolId)
|
||||
internal
|
||||
returns (IStructs.PoolRewards memory rewards)
|
||||
{
|
||||
UnfinalizedMembersReward memory reward =
|
||||
unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId];
|
||||
delete unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId];
|
||||
rewards.membersReward = reward.reward;
|
||||
rewards.membersStake = reward.stake;
|
||||
_recordRewardForDelegators(poolId, reward.reward, reward.stake);
|
||||
emit FinalizePool(poolId, reward.reward, reward.stake);
|
||||
}
|
||||
|
||||
/// @dev Overridden to use unfinalizedMembersRewardByPoolByEpoch.
|
||||
function _getUnfinalizedPoolRewards(bytes32 poolId)
|
||||
internal
|
||||
view
|
||||
returns (IStructs.PoolRewards memory rewards)
|
||||
{
|
||||
UnfinalizedMembersReward storage reward =
|
||||
unfinalizedMembersRewardByPoolByEpoch[currentEpoch][poolId];
|
||||
rewards.membersReward = reward.reward;
|
||||
rewards.membersStake = reward.stake;
|
||||
}
|
||||
}
|
||||
|
@ -21,11 +21,11 @@ pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../src/interfaces/IStructs.sol";
|
||||
import "../src/libs/LibCobbDouglas.sol";
|
||||
import "../src/Staking.sol";
|
||||
import "./TestStaking.sol";
|
||||
|
||||
|
||||
contract TestFinalizer is
|
||||
Staking
|
||||
TestStaking
|
||||
{
|
||||
event RecordRewardForDelegatorsCall(
|
||||
bytes32 poolId,
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { expect } from '@0x/contracts-test-utils';
|
||||
import { constants, expect } from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { StakingApiWrapper } from '../utils/api_wrapper';
|
||||
import {
|
||||
MemberBalancesByPoolId,
|
||||
MembersByPoolId,
|
||||
OperatorBalanceByPoolId,
|
||||
DelegatorBalancesByPoolId,
|
||||
DelegatorsByPoolId,
|
||||
OperatorByPoolId,
|
||||
OperatorShareByPoolId,
|
||||
RewardByPoolId,
|
||||
RewardVaultBalance,
|
||||
RewardVaultBalanceByPoolId,
|
||||
} from '../utils/types';
|
||||
|
||||
@ -19,59 +20,67 @@ interface Reward {
|
||||
poolId: string;
|
||||
}
|
||||
|
||||
const { PPM_100_PERCENT } = constants;
|
||||
|
||||
// tslint:disable: prefer-conditional-expression
|
||||
export class FinalizerActor extends BaseActor {
|
||||
private readonly _poolIds: string[];
|
||||
private readonly _operatorByPoolId: OperatorByPoolId;
|
||||
private readonly _membersByPoolId: MembersByPoolId;
|
||||
private readonly _delegatorsByPoolId: DelegatorsByPoolId;
|
||||
|
||||
constructor(
|
||||
owner: string,
|
||||
stakingApiWrapper: StakingApiWrapper,
|
||||
poolIds: string[],
|
||||
operatorByPoolId: OperatorByPoolId,
|
||||
membersByPoolId: MembersByPoolId,
|
||||
delegatorsByPoolId: DelegatorsByPoolId,
|
||||
) {
|
||||
super(owner, stakingApiWrapper);
|
||||
this._poolIds = _.cloneDeep(poolIds);
|
||||
this._operatorByPoolId = _.cloneDeep(operatorByPoolId);
|
||||
this._membersByPoolId = _.cloneDeep(membersByPoolId);
|
||||
this._delegatorsByPoolId = _.cloneDeep(delegatorsByPoolId);
|
||||
}
|
||||
|
||||
public async finalizeAsync(rewards: Reward[] = []): Promise<void> {
|
||||
// cache initial info and balances
|
||||
const operatorShareByPoolId = await this._getOperatorShareByPoolIdAsync(this._poolIds);
|
||||
const rewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds);
|
||||
const memberBalancesByPoolId = await this._getMemberBalancesByPoolIdAsync(this._membersByPoolId);
|
||||
const operatorBalanceByPoolId = await this._getOperatorBalanceByPoolIdAsync(this._operatorByPoolId);
|
||||
const operatorShareByPoolId =
|
||||
await this._getOperatorShareByPoolIdAsync(this._poolIds);
|
||||
const rewardVaultBalanceByPoolId =
|
||||
await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds);
|
||||
const delegatorBalancesByPoolId =
|
||||
await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId);
|
||||
const delegatorStakesByPoolId =
|
||||
await this._getDelegatorStakesByPoolIdAsync(this._delegatorsByPoolId);
|
||||
// compute expected changes
|
||||
const [
|
||||
expectedOperatorBalanceByPoolId,
|
||||
expectedRewardVaultBalanceByPoolId,
|
||||
] = await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync(
|
||||
const expectedRewardVaultBalanceByPoolId =
|
||||
await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync(
|
||||
rewards,
|
||||
operatorBalanceByPoolId,
|
||||
rewardVaultBalanceByPoolId,
|
||||
operatorShareByPoolId,
|
||||
);
|
||||
const memberRewardByPoolId = _.mapValues(_.keyBy(rewards, 'poolId'), r => {
|
||||
return r.reward.minus(r.reward.times(operatorShareByPoolId[r.poolId]).dividedToIntegerBy(100));
|
||||
});
|
||||
const expectedMemberBalancesByPoolId = await this._computeExpectedMemberBalancesByPoolIdAsync(
|
||||
this._membersByPoolId,
|
||||
memberBalancesByPoolId,
|
||||
memberRewardByPoolId,
|
||||
const totalRewardsByPoolId =
|
||||
_.zipObject(_.map(rewards, 'poolId'), _.map(rewards, 'reward'));
|
||||
const expectedDelegatorBalancesByPoolId =
|
||||
await this._computeExpectedDelegatorBalancesByPoolIdAsync(
|
||||
this._delegatorsByPoolId,
|
||||
delegatorBalancesByPoolId,
|
||||
delegatorStakesByPoolId,
|
||||
operatorShareByPoolId,
|
||||
totalRewardsByPoolId,
|
||||
);
|
||||
// finalize
|
||||
await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
|
||||
// assert reward vault changes
|
||||
const finalRewardVaultBalanceByPoolId = await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds);
|
||||
const finalRewardVaultBalanceByPoolId =
|
||||
await this._getRewardVaultBalanceByPoolIdAsync(this._poolIds);
|
||||
expect(finalRewardVaultBalanceByPoolId, 'final pool balances in reward vault').to.be.deep.equal(
|
||||
expectedRewardVaultBalanceByPoolId,
|
||||
);
|
||||
// assert member balances
|
||||
const finalMemberBalancesByPoolId = await this._getMemberBalancesByPoolIdAsync(this._membersByPoolId);
|
||||
expect(finalMemberBalancesByPoolId, 'final delegator balances in reward vault').to.be.deep.equal(
|
||||
expectedMemberBalancesByPoolId,
|
||||
// assert delegator balances
|
||||
const finalDelegatorBalancesByPoolId =
|
||||
await this._getDelegatorBalancesByPoolIdAsync(this._delegatorsByPoolId);
|
||||
expect(finalDelegatorBalancesByPoolId, 'final delegator balances in reward vault').to.be.deep.equal(
|
||||
expectedDelegatorBalancesByPoolId,
|
||||
);
|
||||
// assert operator balances
|
||||
const finalOperatorBalanceByPoolId = await this._getOperatorBalanceByPoolIdAsync(this._operatorByPoolId);
|
||||
@ -80,55 +89,100 @@ export class FinalizerActor extends BaseActor {
|
||||
);
|
||||
}
|
||||
|
||||
private async _computeExpectedMemberBalancesByPoolIdAsync(
|
||||
membersByPoolId: MembersByPoolId,
|
||||
memberBalancesByPoolId: MemberBalancesByPoolId,
|
||||
rewardByPoolId: { [key: string]: BigNumber },
|
||||
): Promise<MemberBalancesByPoolId> {
|
||||
const expectedMemberBalancesByPoolId = _.cloneDeep(memberBalancesByPoolId);
|
||||
for (const poolId of Object.keys(membersByPoolId)) {
|
||||
if (rewardByPoolId[poolId] === undefined) {
|
||||
private async _computeExpectedDelegatorBalancesByPoolIdAsync(
|
||||
delegatorsByPoolId: DelegatorsByPoolId,
|
||||
delegatorBalancesByPoolId: DelegatorBalancesByPoolId,
|
||||
delegatorStakesByPoolId: DelegatorBalancesByPoolId,
|
||||
operatorShareByPoolId: OperatorShareByPoolId,
|
||||
totalRewardByPoolId: RewardByPoolId,
|
||||
): Promise<DelegatorBalancesByPoolId> {
|
||||
const expectedDelegatorBalancesByPoolId = _.cloneDeep(delegatorBalancesByPoolId);
|
||||
for (const poolId of Object.keys(delegatorsByPoolId)) {
|
||||
if (totalRewardByPoolId[poolId] === undefined) {
|
||||
continue;
|
||||
}
|
||||
const totalStakeDelegatedToPool = (await this._stakingApiWrapper.stakingContract.getTotalStakeDelegatedToPool.callAsync(
|
||||
poolId,
|
||||
)).currentEpochBalance;
|
||||
for (const member of membersByPoolId[poolId]) {
|
||||
if (totalStakeDelegatedToPool.eq(0)) {
|
||||
expectedMemberBalancesByPoolId[poolId][member] = new BigNumber(0);
|
||||
} else {
|
||||
const stakeDelegatedToPoolByMember = (await this._stakingApiWrapper.stakingContract.getStakeDelegatedToPoolByOwner.callAsync(
|
||||
member,
|
||||
poolId,
|
||||
)).currentEpochBalance;
|
||||
const rewardThisEpoch = rewardByPoolId[poolId]
|
||||
.times(stakeDelegatedToPoolByMember)
|
||||
.dividedToIntegerBy(totalStakeDelegatedToPool);
|
||||
expectedMemberBalancesByPoolId[poolId][member] =
|
||||
memberBalancesByPoolId[poolId][member] === undefined
|
||||
? rewardThisEpoch
|
||||
: memberBalancesByPoolId[poolId][member].plus(rewardThisEpoch);
|
||||
|
||||
const operator = this._operatorByPoolId[poolId];
|
||||
const [, membersStakeInPool] =
|
||||
await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId);
|
||||
const operatorShare = operatorShareByPoolId[poolId].dividedBy(PPM_100_PERCENT);
|
||||
const totalReward = totalRewardByPoolId[poolId];
|
||||
const operatorReward = membersStakeInPool.eq(0) ?
|
||||
totalReward :
|
||||
totalReward.times(operatorShare).integerValue(BigNumber.ROUND_DOWN);
|
||||
const membersTotalReward = totalReward.minus(operatorReward);
|
||||
|
||||
for (const delegator of delegatorsByPoolId[poolId]) {
|
||||
let delegatorReward = new BigNumber(0);
|
||||
if (delegator === operator) {
|
||||
delegatorReward = operatorReward;
|
||||
} else if (membersStakeInPool.gt(0)) {
|
||||
const delegatorStake = delegatorStakesByPoolId[poolId][delegator];
|
||||
delegatorReward = delegatorStake
|
||||
.times(membersTotalReward)
|
||||
.dividedBy(membersStakeInPool)
|
||||
.integerValue(BigNumber.ROUND_DOWN);
|
||||
}
|
||||
const currentBalance = expectedDelegatorBalancesByPoolId[poolId][delegator] || 0;
|
||||
expectedDelegatorBalancesByPoolId[poolId][delegator] = delegatorReward.plus(currentBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
return expectedMemberBalancesByPoolId;
|
||||
return expectedDelegatorBalancesByPoolId;
|
||||
}
|
||||
|
||||
private async _getMemberBalancesByPoolIdAsync(membersByPoolId: MembersByPoolId): Promise<MemberBalancesByPoolId> {
|
||||
const memberBalancesByPoolId: MemberBalancesByPoolId = {};
|
||||
for (const poolId of Object.keys(membersByPoolId)) {
|
||||
const members = membersByPoolId[poolId];
|
||||
memberBalancesByPoolId[poolId] = {};
|
||||
for (const member of members) {
|
||||
memberBalancesByPoolId[poolId][
|
||||
member
|
||||
] = await this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator.callAsync(
|
||||
private async _getDelegatorBalancesByPoolIdAsync(
|
||||
delegatorsByPoolId: DelegatorsByPoolId,
|
||||
): Promise<DelegatorBalancesByPoolId> {
|
||||
const computeRewardBalanceOfDelegator =
|
||||
this._stakingApiWrapper.stakingContract.computeRewardBalanceOfDelegator;
|
||||
const rewardVaultBalanceOfOperator =
|
||||
this._stakingApiWrapper.rewardVaultContract.balanceOfOperator;
|
||||
const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {};
|
||||
|
||||
for (const poolId of Object.keys(delegatorsByPoolId)) {
|
||||
const operator = this._operatorByPoolId[poolId];
|
||||
const delegators = delegatorsByPoolId[poolId];
|
||||
delegatorBalancesByPoolId[poolId] = {};
|
||||
for (const delegator of delegators) {
|
||||
let balance =
|
||||
new BigNumber(delegatorBalancesByPoolId[poolId][delegator] || 0);
|
||||
if (delegator === operator) {
|
||||
balance = balance.plus(
|
||||
await rewardVaultBalanceOfOperator.callAsync(poolId),
|
||||
);
|
||||
} else {
|
||||
balance = balance.plus(
|
||||
await computeRewardBalanceOfDelegator.callAsync(
|
||||
poolId,
|
||||
member,
|
||||
delegator,
|
||||
),
|
||||
);
|
||||
}
|
||||
delegatorBalancesByPoolId[poolId][delegator] = balance;
|
||||
}
|
||||
return memberBalancesByPoolId;
|
||||
}
|
||||
return delegatorBalancesByPoolId;
|
||||
}
|
||||
|
||||
private async _getDelegatorStakesByPoolIdAsync(
|
||||
delegatorsByPoolId: DelegatorsByPoolId,
|
||||
): Promise<DelegatorBalancesByPoolId> {
|
||||
const getStakeDelegatedToPoolByOwner =
|
||||
this._stakingApiWrapper.stakingContract.getStakeDelegatedToPoolByOwner;
|
||||
const delegatorBalancesByPoolId: DelegatorBalancesByPoolId = {};
|
||||
for (const poolId of Object.keys(delegatorsByPoolId)) {
|
||||
const delegators = delegatorsByPoolId[poolId];
|
||||
delegatorBalancesByPoolId[poolId] = {};
|
||||
for (const delegator of delegators) {
|
||||
delegatorBalancesByPoolId[poolId][
|
||||
delegator
|
||||
] = (await getStakeDelegatedToPoolByOwner.callAsync(
|
||||
delegator,
|
||||
poolId,
|
||||
)).currentEpochBalance;
|
||||
}
|
||||
}
|
||||
return delegatorBalancesByPoolId;
|
||||
}
|
||||
|
||||
private async _computeExpectedRewardVaultBalanceAsyncByPoolIdAsync(
|
||||
@ -141,13 +195,10 @@ export class FinalizerActor extends BaseActor {
|
||||
const expectedRewardVaultBalanceByPoolId = _.cloneDeep(rewardVaultBalanceByPoolId);
|
||||
for (const reward of rewards) {
|
||||
const operatorShare = operatorShareByPoolId[reward.poolId];
|
||||
[
|
||||
expectedOperatorBalanceByPoolId[reward.poolId],
|
||||
expectedRewardVaultBalanceByPoolId[reward.poolId],
|
||||
] = await this._computeExpectedRewardVaultBalanceAsync(
|
||||
expectedRewardVaultBalanceByPoolId[reward.poolId] =
|
||||
await this._computeExpectedRewardVaultBalanceAsync(
|
||||
reward.poolId,
|
||||
reward.reward,
|
||||
expectedOperatorBalanceByPoolId[reward.poolId],
|
||||
expectedRewardVaultBalanceByPoolId[reward.poolId],
|
||||
operatorShare,
|
||||
);
|
||||
@ -161,13 +212,11 @@ export class FinalizerActor extends BaseActor {
|
||||
operatorBalance: BigNumber,
|
||||
rewardVaultBalance: BigNumber,
|
||||
operatorShare: BigNumber,
|
||||
): Promise<[BigNumber, BigNumber]> {
|
||||
const totalStakeDelegatedToPool = (await this._stakingApiWrapper.stakingContract.getTotalStakeDelegatedToPool.callAsync(
|
||||
poolId,
|
||||
)).currentEpochBalance;
|
||||
const operatorPortion = totalStakeDelegatedToPool.eq(0)
|
||||
): Promise<RewardVaultBalance> {
|
||||
const [, membersStakeInPool] = await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId);
|
||||
const operatorPortion = membersStakeInPool.eq(0)
|
||||
? reward
|
||||
: reward.times(operatorShare).dividedToIntegerBy(100);
|
||||
: reward.times(operatorShare).dividedToIntegerBy(PPM_100_PERCENT);
|
||||
const membersPortion = reward.minus(operatorPortion);
|
||||
return [operatorBalance.plus(operatorPortion), rewardVaultBalance.plus(membersPortion)];
|
||||
}
|
||||
@ -184,6 +233,22 @@ export class FinalizerActor extends BaseActor {
|
||||
return operatorBalanceByPoolId;
|
||||
}
|
||||
|
||||
private async _getOperatorAndDelegatorsStakeInPoolAsync(
|
||||
poolId: string,
|
||||
): Promise<[BigNumber, BigNumber]> {
|
||||
const stakingContract = this._stakingApiWrapper.stakingContract;
|
||||
const operator = await stakingContract.getPoolOperator.callAsync(poolId);
|
||||
const totalStakeInPool = (await stakingContract.getTotalStakeDelegatedToPool.callAsync(
|
||||
poolId,
|
||||
)).currentEpochBalance;
|
||||
const operatorStakeInPool = (await stakingContract.getStakeDelegatedToPoolByOwner.callAsync(
|
||||
operator,
|
||||
poolId,
|
||||
)).currentEpochBalance;
|
||||
const membersStakeInPool = totalStakeInPool.minus(operatorStakeInPool);
|
||||
return [operatorStakeInPool, membersStakeInPool];
|
||||
}
|
||||
|
||||
private async _getOperatorShareByPoolIdAsync(poolIds: string[]): Promise<OperatorShareByPoolId> {
|
||||
const operatorShareByPoolId: OperatorShareByPoolId = {};
|
||||
for (const poolId of poolIds) {
|
||||
|
@ -6,7 +6,7 @@ import { artifacts, IStakingEventsParamsSetEventArgs, MixinParamsContract } from
|
||||
import { constants as stakingConstants } from './utils/constants';
|
||||
import { StakingParams } from './utils/types';
|
||||
|
||||
blockchainTests('Configurable Parameters', env => {
|
||||
blockchainTests('Configurable Parameters unit tests', env => {
|
||||
let testContract: MixinParamsContract;
|
||||
let authorizedAddress: string;
|
||||
let notAuthorizedAddress: string;
|
||||
|
@ -10,7 +10,7 @@ import { PoolOperatorActor } from './actors/pool_operator_actor';
|
||||
import { StakerActor } from './actors/staker_actor';
|
||||
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
|
||||
import { toBaseUnitAmount } from './utils/number_utils';
|
||||
import { MembersByPoolId, OperatorByPoolId, StakeInfo, StakeStatus } from './utils/types';
|
||||
import { DelegatorsByPoolId, OperatorByPoolId, StakeInfo, StakeStatus } from './utils/types';
|
||||
|
||||
// tslint:disable:no-unnecessary-type-assertion
|
||||
// tslint:disable:max-file-line-count
|
||||
@ -67,14 +67,14 @@ blockchainTests.resets('Testing Rewards', env => {
|
||||
const operatorByPoolId: OperatorByPoolId = {};
|
||||
operatorByPoolId[poolId] = poolOperator.getOwner();
|
||||
// associate actors with pools for tracking in Finalizer
|
||||
const membersByPoolId: MembersByPoolId = {};
|
||||
membersByPoolId[poolId] = [actors[0], actors[1]];
|
||||
const stakersByPoolId: DelegatorsByPoolId = {};
|
||||
stakersByPoolId[poolId] = actors.slice(0, 3);
|
||||
// create Finalizer actor
|
||||
finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, membersByPoolId);
|
||||
finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, stakersByPoolId);
|
||||
// Skip to next epoch so operator stake is realized.
|
||||
await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
|
||||
});
|
||||
describe('Reward Simulation', () => {
|
||||
describe.skip('Reward Simulation', () => {
|
||||
interface EndBalances {
|
||||
// staker 1
|
||||
stakerRewardVaultBalance_1?: BigNumber;
|
||||
@ -399,12 +399,9 @@ blockchainTests.resets('Testing Rewards', env => {
|
||||
toBaseUnitAmount(0),
|
||||
toBaseUnitAmount(17),
|
||||
];
|
||||
const totalRewardsAfterAddingMoreStake = new BigNumber(
|
||||
_.sumBy(rewardsAfterAddingMoreStake, v => {
|
||||
return v.toNumber();
|
||||
}),
|
||||
);
|
||||
const totalRewardsAfterAddingMoreStake = BigNumber.sum(...rewardsAfterAddingMoreStake);
|
||||
const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
|
||||
const totalStake = BigNumber.sum(...stakeAmounts);
|
||||
// first staker delegates (epoch 0)
|
||||
await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
|
||||
// skip epoch, so first staker can start earning rewards
|
||||
@ -419,7 +416,16 @@ blockchainTests.resets('Testing Rewards', env => {
|
||||
}
|
||||
// sanity check final balances
|
||||
await validateEndBalances({
|
||||
stakerRewardVaultBalance_1: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
|
||||
stakerRewardVaultBalance_1: rewardBeforeAddingMoreStake.plus(
|
||||
totalRewardsAfterAddingMoreStake
|
||||
.times(stakeAmounts[0])
|
||||
.dividedBy(totalStake)
|
||||
.integerValue(BigNumber.ROUND_DOWN),
|
||||
),
|
||||
stakerRewardVaultBalance_2: totalRewardsAfterAddingMoreStake
|
||||
.times(stakeAmounts[1])
|
||||
.dividedBy(totalStake)
|
||||
.integerValue(BigNumber.ROUND_DOWN),
|
||||
poolRewardVaultBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
|
||||
membersRewardVaultBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
|
||||
});
|
||||
@ -464,11 +470,7 @@ blockchainTests.resets('Testing Rewards', env => {
|
||||
toBaseUnitAmount(0),
|
||||
toBaseUnitAmount(17),
|
||||
];
|
||||
const totalRewardsNotForDelegator = new BigNumber(
|
||||
_.sumBy(rewardsNotForDelegator, v => {
|
||||
return v.toNumber();
|
||||
}),
|
||||
);
|
||||
const totalRewardsNotForDelegator = BigNumber.sum(...rewardsNotForDelegator);
|
||||
const stakeAmount = toBaseUnitAmount(4);
|
||||
await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
|
||||
// skip epoch, so first staker can start earning rewards
|
||||
@ -492,7 +494,7 @@ blockchainTests.resets('Testing Rewards', env => {
|
||||
operatorEthVaultBalance: totalRewardsNotForDelegator,
|
||||
});
|
||||
});
|
||||
it('Should collect fees correctly when leaving and returning to a pool', async () => {
|
||||
it.only('Should collect fees correctly when leaving and returning to a pool', async () => {
|
||||
// first staker delegates (epoch 0)
|
||||
const rewardsForDelegator = [toBaseUnitAmount(10), toBaseUnitAmount(15)];
|
||||
const rewardNotForDelegator = toBaseUnitAmount(7);
|
||||
|
@ -1,8 +1,24 @@
|
||||
import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils';
|
||||
import {
|
||||
blockchainTests,
|
||||
constants,
|
||||
expect,
|
||||
filterLogsToArguments,
|
||||
hexRandom,
|
||||
Numberish,
|
||||
} from '@0x/contracts-test-utils';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { LogEntry } from 'ethereum-types';
|
||||
|
||||
import { artifacts, TestDelegatorRewardsContract } from '../../src';
|
||||
import {
|
||||
artifacts,
|
||||
TestDelegatorRewardsContract,
|
||||
TestDelegatorRewardsDepositEventArgs,
|
||||
TestDelegatorRewardsEvents,
|
||||
} from '../../src';
|
||||
|
||||
blockchainTests('delegator rewards', env => {
|
||||
import { assertRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
|
||||
|
||||
blockchainTests.resets('delegator unit rewards', env => {
|
||||
let testContract: TestDelegatorRewardsContract;
|
||||
|
||||
before(async () => {
|
||||
@ -14,9 +30,543 @@ blockchainTests('delegator rewards', env => {
|
||||
);
|
||||
});
|
||||
|
||||
interface RewardPoolMembersOpts {
|
||||
poolId: string;
|
||||
reward: Numberish;
|
||||
stake: Numberish;
|
||||
}
|
||||
|
||||
async function rewardPoolMembersAsync(
|
||||
opts?: Partial<RewardPoolMembersOpts>,
|
||||
): Promise<RewardPoolMembersOpts> {
|
||||
const _opts = {
|
||||
poolId: hexRandom(),
|
||||
reward: getRandomInteger(1, toBaseUnitAmount(100)),
|
||||
stake: getRandomInteger(1, toBaseUnitAmount(10)),
|
||||
...opts,
|
||||
};
|
||||
await testContract.recordRewardForDelegators.awaitTransactionSuccessAsync(
|
||||
_opts.poolId,
|
||||
new BigNumber(_opts.reward),
|
||||
new BigNumber(_opts.stake),
|
||||
);
|
||||
return _opts;
|
||||
}
|
||||
|
||||
interface SetUnfinalizedMembersRewardsOpts {
|
||||
poolId: string;
|
||||
reward: Numberish;
|
||||
stake: Numberish;
|
||||
}
|
||||
|
||||
async function setUnfinalizedMembersRewardsAsync(
|
||||
opts?: Partial<SetUnfinalizedMembersRewardsOpts>,
|
||||
): Promise<SetUnfinalizedMembersRewardsOpts> {
|
||||
const _opts = {
|
||||
poolId: hexRandom(),
|
||||
reward: getRandomInteger(1, toBaseUnitAmount(100)),
|
||||
stake: getRandomInteger(1, toBaseUnitAmount(10)),
|
||||
...opts,
|
||||
};
|
||||
await testContract.setUnfinalizedMembersRewards.awaitTransactionSuccessAsync(
|
||||
_opts.poolId,
|
||||
new BigNumber(_opts.reward),
|
||||
new BigNumber(_opts.stake),
|
||||
);
|
||||
return _opts;
|
||||
}
|
||||
|
||||
type ResultWithDeposit<T extends {}> = T & {
|
||||
deposit: BigNumber;
|
||||
};
|
||||
|
||||
interface DelegateStakeOpts {
|
||||
delegator: string;
|
||||
stake: Numberish;
|
||||
}
|
||||
|
||||
async function delegateStakeNowAsync(
|
||||
poolId: string,
|
||||
opts?: Partial<DelegateStakeOpts>,
|
||||
): Promise<ResultWithDeposit<DelegateStakeOpts>> {
|
||||
return delegateStakeAsync(poolId, opts, true);
|
||||
}
|
||||
|
||||
async function delegateStakeAsync(
|
||||
poolId: string,
|
||||
opts?: Partial<DelegateStakeOpts>,
|
||||
now?: boolean,
|
||||
): Promise<ResultWithDeposit<DelegateStakeOpts>> {
|
||||
const _opts = {
|
||||
delegator: randomAddress(),
|
||||
stake: getRandomInteger(1, toBaseUnitAmount(10)),
|
||||
...opts,
|
||||
};
|
||||
const fn = now ? testContract.delegateStakeNow : testContract.delegateStake;
|
||||
const receipt = await fn.awaitTransactionSuccessAsync(
|
||||
_opts.delegator,
|
||||
poolId,
|
||||
new BigNumber(_opts.stake),
|
||||
);
|
||||
return {
|
||||
..._opts,
|
||||
deposit: getDepositFromLogs(receipt.logs, poolId, _opts.delegator),
|
||||
};
|
||||
}
|
||||
|
||||
async function undelegateStakeAsync(
|
||||
poolId: string,
|
||||
delegator: string,
|
||||
stake?: Numberish,
|
||||
): Promise<ResultWithDeposit<{ stake: BigNumber }>> {
|
||||
const _stake = new BigNumber(
|
||||
stake || (await
|
||||
testContract
|
||||
.getStakeDelegatedToPoolByOwner
|
||||
.callAsync(delegator, poolId)
|
||||
).currentEpochBalance,
|
||||
);
|
||||
const receipt = await testContract.undelegateStake.awaitTransactionSuccessAsync(
|
||||
delegator,
|
||||
poolId,
|
||||
_stake,
|
||||
);
|
||||
return {
|
||||
stake: _stake,
|
||||
deposit: getDepositFromLogs(receipt.logs, poolId, delegator),
|
||||
};
|
||||
}
|
||||
|
||||
function getDepositFromLogs(logs: LogEntry[], poolId: string, delegator?: string): BigNumber {
|
||||
const events =
|
||||
filterLogsToArguments<TestDelegatorRewardsDepositEventArgs>(
|
||||
logs,
|
||||
TestDelegatorRewardsEvents.Deposit,
|
||||
);
|
||||
if (events.length > 0) {
|
||||
expect(events.length).to.eq(1);
|
||||
expect(events[0].poolId).to.eq(poolId);
|
||||
if (delegator !== undefined) {
|
||||
expect(events[0].member).to.eq(delegator);
|
||||
}
|
||||
return events[0].balance;
|
||||
}
|
||||
return constants.ZERO_AMOUNT;
|
||||
}
|
||||
|
||||
async function advanceEpochAsync(): Promise<number> {
|
||||
await testContract.advanceEpoch.awaitTransactionSuccessAsync();
|
||||
const epoch = await testContract.getCurrentEpoch.callAsync();
|
||||
return epoch.toNumber();
|
||||
}
|
||||
|
||||
async function getDelegatorRewardAsync(poolId: string, delegator: string): Promise<BigNumber> {
|
||||
return testContract.computeRewardBalanceOfDelegator.callAsync(
|
||||
poolId,
|
||||
delegator,
|
||||
);
|
||||
}
|
||||
|
||||
async function touchStakeAsync(poolId: string, delegator: string): Promise<ResultWithDeposit<{}>> {
|
||||
return undelegateStakeAsync(poolId, delegator, 0);
|
||||
}
|
||||
|
||||
async function finalizePoolAsync(poolId: string): Promise<ResultWithDeposit<{}>> {
|
||||
const receipt = await testContract.internalFinalizePool.awaitTransactionSuccessAsync(poolId);
|
||||
return {
|
||||
deposit: getDepositFromLogs(receipt.logs, poolId),
|
||||
};
|
||||
}
|
||||
|
||||
function randomAddress(): string {
|
||||
return hexRandom(constants.ADDRESS_LENGTH);
|
||||
}
|
||||
|
||||
function computeDelegatorRewards(
|
||||
totalRewards: Numberish,
|
||||
delegatorStake: Numberish,
|
||||
totalDelegatorStake: Numberish,
|
||||
): BigNumber {
|
||||
return new BigNumber(totalRewards)
|
||||
.times(delegatorStake)
|
||||
.dividedBy(new BigNumber(totalDelegatorStake))
|
||||
.integerValue(BigNumber.ROUND_DOWN);
|
||||
}
|
||||
|
||||
describe('computeRewardBalanceOfDelegator()', () => {
|
||||
it('does stuff', () => {
|
||||
// TODO
|
||||
it('nothing in epoch 0 for delegator with no stake', async () => {
|
||||
const { poolId } = await rewardPoolMembersAsync();
|
||||
const delegator = randomAddress();
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(delegatorReward).to.bignumber.eq(0);
|
||||
});
|
||||
|
||||
it('nothing in epoch 1 for delegator with no stake', async () => {
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
const { poolId } = await rewardPoolMembersAsync();
|
||||
const delegator = randomAddress();
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(delegatorReward).to.bignumber.eq(0);
|
||||
});
|
||||
|
||||
it('nothing in epoch 0 for delegator staked in epoch 0', async () => {
|
||||
const { poolId } = await rewardPoolMembersAsync();
|
||||
// Assign active stake to pool in epoch 0, which is usuaslly not
|
||||
// possible due to delegating delays.
|
||||
const { delegator } = await delegateStakeNowAsync(poolId);
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(delegatorReward).to.bignumber.eq(0);
|
||||
});
|
||||
|
||||
it('nothing in epoch 1 for delegator delegating in epoch 1', async () => {
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
const { poolId } = await rewardPoolMembersAsync();
|
||||
const { delegator } = await delegateStakeAsync(poolId);
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(delegatorReward).to.bignumber.eq(0);
|
||||
});
|
||||
|
||||
it('nothing in epoch 1 for delegator delegating in epoch 0', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
// rewards paid for stake in epoch 0.
|
||||
await rewardPoolMembersAsync({ poolId, stake });
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(delegatorReward).to.bignumber.eq(0);
|
||||
});
|
||||
|
||||
it('all rewards from epoch 2 for delegator delegating in epoch 0', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
// rewards paid for stake in epoch 1.
|
||||
const { reward } = await rewardPoolMembersAsync({ poolId, stake });
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(delegatorReward).to.bignumber.eq(reward);
|
||||
});
|
||||
|
||||
it('all rewards from epoch 2 and 3 for delegator delegating in epoch 0', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
const { reward: reward1 } = await rewardPoolMembersAsync({ poolId, stake });
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
const { reward: reward2 } = await rewardPoolMembersAsync({ poolId, stake });
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(delegatorReward).to.bignumber.eq(BigNumber.sum(reward1, reward2));
|
||||
});
|
||||
|
||||
it('partial rewards from epoch 2 and 3 for delegator partially delegating in epoch 0', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake: delegatorStake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
// rewards paid for stake in epoch 1.
|
||||
const { reward, stake: rewardStake } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake: new BigNumber(delegatorStake).times(2) },
|
||||
);
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
const expectedDelegatorRewards = computeDelegatorRewards(reward, delegatorStake, rewardStake);
|
||||
assertRoughlyEquals(delegatorReward, expectedDelegatorRewards);
|
||||
});
|
||||
|
||||
it.only('has correct reward immediately after unstaking', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
// rewards paid for stake in epoch 1.
|
||||
const { reward } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake },
|
||||
);
|
||||
await undelegateStakeAsync(poolId, delegator);
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(delegatorReward).to.bignumber.eq(reward);
|
||||
});
|
||||
|
||||
it('computes correct rewards for 2 staggered delegators', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake A now active)
|
||||
const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId);
|
||||
const totalStake = BigNumber.sum(stakeA, stakeB);
|
||||
await advanceEpochAsync(); // epoch 2 (stake B now active)
|
||||
// rewards paid for stake in epoch 1 (delegator A only)
|
||||
const { reward: reward1 } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake: stakeA },
|
||||
);
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
// rewards paid for stake in epoch 2 (delegator A and B)
|
||||
const { reward: reward2 } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake: totalStake },
|
||||
);
|
||||
const delegatorRewardA = await getDelegatorRewardAsync(poolId, delegatorA);
|
||||
const expectedDelegatorRewardA = BigNumber.sum(
|
||||
computeDelegatorRewards(reward1, stakeA, stakeA),
|
||||
computeDelegatorRewards(reward2, stakeA, totalStake),
|
||||
);
|
||||
assertRoughlyEquals(delegatorRewardA, expectedDelegatorRewardA);
|
||||
const delegatorRewardB = await getDelegatorRewardAsync(poolId, delegatorB);
|
||||
const expectedDelegatorRewardB = BigNumber.sum(
|
||||
computeDelegatorRewards(reward2, stakeB, totalStake),
|
||||
);
|
||||
assertRoughlyEquals(delegatorRewardB, expectedDelegatorRewardB);
|
||||
});
|
||||
|
||||
it('computes correct rewards for 2 staggered delegators with a 2 epoch gap between payments', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake A now active)
|
||||
const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId);
|
||||
const totalStake = BigNumber.sum(stakeA, stakeB);
|
||||
await advanceEpochAsync(); // epoch 2 (stake B now active)
|
||||
// rewards paid for stake in epoch 1 (delegator A only)
|
||||
const { reward: reward1 } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake: stakeA },
|
||||
);
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
await advanceEpochAsync(); // epoch 4
|
||||
// rewards paid for stake in epoch 3 (delegator A and B)
|
||||
const { reward: reward2 } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake: totalStake },
|
||||
);
|
||||
const delegatorRewardA = await getDelegatorRewardAsync(poolId, delegatorA);
|
||||
const expectedDelegatorRewardA = BigNumber.sum(
|
||||
computeDelegatorRewards(reward1, stakeA, stakeA),
|
||||
computeDelegatorRewards(reward2, stakeA, totalStake),
|
||||
);
|
||||
assertRoughlyEquals(delegatorRewardA, expectedDelegatorRewardA);
|
||||
const delegatorRewardB = await getDelegatorRewardAsync(poolId, delegatorB);
|
||||
const expectedDelegatorRewardB = BigNumber.sum(
|
||||
computeDelegatorRewards(reward2, stakeB, totalStake),
|
||||
);
|
||||
assertRoughlyEquals(delegatorRewardB, expectedDelegatorRewardB);
|
||||
});
|
||||
|
||||
it('correct rewards for rewards with different stakes', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake: delegatorStake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
// rewards paid for stake in epoch 1.
|
||||
const { reward: reward1, stake: rewardStake1 } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake: new BigNumber(delegatorStake).times(2) },
|
||||
);
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
// rewards paid for stake in epoch 2
|
||||
const { reward: reward2, stake: rewardStake2 } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake: new BigNumber(delegatorStake).times(3) },
|
||||
);
|
||||
const delegatorReward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
const expectedDelegatorReward = BigNumber.sum(
|
||||
computeDelegatorRewards(reward1, delegatorStake, rewardStake1),
|
||||
computeDelegatorRewards(reward2, delegatorStake, rewardStake2),
|
||||
);
|
||||
assertRoughlyEquals(delegatorReward, expectedDelegatorReward);
|
||||
});
|
||||
|
||||
describe('with unfinalized rewards', async () => {
|
||||
it('nothing with only unfinalized rewards from epoch 1 for deleator with nothing delegated', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId, { stake: 0 });
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
await setUnfinalizedMembersRewardsAsync({ poolId, stake });
|
||||
const reward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(reward).to.bignumber.eq(0);
|
||||
});
|
||||
|
||||
it('nothing with only unfinalized rewards from epoch 1 for deleator delegating in epoch 0', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
await setUnfinalizedMembersRewardsAsync({ poolId, stake });
|
||||
const reward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(reward).to.bignumber.eq(0);
|
||||
});
|
||||
|
||||
it('returns only unfinalized rewards from epoch 2 for delegator delegating in epoch 1', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake });
|
||||
const reward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(reward).to.bignumber.eq(unfinalizedReward);
|
||||
});
|
||||
|
||||
it('returns only unfinalized rewards from epoch 3 for delegator delegating in epoch 1', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake });
|
||||
const reward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
expect(reward).to.bignumber.eq(unfinalizedReward);
|
||||
});
|
||||
|
||||
it('returns unfinalized rewards from epoch 3 + rewards from epoch 2 for delegator delegating in epoch 1', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake });
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake });
|
||||
const reward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
const expectedReward = BigNumber.sum(prevReward, unfinalizedReward);
|
||||
expect(reward).to.bignumber.eq(expectedReward);
|
||||
});
|
||||
|
||||
it('returns unfinalized rewards from epoch 4 + rewards from epoch 2 for delegator delegating in epoch 1', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake });
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
await advanceEpochAsync(); // epoch 4
|
||||
const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake });
|
||||
const reward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
const expectedReward = BigNumber.sum(prevReward, unfinalizedReward);
|
||||
expect(reward).to.bignumber.eq(expectedReward);
|
||||
});
|
||||
|
||||
it('returns correct rewards if unfinalized stake is different from previous rewards', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
const { reward: prevReward, stake: prevStake } = await rewardPoolMembersAsync(
|
||||
{ poolId, stake: new BigNumber(stake).times(2) },
|
||||
);
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
await advanceEpochAsync(); // epoch 4
|
||||
const { reward: unfinalizedReward, stake: unfinalizedStake } =
|
||||
await setUnfinalizedMembersRewardsAsync(
|
||||
{ poolId, stake: new BigNumber(stake).times(5) },
|
||||
);
|
||||
const reward = await getDelegatorRewardAsync(poolId, delegator);
|
||||
const expectedReward = BigNumber.sum(
|
||||
computeDelegatorRewards(prevReward, stake, prevStake),
|
||||
computeDelegatorRewards(unfinalizedReward, stake, unfinalizedStake),
|
||||
);
|
||||
assertRoughlyEquals(reward, expectedReward);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reward transfers', async () => {
|
||||
it('transfers all rewards to eth vault when touching stake', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator, stake } = await delegateStakeAsync(poolId);
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
// rewards paid for stake in epoch 1
|
||||
const { reward } = await rewardPoolMembersAsync({ poolId, stake });
|
||||
const { deposit } = await touchStakeAsync(poolId, delegator);
|
||||
expect(deposit).to.bignumber.eq(reward);
|
||||
});
|
||||
|
||||
it('does not collect extra rewards from delegating more stake in the reward epoch', async () => {
|
||||
const poolId = hexRandom();
|
||||
const stakeResults = [];
|
||||
// stake
|
||||
stakeResults.push(await delegateStakeAsync(poolId));
|
||||
const { delegator, stake } = stakeResults[0];
|
||||
const totalStake = new BigNumber(stake).times(2);
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
// add more stake.
|
||||
stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake }));
|
||||
await advanceEpochAsync(); // epoch 1 (2 * stake now active)
|
||||
// reward for epoch 1, using 2 * stake so delegator should
|
||||
// only be entitled to a fraction of the rewards.
|
||||
const { reward } = await rewardPoolMembersAsync({ poolId, stake: totalStake });
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
// touch the stake one last time
|
||||
stakeResults.push(await touchStakeAsync(poolId, delegator));
|
||||
// Should only see deposits for epoch 2.
|
||||
const expectedDeposit = computeDelegatorRewards(reward, stake, totalStake);
|
||||
const allDeposits = stakeResults.map(r => r.deposit);
|
||||
assertRoughlyEquals(BigNumber.sum(...allDeposits), expectedDeposit);
|
||||
});
|
||||
|
||||
it('only collects rewards from staked epochs', async () => {
|
||||
const poolId = hexRandom();
|
||||
const stakeResults = [];
|
||||
// stake
|
||||
stakeResults.push(await delegateStakeAsync(poolId));
|
||||
const { delegator, stake } = stakeResults[0];
|
||||
await advanceEpochAsync(); // epoch 1 (stake now active)
|
||||
// unstake before and after reward payout, to be extra sneaky.
|
||||
const unstake1 = new BigNumber(stake).dividedToIntegerBy(2);
|
||||
stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake1));
|
||||
// reward for epoch 0
|
||||
await rewardPoolMembersAsync({ poolId, stake });
|
||||
const unstake2 = new BigNumber(stake).minus(unstake1);
|
||||
stakeResults.push(await undelegateStakeAsync(poolId, delegator, unstake2));
|
||||
await advanceEpochAsync(); // epoch 2 (no active stake)
|
||||
// reward for epoch 1
|
||||
const { reward } = await rewardPoolMembersAsync({ poolId, stake });
|
||||
// re-stake
|
||||
stakeResults.push(await delegateStakeAsync(poolId, { delegator, stake }));
|
||||
await advanceEpochAsync(); // epoch 3 (stake now active)
|
||||
// reward for epoch 2
|
||||
await rewardPoolMembersAsync({ poolId, stake });
|
||||
// touch the stake one last time
|
||||
stakeResults.push(await touchStakeAsync(poolId, delegator));
|
||||
// Should only see deposits for epoch 2.
|
||||
const allDeposits = stakeResults.map(r => r.deposit);
|
||||
assertRoughlyEquals(BigNumber.sum(...allDeposits), reward);
|
||||
});
|
||||
|
||||
it('delegator B collects correct rewards after delegator A finalizes', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId);
|
||||
const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId);
|
||||
const totalStake = BigNumber.sum(stakeA, stakeB);
|
||||
await advanceEpochAsync(); // epoch 1 (stakes now active)
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
// rewards paid for stake in epoch 1
|
||||
const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake: totalStake });
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
// unfinalized rewards for stake in epoch 2
|
||||
const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake: totalStake });
|
||||
const totalRewards = BigNumber.sum(prevReward, unfinalizedReward);
|
||||
// delegator A will finalize and collect rewards by touching stake.
|
||||
const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA);
|
||||
assertRoughlyEquals(depositA, computeDelegatorRewards(totalRewards, stakeA, totalStake));
|
||||
// delegator B will collect rewards by touching stake
|
||||
const { deposit: depositB } = await touchStakeAsync(poolId, delegatorB);
|
||||
assertRoughlyEquals(depositB, computeDelegatorRewards(totalRewards, stakeB, totalStake));
|
||||
});
|
||||
|
||||
it('delegator A and B collect correct rewards after external finalization', async () => {
|
||||
const poolId = hexRandom();
|
||||
const { delegator: delegatorA, stake: stakeA } = await delegateStakeAsync(poolId);
|
||||
const { delegator: delegatorB, stake: stakeB } = await delegateStakeAsync(poolId);
|
||||
const totalStake = BigNumber.sum(stakeA, stakeB);
|
||||
await advanceEpochAsync(); // epoch 1 (stakes now active)
|
||||
await advanceEpochAsync(); // epoch 2
|
||||
// rewards paid for stake in epoch 1
|
||||
const { reward: prevReward } = await rewardPoolMembersAsync({ poolId, stake: totalStake });
|
||||
await advanceEpochAsync(); // epoch 3
|
||||
// unfinalized rewards for stake in epoch 2
|
||||
const { reward: unfinalizedReward } = await setUnfinalizedMembersRewardsAsync({ poolId, stake: totalStake });
|
||||
const totalRewards = BigNumber.sum(prevReward, unfinalizedReward);
|
||||
// finalize
|
||||
await finalizePoolAsync(poolId);
|
||||
// delegator A will collect rewards by touching stake.
|
||||
const { deposit: depositA } = await touchStakeAsync(poolId, delegatorA);
|
||||
assertRoughlyEquals(depositA, computeDelegatorRewards(totalRewards, stakeA, totalStake));
|
||||
// delegator B will collect rewards by touching stake
|
||||
const { deposit: depositB } = await touchStakeAsync(poolId, delegatorB);
|
||||
assertRoughlyEquals(depositB, computeDelegatorRewards(totalRewards, stakeB, totalStake));
|
||||
});
|
||||
});
|
||||
});
|
||||
// tslint:disable: max-file-line-count
|
||||
|
@ -23,16 +23,14 @@ import {
|
||||
} from '../../src';
|
||||
import { getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
|
||||
|
||||
blockchainTests.resets('finalizer tests', env => {
|
||||
blockchainTests.resets('finalizer unit tests', env => {
|
||||
const { ZERO_AMOUNT } = constants;
|
||||
const INITIAL_EPOCH = 0;
|
||||
const INITIAL_BALANCE = toBaseUnitAmount(32);
|
||||
let senderAddress: string;
|
||||
let rewardReceiverAddress: string;
|
||||
let testContract: TestFinalizerContract;
|
||||
|
||||
before(async () => {
|
||||
[senderAddress] = await env.getAccountAddressesAsync();
|
||||
rewardReceiverAddress = hexRandom(constants.ADDRESS_LENGTH);
|
||||
testContract = await TestFinalizerContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestFinalizer,
|
||||
@ -48,7 +46,7 @@ blockchainTests.resets('finalizer tests', env => {
|
||||
async function sendEtherAsync(to: string, amount: Numberish): Promise<void> {
|
||||
await env.web3Wrapper.awaitTransactionSuccessAsync(
|
||||
await env.web3Wrapper.sendTransactionAsync({
|
||||
from: senderAddress,
|
||||
from: (await env.getAccountAddressesAsync())[0],
|
||||
to,
|
||||
value: new BigNumber(amount),
|
||||
}),
|
||||
|
@ -121,7 +121,7 @@ export interface RewardByPoolId {
|
||||
[key: string]: BigNumber;
|
||||
}
|
||||
|
||||
export interface MemberBalancesByPoolId {
|
||||
export interface DelegatorBalancesByPoolId {
|
||||
[key: string]: BalanceByOwner;
|
||||
}
|
||||
|
||||
@ -129,6 +129,6 @@ export interface OperatorByPoolId {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface MembersByPoolId {
|
||||
export interface DelegatorsByPoolId {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
@ -57,6 +57,9 @@ library LibFractions {
|
||||
pure
|
||||
returns (uint256 result)
|
||||
{
|
||||
if (s == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (n2 == 0) {
|
||||
return result = s
|
||||
.safeMul(n1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user