@0x/contracts-staking: Rename Tuned event to ParamsChanged.

`@0x/contracts-staking`: Merge `exchange_fees` unit tests into `protocol_fees` unit tests.
`@0x/contracts-staking`: Remove `ProtocolFeeActor` and any use of it.
`@0x/contracts-staking`: Remove unused constants.
`@0x/contracts-staking`: Move WETH assertion constructor into `MixinDeploymentConstants`.
`@0x/contracts-staking`: Add more unit tests.
This commit is contained in:
Lawrence Forman 2019-09-10 00:25:10 -04:00
parent d8d791e4f0
commit 2ed39cd18d
24 changed files with 490 additions and 528 deletions

View File

@ -26,6 +26,7 @@ import "../libs/LibStakingRichErrors.sol";
import "../libs/LibFixedMath.sol";
import "../immutable/MixinStorage.sol";
import "../immutable/MixinConstants.sol";
import "../immutable/MixinDeploymentConstants.sol";
import "../interfaces/IStakingEvents.sol";
import "../interfaces/IStructs.sol";
import "../stake/MixinStakeBalances.sol";
@ -47,6 +48,7 @@ import "./MixinExchangeManager.sol";
contract MixinExchangeFees is
IStakingEvents,
MixinConstants,
MixinDeploymentConstants,
Ownable,
MixinStorage,
MixinZrxVault,
@ -111,7 +113,7 @@ contract MixinExchangeFees is
if (poolStake >= minimumPoolStake) {
// Credit the pool.
uint256 _feesCollectedThisEpoch = protocolFeesThisEpochByPool[poolId];
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(amount);
protocolFeesThisEpochByPool[poolId] = _feesCollectedThisEpoch.safeAdd(protocolFeePaid);
if (_feesCollectedThisEpoch == 0) {
activePoolsThisEpoch.push(poolId);
}

View File

@ -39,7 +39,4 @@ contract MixinConstants
uint64 constant internal INITIAL_EPOCH = 0;
uint256 constant internal MIN_TOKEN_VALUE = 10**18;
// TODO(dorothy-zbornak): Remove when signatures are removed from maker handshake.
uint256 constant internal CHAIN_ID = 1;
}

View File

