@0x/contracts-exchange: Fixing tests and writing new ones.

This commit is contained in:
Lawrence Forman 2019-09-13 19:48:54 -04:00 committed by Lawrence Forman
parent d548ddac0d
commit a1aad2e55e
11 changed files with 469 additions and 200 deletions

View File

@ -19,6 +19,7 @@
pragma solidity ^0.5.9; pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol";
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
import "../libs/LibStakingRichErrors.sol"; import "../libs/LibStakingRichErrors.sol";
@ -71,7 +72,8 @@ contract MixinExchangeFees is
{ {
_assertValidProtocolFee(protocolFeePaid); _assertValidProtocolFee(protocolFeePaid);
// Transfer the protocol fee to this address if it should be paid in WETH. // Transfer the protocol fee to this address if it should be paid in
// WETH.
if (msg.value == 0) { if (msg.value == 0) {
wethAssetProxy.transferFrom( wethAssetProxy.transferFrom(
WETH_ASSET_DATA, WETH_ASSET_DATA,
@ -89,7 +91,8 @@ contract MixinExchangeFees is
return; return;
} }
uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance; uint256 poolStake =
getTotalStakeDelegatedToPool(poolId).currentEpochBalance;
// Ignore pools with dust stake. // Ignore pools with dust stake.
if (poolStake < minimumPoolStake) { if (poolStake < minimumPoolStake) {
return; return;
@ -103,35 +106,24 @@ contract MixinExchangeFees is
// If the pool was previously inactive in this epoch, initialize it. // If the pool was previously inactive in this epoch, initialize it.
if (pool.feesCollected == 0) { if (pool.feesCollected == 0) {
// Compute weighted stake. // Compute member and total weighted stake.
uint256 operatorStake = getStakeDelegatedToPoolByOwner( (pool.membersStake, pool.weightedStake) =
rewardVault.operatorOf(poolId), _computeMembersAndWeightedStake(poolId, poolStake);
poolId
).currentEpochBalance;
pool.weightedStake = operatorStake.safeAdd(
poolStake
.safeSub(operatorStake)
.safeMul(rewardDelegatedStakeWeight)
.safeDiv(PPM_DENOMINATOR)
);
// Compute delegated (non-operator) stake.
pool.delegatedStake = poolStake.safeSub(operatorStake);
// Increase the total weighted stake. // Increase the total weighted stake.
totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch.safeAdd( totalWeightedStakeThisEpoch =
pool.weightedStake totalWeightedStakeThisEpoch.safeAdd(pool.weightedStake);
);
// Increase the numberof active pools. // Increase the numberof active pools.
numActivePoolsThisEpoch += 1; numActivePoolsThisEpoch += 1;
// Emit an event so keepers know what pools to pass into `finalize()`. // Emit an event so keepers know what pools to pass into
// `finalize()`.
emit StakingPoolActivated(currentEpoch, poolId); emit StakingPoolActivated(currentEpoch, poolId);
} }
// Credit the fees to the pool. // Credit the fees to the pool.
pool.feesCollected = protocolFeePaid; pool.feesCollected = pool.feesCollected.safeAdd(protocolFeePaid);
// Increase the total fees collected this epoch. // Increase the total fees collected this epoch.
totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd( totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd(
@ -142,7 +134,8 @@ contract MixinExchangeFees is
activePoolsThisEpoch[poolId] = pool; activePoolsThisEpoch[poolId] = pool;
} }
/// @dev Returns the total amount of fees collected thus far, in the current epoch. /// @dev Returns the total amount of fees collected thus far, in the current
/// epoch.
/// @return _totalFeesCollectedThisEpoch Total fees collected this epoch. /// @return _totalFeesCollectedThisEpoch Total fees collected this epoch.
function getTotalProtocolFeesThisEpoch() function getTotalProtocolFeesThisEpoch()
external external
@ -152,9 +145,21 @@ contract MixinExchangeFees is
_totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch; _totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch;
} }
/// @dev Returns the total balance of this contract, including WETH.
/// @return totalBalance Total balance.
function getTotalBalance()
external
view
returns (uint256 totalBalance)
{
totalBalance = address(this).balance +
IEtherToken(WETH_ADDRESS).balanceOf(address(this));
}
/// @dev Returns the amount of fees attributed to the input pool this epoch. /// @dev Returns the amount of fees attributed to the input pool this epoch.
/// @param poolId Pool Id to query. /// @param poolId Pool Id to query.
/// @return feesCollectedByPool Amount of fees collected by the pool this epoch. /// @return feesCollectedByPool Amount of fees collected by the pool this
/// epoch.
function getProtocolFeesThisEpochByPool(bytes32 poolId) function getProtocolFeesThisEpochByPool(bytes32 poolId)
external external
view view
@ -168,20 +173,55 @@ contract MixinExchangeFees is
feesCollected = pool.feesCollected; feesCollected = pool.feesCollected;
} }
/// @dev Checks that the protocol fee passed into `payProtocolFee()` is valid. /// @dev Computes the members and weighted stake for a pool at the current
/// @param protocolFeePaid The `protocolFeePaid` parameter to `payProtocolFee.` /// epoch.
/// @param poolId ID of the pool.
/// @param totalStake Total (unweighted) stake in the pool.
/// @return membersStake Non-operator stake in the pool.
/// @return weightedStake Weighted stake of the pool.
function _computeMembersAndWeightedStake(
bytes32 poolId,
uint256 totalStake
)
private
view
returns (uint256 membersStake, uint256 weightedStake)
{
uint256 operatorStake = getStakeDelegatedToPoolByOwner(
getPoolOperator(poolId),
poolId
).currentEpochBalance;
membersStake = totalStake.safeSub(operatorStake);
weightedStake = operatorStake.safeAdd(
membersStake
.safeMul(rewardDelegatedStakeWeight)
.safeDiv(PPM_DENOMINATOR)
);
}
/// @dev Checks that the protocol fee passed into `payProtocolFee()` is
/// valid.
/// @param protocolFeePaid The `protocolFeePaid` parameter to
/// `payProtocolFee.`
function _assertValidProtocolFee(uint256 protocolFeePaid) function _assertValidProtocolFee(uint256 protocolFeePaid)
private private
view view
{ {
if (protocolFeePaid == 0 || (msg.value != protocolFeePaid && msg.value != 0)) { if (protocolFeePaid == 0 ||
LibRichErrors.rrevert(LibStakingRichErrors.InvalidProtocolFeePaymentError( (msg.value != protocolFeePaid && msg.value != 0)) {
LibRichErrors.rrevert(
LibStakingRichErrors.InvalidProtocolFeePaymentError(
protocolFeePaid == 0 ? protocolFeePaid == 0 ?
LibStakingRichErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid : LibStakingRichErrors
LibStakingRichErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment, .ProtocolFeePaymentErrorCodes
.ZeroProtocolFeePaid :
LibStakingRichErrors
.ProtocolFeePaymentErrorCodes
.MismatchedFeeAndPayment,
protocolFeePaid, protocolFeePaid,
msg.value msg.value
)); )
);
} }
} }
} }

