Merge pull request #2316 from 0xProject/tests/3.0/StakingMixinCumulativeRewardsUnitTEsts

Unit tests for MixinCumulativeRewards
This commit is contained in:
Greg Hysz 2019-11-05 16:00:52 -08:00 committed by GitHub
commit 44793a9cf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 392 additions and 1 deletions

View File

@ -29,6 +29,10 @@
{
"note": "Unit tests for MixinScheduler",
"pr": 2314
},
{
"note": "Unit tests for MixinCumulativeRewards",
"pr": 2316
}
]
},

View File

@ -0,0 +1,125 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "./TestStaking.sol";
contract TestMixinCumulativeRewards is
TestStaking
{
constructor(
address wethAddress,
address zrxVaultAddress
)
public
TestStaking(
wethAddress,
zrxVaultAddress
)
{
_addAuthorizedAddress(msg.sender);
init();
_removeAuthorizedAddressAtIndex(msg.sender, 0);
}
/// @dev Exposes `_isCumulativeRewardSet`
function isCumulativeRewardSet(IStructs.Fraction memory cumulativeReward)
public
pure
returns (bool)
{
return _isCumulativeRewardSet(cumulativeReward);
}
/// @dev Exposes `_addCumulativeReward`
function addCumulativeReward(
bytes32 poolId,
uint256 reward,
uint256 stake
)
public
{
_addCumulativeReward(poolId, reward, stake);
}
/// @dev Exposes `_updateCumulativeReward`
function updateCumulativeReward(bytes32 poolId)
public
{
_updateCumulativeReward(poolId);
}
/// @dev Exposes _computeMemberRewardOverInterval
function computeMemberRewardOverInterval(
bytes32 poolId,
uint256 memberStakeOverInterval,
uint256 beginEpoch,
uint256 endEpoch
)
public
view
returns (uint256 reward)
{
return _computeMemberRewardOverInterval(poolId, memberStakeOverInterval, beginEpoch, endEpoch);
}
/// @dev Increments current epoch by 1
function incrementEpoch()
public
{
currentEpoch += 1;
}
/// @dev Stores an arbitrary cumulative reward for a given epoch.
/// Also sets the `_cumulativeRewardsByPoolLastStored` to the input epoch.
function storeCumulativeReward(
bytes32 poolId,
IStructs.Fraction memory cumulativeReward,
uint256 epoch
)
public
{
_cumulativeRewardsByPool[poolId][epoch] = cumulativeReward;
_cumulativeRewardsByPoolLastStored[poolId] = epoch;
}
/// @dev Returns the most recent cumulative reward for a given pool.
function getMostRecentCumulativeReward(bytes32 poolId)
public
returns (IStructs.Fraction memory)
{
uint256 mostRecentEpoch = _cumulativeRewardsByPoolLastStored[poolId];
return _cumulativeRewardsByPool[poolId][mostRecentEpoch];
}
/// @dev Returns the raw cumulative reward for a given pool in an epoch.
/// This is considered "raw" because the internal implementation
/// (_getCumulativeRewardAtEpochRaw) will query other state variables
/// to determine the most accurate cumulative reward for a given epoch.
function getCumulativeRewardAtEpochRaw(bytes32 poolId, uint256 epoch)
public
returns (IStructs.Fraction memory)
{
return _cumulativeRewardsByPool[poolId][epoch];
}
}

View File

