Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Make it easier to use validateOrderFillableOrThrowAsync #2096

Merged
merged 6 commits into from
Sep 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/contract-addresses/CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
[
{
"version": "3.2.0",
"changes": [
{
"note": "Added `getNetworkIdByExchangeAddressOrThrow`",
"pr": 2096
}
]
},
{
"version": "3.1.0",
"changes": [
Expand Down
18 changes: 18 additions & 0 deletions packages/contract-addresses/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
);
}
9 changes: 9 additions & 0 deletions packages/migrations/CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
[
{
"version": "4.3.2",
"changes": [
{
"note": "Removed dependency on @0x/order-utils",
Copy link
Contributor Author

@xianny xianny Sep 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to introduce a dependency on @0x/migrations to @0x/order-utils for tests (which were retrieved from the old exchange wrapper tests). I've run into similar issues with tests introducing circular dependencies for other packages. Would like to discuss the possibility of developing an integration test suite for any tests that run with ganache and the standard 0x migration. Two benefits: 1) potentially speed up our testing time, by doing one migration + snapshots instead of separate migrations in numerous unit test suites, and 2) reduce package size/unnecessary dependency trees in the published packages.

Since we intend to remove these methods in a couple months, this seems like the easiest solution for now to unblock this task.

"pr": 2096
}
]
},
{
"timestamp": 1567521715,
"version": "4.3.1",
Expand Down
1 change: 0 additions & 1 deletion packages/migrations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 34 additions & 6 deletions packages/migrations/src/migration.ts
Original file line number Diff line number Diff line change
@@ -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 {
fabioberger marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/order-utils/CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
[
{
"version": "8.4.0",
"changes": [
{
"note": "Implement `simpleValidateOrderFillableOrThrowAsync`",
"pr": 2096
}
]
},
{
"timestamp": 1567521715,
"version": "8.3.1",
Expand Down
4 changes: 3 additions & 1 deletion packages/order-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to refactor this to use DevUtils.

devUtils.getAssetProxyAllowance.callAsync(owner, assetData)

Which handles ERC20, ERC721, ERC1155 and MAP.

const proxyAllowance = await this._devUtilsContract.getAssetProxyAllowance.callAsync(userAddress, assetData);
return proxyAllowance;
}
}
1 change: 1 addition & 0 deletions packages/order-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export {
FeeOrdersAndRemainingFeeAmount,
OrdersAndRemainingTakerFillAmount,
OrdersAndRemainingMakerFillAmount,
ValidateOrderFillableOpts,
} from './types';

export { NetworkId } from '@0x/contract-addresses';
78 changes: 77 additions & 1 deletion packages/order-utils/src/order_validation_utils.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExchangeTransferSimulator is useful for basically one reason, If the user is trading XYZ for ZRX and the order has fees, then the ExchangeTransferSimulator can handle this case (as it simulates the transfer of ZRX to maker prior to fee payment). This is an annoying case to handle for and is often overlooked, resulting in invalid orders for completely valid orders if only Balances and Allowances are validated.

I do not believe DevUtils currently handles this edge case.

Extrapolating to V3. I'm selling a Kitty for 100 WETH, there is a 1 WETH fee. I have 0 WETH. This order is valid as there will be funds for fees during settlement but not prior to settlement.

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;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const validateOrderFillableOptsSchema = {
id: '/ValidateOrderFillableOpts',
properties: {
expectedFillTakerTokenAmount: { $ref: '/wholeNumberSchema' },
},
type: 'object',
};
6 changes: 6 additions & 0 deletions packages/order-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions packages/order-utils/test/exchange_transfer_simulator_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('ExchangeTransferSimulator', async () => {
from: devConstants.TESTRPC_FIRST_ADDRESS,
};

await blockchainLifecycle.startAsync();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was affecting the new unit tests in order_validation_utils_test.ts that are also blockchain dependent.

const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
artifacts.ERC20Proxy,
provider,
Expand Down Expand Up @@ -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;
Expand Down
Loading