View File

@ -25,11 +25,11 @@ interface IStructs {
/// (see MixinExchangeFees). /// (see MixinExchangeFees).
/// @param feesCollected Fees collected in ETH by this pool. /// @param feesCollected Fees collected in ETH by this pool.
/// @param weightedStake Amount of weighted stake in the pool. /// @param weightedStake Amount of weighted stake in the pool.
/// @param delegatedStake Amount of delegated, non-operator stake in the pool. /// @param membersStake Amount of non-operator stake in the pool.
struct ActivePool { struct ActivePool {
uint256 feesCollected; uint256 feesCollected;
uint256 weightedStake; uint256 weightedStake;
uint256 delegatedStake; uint256 membersStake;
} }
/// @dev Rewards credited to a pool during finalization. /// @dev Rewards credited to a pool during finalization.

View File

@ -113,6 +113,17 @@ contract MixinStakingPool is
return poolById[poolId]; return poolById[poolId];
} }
/// @dev Look up the operator of a pool.
/// @param poolId The ID of the pool.
/// @return operatorAddress The pool operator.
function getPoolOperator(bytes32 poolId)
public
view
returns (address operatorAddress)
{
return rewardVault.operatorOf(poolId);
}
/// @dev Computes the unique id that comes after the input pool id. /// @dev Computes the unique id that comes after the input pool id.
/// @param poolId Unique id of pool. /// @param poolId Unique id of pool.
/// @return Next pool id after input pool. /// @return Next pool id after input pool.

View File

