import { expect } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { StakingApiWrapper } from '../utils/api_wrapper'; import { MemberBalancesByPoolId, MembersByPoolId, OperatorByPoolId, OperatorShareByPoolId, RewardVaultBalance, RewardVaultBalanceByPoolId, } from '../utils/types'; import { BaseActor } from './base_actor'; interface Reward { reward: BigNumber; poolId: string; } export class FinalizerActor extends BaseActor { private readonly _poolIds: string[]; // @TODO (hysz): this will be used later to liquidate the reward vault. // tslint:disable-next-line no-unused-variable private readonly _operatorByPoolId: OperatorByPoolId; private readonly _membersByPoolId: MembersByPoolId; constructor( owner: string, stakingApiWrapper: StakingApiWrapper, poolIds: string[], operatorByPoolId: OperatorByPoolId, membersByPoolId: MembersByPoolId, ) { super(owner, stakingApiWrapper); this._poolIds = _.cloneDeep(poolIds); this._operatorByPoolId = _.cloneDeep(operatorByPoolId); this._membersByPoolId = _.cloneDeep(membersByPoolId); } public async finalizeAsync(rewards: Reward[] = []): Promise { // 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); // compute expected changes const expectedRewardVaultBalanceByPoolId = await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( rewards, 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, ); // finalize await this._stakingApiWrapper.utils.skipToNextEpochAsync(); // assert reward vault changes 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, ); } private async _computeExpectedMemberBalancesByPoolIdAsync( membersByPoolId: MembersByPoolId, memberBalancesByPoolId: MemberBalancesByPoolId, rewardByPoolId: { [key: string]: BigNumber }, ): Promise { const expectedMemberBalancesByPoolId = _.cloneDeep(memberBalancesByPoolId); for (const poolId of Object.keys(membersByPoolId)) { if (rewardByPoolId[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); } } } return expectedMemberBalancesByPoolId; } private async _getMemberBalancesByPoolIdAsync(membersByPoolId: MembersByPoolId): Promise { 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( poolId, member, ); } } return memberBalancesByPoolId; } private async _computeExpectedRewardVaultBalanceAsyncByPoolIdAsync( rewards: Reward[], rewardVaultBalanceByPoolId: RewardVaultBalanceByPoolId, operatorShareByPoolId: OperatorShareByPoolId, ): Promise { const expectedRewardVaultBalanceByPoolId = _.cloneDeep(rewardVaultBalanceByPoolId); for (const reward of rewards) { const operatorShare = operatorShareByPoolId[reward.poolId]; expectedRewardVaultBalanceByPoolId[reward.poolId] = await this._computeExpectedRewardVaultBalanceAsync( reward.poolId, reward.reward, expectedRewardVaultBalanceByPoolId[reward.poolId], operatorShare, ); } return expectedRewardVaultBalanceByPoolId; } private async _computeExpectedRewardVaultBalanceAsync( poolId: string, reward: BigNumber, rewardVaultBalance: RewardVaultBalance, operatorShare: BigNumber, ): Promise { const totalStakeDelegatedToPool = (await this._stakingApiWrapper.stakingContract.getTotalStakeDelegatedToPool.callAsync( poolId, )).currentEpochBalance; const operatorPortion = totalStakeDelegatedToPool.eq(0) ? reward : reward.times(operatorShare).dividedToIntegerBy(100); const membersPortion = reward.minus(operatorPortion); return { poolBalance: rewardVaultBalance.poolBalance.plus(reward), operatorBalance: rewardVaultBalance.operatorBalance.plus(operatorPortion), membersBalance: rewardVaultBalance.membersBalance.plus(membersPortion), }; } private async _getOperatorShareByPoolIdAsync(poolIds: string[]): Promise { const operatorShareByPoolId: OperatorShareByPoolId = {}; for (const poolId of poolIds) { const pool = await this._stakingApiWrapper.rewardVaultContract.poolById.callAsync(poolId); const operatorShare = new BigNumber(pool[1]); operatorShareByPoolId[poolId] = operatorShare; } return operatorShareByPoolId; } private async _getRewardVaultBalanceByPoolIdAsync(poolIds: string[]): Promise { const rewardVaultBalanceByPoolId: RewardVaultBalanceByPoolId = {}; for (const poolId of poolIds) { rewardVaultBalanceByPoolId[poolId] = await this._getRewardVaultBalanceAsync(poolId); } return rewardVaultBalanceByPoolId; } private async _getRewardVaultBalanceAsync(poolId: string): Promise { const pool = await this._stakingApiWrapper.rewardVaultContract.poolById.callAsync(poolId); const operatorBalance = pool[2]; const membersBalance = pool[3]; return { poolBalance: operatorBalance.plus(membersBalance), operatorBalance, membersBalance, }; } }