@0x/contracts-staking: Gag! All tests passing?

This commit is contained in:
Lawrence Forman
2019-09-19 05:25:46 -04:00
committed by Lawrence Forman
parent d5bbbe802b
commit 86106713dd
6 changed files with 108 additions and 65 deletions

View File

@@ -161,25 +161,20 @@ contract MixinExchangeFees is
view
returns (uint256 totalBalance)
{
totalBalance = address(this).balance +
IEtherToken(WETH_ADDRESS).balanceOf(address(this));
totalBalance = address(this).balance.safeAdd(
IEtherToken(WETH_ADDRESS).balanceOf(address(this))
);
}
/// @dev Returns the amount of fees attributed to the input pool this epoch.
/// @dev Get information on an active staking pool in this epoch.
/// @param poolId Pool Id to query.
/// @return feesCollectedByPool Amount of fees collected by the pool this
/// epoch.
function getProtocolFeesThisEpochByPool(bytes32 poolId)
/// @return pool ActivePool struct.
function getActiveStakingPoolThisEpoch(bytes32 poolId)
external
view
returns (uint256 feesCollected)
returns (IStructs.ActivePool memory pool)
{
// Look up the pool for this epoch. The epoch index is `currentEpoch % 2`
// because we only need to remember state in the current epoch and the
// epoch prior.
IStructs.ActivePool memory pool =
_getActivePoolFromEpoch(currentEpoch, poolId);
feesCollected = pool.feesCollected;
pool = _getActivePoolFromEpoch(currentEpoch, poolId);
}
/// @dev Computes the members and weighted stake for a pool at the current

View File