@ -93,7 +93,7 @@ contract MixinFinalizer is
// Emit an event. // Emit an event.
emit EpochEnded( emit EpochEnded(
closingEpoch, closingEpoch,
numActivePoolsThisEpoch, unfinalizedPoolsRemaining,
unfinalizedRewardsAvailable, unfinalizedRewardsAvailable,
unfinalizedTotalFeesCollected, unfinalizedTotalFeesCollected,
unfinalizedTotalWeightedStake unfinalizedTotalWeightedStake
@ -179,7 +179,9 @@ contract MixinFinalizer is
} }
// Deposit all the rewards at once into the RewardVault. // Deposit all the rewards at once into the RewardVault.
if (rewardsPaid != 0) {
_depositIntoStakingPoolRewardVault(rewardsPaid); _depositIntoStakingPoolRewardVault(rewardsPaid);
}
// Update finalization states. // Update finalization states.
totalRewardsPaidLastEpoch = totalRewardsPaidLastEpoch =
@ -310,6 +312,11 @@ contract MixinFinalizer is
IStructs.ActivePool memory pool = IStructs.ActivePool memory pool =
_getActivePoolFromEpoch(epoch - 1, poolId); _getActivePoolFromEpoch(epoch - 1, poolId);
// There can't be any rewards if the pool was active or if it has
// no stake.
if (pool.feesCollected == 0 || pool.weightedStake == 0) {
return rewards;
}
// Use the cobb-douglas function to compute the total reward. // Use the cobb-douglas function to compute the total reward.
uint256 totalReward = LibCobbDouglas._cobbDouglas( uint256 totalReward = LibCobbDouglas._cobbDouglas(
@ -323,16 +330,13 @@ contract MixinFinalizer is
); );
// Split the reward between the operator and delegators. // Split the reward between the operator and delegators.
if (pool.delegatedStake == 0) { if (pool.membersStake == 0) {
rewards.operatorReward = totalReward; rewards.operatorReward = totalReward;
} else { } else {
(rewards.operatorReward, rewards.membersReward) = (rewards.operatorReward, rewards.membersReward) =
rewardVault.splitAmountBetweenOperatorAndMembers( _splitAmountBetweenOperatorAndMembers(poolId, totalReward);
poolId,
totalReward
);
} }
rewards.membersStake = pool.delegatedStake; rewards.membersStake = pool.membersStake;
} }
/// @dev Converts the entire WETH balance of the contract into ETH. /// @dev Converts the entire WETH balance of the contract into ETH.
@ -344,6 +348,48 @@ contract MixinFinalizer is
} }
} }
/// @dev Splits an amount between the pool operator and members of the
/// pool based on the pool operator's share.
/// @param poolId The ID of the pool.
/// @param amount Amount to to split.
/// @return operatorPortion Portion of `amount` attributed to the operator.
/// @return membersPortion Portion of `amount` attributed to the pool.
function _splitAmountBetweenOperatorAndMembers(
bytes32 poolId,
uint256 amount
)
internal
view
returns (uint256 operatorReward, uint256 membersReward)
{
(operatorReward, membersReward) =
rewardVault.splitAmountBetweenOperatorAndMembers(poolId, amount);
}
/// @dev Record a deposit for a pool in the RewardVault.
/// @param poolId ID of the pool.
/// @param amount Amount in ETH to record.
/// @param operatorOnly Only attribute amount to operator.
/// @return operatorPortion Portion of `amount` attributed to the operator.
/// @return membersPortion Portion of `amount` attributed to the pool.
function _recordDepositInRewardVaultFor(
bytes32 poolId,
uint256 amount,
bool operatorOnly
)
internal
returns (
uint256 operatorPortion,
uint256 membersPortion
)
{
(operatorPortion, membersPortion) = rewardVault.recordDepositFor(
poolId,
amount,
operatorOnly
);
}
/// @dev Computes the reward owed to a pool during finalization and /// @dev Computes the reward owed to a pool during finalization and
/// credits it to that pool for the CURRENT epoch. /// credits it to that pool for the CURRENT epoch.
/// @param poolId The pool's ID. /// @param poolId The pool's ID.
@ -356,6 +402,12 @@ contract MixinFinalizer is
private private
returns (IStructs.PoolRewards memory rewards) returns (IStructs.PoolRewards memory rewards)
{ {
// There can't be any rewards if the pool was active or if it has
// no stake.
if (pool.feesCollected == 0 || pool.weightedStake == 0) {
return rewards;
}
// Use the cobb-douglas function to compute the total reward. // Use the cobb-douglas function to compute the total reward.
uint256 totalReward = LibCobbDouglas._cobbDouglas( uint256 totalReward = LibCobbDouglas._cobbDouglas(
unfinalizedRewardsAvailable, unfinalizedRewardsAvailable,
@ -369,20 +421,20 @@ contract MixinFinalizer is
// Credit the pool the reward in the RewardVault. // Credit the pool the reward in the RewardVault.
(rewards.operatorReward, rewards.membersReward) = (rewards.operatorReward, rewards.membersReward) =
rewardVault.recordDepositFor( _recordDepositInRewardVaultFor(
poolId, poolId,
totalReward, totalReward,
// If no delegated stake, all rewards go to the operator. // If no delegated stake, all rewards go to the operator.
pool.delegatedStake == 0 pool.membersStake == 0
); );
rewards.membersStake = pool.delegatedStake; rewards.membersStake = pool.membersStake;
// Sync delegator rewards. // Sync delegator rewards.
if (rewards.membersReward != 0) { if (rewards.membersReward != 0) {
_recordRewardForDelegators( _recordRewardForDelegators(
poolId, poolId,
rewards.membersReward, rewards.membersReward,
pool.delegatedStake pool.membersStake
); );
} }
} }

View File

@ -0,0 +1,145 @@
/*
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 "../src/interfaces/IStructs.sol";
import "../src/Staking.sol";
contract TestFinalizer is
Staking
{
struct RecordedReward {
uint256 membersReward;
uint256 membersStake;
}
struct DepositedReward {
uint256 totalReward;
bool operatorOnly;
}
mapping (bytes32 => uint32) internal _operatorSharesByPool;
mapping (bytes32 => RecordedReward) internal _recordedRewardsByPool;
mapping (bytes32 => DepositedReward) internal _depositedRewardsByPool;
function getFinalizationState()
external
view
returns (
uint256 _closingEpoch,
uint256 _unfinalizedPoolsRemaining,
uint256 _unfinalizedRewardsAvailable,
uint256 _unfinalizedTotalFeesCollected,
uint256 _unfinalizedTotalWeightedStake
)
{
_closingEpoch = currentEpoch - 1;
_unfinalizedPoolsRemaining = unfinalizedPoolsRemaining;
_unfinalizedRewardsAvailable = unfinalizedRewardsAvailable;
_unfinalizedTotalFeesCollected = unfinalizedTotalFeesCollected;
_unfinalizedTotalWeightedStake = unfinalizedTotalWeightedStake;
}
function addActivePool(
bytes32 poolId,
uint32 operatorShare,
uint256 feesCollected,
uint256 membersStake,
uint256 weightedStake
)
external
{
mapping (bytes32 => IStructs.ActivePool) storage activePools =
_getActivePoolsFromEpoch(currentEpoch);
assert(activePools[poolId].feesCollected == 0);
_operatorSharesByPool[poolId] = operatorShare;
activePools[poolId] = IStructs.ActivePool({
feesCollected: feesCollected,
membersStake: membersStake,
weightedStake: weightedStake
});
totalFeesCollectedThisEpoch += feesCollected;
totalWeightedStakeThisEpoch += weightedStake;
numActivePoolsThisEpoch += 1;
}
/// @dev Overridden to just store inputs.
function _recordRewardForDelegators(
bytes32 poolId,
uint256 membersReward,
uint256 membersStake
)
internal
{
_recordedRewardsByPool[poolId] = RecordedReward({
membersReward: membersReward,
membersStake: membersStake
});
}
/// @dev Overridden to store inputs and do some really basic math.
function _recordDepositInRewardVaultFor(
bytes32 poolId,
uint256 totalReward,
bool operatorOnly
)
internal
returns (
uint256 operatorPortion,
uint256 membersPortion
)
{
_depositedRewardsByPool[poolId] = DepositedReward({
totalReward: totalReward,
operatorOnly: operatorOnly
});
if (operatorOnly) {
operatorPortion = totalReward;
} else {
(operatorPortion, membersPortion) =
_splitAmountBetweenOperatorAndMembers(poolId, totalReward);
}
}
/// @dev Overridden to do some really basic math.
function _splitAmountBetweenOperatorAndMembers(
bytes32 poolId,
uint256 amount
)
internal
view
returns (uint256 operatorPortion, uint256 membersPortion)
{
uint32 operatorShare = _operatorSharesByPool[poolId];
operatorPortion = operatorShare * amount / PPM_DENOMINATOR;
membersPortion = amount - operatorPortion;
}
/// @dev Overriden to always succeed.
function _goToNextEpoch() internal {
currentEpoch += 1;
}
/// @dev Overridden to do nothing.
function _unwrapWETH() internal {
// NOOP
}
}

View File

@ -20,6 +20,7 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol"; import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol";
import "../src/interfaces/IStructs.sol";
import "../src/Staking.sol"; import "../src/Staking.sol";
@ -27,7 +28,8 @@ contract TestProtocolFees is
Staking Staking
{ {
struct TestPool { struct TestPool {
uint256 stake; uint256 operatorStake;
uint256 membersStake;
mapping(address => bool) isMaker; mapping(address => bool) isMaker;
} }
@ -58,13 +60,15 @@ contract TestProtocolFees is
/// @dev Create a test pool. /// @dev Create a test pool.
function createTestPool( function createTestPool(
bytes32 poolId, bytes32 poolId,
uint256 stake, uint256 operatorStake,
uint256 membersStake,
address[] memory makerAddresses address[] memory makerAddresses
) )
public public
{ {
TestPool storage pool = _testPools[poolId]; TestPool storage pool = _testPools[poolId];
pool.stake = stake; pool.operatorStake = operatorStake;
pool.membersStake = membersStake;
for (uint256 i = 0; i < makerAddresses.length; ++i) { for (uint256 i = 0; i < makerAddresses.length; ++i) {
pool.isMaker[makerAddresses[i]] = true; pool.isMaker[makerAddresses[i]] = true;
_makersToTestPoolIds[makerAddresses[i]] = poolId; _makersToTestPoolIds[makerAddresses[i]] = poolId;
@ -86,10 +90,34 @@ contract TestProtocolFees is
view view
returns (IStructs.StakeBalance memory balance) returns (IStructs.StakeBalance memory balance)
{ {
uint256 stake = _testPools[poolId].stake; TestPool memory pool = _testPools[poolId];
uint256 stake = pool.operatorStake + pool.membersStake;
return IStructs.StakeBalance({ return IStructs.StakeBalance({
currentEpochBalance: stake, currentEpochBalance: stake,
nextEpochBalance: stake nextEpochBalance: stake
}); });
} }
/// @dev Overridden to use test pools.
function getStakeDelegatedToPoolByOwner(address, bytes32 poolId)
public
view
returns (IStructs.StakeBalance memory balance)
{
TestPool memory pool = _testPools[poolId];
return IStructs.StakeBalance({
currentEpochBalance: pool.operatorStake,
nextEpochBalance: pool.operatorStake
});
}
/// @dev Overridden to use test pools.
function getPoolOperator(bytes32)
public
view
returns (address operatorAddress)
{
// Just return nil, we won't use it.
return address(0);
}
} }

View File

@ -146,6 +146,15 @@ export class StakerActor extends BaseActor {
expect(finalZrxBalanceOfVault, 'final balance of zrx vault').to.be.bignumber.equal(initZrxBalanceOfVault); expect(finalZrxBalanceOfVault, 'final balance of zrx vault').to.be.bignumber.equal(initZrxBalanceOfVault);
} }
public async stakeWithPoolAsync(poolId: string, amount: BigNumber): Promise<void> {
await this.stakeAsync(amount);
await this.moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
amount,
);
}
public async goToNextEpochAsync(): Promise<void> { public async goToNextEpochAsync(): Promise<void> {
// cache balances // cache balances
const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync(); const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync();

View File

@ -4,6 +4,7 @@ import {
expect, expect,
filterLogsToArguments, filterLogsToArguments,
hexRandom, hexRandom,
Numberish,
randomAddress, randomAddress,
} from '@0x/contracts-test-utils'; } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils'; import { StakingRevertErrors } from '@0x/order-utils';
@ -54,9 +55,26 @@ blockchainTests('Protocol Fee Unit Tests', env => {
wethAssetData = await testContract.getWethAssetData.callAsync(); wethAssetData = await testContract.getWethAssetData.callAsync();
}); });
async function createTestPoolAsync(stake: BigNumber, makers: string[]): Promise<string> { interface CreatePoolOpts {
operatorStake: Numberish;
membersStake: Numberish;
makers: string[];
}
async function createTestPoolAsync(opts: Partial<CreatePoolOpts>): Promise<string> {
const _opts = {
operatorStake: 0,
membersStake: 0,
makers: [],
...opts,
};
const poolId = hexRandom(); const poolId = hexRandom();
await testContract.createTestPool.awaitTransactionSuccessAsync(poolId, stake, makers); await testContract.createTestPool.awaitTransactionSuccessAsync(
poolId,
new BigNumber(_opts.operatorStake),
new BigNumber(_opts.membersStake),
_opts.makers,
);
return poolId; return poolId;
} }
@ -154,7 +172,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
} }
it('should not transfer WETH if value is sent', async () => { it('should not transfer WETH if value is sent', async () => {
await createTestPoolAsync(minimumStake, []); await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
payerAddress, payerAddress,
@ -164,8 +182,8 @@ blockchainTests('Protocol Fee Unit Tests', env => {
assertNoWETHTransferLogs(receipt.logs); assertNoWETHTransferLogs(receipt.logs);
}); });
it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => { it('should credit pool if the maker is in a pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
payerAddress, payerAddress,
@ -177,8 +195,8 @@ blockchainTests('Protocol Fee Unit Tests', env => {
expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID); expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID);
}); });
it('should not update `protocolFeesThisEpochByPool` if maker is not in a pool', async () => { it('should not credit the pool if maker is not in a pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, []); const poolId = await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
payerAddress, payerAddress,
@ -191,7 +209,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}); });
it('fees paid to the same maker should go to the same pool', async () => { it('fees paid to the same maker should go to the same pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async () => { const payAsync = async () => {
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
@ -225,7 +243,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
} }
it('should transfer WETH if no value is sent and the maker is not in a pool', async () => { it('should transfer WETH if no value is sent and the maker is not in a pool', async () => {
await createTestPoolAsync(minimumStake, []); await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
payerAddress, payerAddress,
@ -236,7 +254,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}); });
it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => { it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
payerAddress, payerAddress,
@ -249,7 +267,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}); });
it('should not update `protocolFeesThisEpochByPool` if maker is not in a pool', async () => { it('should not update `protocolFeesThisEpochByPool` if maker is not in a pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, []); const poolId = await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
payerAddress, payerAddress,
@ -262,7 +280,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}); });
it('fees paid to the same maker should go to the same pool', async () => { it('fees paid to the same maker should go to the same pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async () => { const payAsync = async () => {
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync( const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
@ -280,7 +298,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}); });
it('fees paid to the same maker in WETH then ETH should go to the same pool', async () => { it('fees paid to the same maker in WETH then ETH should go to the same pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async (inWETH: boolean) => { const payAsync = async (inWETH: boolean) => {
await testContract.payProtocolFee.awaitTransactionSuccessAsync( await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
@ -303,7 +321,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
describe('Multiple makers', () => { describe('Multiple makers', () => {
it('fees paid to different makers in the same pool go to that pool', async () => { it('fees paid to different makers in the same pool go to that pool', async () => {
const otherMakerAddress = randomAddress(); const otherMakerAddress = randomAddress();
const poolId = await createTestPoolAsync(minimumStake, [makerAddress, otherMakerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress, otherMakerAddress] });
const payAsync = async (_makerAddress: string) => { const payAsync = async (_makerAddress: string) => {
await testContract.payProtocolFee.awaitTransactionSuccessAsync( await testContract.payProtocolFee.awaitTransactionSuccessAsync(
_makerAddress, _makerAddress,
@ -322,8 +340,8 @@ blockchainTests('Protocol Fee Unit Tests', env => {
it('fees paid to makers in different pools go to their respective pools', async () => { it('fees paid to makers in different pools go to their respective pools', async () => {
const [fee, otherFee] = _.times(2, () => getRandomPortion(DEFAULT_PROTOCOL_FEE_PAID)); const [fee, otherFee] = _.times(2, () => getRandomPortion(DEFAULT_PROTOCOL_FEE_PAID));
const otherMakerAddress = randomAddress(); const otherMakerAddress = randomAddress();
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const otherPoolId = await createTestPoolAsync(minimumStake, [otherMakerAddress]); const otherPoolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [otherMakerAddress]});
const payAsync = async (_poolId: string, _makerAddress: string, _fee: BigNumber) => { const payAsync = async (_poolId: string, _makerAddress: string, _fee: BigNumber) => {
// prettier-ignore // prettier-ignore
await testContract.payProtocolFee.awaitTransactionSuccessAsync( await testContract.payProtocolFee.awaitTransactionSuccessAsync(
@ -346,7 +364,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
describe('Dust stake', () => { describe('Dust stake', () => {
it('credits pools with stake > minimum', async () => { it('credits pools with stake > minimum', async () => {
const poolId = await createTestPoolAsync(minimumStake.plus(1), [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake.plus(1), makers: [makerAddress] });
await testContract.payProtocolFee.awaitTransactionSuccessAsync( await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
constants.NULL_ADDRESS, constants.NULL_ADDRESS,
@ -358,7 +376,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}); });
it('credits pools with stake == minimum', async () => { it('credits pools with stake == minimum', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
await testContract.payProtocolFee.awaitTransactionSuccessAsync( await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
constants.NULL_ADDRESS, constants.NULL_ADDRESS,
@ -370,7 +388,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}); });
it('does not credit pools with stake < minimum', async () => { it('does not credit pools with stake < minimum', async () => {
const poolId = await createTestPoolAsync(minimumStake.minus(1), [makerAddress]); const poolId = await createTestPoolAsync({ operatorStake: minimumStake.minus(1), makers: [makerAddress] });
await testContract.payProtocolFee.awaitTransactionSuccessAsync( await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress, makerAddress,
constants.NULL_ADDRESS, constants.NULL_ADDRESS,

View File

@ -6,6 +6,7 @@ import * as _ from 'lodash';
import { artifacts } from '../src'; import { artifacts } from '../src';
import { FinalizerActor } from './actors/finalizer_actor'; import { FinalizerActor } from './actors/finalizer_actor';
import { PoolOperatorActor } from './actors/pool_operator_actor';
import { StakerActor } from './actors/staker_actor'; import { StakerActor } from './actors/staker_actor';
import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper'; import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
import { toBaseUnitAmount } from './utils/number_utils'; import { toBaseUnitAmount } from './utils/number_utils';
@ -26,8 +27,9 @@ blockchainTests.resets('Testing Rewards', env => {
let erc20Wrapper: ERC20Wrapper; let erc20Wrapper: ERC20Wrapper;
// test parameters // test parameters
let stakers: StakerActor[]; let stakers: StakerActor[];
let poolOperatorStaker: StakerActor;
let poolId: string; let poolId: string;
let poolOperator: string; let poolOperator: PoolOperatorActor;
let finalizer: FinalizerActor; let finalizer: FinalizerActor;
// tests // tests
before(async () => { before(async () => {
@ -43,7 +45,7 @@ blockchainTests.resets('Testing Rewards', env => {
stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking); stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking);
// set up staking parameters // set up staking parameters
await stakingApiWrapper.utils.setParamsAsync({ await stakingApiWrapper.utils.setParamsAsync({
minimumPoolStake: new BigNumber(0), minimumPoolStake: new BigNumber(1),
cobbDouglasAlphaNumerator: new BigNumber(1), cobbDouglasAlphaNumerator: new BigNumber(1),
cobbDouglasAlphaDenominator: new BigNumber(6), cobbDouglasAlphaDenominator: new BigNumber(6),
rewardVaultAddress: stakingApiWrapper.rewardVaultContract.address, rewardVaultAddress: stakingApiWrapper.rewardVaultContract.address,
@ -51,22 +53,26 @@ blockchainTests.resets('Testing Rewards', env => {
zrxVaultAddress: stakingApiWrapper.zrxVaultContract.address, zrxVaultAddress: stakingApiWrapper.zrxVaultContract.address,
}); });
// setup stakers // setup stakers
stakers = [new StakerActor(actors[0], stakingApiWrapper), new StakerActor(actors[1], stakingApiWrapper)]; stakers = actors.slice(0, 2).map(a => new StakerActor(a, stakingApiWrapper));
// setup pools // setup pools
poolOperator = actors[2]; poolOperator = new PoolOperatorActor(actors[2], stakingApiWrapper);
poolId = await stakingApiWrapper.utils.createStakingPoolAsync(poolOperator, 0, true); // add operator as maker // Create a pool where all rewards go to members.
poolId = await poolOperator.createStakingPoolAsync(0, true);
// Stake something in the pool or else it won't get any rewards.
poolOperatorStaker = new StakerActor(poolOperator.getOwner(), stakingApiWrapper);
await poolOperatorStaker.stakeWithPoolAsync(poolId, new BigNumber(1));
// set exchange address // set exchange address
await stakingApiWrapper.stakingContract.addExchangeAddress.awaitTransactionSuccessAsync(exchangeAddress); await stakingApiWrapper.stakingContract.addExchangeAddress.awaitTransactionSuccessAsync(exchangeAddress);
// associate operators for tracking in Finalizer // associate operators for tracking in Finalizer
const operatorByPoolId: OperatorByPoolId = {}; const operatorByPoolId: OperatorByPoolId = {};
operatorByPoolId[poolId] = poolOperator; operatorByPoolId[poolId] = poolOperator.getOwner();
operatorByPoolId[poolId] = poolOperator;
// associate actors with pools for tracking in Finalizer // associate actors with pools for tracking in Finalizer
const membersByPoolId: MembersByPoolId = {}; const membersByPoolId: MembersByPoolId = {};
membersByPoolId[poolId] = [actors[0], actors[1]]; membersByPoolId[poolId] = [actors[0], actors[1]];
membersByPoolId[poolId] = [actors[0], actors[1]];
// create Finalizer actor // create Finalizer actor
finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, membersByPoolId); finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, membersByPoolId);
// Skip to next epoch so operator stake is realized.
await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
}); });
describe('Reward Simulation', () => { describe('Reward Simulation', () => {
interface EndBalances { interface EndBalances {
@ -154,7 +160,7 @@ blockchainTests.resets('Testing Rewards', env => {
const fee = _fee !== undefined ? _fee : ZERO; const fee = _fee !== undefined ? _fee : ZERO;
if (!fee.eq(ZERO)) { if (!fee.eq(ZERO)) {
await stakingApiWrapper.stakingContract.payProtocolFee.awaitTransactionSuccessAsync( await stakingApiWrapper.stakingContract.payProtocolFee.awaitTransactionSuccessAsync(
poolOperator, poolOperator.getOwner(),
takerAddress, takerAddress,
fee, fee,
{ from: exchangeAddress, value: fee }, { from: exchangeAddress, value: fee },
@ -196,12 +202,7 @@ blockchainTests.resets('Testing Rewards', env => {
(staker joins this epoch but is active next epoch)`, async () => { (staker joins this epoch but is active next epoch)`, async () => {
// delegate // delegate
const amount = toBaseUnitAmount(4); const amount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(amount); await stakers[0].stakeWithPoolAsync(poolId, amount);
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
amount,
);
// finalize // finalize
const reward = toBaseUnitAmount(10); const reward = toBaseUnitAmount(10);
await payProtocolFeeAndFinalize(reward); await payProtocolFeeAndFinalize(reward);
@ -213,12 +214,7 @@ blockchainTests.resets('Testing Rewards', env => {
it('Should give pool reward to delegator', async () => { it('Should give pool reward to delegator', async () => {
// delegate // delegate
const amount = toBaseUnitAmount(4); const amount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(amount); await stakers[0].stakeWithPoolAsync(poolId, amount);
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
amount,
);
// skip epoch, so staker can start earning rewards // skip epoch, so staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// finalize // finalize
@ -232,22 +228,12 @@ blockchainTests.resets('Testing Rewards', env => {
}); });
}); });
it('Should split pool reward between delegators', async () => { it('Should split pool reward between delegators', async () => {
// first staker delegates
const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
const totalStakeAmount = toBaseUnitAmount(10); const totalStakeAmount = toBaseUnitAmount(10);
await stakers[0].stakeAsync(stakeAmounts[0]); // first staker delegates
await stakers[0].moveStakeAsync( await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[0],
);
// second staker delegates // second staker delegates
await stakers[1].stakeAsync(stakeAmounts[1]); await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
await stakers[1].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[1],
);
// skip epoch, so staker can start earning rewards // skip epoch, so staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// finalize // finalize
@ -299,24 +285,14 @@ blockchainTests.resets('Testing Rewards', env => {
}); });
}); });
it('Should give pool reward to delegators only for the epoch during which they delegated', async () => { it('Should give pool reward to delegators only for the epoch during which they delegated', async () => {
// first staker delegates (epoch 0)
const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
const totalStakeAmount = toBaseUnitAmount(10); const totalStakeAmount = toBaseUnitAmount(10);
await stakers[0].stakeAsync(stakeAmounts[0]); // first staker delegates (epoch 0)
await stakers[0].moveStakeAsync( await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[0],
);
// skip epoch, so first staker can start earning rewards // skip epoch, so first staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// second staker delegates (epoch 1) // second staker delegates (epoch 1)
await stakers[1].stakeAsync(stakeAmounts[1]); await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
await stakers[1].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[1],
);
// only the first staker will get this reward // only the first staker will get this reward
const rewardForOnlyFirstDelegator = toBaseUnitAmount(10); const rewardForOnlyFirstDelegator = toBaseUnitAmount(10);
await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator); await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator);
@ -349,24 +325,14 @@ blockchainTests.resets('Testing Rewards', env => {
return v.toNumber(); return v.toNumber();
}); });
const totalSharedRewards = new BigNumber(totalSharedRewardsAsNumber); const totalSharedRewards = new BigNumber(totalSharedRewardsAsNumber);
// first staker delegates (epoch 0)
const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
const totalStakeAmount = toBaseUnitAmount(10); const totalStakeAmount = toBaseUnitAmount(10);
await stakers[0].stakeAsync(stakeAmounts[0]); // first staker delegates (epoch 0)
await stakers[0].moveStakeAsync( await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[0],
);
// skip epoch, so first staker can start earning rewards // skip epoch, so first staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// second staker delegates (epoch 1) // second staker delegates (epoch 1)
await stakers[1].stakeAsync(stakeAmounts[1]); await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
await stakers[1].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[1],
);
// only the first staker will get this reward // only the first staker will get this reward
await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator); await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator);
// earn a bunch of rewards // earn a bunch of rewards
@ -386,14 +352,9 @@ blockchainTests.resets('Testing Rewards', env => {
}); });
}); });
it('Should send existing rewards from reward vault to eth vault correctly when undelegating stake', async () => { it('Should send existing rewards from reward vault to eth vault correctly when undelegating stake', async () => {
// first staker delegates (epoch 0)
const stakeAmount = toBaseUnitAmount(4); const stakeAmount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(stakeAmount); // first staker delegates (epoch 0)
await stakers[0].moveStakeAsync( await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// skip epoch, so first staker can start earning rewards // skip epoch, so first staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// earn reward // earn reward
@ -412,26 +373,16 @@ blockchainTests.resets('Testing Rewards', env => {
}); });
}); });
it('Should send existing rewards from reward vault to eth vault correctly when delegating more stake', async () => { it('Should send existing rewards from reward vault to eth vault correctly when delegating more stake', async () => {
// first staker delegates (epoch 0)
const stakeAmount = toBaseUnitAmount(4); const stakeAmount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(stakeAmount); // first staker delegates (epoch 0)
await stakers[0].moveStakeAsync( await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// skip epoch, so first staker can start earning rewards // skip epoch, so first staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// earn reward // earn reward
const reward = toBaseUnitAmount(10); const reward = toBaseUnitAmount(10);
await payProtocolFeeAndFinalize(reward); await payProtocolFeeAndFinalize(reward);
// add more stake // add more stake
await stakers[0].stakeAsync(stakeAmount); await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// sanity check final balances // sanity check final balances
await validateEndBalances({ await validateEndBalances({
stakerRewardVaultBalance_1: ZERO, stakerRewardVaultBalance_1: ZERO,
@ -453,23 +404,13 @@ blockchainTests.resets('Testing Rewards', env => {
return v.toNumber(); return v.toNumber();
}), }),
); );
// first staker delegates (epoch 0)
const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)]; const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
await stakers[0].stakeAsync(stakeAmounts[0]); // first staker delegates (epoch 0)
await stakers[0].moveStakeAsync( await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[0],
);
// skip epoch, so first staker can start earning rewards // skip epoch, so first staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// second staker delegates (epoch 1) // second staker delegates (epoch 1)
await stakers[0].stakeAsync(stakeAmounts[1]); await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmounts[1],
);
// only the first staker will get this reward // only the first staker will get this reward
await payProtocolFeeAndFinalize(rewardBeforeAddingMoreStake); await payProtocolFeeAndFinalize(rewardBeforeAddingMoreStake);
// earn a bunch of rewards // earn a bunch of rewards
@ -488,12 +429,7 @@ blockchainTests.resets('Testing Rewards', env => {
const rewardForDelegator = toBaseUnitAmount(10); const rewardForDelegator = toBaseUnitAmount(10);
const rewardNotForDelegator = toBaseUnitAmount(7); const rewardNotForDelegator = toBaseUnitAmount(7);
const stakeAmount = toBaseUnitAmount(4); const stakeAmount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(stakeAmount); await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// skip epoch, so first staker can start earning rewards // skip epoch, so first staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// earn reward // earn reward
@ -534,12 +470,7 @@ blockchainTests.resets('Testing Rewards', env => {
}), }),
); );
const stakeAmount = toBaseUnitAmount(4); const stakeAmount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(stakeAmount); await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// skip epoch, so first staker can start earning rewards // skip epoch, so first staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// earn reward // earn reward
@ -566,12 +497,7 @@ blockchainTests.resets('Testing Rewards', env => {
const rewardsForDelegator = [toBaseUnitAmount(10), toBaseUnitAmount(15)]; const rewardsForDelegator = [toBaseUnitAmount(10), toBaseUnitAmount(15)];
const rewardNotForDelegator = toBaseUnitAmount(7); const rewardNotForDelegator = toBaseUnitAmount(7);
const stakeAmount = toBaseUnitAmount(4); const stakeAmount = toBaseUnitAmount(4);
await stakers[0].stakeAsync(stakeAmount); await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
await stakers[0].moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// skip epoch, so first staker can start earning rewards // skip epoch, so first staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// earn reward // earn reward
@ -668,12 +594,7 @@ blockchainTests.resets('Testing Rewards', env => {
const staker = stakers[0]; const staker = stakers[0];
const stakeAmount = toBaseUnitAmount(5); const stakeAmount = toBaseUnitAmount(5);
// stake and delegate // stake and delegate
await staker.stakeAsync(stakeAmount); await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
// skip epoch, so staker can start earning rewards // skip epoch, so staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
// undelegate some stake // undelegate some stake
@ -703,12 +624,7 @@ blockchainTests.resets('Testing Rewards', env => {
// stake and delegate both // stake and delegate both
const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>;
for (const [staker, stakeAmount] of stakersAndStake) { for (const [staker, stakeAmount] of stakersAndStake) {
await staker.stakeAsync(stakeAmount); await staker.stakeWithPoolAsync(poolId, stakeAmount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
} }
// skip epoch, so staker can start earning rewards // skip epoch, so staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();
@ -739,12 +655,7 @@ blockchainTests.resets('Testing Rewards', env => {
// stake and delegate both // stake and delegate both
const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>; const stakersAndStake = _.zip(stakers.slice(0, 2), stakeAmounts) as Array<[StakerActor, BigNumber]>;
for (const [staker, stakeAmount] of stakersAndStake) { for (const [staker, stakeAmount] of stakersAndStake) {
await staker.stakeAsync(stakeAmount); await staker.stakeWithPoolAsync(poolId, stakeAmount);
await staker.moveStakeAsync(
new StakeInfo(StakeStatus.Active),
new StakeInfo(StakeStatus.Delegated, poolId),
stakeAmount,
);
} }
// skip epoch, so staker can start earning rewards // skip epoch, so staker can start earning rewards
await payProtocolFeeAndFinalize(); await payProtocolFeeAndFinalize();

View File

@ -7,7 +7,7 @@ blockchainTests('delegator rewards', env => {
before(async () => { before(async () => {
testContract = await TestDelegatorRewardsContract.deployFrom0xArtifactAsync( testContract = await TestDelegatorRewardsContract.deployFrom0xArtifactAsync(
artifacts.TestLibFixedMath, artifacts.TestDelegatorRewards,
env.provider, env.provider,
env.txDefaults, env.txDefaults,
artifacts, artifacts,

View File

@ -0,0 +1,55 @@
import { blockchainTests, expect, filterLogsToArguments, Numberish } from '@0x/contracts-test-utils';
import {
artifacts,
IStakingEventsEpochEndedEventArgs,
IStakingEventsEpochFinalizedEventArgs,
IStakingEventsEvents,
TestFinalizerContract,
} from '../../src';
blockchainTests.resets.only('finalization tests', env => {
let testContract: TestFinalizerContract;
const INITIAL_EPOCH = 0;
before(async () => {
testContract = await TestFinalizerContract.deployFrom0xArtifactAsync(
artifacts.TestFinalizer,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('endEpoch()', () => {
it('emits an `EpochEnded` event', async () => {
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const [epochEndedEvent] = filterLogsToArguments<IStakingEventsEpochEndedEventArgs>(
receipt.logs,
IStakingEventsEvents.EpochEnded,
);
expect(epochEndedEvent.epoch).to.bignumber.eq(INITIAL_EPOCH);
expect(epochEndedEvent.numActivePools).to.bignumber.eq(0);
expect(epochEndedEvent.rewardsAvailable).to.bignumber.eq(0);
expect(epochEndedEvent.totalFeesCollected).to.bignumber.eq(0);
expect(epochEndedEvent.totalWeightedStake).to.bignumber.eq(0);
});
it('advances the epoch', async () => {
await testContract.endEpoch.awaitTransactionSuccessAsync();
const currentEpoch = await testContract.getCurrentEpoch.callAsync();
expect(currentEpoch).to.be.bignumber.eq(INITIAL_EPOCH + 1);
});
it('immediately finalizes if there are no active pools', async () => {
const receipt = await testContract.endEpoch.awaitTransactionSuccessAsync();
const [epochFinalizedEvent] = filterLogsToArguments<IStakingEventsEpochFinalizedEventArgs>(
receipt.logs,
IStakingEventsEvents.EpochFinalized,
);
expect(epochFinalizedEvent.epoch).to.bignumber.eq(INITIAL_EPOCH);
expect(epochFinalizedEvent.rewardsPaid).to.bignumber.eq(0);
expect(epochFinalizedEvent.rewardsRemaining).to.bignumber.eq(0);
});
});
});