@ -37,7 +37,7 @@
},
"config": {
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinScheduler|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestMixinStakingPoolRewards|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|TestStorageLayoutAndConstants|ZrxVault).json"
"abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinCumulativeRewards|TestMixinParams|TestMixinScheduler|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestMixinStakingPoolRewards|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|TestStorageLayoutAndConstants|ZrxVault).json"
},
"repository": {
"type": "git",

View File

@ -43,6 +43,7 @@ import * as TestFinalizer from '../generated-artifacts/TestFinalizer.json';
import * as TestInitTarget from '../generated-artifacts/TestInitTarget.json';
import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json';
import * as TestLibSafeDowncast from '../generated-artifacts/TestLibSafeDowncast.json';
import * as TestMixinCumulativeRewards from '../generated-artifacts/TestMixinCumulativeRewards.json';
import * as TestMixinParams from '../generated-artifacts/TestMixinParams.json';
import * as TestMixinScheduler from '../generated-artifacts/TestMixinScheduler.json';
import * as TestMixinStake from '../generated-artifacts/TestMixinStake.json';
@ -98,6 +99,7 @@ export const artifacts = {
TestInitTarget: TestInitTarget as ContractArtifact,
TestLibFixedMath: TestLibFixedMath as ContractArtifact,
TestLibSafeDowncast: TestLibSafeDowncast as ContractArtifact,
TestMixinCumulativeRewards: TestMixinCumulativeRewards as ContractArtifact,
TestMixinParams: TestMixinParams as ContractArtifact,
TestMixinScheduler: TestMixinScheduler as ContractArtifact,
TestMixinStake: TestMixinStake as ContractArtifact,

View File

@ -41,6 +41,7 @@ export * from '../generated-wrappers/test_finalizer';
export * from '../generated-wrappers/test_init_target';
export * from '../generated-wrappers/test_lib_fixed_math';
export * from '../generated-wrappers/test_lib_safe_downcast';
export * from '../generated-wrappers/test_mixin_cumulative_rewards';
export * from '../generated-wrappers/test_mixin_params';
export * from '../generated-wrappers/test_mixin_scheduler';
export * from '../generated-wrappers/test_mixin_stake';

View File

@ -0,0 +1,258 @@
import { blockchainTests, expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { artifacts, TestMixinCumulativeRewardsContract } from '../../src';
import { constants as stakingConstants } from '../utils/constants';
import { toBaseUnitAmount } from '../utils/number_utils';
blockchainTests.resets('MixinCumulativeRewards unit tests', env => {
const ZERO = new BigNumber(0);
const testRewards = [
{
numerator: new BigNumber(1),
denominator: new BigNumber(2),
},
{
numerator: new BigNumber(3),
denominator: new BigNumber(4),
},
];
const sumOfTestRewardsNormalized = {
numerator: new BigNumber(10),
denominator: new BigNumber(8),
};
let testPoolId: string;
let testContract: TestMixinCumulativeRewardsContract;
before(async () => {
// Deploy contracts
testContract = await TestMixinCumulativeRewardsContract.deployFrom0xArtifactAsync(
artifacts.TestMixinCumulativeRewards,
env.provider,
env.txDefaults,
artifacts,
stakingConstants.NIL_ADDRESS,
stakingConstants.NIL_ADDRESS,
);
// Create a test pool
const operatorShare = new BigNumber(1);
const addOperatorAsMaker = true;
const txReceipt = await testContract.createStakingPool.awaitTransactionSuccessAsync(
operatorShare,
addOperatorAsMaker,
);
const createStakingPoolLog = txReceipt.logs[0];
testPoolId = (createStakingPoolLog as any).args.poolId;
});
describe('_isCumulativeRewardSet', () => {
it('Should return true iff denominator is non-zero', async () => {
const isSet = await testContract.isCumulativeRewardSet.callAsync({
numerator: ZERO,
denominator: new BigNumber(1),
});
expect(isSet).to.be.true();
});
it('Should return false iff denominator is zero', async () => {
const isSet = await testContract.isCumulativeRewardSet.callAsync({
numerator: new BigNumber(1),
denominator: ZERO,
});
expect(isSet).to.be.false();
});
});
describe('_addCumulativeReward', () => {
it('Should set value to `reward/stake` if this is the first cumulative reward', async () => {
await testContract.addCumulativeReward.awaitTransactionSuccessAsync(
testPoolId,
testRewards[0].numerator,
testRewards[0].denominator,
);
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward.callAsync(testPoolId);
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
});
it('Should do nothing if a cumulative reward has already been recorded in the current epoch (`lastStoredEpoch == currentEpoch_`)', async () => {
await testContract.addCumulativeReward.awaitTransactionSuccessAsync(
testPoolId,
testRewards[0].numerator,
testRewards[0].denominator,
);
// this call should not overwrite existing value (testRewards[0])
await testContract.addCumulativeReward.awaitTransactionSuccessAsync(
testPoolId,
testRewards[1].numerator,
testRewards[1].denominator,
);
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward.callAsync(testPoolId);
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
});
it('Should set value to normalized sum of `reward/stake` plus most recent cumulative reward, given one exists', async () => {
await testContract.addCumulativeReward.awaitTransactionSuccessAsync(
testPoolId,
testRewards[0].numerator,
testRewards[0].denominator,
);
await testContract.incrementEpoch.awaitTransactionSuccessAsync();
await testContract.addCumulativeReward.awaitTransactionSuccessAsync(
testPoolId,
testRewards[1].numerator,
testRewards[1].denominator,
);
const mostRecentCumulativeReward = await testContract.getMostRecentCumulativeReward.callAsync(testPoolId);
expect(mostRecentCumulativeReward).to.deep.equal(sumOfTestRewardsNormalized);
});
});
describe('_updateCumulativeReward', () => {
it('Should set current cumulative reward to most recent cumulative reward', async () => {
await testContract.addCumulativeReward.awaitTransactionSuccessAsync(
testPoolId,
testRewards[0].numerator,
testRewards[0].denominator,
);
await testContract.incrementEpoch.awaitTransactionSuccessAsync();
await testContract.updateCumulativeReward.awaitTransactionSuccessAsync(testPoolId);
const epoch = new BigNumber(2);
const mostRecentCumulativeReward = await testContract.getCumulativeRewardAtEpochRaw.callAsync(
testPoolId,
epoch,
);
expect(mostRecentCumulativeReward).to.deep.equal(testRewards[0]);
});
});
describe('_computeMemberRewardOverInterval', () => {
const runTest = async (
amountToStake: BigNumber,
epochOfFirstReward: BigNumber,
epochOfSecondReward: BigNumber,
epochOfIntervalStart: BigNumber,
epochOfIntervalEnd: BigNumber,
): Promise<void> => {
// Simulate earning reward
await testContract.storeCumulativeReward.awaitTransactionSuccessAsync(
testPoolId,
testRewards[0],
epochOfFirstReward,
);
await testContract.storeCumulativeReward.awaitTransactionSuccessAsync(
testPoolId,
sumOfTestRewardsNormalized,
epochOfSecondReward,
);
const reward = await testContract.computeMemberRewardOverInterval.callAsync(
testPoolId,
amountToStake,
epochOfIntervalStart,
epochOfIntervalEnd,
);
// Compute expected reward
const lhs = sumOfTestRewardsNormalized.numerator.dividedBy(sumOfTestRewardsNormalized.denominator);
const rhs = testRewards[0].numerator.dividedBy(testRewards[0].denominator);
const expectedReward = lhs.minus(rhs).multipliedBy(amountToStake);
// Assert correctness
expect(reward).to.bignumber.equal(expectedReward);
};
it('Should successfully compute reward over a valid interval when staking non-zero ZRX', async () => {
const amountToStake = toBaseUnitAmount(1);
const epochOfFirstReward = new BigNumber(1);
const epochOfSecondReward = new BigNumber(2);
const epochOfIntervalStart = new BigNumber(1);
const epochOfIntervalEnd = new BigNumber(2);
await runTest(
amountToStake,
epochOfFirstReward,
epochOfSecondReward,
epochOfIntervalStart,
epochOfIntervalEnd,
);
});
it('Should successfully compute reward if no entry for current epoch, but there is an entry for epoch n-1', async () => {
// End epoch = n-1 forces the code to query the previous epoch's cumulative reward
const amountToStake = toBaseUnitAmount(1);
const epochOfFirstReward = new BigNumber(1);
const epochOfSecondReward = new BigNumber(2);
const epochOfIntervalStart = new BigNumber(1);
const epochOfIntervalEnd = new BigNumber(3);
await runTest(
amountToStake,
epochOfFirstReward,
epochOfSecondReward,
epochOfIntervalStart,
epochOfIntervalEnd,
);
});
it('Should successfully compute reward if no entry for current epoch, but there is an entry for epoch n-2', async () => {
// End epoch = n-2 forces the code to query the most recent cumulative reward
const amountToStake = toBaseUnitAmount(1);
const epochOfFirstReward = new BigNumber(1);
const epochOfSecondReward = new BigNumber(2);
const epochOfIntervalStart = new BigNumber(1);
const epochOfIntervalEnd = new BigNumber(4);
await runTest(
amountToStake,
epochOfFirstReward,
epochOfSecondReward,
epochOfIntervalStart,
epochOfIntervalEnd,
);
});
it('Should successfully compute reward are no cumulative reward entries', async () => {
// No entries forces the default cumulatie reward to be used in computations
const stake = toBaseUnitAmount(1);
const beginEpoch = new BigNumber(1);
const endEpoch = new BigNumber(2);
const reward = await testContract.computeMemberRewardOverInterval.callAsync(
testPoolId,
stake,
beginEpoch,
endEpoch,
);
expect(reward).to.bignumber.equal(ZERO);
});
it('Should return zero if no stake was delegated', async () => {
const stake = toBaseUnitAmount(0);
const beginEpoch = new BigNumber(1);
const endEpoch = new BigNumber(2);
const reward = await testContract.computeMemberRewardOverInterval.callAsync(
testPoolId,
stake,
beginEpoch,
endEpoch,
);
expect(reward).to.bignumber.equal(ZERO);
});
it('Should return zero if the start/end of the interval are the same epoch', async () => {
const stake = toBaseUnitAmount(1);
const beginEpoch = new BigNumber(1);
const endEpoch = new BigNumber(1);
const reward = await testContract.computeMemberRewardOverInterval.callAsync(
testPoolId,
stake,
beginEpoch,
endEpoch,
);
expect(reward).to.bignumber.equal(ZERO);
});
it('Should revert if start is greater than the end of the interval', async () => {
const stake = toBaseUnitAmount(1);
const beginEpoch = new BigNumber(2);
const endEpoch = new BigNumber(1);
const tx = testContract.computeMemberRewardOverInterval.callAsync(testPoolId, stake, beginEpoch, endEpoch);
return expect(tx).to.revertWith('CR_INTERVAL_INVALID');
});
});
});

View File

@ -41,6 +41,7 @@
"generated-artifacts/TestInitTarget.json",
"generated-artifacts/TestLibFixedMath.json",
"generated-artifacts/TestLibSafeDowncast.json",
"generated-artifacts/TestMixinCumulativeRewards.json",
"generated-artifacts/TestMixinParams.json",
"generated-artifacts/TestMixinScheduler.json",
"generated-artifacts/TestMixinStake.json",