@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 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/LibSafeMath.sol";
import "../libs/LibStakingRichErrors.sol";
@ -71,7 +72,8 @@ contract MixinExchangeFees is
{
_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) {
wethAssetProxy.transferFrom(
WETH_ASSET_DATA,
@ -89,7 +91,8 @@ contract MixinExchangeFees is
return;
}
uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance;
uint256 poolStake =
getTotalStakeDelegatedToPool(poolId).currentEpochBalance;
// Ignore pools with dust stake.
if (poolStake < minimumPoolStake) {
return;
@ -103,35 +106,24 @@ contract MixinExchangeFees is
// If the pool was previously inactive in this epoch, initialize it.
if (pool.feesCollected == 0) {
// Compute weighted stake.
uint256 operatorStake = getStakeDelegatedToPoolByOwner(
rewardVault.operatorOf(poolId),
poolId
).currentEpochBalance;
pool.weightedStake = operatorStake.safeAdd(
poolStake
.safeSub(operatorStake)
.safeMul(rewardDelegatedStakeWeight)
.safeDiv(PPM_DENOMINATOR)
);
// Compute delegated (non-operator) stake.
pool.delegatedStake = poolStake.safeSub(operatorStake);
// Compute member and total weighted stake.
(pool.membersStake, pool.weightedStake) =
_computeMembersAndWeightedStake(poolId, poolStake);
// Increase the total weighted stake.
totalWeightedStakeThisEpoch = totalWeightedStakeThisEpoch.safeAdd(
pool.weightedStake
);
totalWeightedStakeThisEpoch =
totalWeightedStakeThisEpoch.safeAdd(pool.weightedStake);
// Increase the numberof active pools.
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);
}
// Credit the fees to the pool.
pool.feesCollected = protocolFeePaid;
pool.feesCollected = pool.feesCollected.safeAdd(protocolFeePaid);
// Increase the total fees collected this epoch.
totalFeesCollectedThisEpoch = totalFeesCollectedThisEpoch.safeAdd(
@ -142,7 +134,8 @@ contract MixinExchangeFees is
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.
function getTotalProtocolFeesThisEpoch()
external
@ -152,9 +145,21 @@ contract MixinExchangeFees is
_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.
/// @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)
external
view
@ -168,20 +173,55 @@ contract MixinExchangeFees is
feesCollected = pool.feesCollected;
}
/// @dev Checks that the protocol fee passed into `payProtocolFee()` is valid.
/// @param protocolFeePaid The `protocolFeePaid` parameter to `payProtocolFee.`
/// @dev Computes the members and weighted stake for a pool at the current
/// 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)
private
view
{
if (protocolFeePaid == 0 || (msg.value != protocolFeePaid && msg.value != 0)) {
LibRichErrors.rrevert(LibStakingRichErrors.InvalidProtocolFeePaymentError(
if (protocolFeePaid == 0 ||
(msg.value != protocolFeePaid && msg.value != 0)) {
LibRichErrors.rrevert(
LibStakingRichErrors.InvalidProtocolFeePaymentError(
protocolFeePaid == 0 ?
LibStakingRichErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid :
LibStakingRichErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment,
LibStakingRichErrors
.ProtocolFeePaymentErrorCodes
.ZeroProtocolFeePaid :
LibStakingRichErrors
.ProtocolFeePaymentErrorCodes
.MismatchedFeeAndPayment,
protocolFeePaid,
msg.value
));
)
);
}
}
}

View File

@ -25,11 +25,11 @@ interface IStructs {
/// (see MixinExchangeFees).
/// @param feesCollected Fees collected in ETH by this 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 {
uint256 feesCollected;
uint256 weightedStake;
uint256 delegatedStake;
uint256 membersStake;
}
/// @dev Rewards credited to a pool during finalization.

View File

@ -113,6 +113,17 @@ contract MixinStakingPool is
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.
/// @param poolId Unique id of pool.
/// @return Next pool id after input pool.

View File

@ -93,7 +93,7 @@ contract MixinFinalizer is
// Emit an event.
emit EpochEnded(
closingEpoch,
numActivePoolsThisEpoch,
unfinalizedPoolsRemaining,
unfinalizedRewardsAvailable,
unfinalizedTotalFeesCollected,
unfinalizedTotalWeightedStake
@ -179,7 +179,9 @@ contract MixinFinalizer is
}
// Deposit all the rewards at once into the RewardVault.
if (rewardsPaid != 0) {
_depositIntoStakingPoolRewardVault(rewardsPaid);
}
// Update finalization states.
totalRewardsPaidLastEpoch =
@ -310,6 +312,11 @@ contract MixinFinalizer is
IStructs.ActivePool memory pool =
_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.
uint256 totalReward = LibCobbDouglas._cobbDouglas(
@ -323,16 +330,13 @@ contract MixinFinalizer is
);
// Split the reward between the operator and delegators.
if (pool.delegatedStake == 0) {
if (pool.membersStake == 0) {
rewards.operatorReward = totalReward;
} else {
(rewards.operatorReward, rewards.membersReward) =
rewardVault.splitAmountBetweenOperatorAndMembers(
poolId,
totalReward
);
_splitAmountBetweenOperatorAndMembers(poolId, totalReward);
}
rewards.membersStake = pool.delegatedStake;
rewards.membersStake = pool.membersStake;
}
/// @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
/// credits it to that pool for the CURRENT epoch.
/// @param poolId The pool's ID.
@ -356,6 +402,12 @@ contract MixinFinalizer is
private
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.
uint256 totalReward = LibCobbDouglas._cobbDouglas(
unfinalizedRewardsAvailable,
@ -369,20 +421,20 @@ contract MixinFinalizer is
// Credit the pool the reward in the RewardVault.
(rewards.operatorReward, rewards.membersReward) =
rewardVault.recordDepositFor(
_recordDepositInRewardVaultFor(
poolId,
totalReward,
// 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.
if (rewards.membersReward != 0) {
_recordRewardForDelegators(
poolId,
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;
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol";
import "../src/interfaces/IStructs.sol";
import "../src/Staking.sol";
@ -27,7 +28,8 @@ contract TestProtocolFees is
Staking
{
struct TestPool {
uint256 stake;
uint256 operatorStake;
uint256 membersStake;
mapping(address => bool) isMaker;
}
@ -58,13 +60,15 @@ contract TestProtocolFees is
/// @dev Create a test pool.
function createTestPool(
bytes32 poolId,
uint256 stake,
uint256 operatorStake,
uint256 membersStake,
address[] memory makerAddresses
)
public
{
TestPool storage pool = _testPools[poolId];
pool.stake = stake;
pool.operatorStake = operatorStake;
pool.membersStake = membersStake;
for (uint256 i = 0; i < makerAddresses.length; ++i) {
pool.isMaker[makerAddresses[i]] = true;
_makersToTestPoolIds[makerAddresses[i]] = poolId;
@ -86,10 +90,34 @@ contract TestProtocolFees is
view
returns (IStructs.StakeBalance memory balance)
{
uint256 stake = _testPools[poolId].stake;
TestPool memory pool = _testPools[poolId];
uint256 stake = pool.operatorStake + pool.membersStake;
return IStructs.StakeBalance({
currentEpochBalance: 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);
}
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> {
// cache balances
const initZrxBalanceOfVault = await this._stakingApiWrapper.utils.getZrxTokenBalanceOfZrxVaultAsync();

View File

@ -4,6 +4,7 @@ import {
expect,
filterLogsToArguments,
hexRandom,
Numberish,
randomAddress,
} from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
@ -54,9 +55,26 @@ blockchainTests('Protocol Fee Unit Tests', env => {
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();
await testContract.createTestPool.awaitTransactionSuccessAsync(poolId, stake, makers);
await testContract.createTestPool.awaitTransactionSuccessAsync(
poolId,
new BigNumber(_opts.operatorStake),
new BigNumber(_opts.membersStake),
_opts.makers,
);
return poolId;
}
@ -154,7 +172,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
}
it('should not transfer WETH if value is sent', async () => {
await createTestPoolAsync(minimumStake, []);
await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
@ -164,8 +182,8 @@ blockchainTests('Protocol Fee Unit Tests', env => {
assertNoWETHTransferLogs(receipt.logs);
});
it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
it('should credit pool if the maker is in a pool', async () => {
const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
@ -177,8 +195,8 @@ blockchainTests('Protocol Fee Unit Tests', env => {
expect(poolFees).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID);
});
it('should not update `protocolFeesThisEpochByPool` if maker is not in a pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, []);
it('should not credit the pool if maker is not in a pool', async () => {
const poolId = await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
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 () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async () => {
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
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 () => {
await createTestPoolAsync(minimumStake, []);
await createTestPoolAsync({ operatorStake: minimumStake });
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
@ -236,7 +254,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
});
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(
makerAddress,
payerAddress,
@ -249,7 +267,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
});
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(
makerAddress,
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 () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async () => {
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
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 () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const payAsync = async (inWETH: boolean) => {
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
@ -303,7 +321,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
describe('Multiple makers', () => {
it('fees paid to different makers in the same pool go to that pool', async () => {
const otherMakerAddress = randomAddress();
const poolId = await createTestPoolAsync(minimumStake, [makerAddress, otherMakerAddress]);
const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress, otherMakerAddress] });
const payAsync = async (_makerAddress: string) => {
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
_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 () => {
const [fee, otherFee] = _.times(2, () => getRandomPortion(DEFAULT_PROTOCOL_FEE_PAID));
const otherMakerAddress = randomAddress();
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
const otherPoolId = await createTestPoolAsync(minimumStake, [otherMakerAddress]);
const poolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [makerAddress] });
const otherPoolId = await createTestPoolAsync({ operatorStake: minimumStake, makers: [otherMakerAddress]});
const payAsync = async (_poolId: string, _makerAddress: string, _fee: BigNumber) => {
// prettier-ignore
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
@ -346,7 +364,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
describe('Dust stake', () => {
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(
makerAddress,
constants.NULL_ADDRESS,
@ -358,7 +376,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
});
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(
makerAddress,
constants.NULL_ADDRESS,
@ -370,7 +388,7 @@ blockchainTests('Protocol Fee Unit Tests', env => {
});
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(
makerAddress,
constants.NULL_ADDRESS,

View File

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

View File

@ -7,7 +7,7 @@ blockchainTests('delegator rewards', env => {
before(async () => {
testContract = await TestDelegatorRewardsContract.deployFrom0xArtifactAsync(
artifacts.TestLibFixedMath,
artifacts.TestDelegatorRewards,
env.provider,
env.txDefaults,
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);
});
});
});