diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index dcfcd0a2a6..9609d79d27 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -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); } diff --git a/contracts/staking/contracts/src/immutable/MixinConstants.sol b/contracts/staking/contracts/src/immutable/MixinConstants.sol index c5fcd358a9..50df13aa0f 100644 --- a/contracts/staking/contracts/src/immutable/MixinConstants.sol +++ b/contracts/staking/contracts/src/immutable/MixinConstants.sol @@ -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; } diff --git a/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol b/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol index 9214976a65..53d716e318 100644 --- a/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol +++ b/contracts/staking/contracts/src/immutable/MixinDeploymentConstants.sol @@ -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()); + } + } } diff --git a/contracts/staking/contracts/src/immutable/MixinStorage.sol b/contracts/staking/contracts/src/immutable/MixinStorage.sol index fdce971fd3..677ac62204 100644 --- a/contracts/staking/contracts/src/immutable/MixinStorage.sol +++ b/contracts/staking/contracts/src/immutable/MixinStorage.sol @@ -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; diff --git a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol index 56450c571e..2cac7d812a 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingEvents.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingEvents.sol @@ -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, diff --git a/contracts/staking/contracts/src/sys/MixinParams.sol b/contracts/staking/contracts/src/sys/MixinParams.sol index 88adff12d8..efe8b23706 100644 --- a/contracts/staking/contracts/src/sys/MixinParams.sol +++ b/contracts/staking/contracts/src/sys/MixinParams.sol @@ -65,7 +65,7 @@ contract MixinParams is cobbDouglasAlphaNumerator = _cobbDouglasAlphaNumerator; cobbDouglasAlphaDenomintor = _cobbDouglasAlphaDenomintor; - emit Tuned( + emit ParamsChanged( epochDurationInSeconds, rewardDelegatedStakeWeight, minimumPoolStake, diff --git a/contracts/staking/contracts/test/TestExchangeFees.sol b/contracts/staking/contracts/test/TestExchangeFees.sol deleted file mode 100644 index 98ac0af184..0000000000 --- a/contracts/staking/contracts/test/TestExchangeFees.sol +++ /dev/null @@ -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 - }); - } -} diff --git a/contracts/staking/contracts/test/TestProtocolFees.sol b/contracts/staking/contracts/test/TestProtocolFees.sol index a0141e1ed1..4a543405c6 100644 --- a/contracts/staking/contracts/test/TestProtocolFees.sol +++ b/contracts/staking/contracts/test/TestProtocolFees.sol @@ -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 + }); + } } diff --git a/contracts/staking/contracts/test/TestStakingProxy.sol b/contracts/staking/contracts/test/TestStakingProxy.sol index f071cf32c3..5e50267811 100644 --- a/contracts/staking/contracts/test/TestStakingProxy.sol +++ b/contracts/staking/contracts/test/TestStakingProxy.sol @@ -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) { diff --git a/contracts/staking/package.json b/contracts/staking/package.json index 17edfc5af8..9aef1de811 100644 --- a/contracts/staking/package.json +++ b/contracts/staking/package.json @@ -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", diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index 2dde2533ae..acb5e69e94 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -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, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index ca5d98f4ba..e848e941f0 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -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'; diff --git a/contracts/staking/test/actors/protocol_fee_actor.ts b/contracts/staking/test/actors/protocol_fee_actor.ts deleted file mode 100644 index 7bda9536d1..0000000000 --- a/contracts/staking/test/actors/protocol_fee_actor.ts +++ /dev/null @@ -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 { - // 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; - 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 diff --git a/contracts/staking/test/epoch_test.ts b/contracts/staking/test/epoch_test.ts index e4c1bda731..0d1a6f51a7 100644 --- a/contracts/staking/test/epoch_test.ts +++ b/contracts/staking/test/epoch_test.ts @@ -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 ///// { diff --git a/contracts/staking/test/exchange_fees.ts b/contracts/staking/test/exchange_fees.ts deleted file mode 100644 index cd583d4757..0000000000 --- a/contracts/staking/test/exchange_fees.ts +++ /dev/null @@ -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 diff --git a/contracts/staking/test/migration.ts b/contracts/staking/test/migration.ts index b7ac88a45b..96a964b31d 100644 --- a/contracts/staking/test/migration.ts +++ b/contracts/staking/test/migration.ts @@ -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( + 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); diff --git a/contracts/staking/test/params.ts b/contracts/staking/test/params.ts index f9760658ac..2924fd3098 100644 --- a/contracts/staking/test/params.ts +++ b/contracts/staking/test/params.ts @@ -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, from?: string): Promise { + async function setParamsAndAssertAsync(params: Partial, from?: string): Promise { 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(receipt.logs, 'Tuned')[0]; + const event = filterLogsToArguments(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); diff --git a/contracts/staking/test/pools_test.ts b/contracts/staking/test/pools_test.ts index 09680c28b1..b5f729dca3 100644 --- a/contracts/staking/test/pools_test.ts +++ b/contracts/staking/test/pools_test.ts @@ -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 diff --git a/contracts/staking/test/protocol_fees.ts b/contracts/staking/test/protocol_fees.ts index c79009d9e2..36764a8abd 100644 --- a/contracts/staking/test/protocol_fees.ts +++ b/contracts/staking/test/protocol_fees.ts @@ -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 { + 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( + 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( + 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 diff --git a/contracts/staking/test/rewards_test.ts b/contracts/staking/test/rewards_test.ts index f71afd51f2..62f74b0837 100644 --- a/contracts/staking/test/rewards_test.ts +++ b/contracts/staking/test/rewards_test.ts @@ -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), diff --git a/contracts/staking/test/utils/api_wrapper.ts b/contracts/staking/test/utils/api_wrapper.ts index e65f5d471d..dd704c849f 100644 --- a/contracts/staking/test/utils/api_wrapper.ts +++ b/contracts/staking/test/utils/api_wrapper.ts @@ -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 => { // 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 => { return this.zrxTokenContract.balanceOf.callAsync(this.zrxVaultContract.address); }, + + setParamsAsync: async (params: Partial): Promise => { + 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 => { + return (_.zipObject( + [ + 'epochDurationInSeconds', + 'rewardDelegatedStakeWeight', + 'minimumPoolStake', + 'maximumMakersInPool', + 'cobbDouglasAlphaNumerator', + 'cobbDouglasAlphaDenomintor', + ], + await this.stakingContract.getParams.callAsync(), + ) as any) as StakingParams; + }, }; private readonly _web3Wrapper: Web3Wrapper; diff --git a/contracts/staking/test/utils/constants.ts b/contracts/staking/test/utils/constants.ts index 59c4bb55a5..e7221d63e6 100644 --- a/contracts/staking/test/utils/constants.ts +++ b/contracts/staking/test/utils/constants.ts @@ -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% diff --git a/contracts/staking/test/utils/types.ts b/contracts/staking/test/utils/types.ts index e3bce443dc..87efe0ecf8 100644 --- a/contracts/staking/test/utils/types.ts +++ b/contracts/staking/test/utils/types.ts @@ -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; diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index bc41c70cee..b05f439499 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -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",