Make it easier to use validateOrderFillableOrThrowAsync (#2096)

* make it easier to use validateOrderFillableOrThrowAsync

* add unit tests, use DevUtils

* remove dependency on @0x/order-utils from @0x/migrations
This commit is contained in:
Xianny
2019-09-10 16:44:19 -07:00
committed by GitHub
parent d912175a7a
commit 9eba6d7146
16 changed files with 378 additions and 10 deletions

View File

@@ -1,4 +1,13 @@
[
{
"version": "3.2.0",
"changes": [
{
"note": "Added `getNetworkIdByExchangeAddressOrThrow`",
"pr": 2096
}
]
},
{
"version": "3.1.0",
"changes": [

View File

@@ -131,3 +131,21 @@ export function getContractAddressesForNetworkOrThrow(networkId: NetworkId): Con
}
return networkToAddresses[networkId];
}
/**
* Uses a given exchange address to look up the network id that the exchange contract is deployed
* on. Only works for Ethereum mainnet or a supported testnet. Throws if the exchange address
* does not correspond to a known deployed exchange contract.
* @param exchangeAddress The exchange address of concern
* @returns The network ID on which the exchange contract is deployed
*/
export function getNetworkIdByExchangeAddressOrThrow(exchangeAddress: string): NetworkId {
for (const networkId of Object.keys(networkToAddresses)) {
if (networkToAddresses[networkId as any].exchange === exchangeAddress) {
return (networkId as any) as NetworkId;
}
}
throw new Error(
`Unknown exchange address (${exchangeAddress}). No known 0x Exchange Contract deployed at this address.`,
);
}

View File

@@ -1,4 +1,13 @@
[
{
"version": "4.3.2",
"changes": [
{
"note": "Removed dependency on @0x/order-utils",
"pr": 2096
}
]
},
{
"timestamp": 1567521715,
"version": "4.3.1",

View File

@@ -59,7 +59,6 @@
"@0x/base-contract": "^5.3.3",
"@0x/contract-addresses": "^3.1.0",
"@0x/contract-artifacts": "^2.2.1",
"@0x/order-utils": "^8.3.1",
"@0x/sol-compiler": "^3.1.14",
"@0x/subproviders": "^5.0.3",
"@0x/typescript-typings": "^4.2.5",

View File

@@ -1,15 +1,43 @@
import * as wrappers from '@0x/abi-gen-wrappers';
import { ContractAddresses } from '@0x/contract-addresses';
import * as artifacts from '@0x/contract-artifacts';
import { assetDataUtils } from '@0x/order-utils';
import { Web3ProviderEngine } from '@0x/subproviders';
import { BigNumber, providerUtils } from '@0x/utils';
import { AbiEncoder, BigNumber, providerUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { SupportedProvider, TxData } from 'ethereum-types';
import { MethodAbi, SupportedProvider, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { erc20TokenInfo, erc721TokenInfo } from './utils/token_info';
// HACK (xianny): Copied from @0x/order-utils to get rid of circular dependency
/**
* Encodes an ERC20 token address into a hex encoded assetData string, usable in the makerAssetData or
* takerAssetData fields in a 0x order.
* @param tokenAddress The ERC20 token address to encode
* @return The hex encoded assetData string
*/
function encodeERC20AssetData(tokenAddress: string): string {
const ERC20_METHOD_ABI: MethodAbi = {
constant: false,
inputs: [
{
name: 'tokenContract',
type: 'address',
},
],
name: 'ERC20Token',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
};
const encodingRules: AbiEncoder.EncodingRules = { shouldOptimize: true };
const abiEncoder = new AbiEncoder.Method(ERC20_METHOD_ABI);
const args = [tokenAddress];
const assetData = abiEncoder.encode(args, encodingRules);
return assetData;
}
/**
* Creates and deploys all the contracts that are required for the latest
* version of the 0x protocol.
@@ -55,7 +83,7 @@ export async function runMigrationsAsync(
);
// Exchange
const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address);
const zrxAssetData = encodeERC20AssetData(zrxToken.address);
const exchange = await wrappers.ExchangeContract.deployFrom0xArtifactAsync(
artifacts.Exchange,
provider,
@@ -173,8 +201,8 @@ export async function runMigrationsAsync(
txDefaults,
artifacts,
exchange.address,
assetDataUtils.encodeERC20AssetData(zrxToken.address),
assetDataUtils.encodeERC20AssetData(etherToken.address),
encodeERC20AssetData(zrxToken.address),
encodeERC20AssetData(etherToken.address),
);
// OrderValidator

View File

@@ -1,4 +1,13 @@
[
{
"version": "8.4.0",
"changes": [
{
"note": "Implement `simpleValidateOrderFillableOrThrowAsync`",
"pr": 2096
}
]
},
{
"timestamp": 1567521715,
"version": "8.3.1",

View File

@@ -13,7 +13,7 @@
"test": "yarn run_mocha",
"rebuild_and_test": "run-s build test",
"test:circleci": "yarn test:coverage",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --timeout 10000 --bail --exit",
"test:coverage": "nyc npm run test --all && yarn coverage:report:lcov",
"coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info",
"clean": "shx rm -rf lib generated_docs",
@@ -40,6 +40,8 @@
"homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md",
"devDependencies": {
"@0x/dev-utils": "^2.3.2",
"@0x/migrations": "^4.3.1",
"@0x/subproviders": "^5.0.3",
"@0x/ts-doc-gen": "^0.0.21",
"@0x/tslint-config": "^3.0.1",
"@types/bn.js": "^4.11.0",

View File

@@ -0,0 +1,20 @@
import { DevUtilsContract } from '@0x/abi-gen-wrappers';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
export class AssetBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher {
private readonly _devUtilsContract: DevUtilsContract;
constructor(devUtilsContract: DevUtilsContract) {
this._devUtilsContract = devUtilsContract;
}
public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
const balance = await this._devUtilsContract.getBalance.callAsync(userAddress, assetData);
return balance;
}
public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> {
const proxyAllowance = await this._devUtilsContract.getAssetProxyAllowance.callAsync(userAddress, assetData);
return proxyAllowance;
}
}

View File

@@ -80,6 +80,7 @@ export {
FeeOrdersAndRemainingFeeAmount,
OrdersAndRemainingTakerFillAmount,
OrdersAndRemainingMakerFillAmount,
ValidateOrderFillableOpts,
} from './types';
export { NetworkId } from '@0x/contract-addresses';

View File

@@ -1,21 +1,29 @@
import {
DevUtilsContract,
ExchangeContract,
getContractAddressesForNetworkOrThrow,
IAssetProxyContract,
NetworkId,
} from '@0x/abi-gen-wrappers';
import { assert } from '@0x/assert';
import { getNetworkIdByExchangeAddressOrThrow } from '@0x/contract-addresses';
import { ExchangeContractErrs, RevertReason, SignedOrder } from '@0x/types';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
import { AssetBalanceAndProxyAllowanceFetcher } from './asset_balance_and_proxy_allowance_fetcher';
import { assetDataUtils } from './asset_data_utils';
import { constants } from './constants';
import { ExchangeTransferSimulator } from './exchange_transfer_simulator';
import { orderCalculationUtils } from './order_calculation_utils';
import { orderHashUtils } from './order_hash';
import { OrderStateUtils } from './order_state_utils';
import { validateOrderFillableOptsSchema } from './schemas/validate_order_fillable_opts_schema';
import { signatureUtils } from './signature_utils';
import { TradeSide, TransferType, TypedDataError } from './types';
import { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
import { TradeSide, TransferType, TypedDataError, ValidateOrderFillableOpts } from './types';
import { utils } from './utils';
/**
@@ -171,6 +179,74 @@ export class OrderValidationUtils {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
this._provider = provider;
}
// TODO(xianny): remove this method once the smart contracts have been refactored
// to return helpful revert reasons instead of ORDER_UNFILLABLE. Instruct devs
// to make "calls" to validate order fillability + getOrderInfo for fillable amount.
// This method recreates functionality from ExchangeWrapper (@0x/contract-wrappers < 11.0.0)
// to make migrating easier in the interim.
/**
* Validate if the supplied order is fillable, and throw if it isn't
* @param provider The same provider used to interact with contracts
* @param signedOrder SignedOrder of interest
* @param opts ValidateOrderFillableOpts options (e.g expectedFillTakerTokenAmount.
* If it isn't supplied, we check if the order is fillable for the remaining amount.
* To check if the order is fillable for a non-zero amount, set `validateRemainingOrderAmountIsFillable` to false.)
*/
public async simpleValidateOrderFillableOrThrowAsync(
provider: SupportedProvider,
signedOrder: SignedOrder,
opts: ValidateOrderFillableOpts = {},
): Promise<void> {
assert.doesConformToSchema('opts', opts, validateOrderFillableOptsSchema);
const exchangeAddress = signedOrder.exchangeAddress;
const networkId = getNetworkIdByExchangeAddressOrThrow(exchangeAddress);
const { zrxToken, devUtils } = getContractAddressesForNetworkOrThrow(networkId);
const exchangeContract = new ExchangeContract(exchangeAddress, provider);
const balanceAllowanceFetcher = new AssetBalanceAndProxyAllowanceFetcher(
new DevUtilsContract(devUtils, provider),
);
const balanceAllowanceStore = new BalanceAndProxyAllowanceLazyStore(balanceAllowanceFetcher);
const exchangeTradeSimulator = new ExchangeTransferSimulator(balanceAllowanceStore);
// Define fillable taker asset amount
let fillableTakerAssetAmount;
const shouldValidateRemainingOrderAmountIsFillable =
opts.validateRemainingOrderAmountIsFillable === undefined
? true
: opts.validateRemainingOrderAmountIsFillable;
if (opts.expectedFillTakerTokenAmount) {
// If the caller has specified a taker fill amount, we use this for all validation
fillableTakerAssetAmount = opts.expectedFillTakerTokenAmount;
} else if (shouldValidateRemainingOrderAmountIsFillable) {
// Default behaviour is to validate the amount left on the order.
const filledTakerTokenAmount = await exchangeContract.filled.callAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
fillableTakerAssetAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount);
} else {
const orderStateUtils = new OrderStateUtils(balanceAllowanceStore, this._orderFilledCancelledFetcher);
// Calculate the taker amount fillable given the maker balance and allowance
const orderRelevantState = await orderStateUtils.getOpenOrderRelevantStateAsync(signedOrder);
fillableTakerAssetAmount = orderRelevantState.remainingFillableTakerAssetAmount;
}
await this.validateOrderFillableOrThrowAsync(
exchangeTradeSimulator,
signedOrder,
assetDataUtils.encodeERC20AssetData(zrxToken),
fillableTakerAssetAmount,
);
const makerTransferAmount = orderCalculationUtils.getMakerFillAmount(signedOrder, fillableTakerAssetAmount);
await OrderValidationUtils.validateMakerTransferThrowIfInvalidAsync(
networkId,
provider,
signedOrder,
makerTransferAmount,
opts.simulationTakerAddress,
);
}
// TODO(fabio): remove this method once the smart contracts have been refactored
// to return helpful revert reasons instead of ORDER_UNFILLABLE. Instruct devs
// to make "calls" to validate order fillability + getOrderInfo for fillable amount.

View File

@@ -0,0 +1,7 @@
export const validateOrderFillableOptsSchema = {
id: '/ValidateOrderFillableOpts',
properties: {
expectedFillTakerTokenAmount: { $ref: '/wholeNumberSchema' },
},
type: 'object',
};

View File

@@ -25,6 +25,12 @@ export interface CreateOrderOpts {
expirationTimeSeconds?: BigNumber;
}
export interface ValidateOrderFillableOpts {
expectedFillTakerTokenAmount?: BigNumber;
validateRemainingOrderAmountIsFillable?: boolean;
simulationTakerAddress?: string;
}
/**
* remainingFillableMakerAssetAmount: An array of BigNumbers corresponding to the `orders` parameter.
* You can use `OrderStateUtils` `@0x/order-utils` to perform blockchain lookups for these values.

View File

@@ -42,6 +42,7 @@ describe('ExchangeTransferSimulator', async () => {
from: devConstants.TESTRPC_FIRST_ADDRESS,
};
await blockchainLifecycle.startAsync();
const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
artifacts.ERC20Proxy,
provider,
@@ -74,6 +75,9 @@ describe('ExchangeTransferSimulator', async () => {
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
describe('#transferFromAsync', function(): void {
// HACK: For some reason these tests need a slightly longer timeout
const mochaTestTimeoutMs = 3000;

View File

@@ -1,13 +1,23 @@
import { ContractAddresses, DummyERC20TokenContract } from '@0x/abi-gen-wrappers';
import { BlockchainLifecycle, devConstants, tokenUtils } from '@0x/dev-utils';
import { ExchangeContractErrs, RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import 'mocha';
import { runMigrationsOnceAsync } from '@0x/migrations';
import { AbstractOrderFilledCancelledFetcher, assetDataUtils, signatureUtils, SignedOrder } from '../src';
import { OrderValidationUtils } from '../src/order_validation_utils';
import { UntransferrableDummyERC20Token } from './artifacts/UntransferrableDummyERC20Token';
import { chaiSetup } from './utils/chai_setup';
import { testOrderFactory } from './utils/test_order_factory';
import { provider, web3Wrapper } from './utils/web3_wrapper';
chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('OrderValidationUtils', () => {
describe('#isRoundingError', () => {
@@ -67,4 +77,174 @@ describe('OrderValidationUtils', () => {
expect(isRoundingError).to.be.false();
});
});
describe('#validateOrderFillableOrThrowAsync', () => {
let contractAddresses: ContractAddresses;
let orderValidationUtils: OrderValidationUtils;
let makerAddress: string;
let takerAddress: string;
let ownerAddress: string;
let signedOrder: SignedOrder;
let makerTokenContract: DummyERC20TokenContract;
let takerTokenContract: DummyERC20TokenContract;
const txDefaults = {
gas: devConstants.GAS_LIMIT,
from: devConstants.TESTRPC_FIRST_ADDRESS,
};
before(async () => {
contractAddresses = await runMigrationsOnceAsync(provider, txDefaults);
await blockchainLifecycle.startAsync();
const [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
makerTokenContract = new DummyERC20TokenContract(makerTokenAddress, provider, txDefaults);
takerTokenContract = new DummyERC20TokenContract(takerTokenAddress, provider, txDefaults);
[ownerAddress, makerAddress, takerAddress] = await web3Wrapper.getAvailableAddressesAsync();
// create signed order
const [makerAssetData, takerAssetData] = [
assetDataUtils.encodeERC20AssetData(makerTokenContract.address),
assetDataUtils.encodeERC20AssetData(takerTokenContract.address),
];
const defaultOrderParams = {
exchangeAddress: contractAddresses.exchange,
makerAddress,
takerAddress,
makerAssetData,
takerAssetData,
};
const makerAssetAmount = new BigNumber(10);
const takerAssetAmount = new BigNumber(10000000000000000);
const [order] = testOrderFactory.generateTestSignedOrders(
{
...defaultOrderParams,
makerAssetAmount,
takerAssetAmount,
},
1,
);
signedOrder = await signatureUtils.ecSignOrderAsync(provider, order, makerAddress);
// instantiate OrderValidationUtils
const mockOrderFilledFetcher: AbstractOrderFilledCancelledFetcher = {
async getFilledTakerAmountAsync(_orderHash: string): Promise<BigNumber> {
return new BigNumber(0);
},
async isOrderCancelledAsync(_signedOrder: SignedOrder): Promise<boolean> {
return false;
},
getZRXAssetData(): string {
return assetDataUtils.encodeERC20AssetData(contractAddresses.zrxToken);
},
};
orderValidationUtils = new OrderValidationUtils(mockOrderFilledFetcher, provider);
});
after(async () => {
await blockchainLifecycle.revertAsync();
});
beforeEach(async () => {
await blockchainLifecycle.startAsync();
await makerTokenContract.setBalance.awaitTransactionSuccessAsync(
makerAddress,
signedOrder.makerAssetAmount,
);
await takerTokenContract.setBalance.awaitTransactionSuccessAsync(
takerAddress,
signedOrder.takerAssetAmount,
);
await makerTokenContract.approve.awaitTransactionSuccessAsync(
contractAddresses.erc20Proxy,
signedOrder.makerAssetAmount,
{ from: makerAddress },
);
await takerTokenContract.approve.awaitTransactionSuccessAsync(
contractAddresses.erc20Proxy,
signedOrder.takerAssetAmount,
{ from: takerAddress },
);
});
afterEach(async () => {
await blockchainLifecycle.revertAsync();
});
it('should throw if signature is invalid', async () => {
const signedOrderWithInvalidSignature = {
...signedOrder,
signature:
'0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403',
};
return expect(
orderValidationUtils.simpleValidateOrderFillableOrThrowAsync(provider, signedOrderWithInvalidSignature),
).to.be.rejectedWith(RevertReason.InvalidOrderSignature);
});
it('should validate the order with the current balances and allowances for the maker', async () => {
await orderValidationUtils.simpleValidateOrderFillableOrThrowAsync(provider, signedOrder, {
validateRemainingOrderAmountIsFillable: false,
});
});
it('should validate the order with remaining fillable amount for the order', async () => {
await orderValidationUtils.simpleValidateOrderFillableOrThrowAsync(provider, signedOrder);
});
it('should validate the order with specified amount', async () => {
await orderValidationUtils.simpleValidateOrderFillableOrThrowAsync(provider, signedOrder, {
expectedFillTakerTokenAmount: signedOrder.takerAssetAmount,
});
});
it('should throw if the amount is greater than the allowance/balance', async () => {
return expect(
orderValidationUtils.simpleValidateOrderFillableOrThrowAsync(provider, signedOrder, {
// tslint:disable-next-line:custom-no-magic-numbers
expectedFillTakerTokenAmount: new BigNumber(2).pow(256).minus(1),
}),
).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerAllowance);
});
it('should throw when the maker does not have enough balance for the remaining order amount', async () => {
await makerTokenContract.setBalance.awaitTransactionSuccessAsync(
makerAddress,
signedOrder.makerAssetAmount.minus(1),
);
return expect(
orderValidationUtils.simpleValidateOrderFillableOrThrowAsync(provider, signedOrder),
).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
});
it('should validate the order when remaining order amount has some fillable amount', async () => {
await makerTokenContract.setBalance.awaitTransactionSuccessAsync(
makerAddress,
signedOrder.makerAssetAmount.minus(1),
);
await orderValidationUtils.simpleValidateOrderFillableOrThrowAsync(provider, signedOrder, {
validateRemainingOrderAmountIsFillable: false,
});
});
it('should throw when the ERC20 token has transfer restrictions', async () => {
const artifactDependencies = {};
const untransferrableToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
UntransferrableDummyERC20Token,
provider,
{ from: ownerAddress },
artifactDependencies,
'UntransferrableToken',
'UTT',
new BigNumber(18),
// tslint:disable-next-line:custom-no-magic-numbers
new BigNumber(2).pow(20).minus(1),
);
const untransferrableMakerAssetData = assetDataUtils.encodeERC20AssetData(untransferrableToken.address);
const invalidOrder = {
...signedOrder,
makerAssetData: untransferrableMakerAssetData,
};
const invalidSignedOrder = await signatureUtils.ecSignOrderAsync(provider, invalidOrder, makerAddress);
await untransferrableToken.setBalance.awaitTransactionSuccessAsync(
makerAddress,
invalidSignedOrder.makerAssetAmount.plus(1),
);
await untransferrableToken.approve.awaitTransactionSuccessAsync(
contractAddresses.erc20Proxy,
invalidSignedOrder.makerAssetAmount.plus(1),
{ from: makerAddress },
);
return expect(
orderValidationUtils.simpleValidateOrderFillableOrThrowAsync(provider, invalidSignedOrder),
).to.be.rejectedWith(RevertReason.TransferFailed);
});
});
});

View File

@@ -1,6 +1,6 @@
import { web3Factory } from '@0x/dev-utils';
import { Web3ProviderEngine } from '@0x/subproviders';
import { Web3Wrapper } from '@0x/web3-wrapper';
import Web3ProviderEngine = require('web3-provider-engine');
const provider: Web3ProviderEngine = web3Factory.getRpcProvider({ shouldUseInProcessGanache: true });
const web3Wrapper = new Web3Wrapper(provider);