@@ -25,6 +25,15 @@ import "../src/Staking.sol";
contract TestStaking is
Staking
{
/// @dev Overridden to avoid hard-coded WETH.
function getTotalBalance()
external
view
returns (uint256 totalBalance)
{
totalBalance = address(this).balance;
}
// Stub out `_unwrapWETH` to prevent the calls to `finalizeFees` from failing in tests
// that do not relate to protocol fee payments in WETH.
function _unwrapWETH()

View File

@@ -4,6 +4,7 @@ import * as _ from 'lodash';
import { StakingApiWrapper } from '../utils/api_wrapper';
import {
BalanceByOwner,
DelegatorBalancesByPoolId,
DelegatorsByPoolId,
OperatorBalanceByPoolId,
@@ -15,11 +16,6 @@ import {
import { BaseActor } from './base_actor';
interface Reward {
reward: BigNumber;
poolId: string;
}
const { PPM_100_PERCENT } = constants;
// tslint:disable: prefer-conditional-expression
@@ -41,30 +37,31 @@ export class FinalizerActor extends BaseActor {
this._delegatorsByPoolId = _.cloneDeep(delegatorsByPoolId);
}
public async finalizeAsync(rewards: Reward[] = []): Promise<void> {
public async finalizeAsync(): Promise<void> {
// cache initial info and balances
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);
const operatorBalanceByPoolId = await this._getOperatorBalanceByPoolIdAsync(this._operatorByPoolId);
const rewardByPoolId = await this._getRewardByPoolIdAsync(this._poolIds);
// compute expected changes
const [
expectedOperatorBalanceByPoolId,
expectedRewardVaultBalanceByPoolId,
] = await this._computeExpectedRewardVaultBalanceAsyncByPoolIdAsync(
rewards,
rewardByPoolId,
operatorBalanceByPoolId,
rewardVaultBalanceByPoolId,
delegatorStakesByPoolId,
operatorShareByPoolId,
);
const totalRewardsByPoolId = _.zipObject(_.map(rewards, 'poolId'), _.map(rewards, 'reward'));
const expectedDelegatorBalancesByPoolId = await this._computeExpectedDelegatorBalancesByPoolIdAsync(
this._delegatorsByPoolId,
delegatorBalancesByPoolId,
delegatorStakesByPoolId,
operatorShareByPoolId,
totalRewardsByPoolId,
rewardByPoolId,
);
// finalize
await this._stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
@@ -90,33 +87,26 @@ export class FinalizerActor extends BaseActor {
delegatorBalancesByPoolId: DelegatorBalancesByPoolId,
delegatorStakesByPoolId: DelegatorBalancesByPoolId,
operatorShareByPoolId: OperatorShareByPoolId,
totalRewardByPoolId: RewardByPoolId,
rewardByPoolId: RewardByPoolId,
): Promise<DelegatorBalancesByPoolId> {
const expectedDelegatorBalancesByPoolId = _.cloneDeep(delegatorBalancesByPoolId);
for (const poolId of Object.keys(delegatorsByPoolId)) {
if (totalRewardByPoolId[poolId] === undefined) {
continue;
}
const operator = this._operatorByPoolId[poolId];
const [, membersStakeInPool] = await this._getOperatorAndDelegatorsStakeInPoolAsync(poolId);
const operatorShare = operatorShareByPoolId[poolId].dividedBy(PPM_100_PERCENT);
const totalReward = totalRewardByPoolId[poolId];
const totalStakeInPool = BigNumber.sum(...Object.values(delegatorStakesByPoolId[poolId]));
const operatorStakeInPool = delegatorStakesByPoolId[poolId][operator];
const membersStakeInPool = totalStakeInPool.minus(operatorStakeInPool);
const operatorShare = operatorShareByPoolId[poolId];
const totalReward = rewardByPoolId[poolId];
const operatorReward = membersStakeInPool.eq(0)
? totalReward
: totalReward.times(operatorShare).integerValue(BigNumber.ROUND_DOWN);
: totalReward.times(operatorShare).dividedToIntegerBy(PPM_100_PERCENT);
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)) {
if (delegator !== operator && membersStakeInPool.gt(0)) {
const delegatorStake = delegatorStakesByPoolId[poolId][delegator];
delegatorReward = delegatorStake
.times(membersTotalReward)
.dividedBy(membersStakeInPool)
.integerValue(BigNumber.ROUND_DOWN);
delegatorReward = delegatorStake.times(membersTotalReward).dividedToIntegerBy(membersStakeInPool);
}
const currentBalance = expectedDelegatorBalancesByPoolId[poolId][delegator] || 0;
expectedDelegatorBalancesByPoolId[poolId][delegator] = delegatorReward.plus(currentBalance);
@@ -168,23 +158,25 @@ export class FinalizerActor extends BaseActor {
}
private async _computeExpectedRewardVaultBalanceAsyncByPoolIdAsync(
rewards: Reward[],
rewardByPoolId: RewardByPoolId,
operatorBalanceByPoolId: OperatorBalanceByPoolId,
rewardVaultBalanceByPoolId: RewardVaultBalanceByPoolId,
delegatorStakesByPoolId: DelegatorBalancesByPoolId,
operatorShareByPoolId: OperatorShareByPoolId,
): Promise<[RewardVaultBalanceByPoolId, OperatorBalanceByPoolId]> {
const expectedOperatorBalanceByPoolId = _.cloneDeep(operatorBalanceByPoolId);
const expectedRewardVaultBalanceByPoolId = _.cloneDeep(rewardVaultBalanceByPoolId);
for (const reward of rewards) {
const operatorShare = operatorShareByPoolId[reward.poolId];
for (const poolId of Object.keys(rewardByPoolId)) {
const operatorShare = operatorShareByPoolId[poolId];
[
expectedOperatorBalanceByPoolId[reward.poolId],
expectedRewardVaultBalanceByPoolId[reward.poolId],
expectedOperatorBalanceByPoolId[poolId],
expectedRewardVaultBalanceByPoolId[poolId],
] = await this._computeExpectedRewardVaultBalanceAsync(
reward.poolId,
reward.reward,
expectedOperatorBalanceByPoolId[reward.poolId],
expectedRewardVaultBalanceByPoolId[reward.poolId],
poolId,
rewardByPoolId[poolId],
expectedOperatorBalanceByPoolId[poolId],
expectedRewardVaultBalanceByPoolId[poolId],
delegatorStakesByPoolId[poolId],
operatorShare,
);
}
@@ -196,12 +188,13 @@ export class FinalizerActor extends BaseActor {
reward: BigNumber,
operatorBalance: BigNumber,
rewardVaultBalance: BigNumber,
stakeBalances: BalanceByOwner,
operatorShare: BigNumber,
): Promise<[BigNumber, BigNumber]> {
const totalStakeDelegatedToPool = (await this._stakingApiWrapper.stakingContract.getTotalStakeDelegatedToPool.callAsync(
poolId,
)).currentEpochBalance;
const operatorPortion = totalStakeDelegatedToPool.eq(0)
const totalStakeDelegatedToPool = BigNumber.sum(...Object.values(stakeBalances));
const stakeDelegatedToPoolByOperator = stakeBalances[this._operatorByPoolId[poolId]];
const membersStakeDelegatedToPool = totalStakeDelegatedToPool.minus(stakeDelegatedToPoolByOperator);
const operatorPortion = membersStakeDelegatedToPool.eq(0)
? reward
: reward.times(operatorShare).dividedToIntegerBy(PPM_100_PERCENT);
const membersPortion = reward.minus(operatorPortion);
@@ -220,17 +213,6 @@ export class FinalizerActor extends BaseActor {
return operatorBalanceByPoolId;
}
private async _getOperatorAndDelegatorsStakeInPoolAsync(poolId: string): Promise<[BigNumber, BigNumber]> {
const stakingContract = this._stakingApiWrapper.stakingContract;
const operator = (await stakingContract.getStakingPool.callAsync(poolId)).operator;
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) {
@@ -250,4 +232,30 @@ export class FinalizerActor extends BaseActor {
}
return rewardVaultBalanceByPoolId;
}
private async _getRewardByPoolIdAsync(poolIds: string[]): Promise<RewardByPoolId> {
const activePools = await Promise.all(
poolIds.map(async poolId =>
this._stakingApiWrapper.stakingContract.getActiveStakingPoolThisEpoch.callAsync(poolId),
),
);
const totalRewards = await this._stakingApiWrapper.stakingContract.getTotalBalance.callAsync();
const totalFeesCollected = BigNumber.sum(...activePools.map(p => p.feesCollected));
const totalWeightedStake = BigNumber.sum(...activePools.map(p => p.weightedStake));
if (totalRewards.eq(0) || totalFeesCollected.eq(0) || totalWeightedStake.eq(0)) {
return _.zipObject(poolIds, _.times(poolIds.length, () => new BigNumber(0)));
}
const rewards = await Promise.all(
activePools.map(async pool =>
this._stakingApiWrapper.utils.cobbDouglas(
totalRewards,
pool.feesCollected,
totalFeesCollected,
pool.weightedStake,
totalWeightedStake,
),
),
);
return _.zipObject(poolIds, rewards);
}
}

View File

@@ -163,7 +163,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
});
async function getProtocolFeesAsync(poolId: string): Promise<BigNumber> {
return testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
return (await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId)).feesCollected;
}
describe('ETH fees', () => {

View File

@@ -14,7 +14,7 @@ import { DelegatorsByPoolId, OperatorByPoolId, StakeInfo, StakeStatus } from './
// tslint:disable:no-unnecessary-type-assertion
// tslint:disable:max-file-line-count
blockchainTests.resets.skip('Testing Rewards', env => {
blockchainTests.resets('Testing Rewards', env => {
// tokens & addresses
let accounts: string[];
let owner: string;
@@ -166,7 +166,7 @@ blockchainTests.resets.skip('Testing Rewards', env => {
{ from: exchangeAddress, value: fee },
);
}
await finalizer.finalizeAsync([{ reward: fee, poolId }]);
await finalizer.finalizeAsync();
};
const ZERO = new BigNumber(0);
it('Reward balance should be zero if not delegated', async () => {

View File

@@ -16,6 +16,7 @@ import {
StakingEvents,
StakingPoolRewardVaultContract,
StakingProxyContract,
TestCobbDouglasContract,
ZrxVaultContract,
} from '../../src';
@@ -33,6 +34,7 @@ export class StakingApiWrapper {
public ethVaultContract: EthVaultContract;
public rewardVaultContract: StakingPoolRewardVaultContract;
public zrxTokenContract: DummyERC20TokenContract;
public cobbDouglasContract: TestCobbDouglasContract;
public utils = {
// Epoch Utils
fastForwardToNextEpochAsync: async (): Promise<void> => {
@@ -141,6 +143,25 @@ export class StakingApiWrapper {
await this.stakingContract.getParams.callAsync(),
) as any) as StakingParams;
},
cobbDouglas: async (
totalRewards: BigNumber,
ownerFees: BigNumber,
totalFees: BigNumber,
ownerStake: BigNumber,
totalStake: BigNumber,
): Promise<BigNumber> => {
const { cobbDouglasAlphaNumerator, cobbDouglasAlphaDenominator } = await this.utils.getParamsAsync();
return this.cobbDouglasContract.cobbDouglas.callAsync(
totalRewards,
ownerFees,
totalFees,
ownerStake,
totalStake,
new BigNumber(cobbDouglasAlphaNumerator),
new BigNumber(cobbDouglasAlphaDenominator),
);
},
};
private readonly _web3Wrapper: Web3Wrapper;
@@ -154,12 +175,14 @@ export class StakingApiWrapper {
ethVaultContract: EthVaultContract,
rewardVaultContract: StakingPoolRewardVaultContract,
zrxTokenContract: DummyERC20TokenContract,
cobbDouglasContract: TestCobbDouglasContract,
) {
this._web3Wrapper = env.web3Wrapper;
this.zrxVaultContract = zrxVaultContract;
this.ethVaultContract = ethVaultContract;
this.rewardVaultContract = rewardVaultContract;
this.zrxTokenContract = zrxTokenContract;
this.cobbDouglasContract = cobbDouglasContract;
this.stakingContractAddress = stakingContract.address;
this.stakingProxyContract = stakingProxyContract;
@@ -246,6 +269,13 @@ export async function deployAndConfigureContractsAsync(
rewardVaultContract.address,
zrxVaultContract.address,
);
// deploy cobb douglas contract
const cobbDouglasContract = await TestCobbDouglasContract.deployFrom0xArtifactAsync(
artifacts.TestCobbDouglas,
env.provider,
txDefaults,
artifacts,
);
// configure erc20 proxy to accept calls from zrx vault
await erc20ProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync(zrxVaultContract.address);
@@ -264,5 +294,6 @@ export async function deployAndConfigureContractsAsync(
ethVaultContract,
rewardVaultContract,
zrxTokenContract,
cobbDouglasContract,
);
}