@ -18,20 +18,19 @@
pragma solidity ^0.5.9;
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol";
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "../libs/LibStakingRichErrors.sol";
// solhint-disable separate-by-one-line-in-contract
contract MixinDeploymentConstants {
using LibBytes for bytes;
// @TODO SET THESE VALUES FOR DEPLOYMENT
uint256 constant internal EPOCH_DURATION_IN_SECONDS = 1000;
uint256 constant internal TIMELOCK_DURATION_IN_EPOCHS = 3;
// How much delegated stake is weighted vs operator stake, in ppm.
uint32 constant internal REWARD_DELEGATED_STAKE_WEIGHT = 900000; // 90%
uint256 constant internal CHAIN_ID = 1;
// Mainnet WETH9 Address
address constant internal WETH_ADDRESS = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
@ -49,4 +48,14 @@ contract MixinDeploymentConstants {
// Ropsten & Rinkeby Weth Asset Data
// bytes constant internal WETH_ASSET_DATA = hex"f47261b0000000000000000000000000c778417e063141139fce010982780140aa0cd5ab";
/// @dev Ensures that the WETH_ASSET_DATA is correct.
constructor() public {
// Ensure that the WETH_ASSET_DATA is correct.
if (!WETH_ASSET_DATA.equals(
abi.encodeWithSelector(IAssetData(address(0)).ERC20Token.selector, WETH_ADDRESS)
)) {
LibRichErrors.rrevert(LibStakingRichErrors.InvalidWethAssetDataError());
}
}
}

View File

@ -18,9 +18,7 @@
pragma solidity ^0.5.9;
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetData.sol";
import "@0x/contracts-asset-proxy/contracts/src/interfaces/IAssetProxy.sol";
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "@0x/contracts-utils/contracts/src/LibRichErrors.sol";
import "@0x/contracts-utils/contracts/src/Ownable.sol";
import "./MixinConstants.sol";
@ -36,20 +34,6 @@ contract MixinStorage is
MixinConstants,
Ownable
{
using LibBytes for bytes;
/// @dev Ensures that the WETH_ASSET_DATA is correct.
constructor()
public
Ownable()
{
// Ensure that the WETH_ASSET_DATA from MixinDeploymentConstants is correct.
if (!WETH_ASSET_DATA.equals(
abi.encodeWithSelector(IAssetData(address(0)).ERC20Token.selector, WETH_ADDRESS)
)) {
LibRichErrors.rrevert(LibStakingRichErrors.InvalidWethAssetDataError());
}
}
// WETH Asset Proxy
IAssetProxy internal wethAssetProxy;

View File

@ -53,14 +53,14 @@ interface IStakingEvents {
uint256 earliestEndTimeInSeconds
);
/// @dev Emitted whenever hyperparameters are changed via the `tune()` function.
/// @dev Emitted whenever staking parameters are changed via the `setParams()` function.
/// @param epochDurationInSeconds Minimum seconds between epochs.
/// @param rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm.
/// @param minimumPoolStake Minimum amount of stake required in a pool to collect rewards.
/// @param maximumMakersInPool Maximum number of maker addresses allowed to be registered to a pool.
/// @param cobbDouglasAlphaNumerator Numerator for cobb douglas alpha factor.
/// @param cobbDouglasAlphaDenomintor Denominator for cobb douglas alpha factor.
event Tuned(
event ParamsChanged(
uint256 epochDurationInSeconds,
uint32 rewardDelegatedStakeWeight,
uint256 minimumPoolStake,

View File

@ -65,7 +65,7 @@ contract MixinParams is
cobbDouglasAlphaNumerator = _cobbDouglasAlphaNumerator;
cobbDouglasAlphaDenomintor = _cobbDouglasAlphaDenomintor;
emit Tuned(
emit ParamsChanged(
epochDurationInSeconds,
rewardDelegatedStakeWeight,
minimumPoolStake,

View File

@ -1,78 +0,0 @@
/*
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/Staking.sol";
contract TestExchangeFees is
Staking
{
struct TestPool {
uint256 stake;
mapping(address => bool) isMaker;
}
mapping(bytes32 => TestPool) private _testPools;
mapping(address => bytes32) private _makersToTestPoolIds;
constructor(address exchangeAddress) public {
validExchanges[exchangeAddress] = true;
_initMixinParams();
}
/// @dev Create a test pool.
function createTestPool(
bytes32 poolId,
uint256 stake,
address[] memory makerAddresses
)
public
{
TestPool storage pool = _testPools[poolId];
pool.stake = stake;
for (uint256 i = 0; i < makerAddresses.length; ++i) {
pool.isMaker[makerAddresses[i]] = true;
_makersToTestPoolIds[makerAddresses[i]] = poolId;
}
}
/// @dev Overridden to use test pools.
function getStakingPoolIdOfMaker(address makerAddress)
public
view
returns (bytes32)
{
return _makersToTestPoolIds[makerAddress];
}
/// @dev Overridden to use test pools.
function getTotalStakeDelegatedToPool(bytes32 poolId)
public
view
returns (IStructs.StakeBalance memory balance)
{
uint256 stake = _testPools[poolId].stake;
return IStructs.StakeBalance({
currentEpochBalance: stake,
nextEpochBalance: stake
});
}
}

View File

@ -26,10 +26,18 @@ import "../src/Staking.sol";
contract TestProtocolFees is
Staking
{
function setWethProxy(address wethProxyAddress)
external
{
struct TestPool {
uint256 stake;
mapping(address => bool) isMaker;
}
mapping(bytes32 => TestPool) private _testPools;
mapping(address => bytes32) private _makersToTestPoolIds;
constructor(address exchangeAddress, address wethProxyAddress) public {
validExchanges[exchangeAddress] = true;
wethAssetProxy = IAssetProxy(wethProxyAddress);
_initMixinParams();
}
function addMakerToPool(bytes32 poolId, address makerAddress)
@ -39,6 +47,10 @@ contract TestProtocolFees is
poolJoinedByMakerAddress[makerAddress].confirmed = true;
}
function getWethAssetData() external pure returns (bytes memory) {
return WETH_ASSET_DATA;
}
function getActivePoolsByEpoch()
external
view
@ -46,4 +58,42 @@ contract TestProtocolFees is
{
return activePoolsThisEpoch;
}
/// @dev Create a test pool.
function createTestPool(
bytes32 poolId,
uint256 stake,
address[] memory makerAddresses
)
public
{
TestPool storage pool = _testPools[poolId];
pool.stake = stake;
for (uint256 i = 0; i < makerAddresses.length; ++i) {
pool.isMaker[makerAddresses[i]] = true;
_makersToTestPoolIds[makerAddresses[i]] = poolId;
}
}
/// @dev Overridden to use test pools.
function getStakingPoolIdOfMaker(address makerAddress)
public
view
returns (bytes32)
{
return _makersToTestPoolIds[makerAddress];
}
/// @dev Overridden to use test pools.
function getTotalStakeDelegatedToPool(bytes32 poolId)
public
view
returns (IStructs.StakeBalance memory balance)
{
uint256 stake = _testPools[poolId].stake;
return IStructs.StakeBalance({
currentEpochBalance: stake,
nextEpochBalance: stake
});
}
}

View File

@ -28,7 +28,7 @@ contract TestStakingProxy is
// solhint-disable no-empty-blocks
constructor(address _stakingContract)
public
StakingProxy(_stakingContract, address(0))
StakingProxy(_stakingContract, address(0), address(0))
{}
function getAttachedContract() external view returns (address) {

View File

@ -37,7 +37,7 @@
},
"config": {
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStructs|IVaultCore|IZrxVault|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinConstants|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestLibFixedMath|TestProtocolFees|TestProtocolFeesERC20Proxy|TestStaking|TestStorageLayout|ZrxVault).json"
"abis": "./generated-artifacts/@(EthVault|IEthVault|IStaking|IStakingEvents|IStakingPoolRewardVault|IStakingProxy|IStorageInit|IStructs|IVaultCore|IZrxVault|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinConstants|MixinDeploymentConstants|MixinEthVault|MixinExchangeFees|MixinExchangeManager|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewardVault|MixinStakingPoolRewards|MixinStorage|MixinVaultCore|MixinZrxVault|ReadOnlyProxy|Staking|StakingPoolRewardVault|StakingProxy|TestCobbDouglas|TestInitTarget|TestLibFixedMath|TestProtocolFees|TestProtocolFeesERC20Proxy|TestStaking|TestStakingProxy|TestStorageLayout|ZrxVault).json"
},
"repository": {
"type": "git",

View File

@ -21,6 +21,7 @@ import * as LibProxy from '../generated-artifacts/LibProxy.json';
import * as LibSafeDowncast from '../generated-artifacts/LibSafeDowncast.json';
import * as LibStakingRichErrors from '../generated-artifacts/LibStakingRichErrors.json';
import * as MixinConstants from '../generated-artifacts/MixinConstants.json';
import * as MixinDeploymentConstants from '../generated-artifacts/MixinDeploymentConstants.json';
import * as MixinEthVault from '../generated-artifacts/MixinEthVault.json';
import * as MixinExchangeFees from '../generated-artifacts/MixinExchangeFees.json';
import * as MixinExchangeManager from '../generated-artifacts/MixinExchangeManager.json';
@ -40,7 +41,6 @@ import * as Staking from '../generated-artifacts/Staking.json';
import * as StakingPoolRewardVault from '../generated-artifacts/StakingPoolRewardVault.json';
import * as StakingProxy from '../generated-artifacts/StakingProxy.json';
import * as TestCobbDouglas from '../generated-artifacts/TestCobbDouglas.json';
import * as TestExchangeFees from '../generated-artifacts/TestExchangeFees.json';
import * as TestInitTarget from '../generated-artifacts/TestInitTarget.json';
import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json';
import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json';
@ -56,6 +56,7 @@ export const artifacts = {
MixinExchangeFees: MixinExchangeFees as ContractArtifact,
MixinExchangeManager: MixinExchangeManager as ContractArtifact,
MixinConstants: MixinConstants as ContractArtifact,
MixinDeploymentConstants: MixinDeploymentConstants as ContractArtifact,
MixinStorage: MixinStorage as ContractArtifact,
IEthVault: IEthVault as ContractArtifact,
IStaking: IStaking as ContractArtifact,
@ -86,7 +87,6 @@ export const artifacts = {
StakingPoolRewardVault: StakingPoolRewardVault as ContractArtifact,
ZrxVault: ZrxVault as ContractArtifact,
TestCobbDouglas: TestCobbDouglas as ContractArtifact,
TestExchangeFees: TestExchangeFees as ContractArtifact,
TestInitTarget: TestInitTarget as ContractArtifact,
TestLibFixedMath: TestLibFixedMath as ContractArtifact,
TestProtocolFees: TestProtocolFees as ContractArtifact,

View File

@ -19,6 +19,7 @@ export * from '../generated-wrappers/lib_proxy';
export * from '../generated-wrappers/lib_safe_downcast';
export * from '../generated-wrappers/lib_staking_rich_errors';
export * from '../generated-wrappers/mixin_constants';
export * from '../generated-wrappers/mixin_deployment_constants';
export * from '../generated-wrappers/mixin_eth_vault';
export * from '../generated-wrappers/mixin_exchange_fees';
export * from '../generated-wrappers/mixin_exchange_manager';
@ -38,7 +39,6 @@ export * from '../generated-wrappers/staking';
export * from '../generated-wrappers/staking_pool_reward_vault';
export * from '../generated-wrappers/staking_proxy';
export * from '../generated-wrappers/test_cobb_douglas';
export * from '../generated-wrappers/test_exchange_fees';
export * from '../generated-wrappers/test_init_target';
export * from '../generated-wrappers/test_lib_fixed_math';
export * from '../generated-wrappers/test_protocol_fees';

View File

@ -1,129 +0,0 @@
import { constants, expect } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { LogWithDecodedArgs } from 'ethereum-types';
import { TestProtocolFeesContract, TestProtocolFeesERC20ProxyTransferFromCalledEventArgs } from '../../src';
export interface PayProtocolFeeArgs {
poolId: string;
makerAddress: string;
payerAddress: string;
protocolFeePaid: BigNumber;
from: string;
value: BigNumber;
}
// tslint:disable:no-unnecessary-type-assertion
export class ProtocolFeeActor {
private readonly _exchanges: string[];
private readonly _registered_makers: string[];
private readonly _protocolFees: TestProtocolFeesContract;
private readonly _wethAssetData = '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
constructor(exchanges: string[], makers: string[], protocolFees: TestProtocolFeesContract) {
this._exchanges = exchanges;
this._protocolFees = protocolFees;
this._registered_makers = makers;
}
/**
* This function will test the `payProtocolFee()` function, and will revert if the behavior deviates
* whatsoever from the expected behavior.
* @param makerAddress The address of the order's maker.
* @param payerAddress The address that is responsible for paying the protocol fee.
* @param protocolFeePaid The fee that should be paid to the staking contract.
* @param from The address that should send the transaction.
* @param value The amount of value that should be sent in the transaction.
*/
public async payProtocolFeeAsync(args: PayProtocolFeeArgs): Promise<void> {
// Get the original state to compare with afterwards
const originalActivePools = await this._protocolFees.getActivePoolsByEpoch.callAsync();
const originalProtocolFeesCollected = await this._protocolFees.getProtocolFeesThisEpochByPool.callAsync(
args.poolId,
);
// If the poolId is already registered, it should not be added to the active pools list. Otherwise, it should be added.
const shouldBeAdded = !originalActivePools.includes(args.poolId);
// Handle all of the failure cases.
const tx = this._protocolFees.payProtocolFee.awaitTransactionSuccessAsync(
args.makerAddress,
args.payerAddress,
args.protocolFeePaid,
{ from: args.from, value: args.value },
);
if (!this._exchanges.includes(args.from)) {
const expectedError = new StakingRevertErrors.OnlyCallableByExchangeError(args.from);
return expect(tx, 'should revert when the `from` address is not a registered exchange').to.revertWith(
expectedError,
);
} else if (args.protocolFeePaid.eq(0)) {
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid,
constants.ZERO_AMOUNT,
new BigNumber(args.value),
);
return expect(tx, 'should revert when the `protocolFeePaid` is zero').to.revertWith(expectedError);
} else if (!args.protocolFeePaid.eq(args.value) && !args.value.eq(0)) {
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment,
args.protocolFeePaid,
new BigNumber(args.value),
);
return expect(tx, 'should revert when the `protocolFeePaid` and the value are mismatched').to.revertWith(
expectedError,
);
}
// Call the transaction and collect the logs.
const receipt = await tx;
// If WETH should have been paid, an event should be logged. Otherwise, no event should have been logged.
if (args.value.eq(0)) {
// Ensure that one log was recorded.
expect(receipt.logs.length, 'log length should be one').to.be.eq(1);
// Ensure that the correct log was recorded.
const log = receipt.logs[0] as LogWithDecodedArgs<TestProtocolFeesERC20ProxyTransferFromCalledEventArgs>;
expect(log.event, 'log event should be `TransferFromCalled`').to.be.eq('TransferFromCalled');
expect(log.args.assetData, 'log `assetData` should be `wethAssetData`').to.be.eq(this._wethAssetData);
expect(log.args.amount, 'log `amount` should be `protocolFeePaid`').bignumber.to.be.eq(
args.protocolFeePaid,
);
expect(log.args.from, 'log `from` should be `payerAddress`').to.be.eq(args.payerAddress);
expect(log.args.to).to.be.eq(this._protocolFees.address);
} else {
expect(receipt.logs.length, 'log length should be zero').to.be.eq(0);
}
// Get the final state.
const finalActivePools = await this._protocolFees.getActivePoolsByEpoch.callAsync();
const finalProtocolFeesCollected = await this._protocolFees.getProtocolFeesThisEpochByPool.callAsync(
args.poolId,
);
// Check that the active pools list was updated appropriately.
if (shouldBeAdded && this._registered_makers.includes(args.makerAddress)) {
// Check that the pool id was added to the list of active pools for this epoch.
expect(finalActivePools.length, 'final active pools should have been updated').to.be.eq(
originalActivePools.length + 1,
);
expect(finalActivePools.includes(args.poolId), 'final active pools should contain pool id').to.be.true();
} else {
// Check that active pools list was not altered.
expect(finalActivePools, 'final active pools should be identical to original active pools').to.be.deep.eq(
originalActivePools,
);
}
// Check that the pool has the correct amount of fees attributed to it for this epoch.
if (this._registered_makers.includes(args.makerAddress)) {
expect(
finalProtocolFeesCollected,
'final protocol fees should be the original protocol fees plus the fee paid',
).bignumber.to.be.eq(originalProtocolFeesCollected.plus(args.protocolFeePaid));
}
}
}
// tslint:enable:no-unnecessary-type-assertion

View File

@ -28,8 +28,8 @@ blockchainTests('Epochs', env => {
describe('Epochs & TimeLocks', () => {
it('basic epochs & timeLock periods', async () => {
///// 1/3 Validate Assumptions /////
expect(await stakingApiWrapper.stakingContract.getEpochDurationInSeconds.callAsync()).to.be.bignumber.equal(
stakingConstants.DEFAULT_HYPER_PARAMETERS.epochDurationInSeconds,
expect((await stakingApiWrapper.utils.getParamsAsync()).epochDurationInSeconds).to.be.bignumber.equal(
stakingConstants.DEFAULT_PARAMS.epochDurationInSeconds,
);
///// 2/3 Validate Initial Epoch & TimeLock Period /////
{

View File

@ -1,63 +0,0 @@
import { blockchainTests, constants, expect, hexRandom } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { artifacts, TestExchangeFeesContract } from '../src/';
blockchainTests.resets('Fees tests', env => {
let testContract: TestExchangeFeesContract;
before(async () => {
const [ownerAddress] = await env.getAccountAddressesAsync();
testContract = await TestExchangeFeesContract.deployFrom0xArtifactAsync(
artifacts.TestExchangeFees,
env.provider,
env.txDefaults,
artifacts,
ownerAddress,
);
});
describe('payProtocolFee()', () => {
let minimumStake: BigNumber;
function randomAddress(): string {
return hexRandom(constants.ADDRESS_LENGTH);
}
before(async () => {
minimumStake = (await testContract.getParams.callAsync())[2];
});
it('credits pools with stake == minimum', async () => {
const makerAddress = randomAddress();
const feePaid = new BigNumber(1e18);
const poolId = hexRandom();
await testContract.createTestPool.awaitTransactionSuccessAsync(poolId, minimumStake, [makerAddress]);
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
constants.NULL_ADDRESS,
feePaid,
{ value: feePaid },
);
const feesCredited = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(feesCredited).to.bignumber.eq(feePaid);
});
it('does not credit pools with stake < minimum', async () => {
const stake = minimumStake.minus(1);
const makerAddress = randomAddress();
const feePaid = new BigNumber(1e18);
const poolId = hexRandom();
await testContract.createTestPool.awaitTransactionSuccessAsync(poolId, stake, [makerAddress]);
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
constants.NULL_ADDRESS,
feePaid,
{ value: feePaid },
);
const feesCredited = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(feesCredited).to.bignumber.eq(0);
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@ -1,8 +1,14 @@
import { blockchainTests, constants, expect, hexRandom } from '@0x/contracts-test-utils';
import { blockchainTests, constants, expect, filterLogsToArguments, hexRandom } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber, OwnableRevertErrors, StringRevertError } from '@0x/utils';
import { artifacts, StakingContract, TestInitTargetContract, TestStakingProxyContract } from '../src/';
import {
artifacts,
StakingContract,
TestInitTargetContract,
TestStakingProxyContract,
TestStakingProxyStakingContractAttachedToProxyEventArgs,
} from '../src/';
blockchainTests('Migration tests', env => {
let ownerAddress: string;
@ -99,6 +105,21 @@ blockchainTests('Migration tests', env => {
await assertInitStateAsync(proxyContract);
});
it('emits a `StakingContractAttached` event', async () => {
const receipt = await proxyContract.attachStakingContract.awaitTransactionSuccessAsync(
initTargetContract.address,
);
const logsArgs = filterLogsToArguments<TestStakingProxyStakingContractAttachedToProxyEventArgs>(
receipt.logs,
'StakingContractAttached',
);
expect(logsArgs.length).to.eq(1);
for (const args of logsArgs) {
expect(args.newStakingContractAddress).to.eq(initTargetContract.address);
}
await assertInitStateAsync(proxyContract);
});
it('reverts if init() reverts', async () => {
await enableInitRevertsAsync();
const tx = proxyContract.attachStakingContract.awaitTransactionSuccessAsync(initTargetContract.address);

View File

@ -2,7 +2,7 @@ import { blockchainTests, constants, expect, filterLogsToArguments, Numberish }
import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber, OwnableRevertErrors } from '@0x/utils';
import { artifacts, IStakingEventsTunedEventArgs, MixinParamsContract } from '../src/';
import { artifacts, IStakingEventsParamsChangedEventArgs, MixinParamsContract } from '../src/';
blockchainTests('Configurable Parameters', env => {
let testContract: MixinParamsContract;
@ -20,7 +20,7 @@ blockchainTests('Configurable Parameters', env => {
});
blockchainTests.resets('setParams()', () => {
interface HyperParameters {
interface Params {
epochDurationInSeconds: Numberish;
rewardDelegatedStakeWeight: Numberish;
minimumPoolStake: Numberish;
@ -34,13 +34,13 @@ blockchainTests('Configurable Parameters', env => {
const DEFAULT_PARAMS = {
epochDurationInSeconds: TWO_WEEKS,
rewardDelegatedStakeWeight: PPM_90_PERCENT,
minimumPoolStake: '100e18',
minimumPoolStake: constants.DUMMY_TOKEN_DECIMALS.times(100),
maximumMakersInPool: 10,
cobbDouglasAlphaNumerator: 1,
cobbDouglasAlphaDenomintor: 2,
};
async function setParamsAndAssertAsync(params: Partial<HyperParameters>, from?: string): Promise<void> {
async function setParamsAndAssertAsync(params: Partial<Params>, from?: string): Promise<void> {
const _params = {
...DEFAULT_PARAMS,
...params,
@ -56,7 +56,7 @@ blockchainTests('Configurable Parameters', env => {
);
// Assert event.
expect(receipt.logs.length).to.eq(1);
const event = filterLogsToArguments<IStakingEventsTunedEventArgs>(receipt.logs, 'Tuned')[0];
const event = filterLogsToArguments<IStakingEventsParamsChangedEventArgs>(receipt.logs, 'ParamsChanged')[0];
expect(event.epochDurationInSeconds).to.bignumber.eq(_params.epochDurationInSeconds);
expect(event.rewardDelegatedStakeWeight).to.bignumber.eq(_params.rewardDelegatedStakeWeight);
expect(event.minimumPoolStake).to.bignumber.eq(_params.minimumPoolStake);

View File

@ -321,10 +321,7 @@ blockchainTests('Staking Pool Management', env => {
const operatorShare = (39 / 100) * PPM_DENOMINATOR;
const poolOperator = new PoolOperatorActor(operatorAddress, stakingApiWrapper);
const makerAddresses = users.slice(
1,
stakingConstants.DEFAULT_PARAMS.maximumMakersInPool.toNumber() + 2,
);
const makerAddresses = users.slice(1, stakingConstants.DEFAULT_PARAMS.maximumMakersInPool.toNumber() + 2);
const makers = makerAddresses.map(makerAddress => new MakerActor(makerAddress, stakingApiWrapper));
// create pool

View File

@ -1,48 +1,27 @@
import { blockchainTests, constants } from '@0x/contracts-test-utils';
import { blockchainTests, constants, expect, filterLogsToArguments, hexRandom } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { LogEntry } from 'ethereum-types';
import * as _ from 'lodash';
import { artifacts, TestProtocolFeesContract, TestProtocolFeesERC20ProxyContract } from '../src';
import {
artifacts,
TestProtocolFeesContract,
TestProtocolFeesERC20ProxyContract,
TestProtocolFeesERC20ProxyTransferFromCalledEventArgs,
} from '../src';
import { ProtocolFeeActor } from './actors/protocol_fee_actor';
import { getRandomPortion } from './utils/number_utils';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests('Protocol Fee Unit Tests', env => {
// The accounts that will be used during testing.
let owner: string;
let exchange: string;
let nonExchange: string;
let makerAddress: string;
let payerAddress: string;
// The actor that will be used for testng `payProtocolFee` and `_unwrapETH`.
let protocolFeeActor: ProtocolFeeActor;
// The default protocol fee that will be paid -- a somewhat realistic value.
const DEFAULT_PROTOCOL_FEE_PAID = new BigNumber(150000).times(10000000);
// The default pool Id that will be used.
const DEFAULT_POOL_ID = '0x0000000000000000000000000000000000000000000000000000000000000001';
let ownerAddress: string;
let exchangeAddress: string;
let notExchangeAddress: string;
let testContract: TestProtocolFeesContract;
let wethAssetData: string;
before(async () => {
// Get accounts to represent the exchange and an address that is not a registered exchange.
[
owner,
exchange,
nonExchange,
makerAddress,
payerAddress,
] = (await env.web3Wrapper.getAvailableAddressesAsync()).slice(0, 6);
// Deploy the protocol fees contract.
const protocolFees = await TestProtocolFeesContract.deployFrom0xArtifactAsync(
artifacts.TestProtocolFees,
env.provider,
{
...env.txDefaults,
from: owner,
},
artifacts,
);
[ownerAddress, exchangeAddress, notExchangeAddress] = await env.web3Wrapper.getAvailableAddressesAsync();
// Deploy the erc20Proxy for testing.
const proxy = await TestProtocolFeesERC20ProxyContract.deployFrom0xArtifactAsync(
@ -52,187 +31,349 @@ blockchainTests('Protocol Fee Unit Tests', env => {
{},
);
// Register the test ERC20Proxy in the exchange.
await protocolFees.setWethProxy.awaitTransactionSuccessAsync(proxy.address);
// Deploy the protocol fees contract.
testContract = await TestProtocolFeesContract.deployFrom0xArtifactAsync(
artifacts.TestProtocolFees,
env.provider,
{
...env.txDefaults,
from: ownerAddress,
},
artifacts,
exchangeAddress,
proxy.address,
);
// Register an exchange in the protocol fee contract.
await protocolFees.addExchangeAddress.awaitTransactionSuccessAsync(exchange, { from: owner });
// "Register" the makerAddress in the default pool.
await protocolFees.addMakerToPool.awaitTransactionSuccessAsync(DEFAULT_POOL_ID, makerAddress);
// Initialize the protocol fee actor.
protocolFeeActor = new ProtocolFeeActor([exchange], [makerAddress], protocolFees);
wethAssetData = await testContract.getWethAssetData.callAsync();
});
blockchainTests.resets('payProtocolFee', () => {
it('should revert if called by a non-exchange', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: nonExchange,
value: constants.ZERO_AMOUNT,
async function createTestPoolAsync(stake: BigNumber, makers: string[]): Promise<string> {
const poolId = hexRandom();
await testContract.createTestPool.awaitTransactionSuccessAsync(poolId, stake, makers);
return poolId;
}
blockchainTests.resets('payProtocolFee()', () => {
const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH);
const DEFAULT_PROTOCOL_FEE_PAID = new BigNumber(150e3).times(1e9);
const { ZERO_AMOUNT } = constants;
const makerAddress = randomAddress();
const payerAddress = randomAddress();
let minimumStake: BigNumber;
before(async () => {
minimumStake = (await testContract.getParams.callAsync())[2];
});
describe('forbidden actions', () => {
it('should revert if called by a non-exchange', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: notExchangeAddress },
);
const expectedError = new StakingRevertErrors.OnlyCallableByExchangeError(notExchangeAddress);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `protocolFeePaid` is zero with zero value sent', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
ZERO_AMOUNT,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid,
ZERO_AMOUNT,
ZERO_AMOUNT,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `protocolFeePaid` is zero with non-zero value sent', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
ZERO_AMOUNT,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid,
ZERO_AMOUNT,
DEFAULT_PROTOCOL_FEE_PAID,
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `protocolFeePaid` is < than the provided message value', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID.minus(1) },
);
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment,
DEFAULT_PROTOCOL_FEE_PAID,
DEFAULT_PROTOCOL_FEE_PAID.minus(1),
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if `protocolFeePaid` is > than the provided message value', async () => {
const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID.plus(1) },
);
const expectedError = new StakingRevertErrors.InvalidProtocolFeePaymentError(
StakingRevertErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment,
DEFAULT_PROTOCOL_FEE_PAID,
DEFAULT_PROTOCOL_FEE_PAID.plus(1),
);
return expect(tx).to.revertWith(expectedError);
});
});
it('should revert if `protocolFeePaid` is zero with zero value sent', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: constants.ZERO_AMOUNT,
from: exchange,
value: constants.ZERO_AMOUNT,
describe('ETH fees', () => {
function assertNoWETHTransferLogs(logs: LogEntry[]): void {
const logsArgs = filterLogsToArguments<TestProtocolFeesERC20ProxyTransferFromCalledEventArgs>(
logs,
'TransferFromCalled',
);
expect(logsArgs).to.deep.eq([]);
}
it('should not transfer WETH if value is sent', async () => {
await createTestPoolAsync(minimumStake, []);
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
assertNoWETHTransferLogs(receipt.logs);
});
it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
assertNoWETHTransferLogs(receipt.logs);
const poolFees = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
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, []);
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
assertNoWETHTransferLogs(receipt.logs);
const poolFees = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(poolFees).to.bignumber.eq(ZERO_AMOUNT);
});
it('fees paid to the same maker should go to the same pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
const payAsync = async () => {
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
assertNoWETHTransferLogs(receipt.logs);
};
await payAsync();
await payAsync();
const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2);
const poolFees = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(poolFees).to.bignumber.eq(expectedTotalFees);
});
});
it('should revert if `protocolFeePaid` is zero with non-zero value sent', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: constants.ZERO_AMOUNT,
from: exchange,
value: DEFAULT_PROTOCOL_FEE_PAID,
describe('WETH fees', () => {
function assertWETHTransferLogs(logs: LogEntry[], fromAddress: string, amount: BigNumber): void {
const logsArgs = filterLogsToArguments<TestProtocolFeesERC20ProxyTransferFromCalledEventArgs>(
logs,
'TransferFromCalled',
);
expect(logsArgs.length).to.eq(1);
for (const args of logsArgs) {
expect(args.assetData).to.eq(wethAssetData);
expect(args.from).to.eq(fromAddress);
expect(args.to).to.eq(testContract.address);
expect(args.amount).to.bignumber.eq(amount);
}
}
it('should transfer WETH if no value is sent and the maker is not in a pool', async () => {
await createTestPoolAsync(minimumStake, []);
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID);
});
it('should update `protocolFeesThisEpochByPool` if the maker is in a pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID);
const poolFees = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
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, []);
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID);
const poolFees = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(poolFees).to.bignumber.eq(ZERO_AMOUNT);
});
it('fees paid to the same maker should go to the same pool', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
const payAsync = async () => {
const receipt = await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: ZERO_AMOUNT },
);
assertWETHTransferLogs(receipt.logs, payerAddress, DEFAULT_PROTOCOL_FEE_PAID);
};
await payAsync();
await payAsync();
const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2);
const poolFees = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(poolFees).to.bignumber.eq(expectedTotalFees);
});
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 payAsync = async (inWETH: boolean) => {
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{
from: exchangeAddress,
value: inWETH ? ZERO_AMOUNT : DEFAULT_PROTOCOL_FEE_PAID,
},
);
};
await payAsync(true);
await payAsync(false);
const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2);
const poolFees = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(poolFees).to.bignumber.eq(expectedTotalFees);
});
});
it('should revert if `protocolFeePaid` is different than the provided message value', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID.minus(50),
from: exchange,
value: DEFAULT_PROTOCOL_FEE_PAID,
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 payAsync = async (_makerAddress: string) => {
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
_makerAddress,
payerAddress,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
};
await payAsync(makerAddress);
await payAsync(otherMakerAddress);
const expectedTotalFees = DEFAULT_PROTOCOL_FEE_PAID.times(2);
const poolFees = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(poolFees).to.bignumber.eq(expectedTotalFees);
});
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 payAsync = async (_poolId: string, _makerAddress: string, _fee: BigNumber) => {
// prettier-ignore
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
_makerAddress,
payerAddress,
_fee,
{ from: exchangeAddress, value: _fee },
);
};
await payAsync(poolId, makerAddress, fee);
await payAsync(otherPoolId, otherMakerAddress, otherFee);
const [poolFees, otherPoolFees] = await Promise.all([
testContract.getProtocolFeesThisEpochByPool.callAsync(poolId),
testContract.getProtocolFeesThisEpochByPool.callAsync(otherPoolId),
]);
expect(poolFees).to.bignumber.eq(fee);
expect(otherPoolFees).to.bignumber.eq(otherFee);
});
});
it('should call `transferFrom` in the proxy if no value is sent and the maker is not in a pool', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress: payerAddress, // This is an unregistered maker address
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: constants.ZERO_AMOUNT,
});
});
it('should call `transferFrom` in the proxy and update `protocolFeesThisEpochByPool` if no value is sent and the maker is in a pool', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: constants.ZERO_AMOUNT,
});
});
it('should not call `transferFrom` in the proxy and should not update `protocolFeesThisEpochByPool` if value is sent and the maker is not in a pool', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress: payerAddress, // This is an unregistered maker address
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: DEFAULT_PROTOCOL_FEE_PAID,
});
});
it('should not call `transferFrom` in the proxy and should update `protocolFeesThisEpochByPool` if value is sent and the maker is in a pool', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: DEFAULT_PROTOCOL_FEE_PAID,
});
});
it('should only have one active pool if a fee is paid on behalf of one maker ETH twice', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: DEFAULT_PROTOCOL_FEE_PAID,
describe('Dust stake', () => {
it('credits pools with stake > minimum', async () => {
const poolId = await createTestPoolAsync(minimumStake.plus(1), [makerAddress]);
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
constants.NULL_ADDRESS,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
const feesCredited = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(feesCredited).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID);
});
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: DEFAULT_PROTOCOL_FEE_PAID,
});
});
it('should only have one active pool if a fee is paid on behalf of one maker in WETH and then ETH', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: constants.ZERO_AMOUNT,
it('credits pools with stake == minimum', async () => {
const poolId = await createTestPoolAsync(minimumStake, [makerAddress]);
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
constants.NULL_ADDRESS,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
const feesCredited = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(feesCredited).to.bignumber.eq(DEFAULT_PROTOCOL_FEE_PAID);
});
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: DEFAULT_PROTOCOL_FEE_PAID,
});
});
it('should only have one active pool if a fee is paid on behalf of one maker in ETH and then WETH', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: DEFAULT_PROTOCOL_FEE_PAID,
});
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: constants.ZERO_AMOUNT,
});
});
it('should only have one active pool if a fee is paid on behalf of one maker in WETH twice', async () => {
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: constants.ZERO_AMOUNT,
});
await protocolFeeActor.payProtocolFeeAsync({
poolId: DEFAULT_POOL_ID,
makerAddress,
payerAddress,
protocolFeePaid: DEFAULT_PROTOCOL_FEE_PAID,
from: exchange,
value: constants.ZERO_AMOUNT,
it('does not credit pools with stake < minimum', async () => {
const poolId = await createTestPoolAsync(minimumStake.minus(1), [makerAddress]);
await testContract.payProtocolFee.awaitTransactionSuccessAsync(
makerAddress,
constants.NULL_ADDRESS,
DEFAULT_PROTOCOL_FEE_PAID,
{ from: exchangeAddress, value: DEFAULT_PROTOCOL_FEE_PAID },
);
const feesCredited = await testContract.getProtocolFeesThisEpochByPool.callAsync(poolId);
expect(feesCredited).to.bignumber.eq(0);
});
});
});
});
// tslint:enable:no-unnecessary-type-assertion

