diff --git a/.circleci/config.yml b/.circleci/config.yml index 54b86c86ee..421c1d9a9d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -58,6 +58,16 @@ jobs: keys: - repo-{{ .Environment.CIRCLE_SHA1 }} - run: yarn wsrun test:circleci @0x/contracts-exchange + test-integrations-ganache-3.0: + resource_class: medium+ + docker: + - image: nikolaik/python-nodejs:python3.7-nodejs8 + working_directory: ~/repo + steps: + - restore_cache: + keys: + - repo-{{ .Environment.CIRCLE_SHA1 }} + - run: yarn wsrun test:circleci @0x/contracts-integrations test-contracts-rest-ganache-3.0: resource_class: medium+ docker: @@ -392,6 +402,9 @@ workflows: - test-exchange-ganache-3.0: requires: - build + - test-integrations-ganache-3.0: + requires: + - build - test-contracts-rest-ganache-3.0: requires: - build diff --git a/contracts/asset-proxy/contracts/src/ERC20BridgeProxy.sol b/contracts/asset-proxy/contracts/src/ERC20BridgeProxy.sol index f1b4401224..6efb3b376a 100644 --- a/contracts/asset-proxy/contracts/src/ERC20BridgeProxy.sol +++ b/contracts/asset-proxy/contracts/src/ERC20BridgeProxy.sol @@ -73,7 +73,7 @@ contract ERC20BridgeProxy is uint256 balanceBefore = balanceOf(tokenAddress, to); // Call the bridge, who should transfer `amount` of `tokenAddress` to // `to`. - bytes4 success = IERC20Bridge(bridgeAddress).withdrawTo( + bytes4 success = IERC20Bridge(bridgeAddress).bridgeTransferFrom( tokenAddress, from, to, diff --git a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol index 8cba2ca305..d3860db42b 100644 --- a/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/Eth2DaiBridge.sol @@ -42,7 +42,7 @@ contract Eth2DaiBridge is /// @param amount Minimum amount of `toTokenAddress` tokens to buy. /// @param bridgeData The abi-encoeded "from" token address. /// @return success The magic bytes if successful. - function withdrawTo( + function bridgeTransferFrom( address toTokenAddress, address /* from */, address to, diff --git a/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol b/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol index c5210d7e96..1eb674109f 100644 --- a/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/UniswapBridge.sol @@ -37,7 +37,7 @@ contract UniswapBridge is address constant private UNISWAP_EXCHANGE_FACTORY_ADDRESS = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95; address constant private WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - // Struct to hold `withdrawTo()` local variables in memory and to avoid + // Struct to hold `bridgeTransferFrom()` local variables in memory and to avoid // stack overflows. struct WithdrawToState { IUniswapExchange exchange; @@ -60,7 +60,7 @@ contract UniswapBridge is /// @param amount Minimum amount of `toTokenAddress` tokens to buy. /// @param bridgeData The abi-encoded "from" token address. /// @return success The magic bytes if successful. - function withdrawTo( + function bridgeTransferFrom( address toTokenAddress, address /* from */, address to, diff --git a/contracts/asset-proxy/contracts/src/interfaces/IERC20Bridge.sol b/contracts/asset-proxy/contracts/src/interfaces/IERC20Bridge.sol index 947aa475bb..c32fa76477 100644 --- a/contracts/asset-proxy/contracts/src/interfaces/IERC20Bridge.sol +++ b/contracts/asset-proxy/contracts/src/interfaces/IERC20Bridge.sol @@ -31,7 +31,7 @@ contract IERC20Bridge { /// @param amount Amount of asset to transfer. /// @param bridgeData Arbitrary asset data needed by the bridge contract. /// @return success The magic bytes `0x37708e9b` if successful. - function withdrawTo( + function bridgeTransferFrom( address tokenAddress, address from, address to, diff --git a/contracts/asset-proxy/contracts/test/TestERC20Bridge.sol b/contracts/asset-proxy/contracts/test/TestERC20Bridge.sol index 197ee3eb11..b65f666475 100644 --- a/contracts/asset-proxy/contracts/test/TestERC20Bridge.sol +++ b/contracts/asset-proxy/contracts/test/TestERC20Bridge.sol @@ -72,7 +72,7 @@ contract TestERC20Bridge is testToken.setBalance(owner, balance); } - function withdrawTo( + function bridgeTransferFrom( address tokenAddress, address from, address to, diff --git a/contracts/asset-proxy/test/eth2dai_bridge.ts b/contracts/asset-proxy/test/eth2dai_bridge.ts index f8958a00ef..46829436c5 100644 --- a/contracts/asset-proxy/test/eth2dai_bridge.ts +++ b/contracts/asset-proxy/test/eth2dai_bridge.ts @@ -45,7 +45,7 @@ blockchainTests.resets('Eth2DaiBridge unit tests', env => { }); }); - describe('withdrawTo()', () => { + describe('bridgeTransferFrom()', () => { interface WithdrawToOpts { toTokenAddress?: string; fromTokenAddress?: string; @@ -103,9 +103,9 @@ blockchainTests.resets('Eth2DaiBridge unit tests', env => { _opts.toTokentransferRevertReason, _opts.toTokenTransferReturnData, ); - // Call withdrawTo(). + // Call bridgeTransferFrom(). const [result, { logs }] = await txHelper.getResultAndReceiptAsync( - testContract.withdrawTo, + testContract.bridgeTransferFrom, // "to" token address _opts.toTokenAddress, // Random from address. diff --git a/contracts/asset-proxy/test/uniswap_bridge.ts b/contracts/asset-proxy/test/uniswap_bridge.ts index 48c6d2883b..2f7f6a0290 100644 --- a/contracts/asset-proxy/test/uniswap_bridge.ts +++ b/contracts/asset-proxy/test/uniswap_bridge.ts @@ -52,7 +52,7 @@ blockchainTests.resets('UniswapBridge unit tests', env => { }); }); - describe('withdrawTo()', () => { + describe('bridgeTransferFrom()', () => { interface WithdrawToOpts { fromTokenAddress: string; toTokenAddress: string; @@ -115,9 +115,9 @@ blockchainTests.resets('UniswapBridge unit tests', env => { await testContract.setTokenBalance.awaitTransactionSuccessAsync(_opts.fromTokenAddress, { value: new BigNumber(_opts.fromTokenBalance), }); - // Call withdrawTo(). + // Call bridgeTransferFrom(). const [result, receipt] = await txHelper.getResultAndReceiptAsync( - testContract.withdrawTo, + testContract.bridgeTransferFrom, // The "to" token address. _opts.toTokenAddress, // The "from" address. @@ -203,7 +203,7 @@ blockchainTests.resets('UniswapBridge unit tests', env => { }); it('fails if "from" token does not exist', async () => { - const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( + const tx = testContract.bridgeTransferFrom.awaitTransactionSuccessAsync( randomAddress(), randomAddress(), randomAddress(), @@ -275,7 +275,7 @@ blockchainTests.resets('UniswapBridge unit tests', env => { }); it('fails if "from" token does not exist', async () => { - const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( + const tx = testContract.bridgeTransferFrom.awaitTransactionSuccessAsync( randomAddress(), randomAddress(), randomAddress(), @@ -333,7 +333,7 @@ blockchainTests.resets('UniswapBridge unit tests', env => { }); it('fails if "to" token does not exist', async () => { - const tx = testContract.withdrawTo.awaitTransactionSuccessAsync( + const tx = testContract.bridgeTransferFrom.awaitTransactionSuccessAsync( wethTokenAddress, randomAddress(), randomAddress(), diff --git a/contracts/erc20/contracts/src/ERC20Token.sol b/contracts/erc20/contracts/src/ERC20Token.sol index 4c4737d864..313bf1784d 100644 --- a/contracts/erc20/contracts/src/ERC20Token.sol +++ b/contracts/erc20/contracts/src/ERC20Token.sol @@ -87,13 +87,13 @@ contract ERC20Token is balances[_to] += _value; balances[_from] -= _value; allowed[_from][msg.sender] -= _value; - + emit Transfer( _from, _to, _value ); - + return true; } diff --git a/contracts/exchange/test/balance_stores/local_balance_store.ts b/contracts/exchange/test/balance_stores/local_balance_store.ts index 6f2a65806d..39f7692000 100644 --- a/contracts/exchange/test/balance_stores/local_balance_store.ts +++ b/contracts/exchange/test/balance_stores/local_balance_store.ts @@ -1,4 +1,4 @@ -import { constants } from '@0x/contracts-test-utils'; +import { constants, Numberish } from '@0x/contracts-test-utils'; import { assetDataUtils } from '@0x/order-utils'; import { AssetProxyId } from '@0x/types'; import { BigNumber } from '@0x/utils'; @@ -34,11 +34,24 @@ export class LocalBalanceStore extends BalanceStore { /** * Decreases the ETH balance of an address to simulate gas usage. + * @param senderAddress Address whose ETH balance to decrease. + * @param amount Amount to decrease the balance by. */ - public burnGas(senderAddress: string, amount: BigNumber | number): void { + public burnGas(senderAddress: string, amount: Numberish): void { this._balances.eth[senderAddress] = this._balances.eth[senderAddress].minus(amount); } + /** + * Sends ETH from `fromAddress` to `toAddress`. + * @param fromAddress Sender of ETH. + * @param toAddress Receiver of ETH. + * @param amount Amount of ETH to transfer. + */ + public sendEth(fromAddress: string, toAddress: string, amount: Numberish): void { + this._balances.eth[fromAddress] = this._balances.eth[fromAddress].minus(amount); + this._balances.eth[toAddress] = this._balances.eth[toAddress].plus(amount); + } + /** * Transfers assets from `fromAddress` to `toAddress`. * @param fromAddress Sender of asset(s) diff --git a/contracts/exchange/test/balance_stores/types.ts b/contracts/exchange/test/balance_stores/types.ts index c8090f0e39..75789b614b 100644 --- a/contracts/exchange/test/balance_stores/types.ts +++ b/contracts/exchange/test/balance_stores/types.ts @@ -1,5 +1,5 @@ import { ERC1155MintableContract } from '@0x/contracts-erc1155'; -import { DummyERC20TokenContract, DummyNoReturnERC20TokenContract } from '@0x/contracts-erc20'; +import { DummyERC20TokenContract, DummyNoReturnERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20'; import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { BigNumber } from '@0x/utils'; @@ -15,7 +15,7 @@ interface TokenData { export type TokenAddresses = TokenData; export type TokenContracts = TokenData< - Array, + Array, DummyERC721TokenContract[], ERC1155MintableContract[] >; @@ -29,7 +29,7 @@ export type TokenOwnersByName = Named
; export type TokenAddressesByName = TokenData, Named
, Named
>; export type TokenContractsByName = TokenData< - Named, + Named, Named, Named >; diff --git a/contracts/integrations/contracts/test/TestFramework.sol b/contracts/integrations/contracts/test/TestFramework.sol new file mode 100644 index 0000000000..e8bf6cfdad --- /dev/null +++ b/contracts/integrations/contracts/test/TestFramework.sol @@ -0,0 +1,45 @@ +pragma solidity ^0.5.9; + +import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; + + +// This contract is intended to be used in the unit tests that test the typescript +// test framework found in `test/utils/` +contract TestFramework { + + event Event(string input); + + // bytes4(keccak256("RichRevertErrorSelector(string)")) + bytes4 internal constant RICH_REVERT_ERROR_SELECTOR = 0x49a7e246; + + function emitEvent(string calldata input) + external + { + emit Event(input); + } + + function emptyRevert() + external + { + revert(); + } + + function stringRevert(string calldata message) + external + { + revert(message); + } + + function doNothing() + external + pure + {} // solhint-disable-line no-empty-blocks + + function returnInteger(uint256 integer) + external + pure + returns (uint256) + { + return integer; + } +} diff --git a/contracts/integrations/contracts/test/TestStakingPlaceholder.sol b/contracts/integrations/contracts/test/TestStakingPlaceholder.sol deleted file mode 100644 index 7d53089303..0000000000 --- a/contracts/integrations/contracts/test/TestStakingPlaceholder.sol +++ /dev/null @@ -1,15 +0,0 @@ -pragma solidity ^0.5.9; -pragma experimental ABIEncoderV2; - -import "@0x/contracts-staking/contracts/test/TestStaking.sol"; - - -// TODO(jalextowle): This contract can be removed when the added to this package. -contract TestStakingPlaceholder is - TestStaking -{ - constructor(address wethAddress, address zrxVaultAddress) - public - TestStaking(wethAddress, zrxVaultAddress) - {} // solhint-disable-line no-empty-blocks -} diff --git a/contracts/integrations/package.json b/contracts/integrations/package.json index f96a82547b..3ffd2932f3 100644 --- a/contracts/integrations/package.json +++ b/contracts/integrations/package.json @@ -35,7 +35,7 @@ "compile:truffle": "truffle compile" }, "config": { - "abis": "./generated-artifacts/@(TestStakingPlaceholder).json", + "abis": "./generated-artifacts/@(TestFramework).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/integrations/src/artifacts.ts b/contracts/integrations/src/artifacts.ts index 2c10ff1fe1..6d4eb6b5f1 100644 --- a/contracts/integrations/src/artifacts.ts +++ b/contracts/integrations/src/artifacts.ts @@ -5,5 +5,5 @@ */ import { ContractArtifact } from 'ethereum-types'; -import * as TestStakingPlaceholder from '../generated-artifacts/TestStakingPlaceholder.json'; -export const artifacts = { TestStakingPlaceholder: TestStakingPlaceholder as ContractArtifact }; +import * as TestFramework from '../generated-artifacts/TestFramework.json'; +export const artifacts = { TestFramework: TestFramework as ContractArtifact }; diff --git a/contracts/integrations/src/index.ts b/contracts/integrations/src/index.ts index d55f08ea2d..be79d53fa1 100644 --- a/contracts/integrations/src/index.ts +++ b/contracts/integrations/src/index.ts @@ -1,2 +1,5 @@ export * from './artifacts'; export * from './wrappers'; +export * from '../test/utils/function_assertions'; +export * from '../test/utils/deployment_manager'; +export * from '../test/utils/address_manager'; diff --git a/contracts/integrations/src/wrappers.ts b/contracts/integrations/src/wrappers.ts index 4568c84786..1c09045bc5 100644 --- a/contracts/integrations/src/wrappers.ts +++ b/contracts/integrations/src/wrappers.ts @@ -3,4 +3,4 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ -export * from '../generated-wrappers/test_staking_placeholder'; +export * from '../generated-wrappers/test_framework'; diff --git a/contracts/integrations/test/actors/base.ts b/contracts/integrations/test/actors/base.ts index c3638c817d..3c3abb953c 100644 --- a/contracts/integrations/test/actors/base.ts +++ b/contracts/integrations/test/actors/base.ts @@ -1,27 +1,37 @@ import { DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20'; -import { constants } from '@0x/contracts-test-utils'; +import { constants, TransactionFactory } from '@0x/contracts-test-utils'; +import { SignatureType, SignedZeroExTransaction, ZeroExTransaction } from '@0x/types'; import { BigNumber } from '@0x/utils'; -import { DeploymentManager } from '../deployment/deployment_mananger'; +import { DeploymentManager } from '../utils/deployment_manager'; export type Constructor = new (...args: any[]) => T; export interface ActorConfig { - address: string; name?: string; deployment: DeploymentManager; [mixinProperty: string]: any; } export class Actor { + public static count: number = 0; public readonly address: string; public readonly name: string; + public readonly privateKey: Buffer; public readonly deployment: DeploymentManager; + protected readonly transactionFactory: TransactionFactory; constructor(config: ActorConfig) { - this.address = config.address; - this.name = config.name || config.address; + Actor.count++; + this.address = config.deployment.accounts[Actor.count]; + this.name = config.name || this.address; this.deployment = config.deployment; + this.privateKey = constants.TESTRPC_PRIVATE_KEYS[config.deployment.accounts.indexOf(this.address)]; + this.transactionFactory = new TransactionFactory( + this.privateKey, + config.deployment.exchange.address, + config.deployment.chainId, + ); } /** @@ -51,4 +61,14 @@ export class Actor { { from: this.address }, ); } + + /** + * Signs a transaction. + */ + public async signTransactionAsync( + customTransactionParams: Partial, + signatureType: SignatureType = SignatureType.EthSign, + ): Promise { + return this.transactionFactory.newSignedTransactionAsync(customTransactionParams, signatureType); + } } diff --git a/contracts/integrations/test/actors/fee_recipient.ts b/contracts/integrations/test/actors/fee_recipient.ts new file mode 100644 index 0000000000..a560a81816 --- /dev/null +++ b/contracts/integrations/test/actors/fee_recipient.ts @@ -0,0 +1,47 @@ +import { BaseContract } from '@0x/base-contract'; +import { ApprovalFactory, SignedCoordinatorApproval } from '@0x/contracts-coordinator'; +import { SignatureType, SignedZeroExTransaction } from '@0x/types'; + +import { Actor, ActorConfig, Constructor } from './base'; + +export interface FeeRecipientConfig extends ActorConfig { + verifyingContract?: BaseContract; +} + +export function FeeRecipientMixin(Base: TBase) { + return class extends Base { + public readonly actor: Actor; + public readonly approvalFactory?: ApprovalFactory; + + /** + * The mixin pattern requires that this constructor uses `...args: any[]`, but this class + * really expects a single `FeeRecipientConfig` parameter (assuming `Actor` is used as the + * base class). + */ + constructor(...args: any[]) { + super(...args); + this.actor = (this as any) as Actor; + + const { verifyingContract } = args[0] as FeeRecipientConfig; + if (verifyingContract !== undefined) { + this.approvalFactory = new ApprovalFactory(this.actor.privateKey, verifyingContract.address); + } + } + + /** + * Signs an coordinator transaction. + */ + public signCoordinatorApproval( + transaction: SignedZeroExTransaction, + txOrigin: string, + signatureType: SignatureType = SignatureType.EthSign, + ): SignedCoordinatorApproval { + if (this.approvalFactory === undefined) { + throw new Error('No verifying contract provided in FeeRecipient constructor'); + } + return this.approvalFactory.newSignedApproval(transaction, txOrigin, signatureType); + } + }; +} + +export class FeeRecipient extends FeeRecipientMixin(Actor) {} diff --git a/contracts/integrations/test/actors/index.ts b/contracts/integrations/test/actors/index.ts index ab525a5a70..14934ac89a 100644 --- a/contracts/integrations/test/actors/index.ts +++ b/contracts/integrations/test/actors/index.ts @@ -1,3 +1,6 @@ +export { Actor } from './base'; export { Maker } from './maker'; export { PoolOperator } from './pool_operator'; +export { FeeRecipient } from './fee_recipient'; export * from './hybrids'; +export * from './utils'; diff --git a/contracts/integrations/test/actors/maker.ts b/contracts/integrations/test/actors/maker.ts index 870716c7d2..fc92f4a5fa 100644 --- a/contracts/integrations/test/actors/maker.ts +++ b/contracts/integrations/test/actors/maker.ts @@ -31,9 +31,7 @@ export function MakerMixin(Base: TBase) { chainId: this.actor.deployment.chainId, ...orderConfig, }; - const privateKey = - constants.TESTRPC_PRIVATE_KEYS[this.actor.deployment.accounts.indexOf(this.actor.address)]; - this.orderFactory = new OrderFactory(privateKey, defaultOrderParams); + this.orderFactory = new OrderFactory(this.actor.privateKey, defaultOrderParams); } /** diff --git a/contracts/integrations/test/actors/utils.ts b/contracts/integrations/test/actors/utils.ts new file mode 100644 index 0000000000..0bbc6e07c8 --- /dev/null +++ b/contracts/integrations/test/actors/utils.ts @@ -0,0 +1,8 @@ +import { TokenOwnersByName } from '@0x/contracts-exchange'; +import * as _ from 'lodash'; + +import { Actor } from './base'; + +export function actorAddressesByName(actors: Actor[]): TokenOwnersByName { + return _.zipObject(actors.map(actor => actor.name), actors.map(actor => actor.address)); +} diff --git a/contracts/integrations/test/coordinator/coordinator.ts b/contracts/integrations/test/coordinator/coordinator.ts index c4671a7994..161fd2fe30 100644 --- a/contracts/integrations/test/coordinator/coordinator.ts +++ b/contracts/integrations/test/coordinator/coordinator.ts @@ -1,419 +1,457 @@ -import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy'; -import { ApprovalFactory, artifacts, CoordinatorContract } from '@0x/contracts-coordinator'; -import { artifacts as erc20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20'; +import { CoordinatorContract, SignedCoordinatorApproval } from '@0x/contracts-coordinator'; import { - artifacts as exchangeArtifacts, + BlockchainBalanceStore, + LocalBalanceStore, constants as exchangeConstants, - ExchangeContract, + ExchangeCancelEventArgs, + ExchangeCancelUpToEventArgs, exchangeDataEncoder, + ExchangeEvents, + ExchangeFillEventArgs, ExchangeFunctionName, - TestProtocolFeeCollectorContract, } from '@0x/contracts-exchange'; -import { - blockchainTests, - constants, - hexConcat, - hexSlice, - OrderFactory, - TransactionFactory, -} from '@0x/contracts-test-utils'; -import { assetDataUtils, CoordinatorRevertErrors, transactionHashUtils } from '@0x/order-utils'; +import { blockchainTests, expect, hexConcat, hexSlice, verifyEvents } from '@0x/contracts-test-utils'; +import { assetDataUtils, CoordinatorRevertErrors, orderHashUtils, transactionHashUtils } from '@0x/order-utils'; +import { SignedOrder, SignedZeroExTransaction } from '@0x/types'; import { BigNumber } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; -import { CoordinatorTestFactory } from './coordinator_test_factory'; +import { Actor, actorAddressesByName, FeeRecipient, Maker } from '../actors'; +import { deployCoordinatorAsync } from './deploy_coordinator'; +import { DeploymentManager } from '../utils/deployment_manager'; // tslint:disable:no-unnecessary-type-assertion blockchainTests.resets('Coordinator tests', env => { - let chainId: number; - let makerAddress: string; - let owner: string; - let takerAddress: string; - let feeRecipientAddress: string; - - let erc20Proxy: ERC20ProxyContract; - let erc20TokenA: DummyERC20TokenContract; - let erc20TokenB: DummyERC20TokenContract; - let makerFeeToken: DummyERC20TokenContract; - let takerFeeToken: DummyERC20TokenContract; - let coordinatorContract: CoordinatorContract; - let exchange: ExchangeContract; - let protocolFeeCollector: TestProtocolFeeCollectorContract; - let wethContract: WETH9Contract; - - let erc20Wrapper: ERC20Wrapper; - let orderFactory: OrderFactory; - let takerTransactionFactory: TransactionFactory; - let makerTransactionFactory: TransactionFactory; - let approvalFactory: ApprovalFactory; - let testFactory: CoordinatorTestFactory; - - const GAS_PRICE = new BigNumber(env.txDefaults.gasPrice || constants.DEFAULT_GAS_PRICE); - const PROTOCOL_FEE_MULTIPLIER = new BigNumber(150000); - const PROTOCOL_FEE = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER); + let deployment: DeploymentManager; + let coordinator: CoordinatorContract; + let balanceStore: BlockchainBalanceStore; + + let maker: Maker; + let taker: Actor; + let feeRecipient: FeeRecipient; before(async () => { - chainId = await env.getChainIdAsync(); - const accounts = await env.getAccountAddressesAsync(); - const usedAddresses = ([owner, makerAddress, takerAddress, feeRecipientAddress] = accounts); - - // Deploy Exchange - exchange = await ExchangeContract.deployFrom0xArtifactAsync( - exchangeArtifacts.Exchange, - env.provider, - env.txDefaults, - {}, - new BigNumber(chainId), - ); + deployment = await DeploymentManager.deployAsync(env, { + numErc20TokensToDeploy: 4, + numErc721TokensToDeploy: 0, + numErc1155TokensToDeploy: 0, + }); + coordinator = await deployCoordinatorAsync(deployment, env); - // Set up ERC20 - erc20Wrapper = new ERC20Wrapper(env.provider, usedAddresses, owner); - erc20Proxy = await erc20Wrapper.deployProxyAsync(); - const numDummyErc20ToDeploy = 4; - [erc20TokenA, erc20TokenB, makerFeeToken, takerFeeToken] = await erc20Wrapper.deployDummyTokensAsync( - numDummyErc20ToDeploy, - constants.DUMMY_TOKEN_DECIMALS, - ); - await erc20Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync(exchange.address, { from: owner }); - await exchange.registerAssetProxy.awaitTransactionSuccessAsync(erc20Proxy.address, { from: owner }); - - // Set up WETH - wethContract = await WETH9Contract.deployFrom0xArtifactAsync( - erc20Artifacts.WETH9, - env.provider, - env.txDefaults, - {}, - ); - const weth = new DummyERC20TokenContract(wethContract.address, env.provider); - erc20Wrapper.addDummyTokenContract(weth); - await erc20Wrapper.setBalancesAndAllowancesAsync(); - - // Set up Protocol Fee Collector - protocolFeeCollector = await TestProtocolFeeCollectorContract.deployFrom0xArtifactAsync( - exchangeArtifacts.TestProtocolFeeCollector, - env.provider, - env.txDefaults, + const [makerToken, takerToken, makerFeeToken, takerFeeToken] = deployment.tokens.erc20; + + taker = new Actor({ name: 'Taker', deployment }); + feeRecipient = new FeeRecipient({ + name: 'Fee recipient', + deployment, + verifyingContract: coordinator, + }); + maker = new Maker({ + name: 'Maker', + deployment, + orderConfig: { + senderAddress: coordinator.address, + feeRecipientAddress: feeRecipient.address, + makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + makerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerFeeToken.address), + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerFeeToken.address), + }, + }); + + taker.configureERC20TokenAsync(takerToken); + taker.configureERC20TokenAsync(takerFeeToken); + taker.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address); + maker.configureERC20TokenAsync(makerToken); + maker.configureERC20TokenAsync(makerFeeToken); + + balanceStore = new BlockchainBalanceStore( + { + ...actorAddressesByName([maker, taker, feeRecipient]), + Coordinator: coordinator.address, + StakingProxy: deployment.staking.stakingProxy.address, + }, + { erc20: { makerToken, takerToken, makerFeeToken, takerFeeToken, wETH: deployment.tokens.weth } }, {}, - weth.address, ); - await exchange.setProtocolFeeMultiplier.awaitTransactionSuccessAsync(PROTOCOL_FEE_MULTIPLIER); - await exchange.setProtocolFeeCollectorAddress.awaitTransactionSuccessAsync(protocolFeeCollector.address); - for (const account of usedAddresses) { - await wethContract.deposit.awaitTransactionSuccessAsync({ - from: account, - value: constants.ONE_ETHER, - }); - await wethContract.approve.awaitTransactionSuccessAsync( - protocolFeeCollector.address, - constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, - { - from: account, - }, + }); + + function simulateFills( + orders: SignedOrder[], + txReceipt: TransactionReceiptWithDecodedLogs, + msgValue: BigNumber = new BigNumber(0), + ): LocalBalanceStore { + const localBalanceStore = LocalBalanceStore.create(balanceStore); + // Transaction gas cost + localBalanceStore.burnGas(txReceipt.from, DeploymentManager.gasPrice.times(txReceipt.gasUsed)); + + for (const order of orders) { + // Taker -> Maker + localBalanceStore.transferAsset(taker.address, maker.address, order.takerAssetAmount, order.takerAssetData); + // Maker -> Taker + localBalanceStore.transferAsset(maker.address, taker.address, order.makerAssetAmount, order.makerAssetData); + // Taker -> Fee Recipient + localBalanceStore.transferAsset( + taker.address, + feeRecipient.address, + order.takerFee, + order.takerFeeAssetData, + ); + // Maker -> Fee Recipient + localBalanceStore.transferAsset( + maker.address, + feeRecipient.address, + order.makerFee, + order.makerFeeAssetData, ); + + // Protocol fee + if (msgValue.isGreaterThanOrEqualTo(DeploymentManager.protocolFee)) { + localBalanceStore.sendEth( + txReceipt.from, + deployment.staking.stakingProxy.address, + DeploymentManager.protocolFee, + ); + msgValue = msgValue.minus(DeploymentManager.protocolFee); + } else { + localBalanceStore.transferAsset( + taker.address, + deployment.staking.stakingProxy.address, + DeploymentManager.protocolFee, + assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address), + ); + } } - erc20Wrapper.addTokenOwnerAddress(protocolFeeCollector.address); - - // Deploy Coordinator - coordinatorContract = await CoordinatorContract.deployFrom0xArtifactAsync( - artifacts.Coordinator, - env.provider, - env.txDefaults, - { ...exchangeArtifacts, ...artifacts }, - exchange.address, - new BigNumber(chainId), - ); - erc20Wrapper.addTokenOwnerAddress(coordinatorContract.address); - - // Configure order defaults - const defaultOrderParams = { - ...constants.STATIC_ORDER_PARAMS, - senderAddress: coordinatorContract.address, - makerAddress, - feeRecipientAddress, - makerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenA.address), - takerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenB.address), - makerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerFeeToken.address), - takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerFeeToken.address), - exchangeAddress: exchange.address, - chainId, + + return localBalanceStore; + } + + function expectedFillEvent(order: SignedOrder): ExchangeFillEventArgs { + return { + makerAddress: order.makerAddress, + takerAddress: taker.address, + senderAddress: order.senderAddress, + feeRecipientAddress: order.feeRecipientAddress, + makerAssetData: order.makerAssetData, + takerAssetData: order.takerAssetData, + makerFeeAssetData: order.makerFeeAssetData, + takerFeeAssetData: order.takerFeeAssetData, + makerAssetFilledAmount: order.makerAssetAmount, + takerAssetFilledAmount: order.takerAssetAmount, + makerFeePaid: order.makerFee, + takerFeePaid: order.takerFee, + protocolFeePaid: DeploymentManager.protocolFee, + orderHash: orderHashUtils.getOrderHashHex(order), }; - const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; - const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)]; - const feeRecipientPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(feeRecipientAddress)]; - orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams); - makerTransactionFactory = new TransactionFactory(makerPrivateKey, exchange.address, chainId); - takerTransactionFactory = new TransactionFactory(takerPrivateKey, exchange.address, chainId); - approvalFactory = new ApprovalFactory(feeRecipientPrivateKey, coordinatorContract.address); - testFactory = new CoordinatorTestFactory( - coordinatorContract, - erc20Wrapper, - makerAddress, - takerAddress, - feeRecipientAddress, - protocolFeeCollector.address, - erc20TokenA.address, - erc20TokenB.address, - makerFeeToken.address, - takerFeeToken.address, - weth.address, - GAS_PRICE, - PROTOCOL_FEE_MULTIPLIER, - ); - }); + } describe('single order fills', () => { + let order: SignedOrder; + let data: string; + let transaction: SignedZeroExTransaction; + let approval: SignedCoordinatorApproval; + for (const fnName of exchangeConstants.SINGLE_FILL_FN_NAMES) { + before(async () => { + order = await maker.signOrderAsync(); + data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]); + transaction = await taker.signTransactionAsync({ + data, + gasPrice: DeploymentManager.gasPrice, + }); + approval = feeRecipient.signCoordinatorApproval(transaction, taker.address); + }); + it(`${fnName} should fill the order with a signed approval`, async () => { - const order = await orderFactory.newSignedOrderAsync(); - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const approval = approvalFactory.newSignedApproval(transaction, takerAddress); - const txData = { from: takerAddress, value: PROTOCOL_FEE }; - await testFactory.executeFillTransactionTestAsync( - [order], + await balanceStore.updateBalancesAsync(); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - takerAddress, + taker.address, + transaction.signature, [approval.signature], - txData, + { from: taker.address, value: DeploymentManager.protocolFee }, ); + + const expectedBalances = simulateFills([order], txReceipt, DeploymentManager.protocolFee); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill); }); it(`${fnName} should fill the order if called by approver (eth protocol fee, no refund)`, async () => { - const order = await orderFactory.newSignedOrderAsync(); - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const txData = { from: feeRecipientAddress, value: PROTOCOL_FEE }; - await testFactory.executeFillTransactionTestAsync( - [order], + await balanceStore.updateBalancesAsync(); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - feeRecipientAddress, + feeRecipient.address, + transaction.signature, [], - txData, + { from: feeRecipient.address, value: DeploymentManager.protocolFee }, ); + + const expectedBalances = simulateFills([order], txReceipt, DeploymentManager.protocolFee); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill); }); it(`${fnName} should fill the order if called by approver (eth protocol fee, refund)`, async () => { - const order = await orderFactory.newSignedOrderAsync(); - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const txData = { from: feeRecipientAddress, value: PROTOCOL_FEE.plus(1) }; - await testFactory.executeFillTransactionTestAsync( - [order], + await balanceStore.updateBalancesAsync(); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - feeRecipientAddress, + feeRecipient.address, + transaction.signature, [], - txData, + { from: feeRecipient.address, value: DeploymentManager.protocolFee.plus(1) }, ); + + const expectedBalances = simulateFills([order], txReceipt, DeploymentManager.protocolFee.plus(1)); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill); }); it(`${fnName} should fill the order if called by approver (weth protocol fee, no refund)`, async () => { - const order = await orderFactory.newSignedOrderAsync(); - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const txData = { from: feeRecipientAddress }; - await testFactory.executeFillTransactionTestAsync( - [order], + await balanceStore.updateBalancesAsync(); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - feeRecipientAddress, + feeRecipient.address, + transaction.signature, [], - txData, + { from: feeRecipient.address }, ); + + const expectedBalances = simulateFills([order], txReceipt); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill); }); it(`${fnName} should fill the order if called by approver (weth protocol fee, refund)`, async () => { - const order = await orderFactory.newSignedOrderAsync(); - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const txData = { from: feeRecipientAddress, value: new BigNumber(1) }; - await testFactory.executeFillTransactionTestAsync( - [order], + await balanceStore.updateBalancesAsync(); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - feeRecipientAddress, + feeRecipient.address, + transaction.signature, [], - txData, - ); - }); - it(`${fnName} should fill the order if called by approver`, async () => { - const order = await orderFactory.newSignedOrderAsync(); - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, [order]); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const txData = { from: feeRecipientAddress, value: PROTOCOL_FEE }; - await testFactory.executeFillTransactionTestAsync( - [order], - transaction, - feeRecipientAddress, - [], - txData, + { from: feeRecipient.address, value: new BigNumber(1) }, ); + + const expectedBalances = simulateFills([order], txReceipt, new BigNumber(1)); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + verifyEvents(txReceipt, [expectedFillEvent(order)], ExchangeEvents.Fill); }); it(`${fnName} should revert with no approval signature`, async () => { - const orders = [await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); const transactionHash = transactionHashUtils.getTransactionHashHex(transaction); - await testFactory.executeFillTransactionTestAsync( - orders, + const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - takerAddress, + taker.address, + transaction.signature, [], - { - from: takerAddress, - gas: constants.MAX_EXECUTE_TRANSACTION_GAS, - value: PROTOCOL_FEE, - }, - new CoordinatorRevertErrors.InvalidApprovalSignatureError(transactionHash, feeRecipientAddress), + { from: taker.address, value: DeploymentManager.protocolFee }, ); + + const expectedError = new CoordinatorRevertErrors.InvalidApprovalSignatureError( + transactionHash, + feeRecipient.address, + ); + return expect(tx).to.revertWith(expectedError); }); it(`${fnName} should revert with an invalid approval signature`, async () => { - const orders = [await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const approval = approvalFactory.newSignedApproval(transaction, takerAddress); - const signature = hexConcat( + const approvalSignature = hexConcat( hexSlice(approval.signature, 0, 2), '0xFFFFFFFF', hexSlice(approval.signature, 6), ); const transactionHash = transactionHashUtils.getTransactionHashHex(transaction); - await testFactory.executeFillTransactionTestAsync( - orders, + const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - takerAddress, - [signature], - { from: takerAddress, value: PROTOCOL_FEE }, - new CoordinatorRevertErrors.InvalidApprovalSignatureError(transactionHash, feeRecipientAddress), + taker.address, + transaction.signature, + [approvalSignature], + { from: taker.address, value: DeploymentManager.protocolFee }, + ); + + const expectedError = new CoordinatorRevertErrors.InvalidApprovalSignatureError( + transactionHash, + feeRecipient.address, ); + return expect(tx).to.revertWith(expectedError); }); it(`${fnName} should revert if not called by tx signer or approver`, async () => { - const orders = [await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const approval = approvalFactory.newSignedApproval(transaction, takerAddress); - await testFactory.executeFillTransactionTestAsync( - orders, + const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - takerAddress, + taker.address, + transaction.signature, [approval.signature], - { from: owner, value: PROTOCOL_FEE }, - new CoordinatorRevertErrors.InvalidOriginError(takerAddress), + { from: maker.address, value: DeploymentManager.protocolFee }, ); + + const expectedError = new CoordinatorRevertErrors.InvalidOriginError(taker.address); + return expect(tx).to.revertWith(expectedError); }); } }); describe('batch order fills', () => { + let orders: SignedOrder[]; + let data: string; + let transaction: SignedZeroExTransaction; + let approval: SignedCoordinatorApproval; + for (const fnName of [...exchangeConstants.MARKET_FILL_FN_NAMES, ...exchangeConstants.BATCH_FILL_FN_NAMES]) { + before(async () => { + orders = [await maker.signOrderAsync(), await maker.signOrderAsync()]; + data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); + transaction = await taker.signTransactionAsync({ + data, + gasPrice: DeploymentManager.gasPrice, + }); + approval = feeRecipient.signCoordinatorApproval(transaction, taker.address); + }); + it(`${fnName} should fill the orders with a signed approval`, async () => { - const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const approval = approvalFactory.newSignedApproval(transaction, takerAddress); - await testFactory.executeFillTransactionTestAsync( - orders, + await balanceStore.updateBalancesAsync(); + const value = DeploymentManager.protocolFee.times(orders.length); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - takerAddress, + taker.address, + transaction.signature, [approval.signature], - { - from: takerAddress, - gas: constants.MAX_EXECUTE_TRANSACTION_GAS, - value: PROTOCOL_FEE.times(orders.length), - }, + { from: taker.address, value }, ); + + const expectedBalances = simulateFills(orders, txReceipt, value); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + verifyEvents(txReceipt, orders.map(order => expectedFillEvent(order)), ExchangeEvents.Fill); }); it(`${fnName} should fill the orders if called by approver (eth fee, no refund)`, async () => { - const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - await testFactory.executeFillTransactionTestAsync(orders, transaction, feeRecipientAddress, [], { - from: feeRecipientAddress, - gas: constants.MAX_EXECUTE_TRANSACTION_GAS, - value: PROTOCOL_FEE.times(orders.length), - }); + await balanceStore.updateBalancesAsync(); + const value = DeploymentManager.protocolFee.times(orders.length); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( + transaction, + feeRecipient.address, + transaction.signature, + [], + { from: feeRecipient.address, value }, + ); + + const expectedBalances = simulateFills(orders, txReceipt, value); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + verifyEvents(txReceipt, orders.map(order => expectedFillEvent(order)), ExchangeEvents.Fill); }); it(`${fnName} should fill the orders if called by approver (mixed fees, refund)`, async () => { - const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - await testFactory.executeFillTransactionTestAsync(orders, transaction, feeRecipientAddress, [], { - from: feeRecipientAddress, - gas: constants.MAX_EXECUTE_TRANSACTION_GAS, - value: PROTOCOL_FEE.times(orders.length).plus(1), - }); + await balanceStore.updateBalancesAsync(); + const value = DeploymentManager.protocolFee.plus(1); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( + transaction, + feeRecipient.address, + transaction.signature, + [], + { from: feeRecipient.address, value }, + ); + + const expectedBalances = simulateFills(orders, txReceipt, value); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + verifyEvents(txReceipt, orders.map(order => expectedFillEvent(order)), ExchangeEvents.Fill); }); it(`${fnName} should revert with an invalid approval signature`, async () => { - const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const approval = approvalFactory.newSignedApproval(transaction, takerAddress); - const signature = hexConcat( + const approvalSignature = hexConcat( hexSlice(approval.signature, 0, 2), '0xFFFFFFFF', hexSlice(approval.signature, 6), ); const transactionHash = transactionHashUtils.getTransactionHashHex(transaction); - await testFactory.executeFillTransactionTestAsync( - orders, + const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - takerAddress, - [signature], - { from: takerAddress, value: PROTOCOL_FEE.times(orders.length) }, - new CoordinatorRevertErrors.InvalidApprovalSignatureError(transactionHash, feeRecipientAddress), + taker.address, + transaction.signature, + [approvalSignature], + { from: taker.address, value: DeploymentManager.protocolFee.times(orders.length) }, ); + const expectedError = new CoordinatorRevertErrors.InvalidApprovalSignatureError( + transactionHash, + feeRecipient.address, + ); + return expect(tx).to.revertWith(expectedError); }); it(`${fnName} should revert if not called by tx signer or approver`, async () => { - const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(fnName, orders); - const transaction = await takerTransactionFactory.newSignedTransactionAsync({ data }); - const approval = approvalFactory.newSignedApproval(transaction, takerAddress); - await testFactory.executeFillTransactionTestAsync( - orders, + const tx = coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - takerAddress, + taker.address, + transaction.signature, [approval.signature], - { from: owner, value: PROTOCOL_FEE.times(orders.length) }, - new CoordinatorRevertErrors.InvalidOriginError(takerAddress), + { from: maker.address, value: DeploymentManager.protocolFee.times(orders.length) }, ); + const expectedError = new CoordinatorRevertErrors.InvalidOriginError(taker.address); + return expect(tx).to.revertWith(expectedError); }); } }); describe('cancels', () => { + function expectedCancelEvent(order: SignedOrder): ExchangeCancelEventArgs { + return { + makerAddress: order.makerAddress, + senderAddress: order.senderAddress, + feeRecipientAddress: order.feeRecipientAddress, + makerAssetData: order.makerAssetData, + takerAssetData: order.takerAssetData, + orderHash: orderHashUtils.getOrderHashHex(order), + }; + } + it('cancelOrder call should be successful without an approval', async () => { - const orders = [await orderFactory.newSignedOrderAsync()]; - const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.CancelOrder, orders); - const transaction = await makerTransactionFactory.newSignedTransactionAsync({ data }); - await testFactory.executeCancelTransactionTestAsync( - ExchangeFunctionName.CancelOrder, - orders, + const order = await maker.signOrderAsync(); + const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.CancelOrder, [order]); + const transaction = await maker.signTransactionAsync({ + data, + gasPrice: DeploymentManager.gasPrice, + }); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - makerAddress, + maker.address, + transaction.signature, [], - { - from: makerAddress, - }, + { from: maker.address }, ); + + verifyEvents(txReceipt, [expectedCancelEvent(order)], ExchangeEvents.Cancel); }); it('batchCancelOrders call should be successful without an approval', async () => { - const orders = [await orderFactory.newSignedOrderAsync(), await orderFactory.newSignedOrderAsync()]; + const orders = [await maker.signOrderAsync(), await maker.signOrderAsync()]; const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.BatchCancelOrders, orders); - const transaction = await makerTransactionFactory.newSignedTransactionAsync({ data }); - await testFactory.executeCancelTransactionTestAsync( - ExchangeFunctionName.BatchCancelOrders, - orders, + const transaction = await maker.signTransactionAsync({ + data, + gasPrice: DeploymentManager.gasPrice, + }); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - makerAddress, + maker.address, + transaction.signature, [], - { - from: makerAddress, - }, + { from: maker.address }, ); + + verifyEvents(txReceipt, orders.map(order => expectedCancelEvent(order)), ExchangeEvents.Cancel); }); it('cancelOrdersUpTo call should be successful without an approval', async () => { const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.CancelOrdersUpTo, []); - const transaction = await makerTransactionFactory.newSignedTransactionAsync({ data }); - await testFactory.executeCancelTransactionTestAsync( - ExchangeFunctionName.CancelOrdersUpTo, - [], + const transaction = await maker.signTransactionAsync({ + data, + gasPrice: DeploymentManager.gasPrice, + }); + const txReceipt = await coordinator.executeTransaction.awaitTransactionSuccessAsync( transaction, - makerAddress, + maker.address, + transaction.signature, [], - { - from: makerAddress, - }, + { from: maker.address }, ); + + const expectedEvent: ExchangeCancelUpToEventArgs = { + makerAddress: maker.address, + orderSenderAddress: coordinator.address, + orderEpoch: new BigNumber(1), + }; + verifyEvents(txReceipt, [expectedEvent], ExchangeEvents.CancelUpTo); }); }); }); diff --git a/contracts/integrations/test/coordinator/coordinator_test_factory.ts b/contracts/integrations/test/coordinator/coordinator_test_factory.ts deleted file mode 100644 index bd3ba81f2d..0000000000 --- a/contracts/integrations/test/coordinator/coordinator_test_factory.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { ERC20Wrapper } from '@0x/contracts-asset-proxy'; -import { CoordinatorContract } from '@0x/contracts-coordinator'; -import { - ExchangeCancelEventArgs, - ExchangeCancelUpToEventArgs, - ExchangeEvents, - ExchangeFillEventArgs, - ExchangeFunctionName, -} from '@0x/contracts-exchange'; -import { expect, Numberish, TokenBalances, verifyEvents, web3Wrapper } from '@0x/contracts-test-utils'; -import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; -import { SignedOrder, SignedZeroExTransaction } from '@0x/types'; -import { BigNumber, RevertError } from '@0x/utils'; -import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types'; -import * as _ from 'lodash'; - -export class CoordinatorTestFactory { - private readonly _addresses: string[]; - private readonly _protocolFee: BigNumber; - - private static _expectedCancelEvent(order: SignedOrder): ExchangeCancelEventArgs { - return { - makerAddress: order.makerAddress, - senderAddress: order.senderAddress, - feeRecipientAddress: order.feeRecipientAddress, - makerAssetData: order.makerAssetData, - takerAssetData: order.takerAssetData, - orderHash: orderHashUtils.getOrderHashHex(order), - }; - } - - constructor( - private readonly _coordinatorContract: CoordinatorContract, - private readonly _erc20Wrapper: ERC20Wrapper, - private readonly _makerAddress: string, - private readonly _takerAddress: string, - private readonly _feeRecipientAddress: string, - private readonly _protocolFeeCollectorAddress: string, - private readonly _makerAssetAddress: string, - private readonly _takerAssetAddress: string, - private readonly _makerFeeAssetAddress: string, - private readonly _takerFeeAssetAddress: string, - private readonly _wethAddress: string, - private readonly _gasPrice: BigNumber, - _protocolFeeMultiplier: BigNumber, - ) { - this._addresses = [ - _makerAddress, - _takerAddress, - _coordinatorContract.address, - _feeRecipientAddress, - _protocolFeeCollectorAddress, - ]; - this._protocolFee = _gasPrice.times(_protocolFeeMultiplier); - } - - public async executeFillTransactionTestAsync( - orders: SignedOrder[], - transaction: SignedZeroExTransaction, - txOrigin: string, - approvalSignatures: string[], - txData: Partial, - revertError?: RevertError, - ): Promise { - const initBalances = await this._getTokenBalancesAsync(); - const tx = this._coordinatorContract.executeTransaction.awaitTransactionSuccessAsync( - transaction, - txOrigin, - transaction.signature, - approvalSignatures, - txData, - ); - - if (revertError !== undefined) { - return expect(tx).to.revertWith(revertError); - } - - const transactionReceipt = await tx; - verifyEvents(transactionReceipt, orders.map(order => this._expectedFillEvent(order)), ExchangeEvents.Fill); - - const expectedBalances = this._getExpectedBalances(initBalances, orders, transactionReceipt, txData.value); - await this._verifyBalancesAsync(expectedBalances); - } - - public async executeCancelTransactionTestAsync( - fnName: ExchangeFunctionName, - orders: SignedOrder[], - transaction: SignedZeroExTransaction, - txOrigin: string, - approvalSignatures: string[], - txData: Partial, - ): Promise { - const transactionReceipt = await this._coordinatorContract.executeTransaction.awaitTransactionSuccessAsync( - transaction, - txOrigin, - transaction.signature, - approvalSignatures, - txData, - ); - - if (fnName === ExchangeFunctionName.CancelOrdersUpTo) { - const expectedEvent: ExchangeCancelUpToEventArgs = { - makerAddress: this._makerAddress, - orderSenderAddress: this._coordinatorContract.address, - orderEpoch: new BigNumber(1), - }; - verifyEvents(transactionReceipt, [expectedEvent], ExchangeEvents.CancelUpTo); - } else { - verifyEvents( - transactionReceipt, - orders.map(order => CoordinatorTestFactory._expectedCancelEvent(order)), - ExchangeEvents.Cancel, - ); - } - } - - private async _getTokenBalancesAsync(): Promise { - const erc20Balances = await this._erc20Wrapper.getBalancesAsync(); - const ethBalances = _.zipObject( - this._addresses, - await Promise.all(this._addresses.map(address => web3Wrapper.getBalanceInWeiAsync(address))), - ); - - return { - erc20: erc20Balances, - erc721: {}, - erc1155: {}, - eth: ethBalances, - }; - } - - private _getExpectedBalances( - initBalances: TokenBalances, - orders: SignedOrder[], - txReceipt: TransactionReceiptWithDecodedLogs, - txValue?: Numberish, - ): TokenBalances { - const { erc20: erc20Balances, eth: ethBalances } = initBalances; - let remainingValue = new BigNumber(txValue || 0); - ethBalances[txReceipt.from] = ethBalances[txReceipt.from].minus(this._gasPrice.times(txReceipt.gasUsed)); - - for (const order of orders) { - const [makerAssetAddress, takerAssetAddress, makerFeeAssetAddress, takerFeeAssetAddress] = [ - order.makerAssetData, - order.takerAssetData, - order.makerFeeAssetData, - order.takerFeeAssetData, - ].map(assetData => assetDataUtils.decodeERC20AssetData(assetData).tokenAddress); - - erc20Balances[order.makerAddress][makerAssetAddress] = erc20Balances[order.makerAddress][ - makerAssetAddress - ].minus(order.makerAssetAmount); - erc20Balances[this._takerAddress][makerAssetAddress] = erc20Balances[this._takerAddress][ - makerAssetAddress - ].plus(order.makerAssetAmount); - erc20Balances[order.makerAddress][takerAssetAddress] = erc20Balances[order.makerAddress][ - takerAssetAddress - ].plus(order.takerAssetAmount); - erc20Balances[this._takerAddress][takerAssetAddress] = erc20Balances[this._takerAddress][ - takerAssetAddress - ].minus(order.takerAssetAmount); - erc20Balances[order.makerAddress][makerFeeAssetAddress] = erc20Balances[order.makerAddress][ - makerFeeAssetAddress - ].minus(order.makerFee); - erc20Balances[this._takerAddress][takerFeeAssetAddress] = erc20Balances[this._takerAddress][ - takerFeeAssetAddress - ].minus(order.takerFee); - erc20Balances[order.feeRecipientAddress][makerFeeAssetAddress] = erc20Balances[order.feeRecipientAddress][ - makerFeeAssetAddress - ].plus(order.makerFee); - erc20Balances[order.feeRecipientAddress][takerFeeAssetAddress] = erc20Balances[order.feeRecipientAddress][ - takerFeeAssetAddress - ].plus(order.takerFee); - - if (remainingValue.isGreaterThanOrEqualTo(this._protocolFee)) { - ethBalances[txReceipt.from] = ethBalances[txReceipt.from].minus(this._protocolFee); - ethBalances[this._protocolFeeCollectorAddress] = ethBalances[this._protocolFeeCollectorAddress].plus( - this._protocolFee, - ); - remainingValue = remainingValue.minus(this._protocolFee); - } else { - erc20Balances[this._takerAddress][this._wethAddress] = erc20Balances[this._takerAddress][ - this._wethAddress - ].minus(this._protocolFee); - erc20Balances[this._protocolFeeCollectorAddress][this._wethAddress] = erc20Balances[ - this._protocolFeeCollectorAddress - ][this._wethAddress].plus(this._protocolFee); - } - } - - return { - erc20: erc20Balances, - erc721: {}, - erc1155: {}, - eth: ethBalances, - }; - } - - private async _verifyBalancesAsync(expectedBalances: TokenBalances): Promise { - const { erc20: expectedErc20Balances, eth: expectedEthBalances } = expectedBalances; - const { erc20: actualErc20Balances, eth: actualEthBalances } = await this._getTokenBalancesAsync(); - const ownersByName = { - maker: this._makerAddress, - taker: this._takerAddress, - feeRecipient: this._feeRecipientAddress, - coordinator: this._coordinatorContract.address, - protocolFeeCollector: this._protocolFeeCollectorAddress, - }; - const tokensByName = { - makerAsset: this._makerAssetAddress, - takerAsset: this._takerAssetAddress, - makerFeeAsset: this._makerFeeAssetAddress, - takerFeeAsset: this._takerFeeAssetAddress, - weth: this._wethAddress, - }; - _.forIn(ownersByName, (ownerAddress, ownerName) => { - expect(actualEthBalances[ownerAddress], `${ownerName} eth balance`).to.bignumber.equal( - expectedEthBalances[ownerAddress], - ); - _.forIn(tokensByName, (tokenAddress, tokenName) => { - expect( - actualErc20Balances[ownerAddress][tokenAddress], - `${ownerName} ${tokenName} balance`, - ).to.bignumber.equal(expectedErc20Balances[ownerAddress][tokenAddress]); - }); - }); - } - - private _expectedFillEvent(order: SignedOrder): ExchangeFillEventArgs { - return { - makerAddress: order.makerAddress, - takerAddress: this._takerAddress, - senderAddress: order.senderAddress, - feeRecipientAddress: order.feeRecipientAddress, - makerAssetData: order.makerAssetData, - takerAssetData: order.takerAssetData, - makerFeeAssetData: order.makerFeeAssetData, - takerFeeAssetData: order.takerFeeAssetData, - makerAssetFilledAmount: order.makerAssetAmount, - takerAssetFilledAmount: order.takerAssetAmount, - makerFeePaid: order.makerFee, - takerFeePaid: order.takerFee, - protocolFeePaid: this._protocolFee, - orderHash: orderHashUtils.getOrderHashHex(order), - }; - } -} diff --git a/contracts/integrations/test/coordinator/deploy_coordinator.ts b/contracts/integrations/test/coordinator/deploy_coordinator.ts new file mode 100644 index 0000000000..031af5177c --- /dev/null +++ b/contracts/integrations/test/coordinator/deploy_coordinator.ts @@ -0,0 +1,20 @@ +import { artifacts, CoordinatorContract } from '@0x/contracts-coordinator'; +import { artifacts as exchangeArtifacts } from '@0x/contracts-exchange'; +import { BlockchainTestsEnvironment } from '@0x/contracts-test-utils'; +import { BigNumber } from '@0x/utils'; + +import { DeploymentManager } from '../utils/deployment_manager'; + +export async function deployCoordinatorAsync( + deployment: DeploymentManager, + environment: BlockchainTestsEnvironment, +): Promise { + return await CoordinatorContract.deployFrom0xArtifactAsync( + artifacts.Coordinator, + environment.provider, + deployment.txDefaults, + { ...exchangeArtifacts, ...artifacts }, + deployment.exchange.address, + new BigNumber(deployment.chainId), + ); +} diff --git a/contracts/integrations/test/deployment/deployment_manager_test.ts b/contracts/integrations/test/framework-unit-tests/deployment_manager_test.ts similarity index 99% rename from contracts/integrations/test/deployment/deployment_manager_test.ts rename to contracts/integrations/test/framework-unit-tests/deployment_manager_test.ts index 359f165922..fe36738361 100644 --- a/contracts/integrations/test/deployment/deployment_manager_test.ts +++ b/contracts/integrations/test/framework-unit-tests/deployment_manager_test.ts @@ -2,7 +2,7 @@ import { Authorizable, Ownable } from '@0x/contracts-exchange'; import { constants as stakingConstants } from '@0x/contracts-staking'; import { blockchainTests, expect } from '@0x/contracts-test-utils'; -import { DeploymentManager } from './deployment_mananger'; +import { DeploymentManager } from '../utils/deployment_manager'; blockchainTests('Deployment Manager', env => { let owner: string; diff --git a/contracts/integrations/test/framework-unit-tests/function_assertion_test.ts b/contracts/integrations/test/framework-unit-tests/function_assertion_test.ts new file mode 100644 index 0000000000..945e2082a7 --- /dev/null +++ b/contracts/integrations/test/framework-unit-tests/function_assertion_test.ts @@ -0,0 +1,133 @@ +import { + blockchainTests, + constants, + expect, + filterLogsToArguments, + getRandomInteger, + hexRandom, +} from '@0x/contracts-test-utils'; +import { BigNumber, StringRevertError } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { artifacts, TestFrameworkContract, TestFrameworkEventEventArgs, TestFrameworkEvents } from '../../src'; +import { FunctionAssertion, Result } from '../utils/function_assertions'; + +const { ZERO_AMOUNT, MAX_UINT256 } = constants; + +blockchainTests.resets('FunctionAssertion Unit Tests', env => { + let exampleContract: TestFrameworkContract; + + before(async () => { + exampleContract = await TestFrameworkContract.deployFrom0xArtifactAsync( + artifacts.TestFramework, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('runAsync', () => { + it('should call the before function with the provided arguments', async () => { + let sideEffectTarget = ZERO_AMOUNT; + const assertion = new FunctionAssertion(exampleContract.returnInteger, { + before: async (input: BigNumber) => { + sideEffectTarget = randomInput; + }, + after: async (beforeInfo: any, result: Result, input: BigNumber) => {}, + }); + const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256); + await assertion.runAsync(randomInput); + expect(sideEffectTarget).bignumber.to.be.eq(randomInput); + }); + + it('should call the after function with the provided arguments', async () => { + let sideEffectTarget = ZERO_AMOUNT; + const assertion = new FunctionAssertion(exampleContract.returnInteger, { + before: async (input: BigNumber) => {}, + after: async (beforeInfo: any, result: Result, input: BigNumber) => { + sideEffectTarget = input; + }, + }); + const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256); + await assertion.runAsync(randomInput); + expect(sideEffectTarget).bignumber.to.be.eq(randomInput); + }); + + it('should not fail immediately if the wrapped function fails', async () => { + const assertion = new FunctionAssertion(exampleContract.emptyRevert, { + before: async () => {}, + after: async (beforeInfo: any, result: Result) => {}, + }); + await assertion.runAsync(); + }); + + it('should pass the return value of "before" to "after"', async () => { + const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256); + let sideEffectTarget = ZERO_AMOUNT; + const assertion = new FunctionAssertion(exampleContract.returnInteger, { + before: async (input: BigNumber) => { + return randomInput; + }, + after: async (beforeInfo: any, result: Result, input: BigNumber) => { + sideEffectTarget = beforeInfo; + }, + }); + await assertion.runAsync(randomInput); + expect(sideEffectTarget).bignumber.to.be.eq(randomInput); + }); + + it('should pass the result from the function call to "after"', async () => { + let sideEffectTarget = ZERO_AMOUNT; + const assertion = new FunctionAssertion(exampleContract.returnInteger, { + before: async (input: BigNumber) => {}, + after: async (beforeInfo: any, result: Result, input: BigNumber) => { + sideEffectTarget = result.data; + }, + }); + const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256); + await assertion.runAsync(randomInput); + expect(sideEffectTarget).bignumber.to.be.eq(randomInput); + }); + + it('should pass the receipt from the function call to "after"', async () => { + let sideEffectTarget = {} as TransactionReceiptWithDecodedLogs; + const assertion = new FunctionAssertion(exampleContract.emitEvent, { + before: async (input: string) => {}, + after: async (beforeInfo: any, result: Result, input: string) => { + if (result.receipt) { + sideEffectTarget = result.receipt; + } + }, + }); + + const input = 'emitted data'; + await assertion.runAsync(input); + + // Ensure that the correct events were emitted. + const [event] = filterLogsToArguments( + sideEffectTarget.logs, + TestFrameworkEvents.Event, + ); + expect(event).to.be.deep.eq({ input }); + }); + + it('should pass the error to "after" if the function call fails', async () => { + let sideEffectTarget: Error; + const assertion = new FunctionAssertion(exampleContract.stringRevert, { + before: async string => {}, + after: async (any, result: Result, string) => { + sideEffectTarget = result.data; + }, + }); + const message = 'error message'; + await assertion.runAsync(message); + + const expectedError = new StringRevertError(message); + return expect( + new Promise((resolve, reject) => { + reject(sideEffectTarget); + }), + ).to.revertWith(expectedError); + }); + }); +}); diff --git a/contracts/integrations/test/deployment/deployment_test.ts b/contracts/integrations/test/internal-integration-tests/deployment_test.ts similarity index 100% rename from contracts/integrations/test/deployment/deployment_test.ts rename to contracts/integrations/test/internal-integration-tests/deployment_test.ts diff --git a/contracts/integrations/test/internal-integration-tests/exchange_integration_test.ts b/contracts/integrations/test/internal-integration-tests/exchange_integration_test.ts new file mode 100644 index 0000000000..33269335c8 --- /dev/null +++ b/contracts/integrations/test/internal-integration-tests/exchange_integration_test.ts @@ -0,0 +1,135 @@ +import { blockchainTests, constants, expect, filterLogsToArguments, OrderFactory } from '@0x/contracts-test-utils'; +import { DummyERC20TokenContract, IERC20TokenEvents, IERC20TokenTransferEventArgs } from '@0x/contracts-erc20'; +import { IExchangeEvents, IExchangeFillEventArgs } from '@0x/contracts-exchange'; +import { IStakingEventsEvents } from '@0x/contracts-staking'; +import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; + +import { AddressManager } from '../utils/address_manager'; +import { DeploymentManager } from '../utils/deployment_manager'; + +blockchainTests('Exchange & Staking', env => { + let accounts: string[]; + let makerAddress: string; + let takers: string[] = []; + let delegators: string[] = []; + let feeRecipientAddress: string; + let addressManager: AddressManager; + let deploymentManager: DeploymentManager; + let orderFactory: OrderFactory; + let makerAsset: DummyERC20TokenContract; + let takerAsset: DummyERC20TokenContract; + let feeAsset: DummyERC20TokenContract; + + const GAS_PRICE = 1e9; + + before(async () => { + const chainId = await env.getChainIdAsync(); + accounts = await env.getAccountAddressesAsync(); + [makerAddress, feeRecipientAddress, takers[0], takers[1], ...delegators] = accounts.slice(1); + deploymentManager = await DeploymentManager.deployAsync(env); + + // Create a staking pool with the operator as a maker address. + await deploymentManager.staking.stakingWrapper.createStakingPool.awaitTransactionSuccessAsync( + constants.ZERO_AMOUNT, + true, + { from: makerAddress }, + ); + + // Set up an address for market making. + addressManager = new AddressManager(); + await addressManager.addMakerAsync( + deploymentManager, + { + address: makerAddress, + mainToken: deploymentManager.tokens.erc20[0], + feeToken: deploymentManager.tokens.erc20[2], + }, + env, + deploymentManager.tokens.erc20[1], + feeRecipientAddress, + chainId, + ); + + // Set up two addresses for taking orders. + await Promise.all( + takers.map(taker => + addressManager.addTakerAsync(deploymentManager, { + address: taker, + mainToken: deploymentManager.tokens.erc20[1], + feeToken: deploymentManager.tokens.erc20[2], + }), + ), + ); + }); + + describe('fillOrder', () => { + it('should be able to fill an order', async () => { + const order = await addressManager.makers[0].orderFactory.newSignedOrderAsync({ + makerAddress, + makerAssetAmount: new BigNumber(1), + takerAssetAmount: new BigNumber(1), + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + feeRecipientAddress, + }); + + const receipt = await deploymentManager.exchange.fillOrder.awaitTransactionSuccessAsync( + order, + new BigNumber(1), + order.signature, + { + from: takers[0], + gasPrice: GAS_PRICE, + value: DeploymentManager.protocolFeeMultiplier.times(GAS_PRICE), + }, + ); + + // Ensure that the number of emitted logs is equal to 3. There should have been a fill event + // and two transfer events. A 'StakingPoolActivated' event should not be expected because + // the only staking pool that was created does not have enough stake. + expect(receipt.logs.length).to.be.eq(3); + + // Ensure that the fill event was correct. + const fillArgs = filterLogsToArguments(receipt.logs, IExchangeEvents.Fill); + expect(fillArgs.length).to.be.eq(1); + expect(fillArgs).to.be.deep.eq([ + { + makerAddress, + feeRecipientAddress, + makerAssetData: order.makerAssetData, + takerAssetData: order.takerAssetData, + makerFeeAssetData: order.makerFeeAssetData, + takerFeeAssetData: order.takerFeeAssetData, + orderHash: orderHashUtils.getOrderHashHex(order), + takerAddress: takers[0], + senderAddress: takers[0], + makerAssetFilledAmount: order.makerAssetAmount, + takerAssetFilledAmount: order.takerAssetAmount, + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + protocolFeePaid: DeploymentManager.protocolFeeMultiplier.times(GAS_PRICE), + }, + ]); + + // Ensure that the transfer events were correctly emitted. + const transferArgs = filterLogsToArguments( + receipt.logs, + IERC20TokenEvents.Transfer, + ); + expect(transferArgs.length).to.be.eq(2); + expect(transferArgs).to.be.deep.eq([ + { + _from: takers[0], + _to: makerAddress, + _value: order.takerAssetAmount, + }, + { + _from: makerAddress, + _to: takers[0], + _value: order.makerAssetAmount, + }, + ]); + }); + }); +}); diff --git a/contracts/integrations/test/utils/address_manager.ts b/contracts/integrations/test/utils/address_manager.ts new file mode 100644 index 0000000000..1aa01daedf --- /dev/null +++ b/contracts/integrations/test/utils/address_manager.ts @@ -0,0 +1,97 @@ +import { DummyERC20TokenContract } from '@0x/contracts-erc20'; +import { constants, OrderFactory, BlockchainTestsEnvironment } from '@0x/contracts-test-utils'; +import { assetDataUtils, Order, SignatureType, SignedOrder } from '@0x/order-utils'; + +import { DeploymentManager } from '../../src'; + +interface MarketMaker { + address: string; + orderFactory: OrderFactory; +} + +interface ConfigurationArgs { + address: string; + mainToken: DummyERC20TokenContract; + feeToken: DummyERC20TokenContract; +} + +export class AddressManager { + // A set of addresses that have been configured for market making. + public makers: MarketMaker[]; + + // A set of addresses that have been configured to take orders. + public takers: string[]; + + /** + * Sets up an address to take orders. + */ + public async addTakerAsync(deploymentManager: DeploymentManager, configArgs: ConfigurationArgs): Promise { + // Configure the taker address with the taker and fee tokens. + await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.mainToken); + await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.feeToken); + + // Add the taker to the list of configured taker addresses. + this.takers.push(configArgs.address); + } + + /** + * Sets up an address for market making. + */ + public async addMakerAsync( + deploymentManager: DeploymentManager, + configArgs: ConfigurationArgs, + environment: BlockchainTestsEnvironment, + takerToken: DummyERC20TokenContract, + feeRecipientAddress: string, + chainId: number, + ): Promise { + const accounts = await environment.getAccountAddressesAsync(); + + // Set up order signing for the maker address. + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + makerAddress: configArgs.address, + makerAssetData: assetDataUtils.encodeERC20AssetData(configArgs.mainToken.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + makerFeeAssetData: assetDataUtils.encodeERC20AssetData(configArgs.feeToken.address), + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(configArgs.feeToken.address), + feeRecipientAddress, + exchangeAddress: deploymentManager.exchange.address, + chainId, + }; + const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(configArgs.address)]; + const orderFactory = new OrderFactory(privateKey, defaultOrderParams); + + // Configure the maker address with the maker and fee tokens. + await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.mainToken); + await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.feeToken); + + // Add the maker to the list of configured maker addresses. + this.makers.push({ + address: configArgs.address, + orderFactory, + }); + } + + /** + * Sets up initial account balances for a token and approves the ERC20 asset proxy + * to transfer the token. + */ + protected async _configureTokenForAddressAsync( + deploymentManager: DeploymentManager, + address: string, + token: DummyERC20TokenContract, + ): Promise { + await token.setBalance.awaitTransactionSuccessAsync(address, constants.INITIAL_ERC20_BALANCE); + await token.approve.awaitTransactionSuccessAsync( + deploymentManager.assetProxies.erc20Proxy.address, + constants.MAX_UINT256, + { from: address }, + ); + } + + constructor() { + this.makers = []; + this.takers = []; + } +} diff --git a/contracts/integrations/test/deployment/deployment_mananger.ts b/contracts/integrations/test/utils/deployment_manager.ts similarity index 97% rename from contracts/integrations/test/deployment/deployment_mananger.ts rename to contracts/integrations/test/utils/deployment_manager.ts index 3a8002ce39..ae3a9ad32a 100644 --- a/contracts/integrations/test/deployment/deployment_mananger.ts +++ b/contracts/integrations/test/utils/deployment_manager.ts @@ -122,7 +122,9 @@ export interface DeploymentOptions { } export class DeploymentManager { - public static protocolFeeMultiplier = new BigNumber(150000); + public static readonly protocolFeeMultiplier = new BigNumber(150000); + public static readonly gasPrice = new BigNumber(1e9); // 1 Gwei + public static readonly protocolFee = DeploymentManager.gasPrice.times(DeploymentManager.protocolFeeMultiplier); /** * Fully deploy the 0x exchange and staking contracts and configure the system with the @@ -141,6 +143,7 @@ export class DeploymentManager { const txDefaults = { ...environment.txDefaults, from: owner, + gasPrice: DeploymentManager.gasPrice, }; // Deploy the contracts using the same owner and environment. @@ -148,8 +151,8 @@ export class DeploymentManager { const exchange = await ExchangeContract.deployFrom0xArtifactAsync( exchangeArtifacts.Exchange, environment.provider, - environment.txDefaults, - exchangeArtifacts, + txDefaults, + { ...ERC20Artifacts, ...exchangeArtifacts }, new BigNumber(chainId), ); const governor = await ZeroExGovernorContract.deployFrom0xArtifactAsync( @@ -200,7 +203,7 @@ export class DeploymentManager { staking.stakingProxy, ]); - return new DeploymentManager(assetProxies, governor, exchange, staking, tokens, chainId, accounts); + return new DeploymentManager(assetProxies, governor, exchange, staking, tokens, chainId, accounts, txDefaults); } /** @@ -493,5 +496,6 @@ export class DeploymentManager { public tokens: TokenContracts, public chainId: number, public accounts: string[], + public txDefaults: Partial, ) {} } diff --git a/contracts/integrations/test/utils/function_assertions.ts b/contracts/integrations/test/utils/function_assertions.ts new file mode 100644 index 0000000000..1bb81e3d4d --- /dev/null +++ b/contracts/integrations/test/utils/function_assertions.ts @@ -0,0 +1,68 @@ +import { PromiseWithTransactionHash } from '@0x/base-contract'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +export interface ContractGetterFunction { + callAsync: (...args: any[]) => Promise; +} + +export interface ContractWrapperFunction extends ContractGetterFunction { + awaitTransactionSuccessAsync?: (...args: any[]) => PromiseWithTransactionHash; +} + +export interface Condition { + before: (...args: any[]) => Promise; + after: (beforeInfo: any, result: Result, ...args: any[]) => Promise; +} + +export interface Result { + data?: any; + receipt?: TransactionReceiptWithDecodedLogs; + success: boolean; +} + +export class FunctionAssertion { + // A before and an after assertion that will be called around the wrapper function. + public condition: Condition; + + // The wrapper function that will be wrapped in assertions. + public wrapperFunction: ContractWrapperFunction; + + constructor(wrapperFunction: ContractWrapperFunction, condition: Condition) { + this.condition = condition; + this.wrapperFunction = wrapperFunction; + } + + /** + * Runs the wrapped function and fails if the before or after assertions fail. + * @param ...args The args to the contract wrapper function. + */ + public async runAsync(...args: any[]): Promise<{ beforeInfo: any; afterInfo: any }> { + // Call the before condition. + const beforeInfo = await this.condition.before(...args); + + // Initialize the callResult so that the default success value is true. + let callResult: Result = { success: true }; + + // Try to make the call to the function. If it is successful, pass the + // result and receipt to the after condition. + try { + callResult.data = await this.wrapperFunction.callAsync(...args); + callResult.receipt = + this.wrapperFunction.awaitTransactionSuccessAsync !== undefined + ? await this.wrapperFunction.awaitTransactionSuccessAsync(...args) + : undefined; + } catch (error) { + callResult.data = error; + callResult.success = false; + callResult.receipt = undefined; + } + + // Call the after condition. + const afterInfo = await this.condition.after(beforeInfo, callResult, ...args); + + return { + beforeInfo, + afterInfo, + }; + } +} diff --git a/contracts/integrations/tsconfig.json b/contracts/integrations/tsconfig.json index 541c4adba9..7f0aa6948a 100644 --- a/contracts/integrations/tsconfig.json +++ b/contracts/integrations/tsconfig.json @@ -2,5 +2,5 @@ "extends": "../../tsconfig", "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], - "files": ["generated-artifacts/TestStakingPlaceholder.json"] + "files": ["generated-artifacts/TestFramework.json"] } diff --git a/contracts/staking/CHANGELOG.json b/contracts/staking/CHANGELOG.json index 51328f4e8c..bcdb6fd73a 100644 --- a/contracts/staking/CHANGELOG.json +++ b/contracts/staking/CHANGELOG.json @@ -5,6 +5,18 @@ { "note": "Add more overflow safeguards to `LibFixedMath`", "pr": 2255 + }, + { + "note": "Refactored finalization state.", + "pr": 2276 + }, + { + "note": "Removed protocol fee != 0 assertion.", + "pr": 2278 + }, + { + "note": "Call `StakingProxy.assertValidStorageParams()` in `MixinParams.setParams()`", + "pr": 2279 } ] }, diff --git a/contracts/staking/contracts/src/StakingProxy.sol b/contracts/staking/contracts/src/StakingProxy.sol index cae4fff6af..7ab8c3381c 100644 --- a/contracts/staking/contracts/src/StakingProxy.sol +++ b/contracts/staking/contracts/src/StakingProxy.sol @@ -157,8 +157,8 @@ contract StakingProxy is // Asserts that a stake weight is <= 100%. // Asserts that pools allow >= 1 maker. // Asserts that all addresses are initialized. - function _assertValidStorageParams() - internal + function assertValidStorageParams() + public view { // Epoch length must be between 5 and 30 days long @@ -216,6 +216,6 @@ contract StakingProxy is } // Assert initialized storage values are valid - _assertValidStorageParams(); + assertValidStorageParams(); } } diff --git a/contracts/staking/contracts/src/ZrxVaultBackstop.sol b/contracts/staking/contracts/src/ZrxVaultBackstop.sol index 68a47c9c5f..08f7ab2f99 100644 --- a/contracts/staking/contracts/src/ZrxVaultBackstop.sol +++ b/contracts/staking/contracts/src/ZrxVaultBackstop.sol @@ -23,9 +23,12 @@ import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "./interfaces/IStructs.sol"; import "./interfaces/IZrxVault.sol"; import "./interfaces/IStakingProxy.sol"; +import "./interfaces/IZrxVaultBackstop.sol"; -contract ZrxVaultBackstop { +contract ZrxVaultBackstop is + IZrxVaultBackstop +{ using LibSafeMath for uint256; diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 576fbe22db..6d908881eb 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -41,17 +41,17 @@ contract MixinExchangeFees is /// (MixinExchangeManager). /// @param makerAddress The address of the order's maker. /// @param payerAddress The address of the protocol fee payer. - /// @param protocolFeePaid The protocol fee that should be paid. + /// @param protocolFee The protocol fee amount. This is either passed as ETH or transferred as WETH. function payProtocolFee( address makerAddress, address payerAddress, - uint256 protocolFeePaid + uint256 protocolFee ) external payable onlyExchange { - _assertValidProtocolFee(protocolFeePaid); + _assertValidProtocolFee(protocolFee); // Transfer the protocol fee to this address if it should be paid in // WETH. @@ -60,7 +60,7 @@ contract MixinExchangeFees is getWethContract().transferFrom( payerAddress, address(this), - protocolFeePaid + protocolFee ), "WETH_TRANSFER_FAILED" ); @@ -106,10 +106,10 @@ contract MixinExchangeFees is } // Credit the fees to the pool. - poolStatsPtr.feesCollected = feesCollectedByPool.safeAdd(protocolFeePaid); + poolStatsPtr.feesCollected = feesCollectedByPool.safeAdd(protocolFee); // Increase the total fees collected this epoch. - aggregatedStatsPtr.totalFeesCollected = aggregatedStatsPtr.totalFeesCollected.safeAdd(protocolFeePaid); + aggregatedStatsPtr.totalFeesCollected = aggregatedStatsPtr.totalFeesCollected.safeAdd(protocolFee); } /// @dev Get stats on a staking pool in this epoch. @@ -155,26 +155,18 @@ contract MixinExchangeFees is /// @dev Checks that the protocol fee passed into `payProtocolFee()` is /// valid. - /// @param protocolFeePaid The `protocolFeePaid` parameter to + /// @param protocolFee The `protocolFee` parameter to /// `payProtocolFee.` - function _assertValidProtocolFee(uint256 protocolFeePaid) + function _assertValidProtocolFee(uint256 protocolFee) private view { - if (protocolFeePaid == 0) { + // The protocol fee must equal the value passed to the contract; unless + // the value is zero, in which case the fee is taken in WETH. + if (msg.value != protocolFee && msg.value != 0) { LibRichErrors.rrevert( LibStakingRichErrors.InvalidProtocolFeePaymentError( - LibStakingRichErrors.ProtocolFeePaymentErrorCodes.ZeroProtocolFeePaid, - protocolFeePaid, - msg.value - ) - ); - } - if (msg.value != protocolFeePaid && msg.value != 0) { - LibRichErrors.rrevert( - LibStakingRichErrors.InvalidProtocolFeePaymentError( - LibStakingRichErrors.ProtocolFeePaymentErrorCodes.MismatchedFeeAndPayment, - protocolFeePaid, + protocolFee, msg.value ) ); diff --git a/contracts/staking/contracts/src/interfaces/IStaking.sol b/contracts/staking/contracts/src/interfaces/IStaking.sol index e5b3d9135e..fc336aefcc 100644 --- a/contracts/staking/contracts/src/interfaces/IStaking.sol +++ b/contracts/staking/contracts/src/interfaces/IStaking.sol @@ -19,11 +19,61 @@ pragma solidity ^0.5.9; pragma experimental ABIEncoderV2; +import "@0x/contracts-erc20/contracts/src/interfaces/IEtherToken.sol"; import "./IStructs.sol"; +import "./IZrxVault.sol"; interface IStaking { + /// @dev Adds a new exchange address + /// @param addr Address of exchange contract to add + function addExchangeAddress(address addr) + external; + + /// @dev Create a new staking pool. The sender will be the operator of this pool. + /// Note that an operator must be payable. + /// @param operatorShare Portion of rewards owned by the operator, in ppm. + /// @param addOperatorAsMaker Adds operator to the created pool as a maker for convenience iff true. + /// @return poolId The unique pool id generated for this pool. + function createStakingPool(uint32 operatorShare, bool addOperatorAsMaker) + external + returns (bytes32 poolId); + + /// @dev Decreases the operator share for the given pool (i.e. increases pool rewards for members). + /// @param poolId Unique Id of pool. + /// @param newOperatorShare The newly decreased percentage of any rewards owned by the operator. + function decreaseStakingPoolOperatorShare(bytes32 poolId, uint32 newOperatorShare) + external; + + /// @dev Begins a new epoch, preparing the prior one for finalization. + /// Throws if not enough time has passed between epochs or if the + /// previous epoch was not fully finalized. + /// @return numPoolsToFinalize The number of unfinalized pools. + function endEpoch() + external + returns (uint256); + + /// @dev Instantly finalizes a single pool that earned rewards in the previous + /// epoch, crediting it rewards for members and withdrawing operator's + /// rewards as WETH. This can be called by internal functions that need + /// to finalize a pool immediately. Does nothing if the pool is already + /// finalized or did not earn rewards in the previous epoch. + /// @param poolId The pool ID to finalize. + function finalizePool(bytes32 poolId) + external; + + /// @dev Initialize storage owned by this contract. + /// This function should not be called directly. + /// The StakingProxy contract will call it in `attachStakingContract()`. + function init() + external; + + /// @dev Allows caller to join a staking pool as a maker. + /// @param poolId Unique id of pool. + function joinStakingPoolAsMaker(bytes32 poolId) + external; + /// @dev Moves stake between statuses: 'undelegated' or 'delegated'. /// Delegated stake can also be moved between pools. /// This change comes into effect next epoch. @@ -40,18 +90,162 @@ interface IStaking { /// @dev Pays a protocol fee in ETH. /// @param makerAddress The address of the order's maker. /// @param payerAddress The address that is responsible for paying the protocol fee. - /// @param protocolFeePaid The amount of protocol fees that should be paid. + /// @param protocolFee The amount of protocol fees that should be paid. function payProtocolFee( address makerAddress, address payerAddress, - uint256 protocolFeePaid + uint256 protocolFee ) external payable; - /// @dev Stake ZRX tokens. Tokens are deposited into the ZRX Vault. Unstake to retrieve the ZRX. - /// Stake is in the 'Active' status. + /// @dev Removes an existing exchange address + /// @param addr Address of exchange contract to remove + function removeExchangeAddress(address addr) + external; + + /// @dev Set all configurable parameters at once. + /// @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 _cobbDouglasAlphaNumerator Numerator for cobb douglas alpha factor. + /// @param _cobbDouglasAlphaDenominator Denominator for cobb douglas alpha factor. + function setParams( + uint256 _epochDurationInSeconds, + uint32 _rewardDelegatedStakeWeight, + uint256 _minimumPoolStake, + uint32 _cobbDouglasAlphaNumerator, + uint32 _cobbDouglasAlphaDenominator + ) + external; + + /// @dev Stake ZRX tokens. Tokens are deposited into the ZRX Vault. + /// Unstake to retrieve the ZRX. Stake is in the 'Active' status. /// @param amount of ZRX to stake. function stake(uint256 amount) external; + + /// @dev Unstake. Tokens are withdrawn from the ZRX Vault and returned to + /// the staker. Stake must be in the 'undelegated' status in both the + /// current and next epoch in order to be unstaked. + /// @param amount of ZRX to unstake. + function unstake(uint256 amount) + external; + + /// @dev Withdraws the caller's WETH rewards that have accumulated + /// until the last epoch. + /// @param poolId Unique id of pool. + function withdrawDelegatorRewards(bytes32 poolId) + external; + + /// @dev Computes the reward balance in ETH of a specific member of a pool. + /// @param poolId Unique id of pool. + /// @param member The member of the pool. + /// @return totalReward Balance in ETH. + function computeRewardBalanceOfDelegator(bytes32 poolId, address member) + external + view + returns (uint256 reward); + + /// @dev Computes the reward balance in ETH of the operator of a pool. + /// @param poolId Unique id of pool. + /// @return totalReward Balance in ETH. + function computeRewardBalanceOfOperator(bytes32 poolId) + external + view + returns (uint256 reward); + + /// @dev Returns the earliest end time in seconds of this epoch. + /// The next epoch can begin once this time is reached. + /// Epoch period = [startTimeInSeconds..endTimeInSeconds) + /// @return Time in seconds. + function getCurrentEpochEarliestEndTimeInSeconds() + external + view + returns (uint256); + + /// @dev Gets global stake for a given status. + /// @param stakeStatus UNDELEGATED or DELEGATED + /// @return Global stake for given status. + function getGlobalStakeByStatus(IStructs.StakeStatus stakeStatus) + external + view + returns (IStructs.StoredBalance memory balance); + + /// @dev Gets an owner's stake balances by status. + /// @param staker Owner of stake. + /// @param stakeStatus UNDELEGATED or DELEGATED + /// @return Owner's stake balances for given status. + function getOwnerStakeByStatus( + address staker, + IStructs.StakeStatus stakeStatus + ) + external + view + returns (IStructs.StoredBalance memory balance); + + /// @dev Retrieves all configurable parameter values. + /// @return _epochDurationInSeconds Minimum seconds between epochs. + /// @return _rewardDelegatedStakeWeight How much delegated stake is weighted vs operator stake, in ppm. + /// @return _minimumPoolStake Minimum amount of stake required in a pool to collect rewards. + /// @return _cobbDouglasAlphaNumerator Numerator for cobb douglas alpha factor. + /// @return _cobbDouglasAlphaDenominator Denominator for cobb douglas alpha factor. + function getParams() + external + view + returns ( + uint256 _epochDurationInSeconds, + uint32 _rewardDelegatedStakeWeight, + uint256 _minimumPoolStake, + uint32 _cobbDouglasAlphaNumerator, + uint32 _cobbDouglasAlphaDenominator + ); + + /// @param staker of stake. + /// @param poolId Unique Id of pool. + /// @return Stake delegated to pool by staker. + function getStakeDelegatedToPoolByOwner(address staker, bytes32 poolId) + external + view + returns (IStructs.StoredBalance memory balance); + + /// @dev Returns a staking pool + /// @param poolId Unique id of pool. + function getStakingPool(bytes32 poolId) + external + view + returns (IStructs.Pool memory); + + /// @dev Get stats on a staking pool in this epoch. + /// @param poolId Pool Id to query. + /// @return PoolStats struct for pool id. + function getStakingPoolStatsThisEpoch(bytes32 poolId) + external + view + returns (IStructs.PoolStats memory); + + /// @dev Returns the total stake delegated to a specific staking pool, + /// across all members. + /// @param poolId Unique Id of pool. + /// @return Total stake delegated to pool. + function getTotalStakeDelegatedToPool(bytes32 poolId) + external + view + returns (IStructs.StoredBalance memory balance); + + /// @dev An overridable way to access the deployed WETH contract. + /// Must be view to allow overrides to access state. + /// @return wethContract The WETH contract instance. + function getWethContract() + external + view + returns (IEtherToken wethContract); + + /// @dev An overridable way to access the deployed zrxVault. + /// Must be view to allow overrides to access state. + /// @return zrxVault The zrxVault contract. + function getZrxVault() + external + view + returns (IZrxVault zrxVault); } diff --git a/contracts/staking/contracts/src/interfaces/IStakingProxy.sol b/contracts/staking/contracts/src/interfaces/IStakingProxy.sol index c29560cfdf..03dac64c01 100644 --- a/contracts/staking/contracts/src/interfaces/IStakingProxy.sol +++ b/contracts/staking/contracts/src/interfaces/IStakingProxy.sol @@ -55,4 +55,13 @@ contract IStakingProxy { external view returns (IStructs.ReadOnlyState memory); + + /// @dev Asserts that an epoch is between 5 and 30 days long. + // Asserts that 0 < cobb douglas alpha value <= 1. + // Asserts that a stake weight is <= 100%. + // Asserts that pools allow >= 1 maker. + // Asserts that all addresses are initialized. + function assertValidStorageParams() + external + view; } diff --git a/contracts/staking/contracts/src/interfaces/IZrxVaultBackstop.sol b/contracts/staking/contracts/src/interfaces/IZrxVaultBackstop.sol new file mode 100644 index 0000000000..7994f8845c --- /dev/null +++ b/contracts/staking/contracts/src/interfaces/IZrxVaultBackstop.sol @@ -0,0 +1,29 @@ +/* + + 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; + + +interface IZrxVaultBackstop { + + /// @dev Triggers catastophic failure mode in the zrxzVault iff read-only mode + /// has been continuously set for at least 40 days. + function enterCatastrophicFailureIfProlongedReadOnlyMode() + external; +} diff --git a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol index 30117d9ce8..6acca6d6b7 100644 --- a/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol +++ b/contracts/staking/contracts/src/libs/LibStakingRichErrors.sol @@ -29,11 +29,6 @@ library LibStakingRichErrors { CanOnlyDecreaseOperatorShare } - enum ProtocolFeePaymentErrorCodes { - ZeroProtocolFeePaid, - MismatchedFeeAndPayment - } - enum InitializationErrorCodes { MixinSchedulerAlreadyInitialized, MixinParamsAlreadyInitialized @@ -104,9 +99,9 @@ library LibStakingRichErrors { bytes4 internal constant INVALID_PARAM_VALUE_ERROR_SELECTOR = 0xfc45bd11; - // bytes4(keccak256("InvalidProtocolFeePaymentError(uint8,uint256,uint256)")) + // bytes4(keccak256("InvalidProtocolFeePaymentError(uint256,uint256)")) bytes4 internal constant INVALID_PROTOCOL_FEE_PAYMENT_ERROR_SELECTOR = - 0xefd6cb33; + 0x31d7a505; // bytes4(keccak256("PreviousEpochNotFinalizedError(uint256,uint256)")) bytes4 internal constant PREVIOUS_EPOCH_NOT_FINALIZED_ERROR_SELECTOR = @@ -252,7 +247,6 @@ library LibStakingRichErrors { } function InvalidProtocolFeePaymentError( - ProtocolFeePaymentErrorCodes errorCodes, uint256 expectedProtocolFeePaid, uint256 actualProtocolFeePaid ) @@ -262,7 +256,6 @@ library LibStakingRichErrors { { return abi.encodeWithSelector( INVALID_PROTOCOL_FEE_PAYMENT_ERROR_SELECTOR, - errorCodes, expectedProtocolFeePaid, actualProtocolFeePaid ); diff --git a/contracts/staking/contracts/src/stake/MixinStake.sol b/contracts/staking/contracts/src/stake/MixinStake.sol index 415792238e..b92b354e28 100644 --- a/contracts/staking/contracts/src/stake/MixinStake.sol +++ b/contracts/staking/contracts/src/stake/MixinStake.sol @@ -111,6 +111,17 @@ contract MixinStake is { address staker = msg.sender; + // Sanity check: no-op if no stake is being moved. + if (amount == 0) { + return; + } + + // Sanity check: no-op if moving stake from undelegated to undelegated. + if (from.status == IStructs.StakeStatus.UNDELEGATED && + to.status == IStructs.StakeStatus.UNDELEGATED) { + return; + } + // handle delegation if (from.status == IStructs.StakeStatus.DELEGATED) { _undelegateStake( @@ -167,19 +178,19 @@ contract MixinStake is staker ); - // Increment how much stake the staker has delegated to the input pool. + // Increase how much stake the staker has delegated to the input pool. _increaseNextBalance( _delegatedStakeToPoolByOwner[staker][poolId], amount ); - // Increment how much stake has been delegated to pool. + // Increase how much stake has been delegated to pool. _increaseNextBalance( _delegatedStakeByPoolId[poolId], amount ); - // Increase next balance of global delegated stake + // Increase next balance of global delegated stake. _increaseNextBalance( _globalStakeByStatus[uint8(IStructs.StakeStatus.DELEGATED)], amount @@ -205,19 +216,19 @@ contract MixinStake is staker ); - // decrement how much stake the staker has delegated to the input pool + // Decrease how much stake the staker has delegated to the input pool. _decreaseNextBalance( _delegatedStakeToPoolByOwner[staker][poolId], amount ); - // decrement how much stake has been delegated to pool + // Decrease how much stake has been delegated to pool. _decreaseNextBalance( _delegatedStakeByPoolId[poolId], amount ); - // decrease next balance of global delegated stake + // Decrease next balance of global delegated stake (aggregated across all stakers). _decreaseNextBalance( _globalStakeByStatus[uint8(IStructs.StakeStatus.DELEGATED)], amount diff --git a/contracts/staking/contracts/src/sys/MixinParams.sol b/contracts/staking/contracts/src/sys/MixinParams.sol index 169578f209..4fdb210401 100644 --- a/contracts/staking/contracts/src/sys/MixinParams.sol +++ b/contracts/staking/contracts/src/sys/MixinParams.sol @@ -22,6 +22,7 @@ import "@0x/contracts-utils/contracts/src/LibRichErrors.sol"; import "../immutable/MixinStorage.sol"; import "../immutable/MixinConstants.sol"; import "../interfaces/IStakingEvents.sol"; +import "../interfaces/IStakingProxy.sol"; import "../libs/LibStakingRichErrors.sol"; @@ -53,6 +54,10 @@ contract MixinParams is _cobbDouglasAlphaNumerator, _cobbDouglasAlphaDenominator ); + + // Let the staking proxy enforce that these parameters are within + // acceptable ranges. + IStakingProxy(address(this)).assertValidStorageParams(); } /// @dev Retrieves all configurable parameter values. diff --git a/contracts/staking/contracts/test/TestAssertStorageParams.sol b/contracts/staking/contracts/test/TestAssertStorageParams.sol index aa5c5c62b0..efe5355b89 100644 --- a/contracts/staking/contracts/test/TestAssertStorageParams.sol +++ b/contracts/staking/contracts/test/TestAssertStorageParams.sol @@ -49,7 +49,7 @@ contract TestAssertStorageParams is minimumPoolStake = params.minimumPoolStake; cobbDouglasAlphaNumerator = params.cobbDouglasAlphaNumerator; cobbDouglasAlphaDenominator = params.cobbDouglasAlphaDenominator; - _assertValidStorageParams(); + assertValidStorageParams(); } function _attachStakingContract(address) diff --git a/contracts/staking/contracts/test/TestMixinParams.sol b/contracts/staking/contracts/test/TestMixinParams.sol new file mode 100644 index 0000000000..6c51c584a7 --- /dev/null +++ b/contracts/staking/contracts/test/TestMixinParams.sol @@ -0,0 +1,47 @@ +/* + + 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/sys/MixinParams.sol"; + + +contract TestMixinParams is + MixinParams +{ + bool public shouldFailAssertValidStorageParams; + + /// @dev Set `shouldFailAssertValidStorageParams` + function setShouldFailAssertValidStorageParams(bool shouldFail) + external + { + shouldFailAssertValidStorageParams = shouldFail; + } + + /// @dev `IStakingProxy.assertValidStorageParams()` that reverts if + /// `shouldFailAssertValidStorageParams` is true. + function assertValidStorageParams() + public + view + { + if (shouldFailAssertValidStorageParams) { + revert("ASSERT_VALID_STORAGE_PARAMS_FAILED"); + } + } +} diff --git a/contracts/staking/contracts/test/TestMixinStakingPool.sol b/contracts/staking/contracts/test/TestMixinStakingPool.sol new file mode 100644 index 0000000000..02e370b014 --- /dev/null +++ b/contracts/staking/contracts/test/TestMixinStakingPool.sol @@ -0,0 +1,53 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "../src/interfaces/IStructs.sol"; +import "./TestStakingNoWETH.sol"; + + +contract TestMixinStakingPool is + TestStakingNoWETH +{ + function setLastPoolId(bytes32 poolId) + external + { + lastPoolId = poolId; + } + + function setPoolIdByMaker(bytes32 poolId, address maker) + external + { + poolIdByMaker[maker] = poolId; + } + + // solhint-disable no-empty-blocks + function testOnlyStakingPoolOperatorModifier(bytes32 poolId) + external + view + onlyStakingPoolOperator(poolId) + {} + + function setPoolById(bytes32 poolId, IStructs.Pool memory pool) + public + { + _poolById[poolId] = pool; + } +} diff --git a/contracts/staking/contracts/test/TestStakingProxy.sol b/contracts/staking/contracts/test/TestStakingProxy.sol index e1619f173a..b6c9ae1999 100644 --- a/contracts/staking/contracts/test/TestStakingProxy.sol +++ b/contracts/staking/contracts/test/TestStakingProxy.sol @@ -35,8 +35,8 @@ contract TestStakingProxy is ) {} - function _assertValidStorageParams() - internal + function assertValidStorageParams() + public view { require( diff --git a/contracts/staking/package.json b/contracts/staking/package.json index 0052b78911..dd52f1e46c 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/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|ReadOnlyProxy|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinStake|TestMixinStakeStorage|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault|ZrxVaultBackstop).json" + "abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|IZrxVaultBackstop|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|ReadOnlyProxy|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeStorage|TestMixinStakingPool|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault|ZrxVaultBackstop).json" }, "repository": { "type": "git", diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index 9e31c1564e..039e7abe88 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -12,6 +12,7 @@ import * as IStorage from '../generated-artifacts/IStorage.json'; import * as IStorageInit from '../generated-artifacts/IStorageInit.json'; import * as IStructs from '../generated-artifacts/IStructs.json'; import * as IZrxVault from '../generated-artifacts/IZrxVault.json'; +import * as IZrxVaultBackstop from '../generated-artifacts/IZrxVaultBackstop.json'; import * as LibCobbDouglas from '../generated-artifacts/LibCobbDouglas.json'; import * as LibFixedMath from '../generated-artifacts/LibFixedMath.json'; import * as LibFixedMathRichErrors from '../generated-artifacts/LibFixedMathRichErrors.json'; @@ -47,8 +48,10 @@ import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json' import * as TestLibProxy from '../generated-artifacts/TestLibProxy.json'; import * as TestLibProxyReceiver from '../generated-artifacts/TestLibProxyReceiver.json'; import * as TestLibSafeDowncast from '../generated-artifacts/TestLibSafeDowncast.json'; +import * as TestMixinParams from '../generated-artifacts/TestMixinParams.json'; import * as TestMixinStake from '../generated-artifacts/TestMixinStake.json'; import * as TestMixinStakeStorage from '../generated-artifacts/TestMixinStakeStorage.json'; +import * as TestMixinStakingPool from '../generated-artifacts/TestMixinStakingPool.json'; import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json'; import * as TestStaking from '../generated-artifacts/TestStaking.json'; import * as TestStakingNoWETH from '../generated-artifacts/TestStakingNoWETH.json'; @@ -74,6 +77,7 @@ export const artifacts = { IStorageInit: IStorageInit as ContractArtifact, IStructs: IStructs as ContractArtifact, IZrxVault: IZrxVault as ContractArtifact, + IZrxVaultBackstop: IZrxVaultBackstop as ContractArtifact, LibCobbDouglas: LibCobbDouglas as ContractArtifact, LibFixedMath: LibFixedMath as ContractArtifact, LibFixedMathRichErrors: LibFixedMathRichErrors as ContractArtifact, @@ -101,8 +105,10 @@ export const artifacts = { TestLibProxy: TestLibProxy as ContractArtifact, TestLibProxyReceiver: TestLibProxyReceiver as ContractArtifact, TestLibSafeDowncast: TestLibSafeDowncast as ContractArtifact, + TestMixinParams: TestMixinParams as ContractArtifact, TestMixinStake: TestMixinStake as ContractArtifact, TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact, + TestMixinStakingPool: TestMixinStakingPool as ContractArtifact, TestProtocolFees: TestProtocolFees as ContractArtifact, TestStaking: TestStaking as ContractArtifact, TestStakingNoWETH: TestStakingNoWETH as ContractArtifact, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index 0ce6576d60..f446e89eb5 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -10,6 +10,7 @@ export * from '../generated-wrappers/i_storage'; export * from '../generated-wrappers/i_storage_init'; export * from '../generated-wrappers/i_structs'; export * from '../generated-wrappers/i_zrx_vault'; +export * from '../generated-wrappers/i_zrx_vault_backstop'; export * from '../generated-wrappers/lib_cobb_douglas'; export * from '../generated-wrappers/lib_fixed_math'; export * from '../generated-wrappers/lib_fixed_math_rich_errors'; @@ -45,8 +46,10 @@ export * from '../generated-wrappers/test_lib_fixed_math'; export * from '../generated-wrappers/test_lib_proxy'; export * from '../generated-wrappers/test_lib_proxy_receiver'; export * from '../generated-wrappers/test_lib_safe_downcast'; +export * from '../generated-wrappers/test_mixin_params'; export * from '../generated-wrappers/test_mixin_stake'; export * from '../generated-wrappers/test_mixin_stake_storage'; +export * from '../generated-wrappers/test_mixin_staking_pool'; export * from '../generated-wrappers/test_protocol_fees'; export * from '../generated-wrappers/test_staking'; export * from '../generated-wrappers/test_staking_no_w_e_t_h'; diff --git a/contracts/staking/test/migration_test.ts b/contracts/staking/test/migration_test.ts index 7438bdeb7c..890e002c93 100644 --- a/contracts/staking/test/migration_test.ts +++ b/contracts/staking/test/migration_test.ts @@ -228,7 +228,7 @@ blockchainTests('Migration tests', env => { const expectedError = new StakingRevertErrors.InvalidParamValueError( StakingRevertErrors.InvalidParamValueErrorCodes.InvalidEpochDuration, ); - expect(tx).to.revertWith(expectedError); + return expect(tx).to.revertWith(expectedError); }); it('reverts if epoch duration is > 30 days', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ @@ -238,21 +238,21 @@ blockchainTests('Migration tests', env => { const expectedError = new StakingRevertErrors.InvalidParamValueError( StakingRevertErrors.InvalidParamValueErrorCodes.InvalidEpochDuration, ); - expect(tx).to.revertWith(expectedError); + return expect(tx).to.revertWith(expectedError); }); it('succeeds if epoch duration is 5 days', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ ...stakingConstants.DEFAULT_PARAMS, epochDurationInSeconds: fiveDays, }); - expect(tx).to.be.fulfilled(''); + return expect(tx).to.be.fulfilled(''); }); it('succeeds if epoch duration is 30 days', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ ...stakingConstants.DEFAULT_PARAMS, epochDurationInSeconds: thirtyDays, }); - expect(tx).to.be.fulfilled(''); + return expect(tx).to.be.fulfilled(''); }); it('reverts if alpha denominator is 0', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ @@ -262,7 +262,7 @@ blockchainTests('Migration tests', env => { const expectedError = new StakingRevertErrors.InvalidParamValueError( StakingRevertErrors.InvalidParamValueErrorCodes.InvalidCobbDouglasAlpha, ); - expect(tx).to.revertWith(expectedError); + return expect(tx).to.revertWith(expectedError); }); it('reverts if alpha > 1', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ @@ -273,7 +273,7 @@ blockchainTests('Migration tests', env => { const expectedError = new StakingRevertErrors.InvalidParamValueError( StakingRevertErrors.InvalidParamValueErrorCodes.InvalidCobbDouglasAlpha, ); - expect(tx).to.revertWith(expectedError); + return expect(tx).to.revertWith(expectedError); }); it('succeeds if alpha == 1', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ @@ -281,7 +281,7 @@ blockchainTests('Migration tests', env => { cobbDouglasAlphaNumerator: new BigNumber(1), cobbDouglasAlphaDenominator: new BigNumber(1), }); - expect(tx).to.be.fulfilled(''); + return expect(tx).to.be.fulfilled(''); }); it('succeeds if alpha == 0', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ @@ -289,7 +289,7 @@ blockchainTests('Migration tests', env => { cobbDouglasAlphaNumerator: constants.ZERO_AMOUNT, cobbDouglasAlphaDenominator: new BigNumber(1), }); - expect(tx).to.be.fulfilled(''); + return expect(tx).to.be.fulfilled(''); }); it('reverts if delegation weight is > 100%', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ @@ -299,14 +299,14 @@ blockchainTests('Migration tests', env => { const expectedError = new StakingRevertErrors.InvalidParamValueError( StakingRevertErrors.InvalidParamValueErrorCodes.InvalidRewardDelegatedStakeWeight, ); - expect(tx).to.revertWith(expectedError); + return expect(tx).to.revertWith(expectedError); }); it('succeeds if delegation weight is 100%', async () => { const tx = proxyContract.setAndAssertParams.awaitTransactionSuccessAsync({ ...stakingConstants.DEFAULT_PARAMS, rewardDelegatedStakeWeight: new BigNumber(stakingConstants.PPM), }); - expect(tx).to.be.fulfilled(''); + return expect(tx).to.be.fulfilled(''); }); }); }); diff --git a/contracts/staking/test/unit_tests/params_test.ts b/contracts/staking/test/unit_tests/params_test.ts index f24c6ec983..e49a46a141 100644 --- a/contracts/staking/test/unit_tests/params_test.ts +++ b/contracts/staking/test/unit_tests/params_test.ts @@ -3,20 +3,20 @@ import { AuthorizableRevertErrors, BigNumber } from '@0x/utils'; import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; -import { artifacts, IStakingEventsParamsSetEventArgs, MixinParamsContract } from '../../src/'; +import { artifacts, IStakingEventsParamsSetEventArgs, TestMixinParamsContract } from '../../src/'; import { constants as stakingConstants } from '../utils/constants'; import { StakingParams } from '../utils/types'; blockchainTests('Configurable Parameters unit tests', env => { - let testContract: MixinParamsContract; + let testContract: TestMixinParamsContract; let authorizedAddress: string; let notAuthorizedAddress: string; before(async () => { [authorizedAddress, notAuthorizedAddress] = await env.getAccountAddressesAsync(); - testContract = await MixinParamsContract.deployFrom0xArtifactAsync( - artifacts.MixinParams, + testContract = await TestMixinParamsContract.deployFrom0xArtifactAsync( + artifacts.TestMixinParams, env.provider, env.txDefaults, artifacts, @@ -66,6 +66,12 @@ blockchainTests('Configurable Parameters unit tests', env => { return expect(tx).to.revertWith(expectedError); }); + it('throws if `assertValidStorageParams()` throws`', async () => { + await testContract.setShouldFailAssertValidStorageParams.awaitTransactionSuccessAsync(true); + const tx = setParamsAndAssertAsync({}); + return expect(tx).to.revertWith('ASSERT_VALID_STORAGE_PARAMS_FAILED'); + }); + it('works if called by owner', async () => { return setParamsAndAssertAsync({}); }); diff --git a/contracts/staking/test/unit_tests/protocol_fees_test.ts b/contracts/staking/test/unit_tests/protocol_fees_test.ts index d5b5dcd53d..1c027fad3a 100644 --- a/contracts/staking/test/unit_tests/protocol_fees_test.ts +++ b/contracts/staking/test/unit_tests/protocol_fees_test.ts @@ -90,22 +90,7 @@ blockchainTests('Protocol Fees unit tests', env => { 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 () => { + it('should revert if `protocolFee` is zero with non-zero value sent', async () => { const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -113,14 +98,13 @@ blockchainTests('Protocol Fees unit tests', env => { { 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 () => { + it('should revert if `protocolFee` is < than the provided message value', async () => { const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -128,14 +112,13 @@ blockchainTests('Protocol Fees unit tests', env => { { 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 () => { + it('should revert if `protocolFee` is > than the provided message value', async () => { const tx = testContract.payProtocolFee.awaitTransactionSuccessAsync( makerAddress, payerAddress, @@ -143,7 +126,6 @@ blockchainTests('Protocol Fees unit tests', env => { { 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), ); diff --git a/contracts/staking/test/unit_tests/stake_test.ts b/contracts/staking/test/unit_tests/stake_test.ts index a4b84fd251..76e5dd5d47 100644 --- a/contracts/staking/test/unit_tests/stake_test.ts +++ b/contracts/staking/test/unit_tests/stake_test.ts @@ -373,7 +373,7 @@ blockchainTests.resets('MixinStake unit tests', env => { expect(increaseNextBalanceEvents).to.be.length(0); }); - it('moves the owner stake between the same pointer when both are undelegated', async () => { + it('does nothing when moving the owner stake from undelegated to undelegated', async () => { const amount = getRandomInteger(0, 100e18); const { logs } = await testContract.moveStake.awaitTransactionSuccessAsync( { status: StakeStatus.Undelegated, poolId: VALID_POOL_IDS[0] }, @@ -381,10 +381,18 @@ blockchainTests.resets('MixinStake unit tests', env => { amount, ); const events = filterLogsToArguments(logs, StakeEvents.MoveStakeStorage); - expect(events).to.be.length(1); - expect(events[0].fromBalanceSlot).to.eq(stakerUndelegatedStakeSlot); - expect(events[0].toBalanceSlot).to.eq(stakerUndelegatedStakeSlot); - expect(events[0].amount).to.bignumber.eq(amount); + expect(events).to.be.length(0); + }); + + it('does nothing when moving zero stake', async () => { + const amount = new BigNumber(0); + const { logs } = await testContract.moveStake.awaitTransactionSuccessAsync( + { status: StakeStatus.Delegated, poolId: VALID_POOL_IDS[0] }, + { status: StakeStatus.Delegated, poolId: VALID_POOL_IDS[1] }, + amount, + ); + const events = filterLogsToArguments(logs, StakeEvents.MoveStakeStorage); + expect(events).to.be.length(0); }); it('moves the owner stake between the same pointer when both are delegated', async () => { diff --git a/contracts/staking/test/unit_tests/staking_pool_test.ts b/contracts/staking/test/unit_tests/staking_pool_test.ts new file mode 100644 index 0000000000..fef82105fb --- /dev/null +++ b/contracts/staking/test/unit_tests/staking_pool_test.ts @@ -0,0 +1,373 @@ +import { + blockchainTests, + constants, + expect, + filterLogsToArguments, + hexLeftPad, + hexRandom, + toHex, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { StakingRevertErrors } from '@0x/order-utils'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { + artifacts, + TestMixinStakingPoolContract, + TestMixinStakingPoolEvents, + TestMixinStakingPoolStakingPoolCreatedEventArgs as StakingPoolCreated, +} from '../../src'; + +blockchainTests.resets('MixinStakingPool unit tests', env => { + let testContract: TestMixinStakingPoolContract; + let operator: string; + let maker: string; + let notOperatorOrMaker: string; + + before(async () => { + [operator, maker, notOperatorOrMaker] = await env.getAccountAddressesAsync(); + testContract = await TestMixinStakingPoolContract.deployFrom0xArtifactAsync( + artifacts.TestMixinStakingPool, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + function toNextPoolId(lastPoolId: string): string { + return hexLeftPad(new BigNumber(lastPoolId.slice(2), 16).plus(1)); + } + + function randomOperatorShare(): number { + return _.random(0, constants.PPM_100_PERCENT); + } + + interface CreatePoolOpts { + poolId: string; + operator: string; + operatorShare: number; + } + + async function createPoolAsync(opts?: Partial): Promise { + const _opts = { + poolId: hexRandom(), + operator, + operatorShare: randomOperatorShare(), + ...opts, + }; + await testContract.setPoolById.awaitTransactionSuccessAsync(_opts.poolId, { + operator: _opts.operator, + operatorShare: _opts.operatorShare, + }); + return _opts; + } + + async function addMakerToPoolAsync(poolId: string, _maker: string): Promise { + await testContract.setPoolIdByMaker.awaitTransactionSuccessAsync(poolId, _maker); + } + + describe('onlyStakingPoolOperator modifier', () => { + it('fails if not called by the pool operator', async () => { + const { poolId } = await createPoolAsync(); + const tx = testContract.testOnlyStakingPoolOperatorModifier.callAsync(poolId, { from: notOperatorOrMaker }); + const expectedError = new StakingRevertErrors.OnlyCallableByPoolOperatorError(notOperatorOrMaker, poolId); + return expect(tx).to.revertWith(expectedError); + }); + it('fails if called by a pool maker', async () => { + const { poolId } = await createPoolAsync(); + await addMakerToPoolAsync(poolId, maker); + const tx = testContract.testOnlyStakingPoolOperatorModifier.callAsync(poolId, { from: maker }); + const expectedError = new StakingRevertErrors.OnlyCallableByPoolOperatorError(maker, poolId); + return expect(tx).to.revertWith(expectedError); + }); + it('succeeds if called by the pool operator', async () => { + const { poolId } = await createPoolAsync(); + await testContract.testOnlyStakingPoolOperatorModifier.callAsync(poolId, { from: operator }); + }); + }); + + describe('createStakingPool()', () => { + let nextPoolId: string; + + before(async () => { + nextPoolId = toNextPoolId(await testContract.lastPoolId.callAsync()); + }); + + it('fails if the next pool ID overflows', async () => { + await testContract.setLastPoolId.awaitTransactionSuccessAsync(toHex(constants.MAX_UINT256)); + const tx = testContract.createStakingPool.awaitTransactionSuccessAsync(randomOperatorShare(), false); + const expectedError = new SafeMathRevertErrors.Uint256BinOpError( + SafeMathRevertErrors.BinOpErrorCodes.AdditionOverflow, + constants.MAX_UINT256, + new BigNumber(1), + ); + return expect(tx).to.revertWith(expectedError); + }); + it('fails if the operator share is invalid', async () => { + const operatorShare = constants.PPM_100_PERCENT + 1; + const tx = testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, false); + const expectedError = new StakingRevertErrors.OperatorShareError( + StakingRevertErrors.OperatorShareErrorCodes.OperatorShareTooLarge, + nextPoolId, + operatorShare, + ); + return expect(tx).to.revertWith(expectedError); + }); + it('operator can create and own multiple pools', async () => { + const { logs: logs1 } = await testContract.createStakingPool.awaitTransactionSuccessAsync( + randomOperatorShare(), + false, + ); + const { logs: logs2 } = await testContract.createStakingPool.awaitTransactionSuccessAsync( + randomOperatorShare(), + false, + ); + const createEvents = filterLogsToArguments( + [...logs1, ...logs2], + TestMixinStakingPoolEvents.StakingPoolCreated, + ); + expect(createEvents).to.be.length(2); + const poolIds = createEvents.map(e => e.poolId); + expect(poolIds[0]).to.not.eq(poolIds[1]); + const pools = await Promise.all(poolIds.map(async poolId => testContract.getStakingPool.callAsync(poolId))); + expect(pools[0].operator).to.eq(pools[1].operator); + }); + it('operator can only be maker of one pool', async () => { + await testContract.createStakingPool.awaitTransactionSuccessAsync(randomOperatorShare(), true); + const { logs } = await testContract.createStakingPool.awaitTransactionSuccessAsync( + randomOperatorShare(), + true, + ); + const createEvents = filterLogsToArguments( + logs, + TestMixinStakingPoolEvents.StakingPoolCreated, + ); + const makerPool = await testContract.poolIdByMaker.callAsync(operator); + expect(makerPool).to.eq(createEvents[0].poolId); + }); + it('computes correct next pool ID', async () => { + const { logs } = await testContract.createStakingPool.awaitTransactionSuccessAsync( + randomOperatorShare(), + false, + ); + const createEvents = filterLogsToArguments( + logs, + TestMixinStakingPoolEvents.StakingPoolCreated, + ); + const poolId = createEvents[0].poolId; + expect(poolId).to.eq(nextPoolId); + }); + it('increments last pool ID counter', async () => { + await testContract.createStakingPool.awaitTransactionSuccessAsync(randomOperatorShare(), false); + const lastPoolIdAfter = await testContract.lastPoolId.callAsync(); + expect(lastPoolIdAfter).to.eq(nextPoolId); + }); + it('records pool details', async () => { + const operatorShare = randomOperatorShare(); + await testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, false, { from: operator }); + const pool = await testContract.getStakingPool.callAsync(nextPoolId); + expect(pool.operator).to.eq(operator); + expect(pool.operatorShare).to.bignumber.eq(operatorShare); + }); + it('returns the next pool ID', async () => { + const poolId = await testContract.createStakingPool.callAsync(randomOperatorShare(), false, { + from: operator, + }); + expect(poolId).to.eq(nextPoolId); + }); + it('can add operator as a maker', async () => { + const operatorShare = randomOperatorShare(); + await testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, true, { from: operator }); + const makerPoolId = await testContract.poolIdByMaker.callAsync(operator); + expect(makerPoolId).to.eq(nextPoolId); + }); + it('emits a `StakingPoolCreated` event', async () => { + const operatorShare = randomOperatorShare(); + const { logs } = await testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, false, { + from: operator, + }); + verifyEventsFromLogs( + logs, + [ + { + poolId: nextPoolId, + operator, + operatorShare, + }, + ], + TestMixinStakingPoolEvents.StakingPoolCreated, + ); + }); + it('emits a `MakerStakingPoolSet` event when also joining as a maker', async () => { + const operatorShare = randomOperatorShare(); + const { logs } = await testContract.createStakingPool.awaitTransactionSuccessAsync(operatorShare, true, { + from: operator, + }); + verifyEventsFromLogs( + logs, + [ + { + makerAddress: operator, + poolId: nextPoolId, + }, + ], + TestMixinStakingPoolEvents.MakerStakingPoolSet, + ); + }); + }); + + describe('decreaseStakingPoolOperatorShare()', () => { + it('fails if not called by operator', async () => { + const { poolId, operatorShare } = await createPoolAsync(); + const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( + poolId, + operatorShare - 1, + { from: notOperatorOrMaker }, + ); + const expectedError = new StakingRevertErrors.OnlyCallableByPoolOperatorError(notOperatorOrMaker, poolId); + return expect(tx).to.revertWith(expectedError); + }); + it('fails if called by maker', async () => { + const { poolId, operatorShare } = await createPoolAsync(); + await addMakerToPoolAsync(poolId, maker); + const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( + poolId, + operatorShare - 1, + { from: maker }, + ); + const expectedError = new StakingRevertErrors.OnlyCallableByPoolOperatorError(maker, poolId); + return expect(tx).to.revertWith(expectedError); + }); + it('fails if operator share is equal to current', async () => { + const { poolId, operatorShare } = await createPoolAsync(); + const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( + poolId, + operatorShare, + { from: operator }, + ); + const expectedError = new StakingRevertErrors.OperatorShareError( + StakingRevertErrors.OperatorShareErrorCodes.CanOnlyDecreaseOperatorShare, + poolId, + operatorShare, + ); + return expect(tx).to.revertWith(expectedError); + }); + it('fails if operator share is greater than current', async () => { + const { poolId, operatorShare } = await createPoolAsync(); + const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( + poolId, + operatorShare + 1, + { from: operator }, + ); + const expectedError = new StakingRevertErrors.OperatorShareError( + StakingRevertErrors.OperatorShareErrorCodes.CanOnlyDecreaseOperatorShare, + poolId, + operatorShare + 1, + ); + return expect(tx).to.revertWith(expectedError); + }); + it('fails if operator share is greater than PPM_100_PERCENT', async () => { + const { poolId } = await createPoolAsync(); + const tx = testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( + poolId, + constants.PPM_100_PERCENT + 1, + { from: operator }, + ); + const expectedError = new StakingRevertErrors.OperatorShareError( + StakingRevertErrors.OperatorShareErrorCodes.OperatorShareTooLarge, + poolId, + constants.PPM_100_PERCENT + 1, + ); + return expect(tx).to.revertWith(expectedError); + }); + it('records new operator share', async () => { + const { poolId, operatorShare } = await createPoolAsync(); + await testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( + poolId, + operatorShare - 1, + { from: operator }, + ); + const pool = await testContract.getStakingPool.callAsync(poolId); + expect(pool.operatorShare).to.bignumber.eq(operatorShare - 1); + }); + it('does not modify operator', async () => { + const { poolId, operatorShare } = await createPoolAsync(); + await testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( + poolId, + operatorShare - 1, + { from: operator }, + ); + const pool = await testContract.getStakingPool.callAsync(poolId); + expect(pool.operator).to.eq(operator); + }); + it('emits an `OperatorShareDecreased` event', async () => { + const { poolId, operatorShare } = await createPoolAsync(); + const { logs } = await testContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( + poolId, + operatorShare - 1, + { from: operator }, + ); + verifyEventsFromLogs( + logs, + [ + { + poolId, + oldOperatorShare: operatorShare, + newOperatorShare: operatorShare - 1, + }, + ], + TestMixinStakingPoolEvents.OperatorShareDecreased, + ); + }); + }); + + describe('joinStakingPoolAsMaker()', () => { + it('records sender as maker for the pool', async () => { + const { poolId } = await createPoolAsync(); + await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: maker }); + const makerPoolId = await testContract.poolIdByMaker.callAsync(maker); + expect(makerPoolId).to.eq(poolId); + }); + it('operator can join as maker for the pool', async () => { + const { poolId } = await createPoolAsync(); + await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: operator }); + const makerPoolId = await testContract.poolIdByMaker.callAsync(operator); + expect(makerPoolId).to.eq(poolId); + }); + it('can join the same pool as a maker twice', async () => { + const { poolId } = await createPoolAsync(); + await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: maker }); + await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: maker }); + const makerPoolId = await testContract.poolIdByMaker.callAsync(maker); + expect(makerPoolId).to.eq(poolId); + }); + it('can only be a maker in one pool at a time', async () => { + const { poolId: poolId1 } = await createPoolAsync(); + const { poolId: poolId2 } = await createPoolAsync(); + await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId1, { from: maker }); + let makerPoolId = await testContract.poolIdByMaker.callAsync(maker); + expect(makerPoolId).to.eq(poolId1); + await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId2, { from: maker }); + makerPoolId = await testContract.poolIdByMaker.callAsync(maker); + expect(makerPoolId).to.eq(poolId2); + }); + it('emits a `MakerStakingPoolSet` event', async () => { + const { poolId } = await createPoolAsync(); + const { logs } = await testContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { + from: maker, + }); + verifyEventsFromLogs( + logs, + [ + { + makerAddress: maker, + poolId, + }, + ], + TestMixinStakingPoolEvents.MakerStakingPoolSet, + ); + }); + }); +}); +// tslint:disable: max-file-line-count diff --git a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts index e321c0b0cb..e16f12c323 100644 --- a/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts +++ b/contracts/staking/test/utils/cumulative_reward_tracking_simulation.ts @@ -24,7 +24,7 @@ interface TestLog { export class CumulativeRewardTrackingSimulation { private readonly _amountToStake = toBaseUnitAmount(100); - private readonly _protocolFeeAmount = new BigNumber(10); + private readonly _protocolFee = new BigNumber(10); private readonly _stakingApiWrapper: StakingApiWrapper; private readonly _staker: string; private readonly _poolOperator: string; @@ -141,8 +141,8 @@ export class CumulativeRewardTrackingSimulation { receipt = await this._stakingApiWrapper.stakingContract.payProtocolFee.awaitTransactionSuccessAsync( this._poolOperator, this._takerAddress, - this._protocolFeeAmount, - { from: this._exchangeAddress, value: this._protocolFeeAmount }, + this._protocolFee, + { from: this._exchangeAddress, value: this._protocolFee }, ); break; diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index b8371bcb1b..8236bba76a 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -10,6 +10,7 @@ "generated-artifacts/IStorageInit.json", "generated-artifacts/IStructs.json", "generated-artifacts/IZrxVault.json", + "generated-artifacts/IZrxVaultBackstop.json", "generated-artifacts/LibCobbDouglas.json", "generated-artifacts/LibFixedMath.json", "generated-artifacts/LibFixedMathRichErrors.json", @@ -45,8 +46,10 @@ "generated-artifacts/TestLibProxy.json", "generated-artifacts/TestLibProxyReceiver.json", "generated-artifacts/TestLibSafeDowncast.json", + "generated-artifacts/TestMixinParams.json", "generated-artifacts/TestMixinStake.json", "generated-artifacts/TestMixinStakeStorage.json", + "generated-artifacts/TestMixinStakingPool.json", "generated-artifacts/TestProtocolFees.json", "generated-artifacts/TestStaking.json", "generated-artifacts/TestStakingNoWETH.json", diff --git a/contracts/test-utils/CHANGELOG.json b/contracts/test-utils/CHANGELOG.json index c9655a02a2..fbac14a39b 100644 --- a/contracts/test-utils/CHANGELOG.json +++ b/contracts/test-utils/CHANGELOG.json @@ -97,6 +97,10 @@ { "note": "Add `number_utils.ts` and `hexSize()`", "pr": 2220 + }, + { + "note": "Add `verifyEventsFromLogs()`", + "pr": 2287 } ], "timestamp": 1570135330 diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 48121d3870..75aa8cec38 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -15,7 +15,7 @@ export { export { getLatestBlockTimestampAsync, increaseTimeAndMineBlockAsync } from './block_timestamp'; export { provider, txDefaults, web3Wrapper } from './web3_wrapper'; export { LogDecoder } from './log_decoder'; -export { filterLogs, filterLogsToArguments, verifyEvents } from './log_utils'; +export { filterLogs, filterLogsToArguments, verifyEvents, verifyEventsFromLogs } from './log_utils'; export { signingUtils } from './signing_utils'; export { orderUtils } from './order_utils'; export { typeEncodingUtils } from './type_encoding_utils'; diff --git a/contracts/test-utils/src/log_utils.ts b/contracts/test-utils/src/log_utils.ts index 8c09add427..07086eca5c 100644 --- a/contracts/test-utils/src/log_utils.ts +++ b/contracts/test-utils/src/log_utils.ts @@ -26,9 +26,20 @@ export function verifyEvents( expectedEvents: TEventArgs[], eventName: string, ): void { - const logs = filterLogsToArguments(txReceipt.logs, eventName); - expect(logs.length).to.eq(expectedEvents.length); - logs.forEach((log, index) => { + return verifyEventsFromLogs(txReceipt.logs, expectedEvents, eventName); +} + +/** + * Given a collection of logs, verifies that matching events are identical. + */ +export function verifyEventsFromLogs( + logs: LogEntry[], + expectedEvents: TEventArgs[], + eventName: string, +): void { + const _logs = filterLogsToArguments(logs, eventName); + expect(_logs.length).to.eq(expectedEvents.length); + _logs.forEach((log, index) => { expect(log).to.deep.equal(expectedEvents[index]); }); } diff --git a/contracts/test-utils/src/order_factory.ts b/contracts/test-utils/src/order_factory.ts index 1a2e1187e6..4a20febc4b 100644 --- a/contracts/test-utils/src/order_factory.ts +++ b/contracts/test-utils/src/order_factory.ts @@ -9,10 +9,12 @@ import { signingUtils } from './signing_utils'; export class OrderFactory { private readonly _defaultOrderParams: Partial; private readonly _privateKey: Buffer; + constructor(privateKey: Buffer, defaultOrderParams: Partial) { this._defaultOrderParams = defaultOrderParams; this._privateKey = privateKey; } + public async newSignedOrderAsync( customOrderParams: Partial = {}, signatureType: SignatureType = SignatureType.EthSign, diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index aaa6645a01..9cdec9e65e 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -113,6 +113,10 @@ { "note": "Renamed `OnlyCallableByPoolOperatorOrMakerError` to `OnlyCallableByPoolOperatorError`.", "pr": 2250 + }, + { + "note": "Removed protocol fee != 0 error.", + "pr": 2278 } ], "timestamp": 1570135330 diff --git a/packages/order-utils/src/staking_revert_errors.ts b/packages/order-utils/src/staking_revert_errors.ts index 8debcffe8e..81c0c983ee 100644 --- a/packages/order-utils/src/staking_revert_errors.ts +++ b/packages/order-utils/src/staking_revert_errors.ts @@ -14,11 +14,6 @@ export enum OperatorShareErrorCodes { CanOnlyDecreaseOperatorShare, } -export enum ProtocolFeePaymentErrorCodes { - ZeroProtocolFeePaid, - MismatchedFeeAndPayment, -} - export enum InvalidParamValueErrorCodes { InvalidCobbDouglasAlpha, InvalidRewardDelegatedStakeWeight, @@ -144,14 +139,13 @@ export class InvalidParamValueError extends RevertError { export class InvalidProtocolFeePaymentError extends RevertError { constructor( - errorCode?: ProtocolFeePaymentErrorCodes, expectedProtocolFeePaid?: BigNumber | number | string, actualProtocolFeePaid?: BigNumber | number | string, ) { super( 'InvalidProtocolFeePaymentError', - 'InvalidProtocolFeePaymentError(uint8 errorCode, uint256 expectedProtocolFeePaid, uint256 actualProtocolFeePaid)', - { errorCode, expectedProtocolFeePaid, actualProtocolFeePaid }, + 'InvalidProtocolFeePaymentError(uint256 expectedProtocolFeePaid, uint256 actualProtocolFeePaid)', + { expectedProtocolFeePaid, actualProtocolFeePaid }, ); } } diff --git a/tsconfig.json b/tsconfig.json index c4d37eb392..400566c2d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "sourceMap": true }, // These are not working right now - "exclude": ["./contracts/extensions/**/*", "./contracts/coordinator/**/*"], + "exclude": ["./contracts/extensions/**/*"], // The root of the project is just a list of references and does not contain // any top-level TypeScript code. "include": [], @@ -26,6 +26,7 @@ { "path": "./contracts/erc20" }, { "path": "./contracts/erc721" }, { "path": "./contracts/exchange" }, + { "path": "./contracts/coordinator" }, { "path": "./contracts/exchange-forwarder" }, { "path": "./contracts/exchange-libs" }, // { "path": "./contracts/extensions" }, @@ -33,6 +34,7 @@ { "path": "./contracts/test-utils" }, { "path": "./contracts/utils" }, { "path": "./contracts/dev-utils" }, + { "path": "./contracts/integrations" }, { "path": "./packages/0x.js" }, { "path": "./packages/abi-gen-wrappers" }, { "path": "./packages/abi-gen" },