View File

@ -41,8 +41,8 @@ blockchainTests.resets('Testing Rewards', env => {
erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner);
// deploy staking contracts
stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper, artifacts.TestStaking);
// set up hyper-parameters
await stakingApiWrapper.stakingContract.setParamsAsync({
// set up staking parameters
await stakingApiWrapper.utils.setParamsAsync({
minimumPoolStake: new BigNumber(0),
cobbDouglasAlphaNumerator: new BigNumber(1),
cobbDouglasAlphaDenomintor: new BigNumber(6),

View File

@ -16,6 +16,9 @@ import {
ZrxVaultContract,
} from '../../src';
import { constants as stakingConstants } from './constants';
import { StakingParams } from './types';
export class StakingApiWrapper {
public stakingContractAddress: string; // The address of the real Staking.sol contract
public stakingContract: StakingContract; // The StakingProxy.sol contract wrapped as a StakingContract to borrow API
@ -28,7 +31,7 @@ export class StakingApiWrapper {
// Epoch Utils
fastForwardToNextEpochAsync: async (): Promise<void> => {
// increase timestamp of next block
const epochDurationInSeconds = await this.stakingContract.getEpochDurationInSeconds.callAsync();
const { epochDurationInSeconds } = await this.utils.getParamsAsync();
await this._web3Wrapper.increaseTimeAsync(epochDurationInSeconds.toNumber());
// mine next block
await this._web3Wrapper.mineBlockAsync();
@ -63,6 +66,35 @@ export class StakingApiWrapper {
getZrxTokenBalanceOfZrxVaultAsync: async (): Promise<BigNumber> => {
return this.zrxTokenContract.balanceOf.callAsync(this.zrxVaultContract.address);
},
setParamsAsync: async (params: Partial<StakingParams>): Promise<TransactionReceiptWithDecodedLogs> => {
const _params = {
...stakingConstants.DEFAULT_PARAMS,
...params,
};
return this.stakingContract.setParams.awaitTransactionSuccessAsync(
_params.epochDurationInSeconds,
_params.rewardDelegatedStakeWeight,
_params.minimumPoolStake,
_params.maximumMakersInPool,
_params.cobbDouglasAlphaNumerator,
_params.cobbDouglasAlphaDenomintor,
);
},
getParamsAsync: async (): Promise<StakingParams> => {
return (_.zipObject(
[
'epochDurationInSeconds',
'rewardDelegatedStakeWeight',
'minimumPoolStake',
'maximumMakersInPool',
'cobbDouglasAlphaNumerator',
'cobbDouglasAlphaDenomintor',
],
await this.stakingContract.getParams.callAsync(),
) as any) as StakingParams;
},
};
private readonly _web3Wrapper: Web3Wrapper;

View File

@ -10,7 +10,6 @@ export const constants = {
NIL_POOL_ID: '0x0000000000000000000000000000000000000000000000000000000000000000',
NIL_ADDRESS: '0x0000000000000000000000000000000000000000',
INITIAL_EPOCH: new BigNumber(0),
CHAIN_ID: 1,
DEFAULT_PARAMS: {
epochDurationInSeconds: new BigNumber(TWO_WEEKS),
rewardDelegatedStakeWeight: new BigNumber(0.9 * 1e6), // 90%

View File

@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
import { constants } from './constants';
export interface HyperParameters {
export interface StakingParams {
epochDurationInSeconds: BigNumber;
rewardDelegatedStakeWeight: BigNumber;
minimumPoolStake: BigNumber;

View File

@ -19,6 +19,7 @@
"generated-artifacts/LibSafeDowncast.json",
"generated-artifacts/LibStakingRichErrors.json",
"generated-artifacts/MixinConstants.json",
"generated-artifacts/MixinDeploymentConstants.json",
"generated-artifacts/MixinEthVault.json",
"generated-artifacts/MixinExchangeFees.json",
"generated-artifacts/MixinExchangeManager.json",
@ -38,7 +39,6 @@
"generated-artifacts/StakingPoolRewardVault.json",
"generated-artifacts/StakingProxy.json",
"generated-artifacts/TestCobbDouglas.json",
"generated-artifacts/TestExchangeFees.json",
"generated-artifacts/TestInitTarget.json",
"generated-artifacts/TestLibFixedMath.json",
"generated-artifacts/TestProtocolFees.json",