diff options
author | Fabio Berger <me@fabioberger.com> | 2018-06-12 06:23:48 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-06-12 06:23:48 +0800 |
commit | 7e78f5941ad9cb2fd5225899f79823c7376894c0 (patch) | |
tree | 13fadf90f821e9fead1373a2e567e6d86fe27e0f /packages/order-utils | |
parent | 20f93185975d16c77492f05eb86dd89695539cd9 (diff) | |
parent | bc0ae6be318a15bf8670a6da9a59d9bdb12cadae (diff) | |
download | dexon-sol-tools-7e78f5941ad9cb2fd5225899f79823c7376894c0.tar dexon-sol-tools-7e78f5941ad9cb2fd5225899f79823c7376894c0.tar.gz dexon-sol-tools-7e78f5941ad9cb2fd5225899f79823c7376894c0.tar.bz2 dexon-sol-tools-7e78f5941ad9cb2fd5225899f79823c7376894c0.tar.lz dexon-sol-tools-7e78f5941ad9cb2fd5225899f79823c7376894c0.tar.xz dexon-sol-tools-7e78f5941ad9cb2fd5225899f79823c7376894c0.tar.zst dexon-sol-tools-7e78f5941ad9cb2fd5225899f79823c7376894c0.zip |
Merge branch 'v2-prototype' into feature/combinatorial-testing
* v2-prototype: (68 commits)
Stop exporting ArtifactWriter
Fix no-unused-variable tslint rule to include parameters and fix issues
Fix linter exclude rule
Validate all signature types rather then only ECSignatures
Store the instantiated OrderValidationUtils
Remove global hooks from tests and deploy contracts from within the specific tests
Add EmitStatement to ASTVisitor
Fix tslint issues
Add back artifacts file
Fix a bug in SolCompilerArtifacts adapter config overriding
Move OrderValidationUtils (+ tests) and ExchangeTransferSimulator to order-utils
export parseECSignature method
Export ArtifactWriter from migrations package
Remove unused artifact file
Pass in generated contract wrapper to orderValidationUtils at instantiation
Refactor orderValidationUtils to use the generated contract wrapper instead of the higher-level one
Refactor ExchangeTransferSimulator public interface to accet an AbstractBalanceAndProxyAllowanceLazyStore so that this module could be re-used in different contexts.
Increase timeout for contract migrations
Remove some copy-paste code
Await transactions in migrations
...
Diffstat (limited to 'packages/order-utils')
18 files changed, 855 insertions, 73 deletions
diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index a3e41e427..f3c54711f 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,5 +1,13 @@ [ { + "changes": [ + { + "note": "Export parseECSignature method", + "pr": 684 + } + ] + }, + { "version": "0.1.0", "changes": [ { diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index 6c5ad6780..8f37bb138 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -13,7 +13,7 @@ "pre_build": "run-s update_artifacts generate_contract_wrappers", "transpile": "tsc", "copy_monorepo_scripts": "copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts", - "generate_contract_wrappers": "abi-gen --abis 'lib/src/artifacts/@(Exchange|IWallet|IValidator).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/generated_contract_wrappers --backend ethers", + "generate_contract_wrappers": "abi-gen --abis 'lib/src/artifacts/@(Exchange|IWallet|IValidator|DummyERC20Token|ERC20Proxy|ERC20Token).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/generated_contract_wrappers --backend ethers", "update_artifacts": "for i in ${npm_package_config_contracts}; do copyfiles -u 4 ../migrations/artifacts/2.0.0/$i.json lib/src/artifacts; done;", "test": "yarn run_mocha", "rebuild_and_test": "run-s build test", @@ -29,7 +29,7 @@ "upload_docs_json": "aws s3 cp generated_docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json" }, "config": { - "contracts": "IWallet IValidator Exchange", + "contracts": "IWallet IValidator Exchange DummyERC20Token ERC20Proxy ERC20Token", "postpublish": { "docPublishConfigs": { "extraFileIncludes": [ @@ -52,6 +52,7 @@ "homepage": "https://github.com/0xProject/0x-monorepo/packages/order-utils/README.md", "devDependencies": { "@0xproject/dev-utils": "^0.4.2", + "@0xproject/migrations": "^0.0.6", "@0xproject/monorepo-scripts": "^0.1.20", "@0xproject/tslint-config": "^0.4.18", "@types/ethereumjs-abi": "^0.6.0", diff --git a/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts index 857c6167f..b2760d98e 100644 --- a/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts +++ b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_fetcher.ts @@ -1,6 +1,6 @@ import { BigNumber } from '@0xproject/utils'; export abstract class AbstractBalanceAndProxyAllowanceFetcher { - public abstract async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber>; - public abstract async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber>; + public abstract async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; + public abstract async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; } diff --git a/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_lazy_store.ts b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_lazy_store.ts new file mode 100644 index 000000000..38e08b7fe --- /dev/null +++ b/packages/order-utils/src/abstract/abstract_balance_and_proxy_allowance_lazy_store.ts @@ -0,0 +1,11 @@ +import { BigNumber } from '@0xproject/utils'; + +export abstract class AbstractBalanceAndProxyAllowanceLazyStore { + public abstract async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; + public abstract async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber>; + public abstract setBalance(assetData: string, userAddress: string, balance: BigNumber): void; + public abstract deleteBalance(assetData: string, userAddress: string): void; + public abstract setProxyAllowance(assetData: string, userAddress: string, proxyAllowance: BigNumber): void; + public abstract deleteProxyAllowance(assetData: string, userAddress: string): void; + public abstract deleteAll(): void; +} diff --git a/packages/order-utils/src/artifacts.ts b/packages/order-utils/src/artifacts.ts index f6fd00472..3d2d1e953 100644 --- a/packages/order-utils/src/artifacts.ts +++ b/packages/order-utils/src/artifacts.ts @@ -1,10 +1,14 @@ -import { Artifact } from '@0xproject/types'; +import { ContractArtifact } from '@0xproject/sol-compiler'; +import * as DummyERC20Token from './artifacts/DummyERC20Token.json'; +import * as ERC20Proxy from './artifacts/ERC20Proxy.json'; import * as Exchange from './artifacts/Exchange.json'; import * as IValidator from './artifacts/IValidator.json'; import * as IWallet from './artifacts/IWallet.json'; export const artifacts = { - Exchange: (Exchange as any) as Artifact, - IWallet: (IWallet as any) as Artifact, - IValidator: (IValidator as any) as Artifact, + ERC20Proxy: (ERC20Proxy as any) as ContractArtifact, + DummyERC20Token: (DummyERC20Token as any) as ContractArtifact, + Exchange: (Exchange as any) as ContractArtifact, + IWallet: (IWallet as any) as ContractArtifact, + IValidator: (IValidator as any) as ContractArtifact, }; diff --git a/packages/order-utils/src/asset_proxy_utils.ts b/packages/order-utils/src/asset_proxy_utils.ts index 55f2d56df..915ee5032 100644 --- a/packages/order-utils/src/asset_proxy_utils.ts +++ b/packages/order-utils/src/asset_proxy_utils.ts @@ -1,10 +1,15 @@ -import { AssetProxyId, ERC20ProxyData, ERC721ProxyData, ProxyData } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; +import { AssetProxyId, ERC20AssetData, ERC721AssetData } from '@0xproject/types'; +import { BigNumber, NULL_BYTES } from '@0xproject/utils'; import BN = require('bn.js'); import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; -const ERC20_PROXY_METADATA_BYTE_LENGTH = 21; -const ERC721_PROXY_METADATA_BYTE_LENGTH = 53; +const ERC20_ASSET_DATA_BYTE_LENGTH = 21; +const ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53; +const ASSET_DATA_ADDRESS_OFFSET = 0; +const ERC721_ASSET_DATA_TOKEN_ID_OFFSET = 20; +const ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET = 52; +const ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET = 84; export const assetProxyUtils = { encodeAssetProxyId(assetProxyId: AssetProxyId): Buffer { @@ -40,23 +45,24 @@ export const assetProxyUtils = { const value = new BigNumber(formattedValue, 16); return value; }, - encodeERC20ProxyData(tokenAddress: string): string { + encodeERC20AssetData(tokenAddress: string): string { const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC20); const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); - const encodedMetadata = Buffer.concat([encodedAddress, encodedAssetProxyId]); - const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); - return encodedMetadataHex; + const encodedAssetData = Buffer.concat([encodedAddress, encodedAssetProxyId]); + const encodedAssetDataHex = ethUtil.bufferToHex(encodedAssetData); + return encodedAssetDataHex; }, - decodeERC20ProxyData(proxyData: string): ERC20ProxyData { - const encodedProxyMetadata = ethUtil.toBuffer(proxyData); - if (encodedProxyMetadata.byteLength !== ERC20_PROXY_METADATA_BYTE_LENGTH) { + decodeERC20AssetData(proxyData: string): ERC20AssetData { + const encodedAssetData = ethUtil.toBuffer(proxyData); + if (encodedAssetData.byteLength !== ERC20_ASSET_DATA_BYTE_LENGTH) { throw new Error( `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 21. Got ${ - encodedProxyMetadata.byteLength + encodedAssetData.byteLength }`, ); } - const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + const assetProxyIdOffset = encodedAssetData.byteLength - 1; + const encodedAssetProxyId = encodedAssetData.slice(assetProxyIdOffset); const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); if (assetProxyId !== AssetProxyId.ERC20) { throw new Error( @@ -65,33 +71,45 @@ export const assetProxyUtils = { }), but got ${assetProxyId}`, ); } - const addressOffset = ERC20_PROXY_METADATA_BYTE_LENGTH - 1; - const encodedTokenAddress = encodedProxyMetadata.slice(0, addressOffset); + const encodedTokenAddress = encodedAssetData.slice(ASSET_DATA_ADDRESS_OFFSET, assetProxyIdOffset); const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); - const erc20ProxyData = { + const erc20AssetData = { assetProxyId, tokenAddress, }; - return erc20ProxyData; + return erc20AssetData; }, - encodeERC721ProxyData(tokenAddress: string, tokenId: BigNumber): string { + encodeERC721AssetData(tokenAddress: string, tokenId: BigNumber, receiverData?: string): string { const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC721); const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); const encodedTokenId = assetProxyUtils.encodeUint256(tokenId); - const encodedMetadata = Buffer.concat([encodedAddress, encodedTokenId, encodedAssetProxyId]); - const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); - return encodedMetadataHex; + let encodedAssetData = Buffer.concat([encodedAddress, encodedTokenId]); + if (!_.isUndefined(receiverData)) { + const encodedReceiverData = ethUtil.toBuffer(receiverData); + const receiverDataLength = new BigNumber(encodedReceiverData.byteLength); + const encodedReceiverDataLength = assetProxyUtils.encodeUint256(receiverDataLength); + encodedAssetData = Buffer.concat([encodedAssetData, encodedReceiverDataLength, encodedReceiverData]); + } + encodedAssetData = Buffer.concat([encodedAssetData, encodedAssetProxyId]); + const encodedAssetDataHex = ethUtil.bufferToHex(encodedAssetData); + return encodedAssetDataHex; }, - decodeERC721ProxyData(proxyData: string): ERC721ProxyData { - const encodedProxyMetadata = ethUtil.toBuffer(proxyData); - if (encodedProxyMetadata.byteLength !== ERC721_PROXY_METADATA_BYTE_LENGTH) { + decodeERC721AssetData(assetData: string): ERC721AssetData { + const encodedAssetData = ethUtil.toBuffer(assetData); + if (encodedAssetData.byteLength < ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH) { throw new Error( - `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 53. Got ${ - encodedProxyMetadata.byteLength + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be at least 53. Got ${ + encodedAssetData.byteLength }`, ); } - const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + + const encodedTokenAddress = encodedAssetData.slice( + ASSET_DATA_ADDRESS_OFFSET, + ERC721_ASSET_DATA_TOKEN_ID_OFFSET, + ); + const proxyIdOffset = encodedAssetData.byteLength - 1; + const encodedAssetProxyId = encodedAssetData.slice(proxyIdOffset); const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); if (assetProxyId !== AssetProxyId.ERC721) { throw new Error( @@ -100,50 +118,63 @@ export const assetProxyUtils = { }), but got ${assetProxyId}`, ); } - const addressOffset = ERC20_PROXY_METADATA_BYTE_LENGTH - 1; - const encodedTokenAddress = encodedProxyMetadata.slice(0, addressOffset); const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); - const tokenIdOffset = ERC721_PROXY_METADATA_BYTE_LENGTH - 1; - const encodedTokenId = encodedProxyMetadata.slice(addressOffset, tokenIdOffset); + const encodedTokenId = encodedAssetData.slice( + ERC721_ASSET_DATA_TOKEN_ID_OFFSET, + ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET, + ); const tokenId = assetProxyUtils.decodeUint256(encodedTokenId); - const erc721ProxyData = { + let receiverData = NULL_BYTES; + const lengthUpToReceiverDataLength = ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET + 1; + if (encodedAssetData.byteLength > lengthUpToReceiverDataLength) { + const encodedReceiverDataLength = encodedAssetData.slice( + ERC721_ASSET_DATA_RECEIVER_DATA_LENGTH_OFFSET, + ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET, + ); + const receiverDataLength = assetProxyUtils.decodeUint256(encodedReceiverDataLength); + const lengthUpToReceiverData = ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET + 1; + const expectedReceiverDataLength = new BigNumber(encodedAssetData.byteLength - lengthUpToReceiverData); + if (!receiverDataLength.equals(expectedReceiverDataLength)) { + throw new Error( + `Data length (${receiverDataLength}) does not match actual length of data (${expectedReceiverDataLength})`, + ); + } + const encodedReceiverData = encodedAssetData.slice( + ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET, + receiverDataLength.add(ERC721_ASSET_DATA_RECEIVER_DATA_OFFSET).toNumber(), + ); + receiverData = ethUtil.bufferToHex(encodedReceiverData); + } + const erc721AssetData: ERC721AssetData = { assetProxyId, tokenAddress, tokenId, + receiverData, }; - return erc721ProxyData; + return erc721AssetData; }, - decodeProxyDataId(proxyData: string): AssetProxyId { - const encodedProxyMetadata = ethUtil.toBuffer(proxyData); - if (encodedProxyMetadata.byteLength < 1) { + decodeAssetDataId(assetData: string): AssetProxyId { + const encodedAssetData = ethUtil.toBuffer(assetData); + if (encodedAssetData.byteLength < 1) { throw new Error( `Could not decode Proxy Data. Expected length of encoded data to be at least 1. Got ${ - encodedProxyMetadata.byteLength + encodedAssetData.byteLength }`, ); } - const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + const encodedAssetProxyId = encodedAssetData.slice(-1); const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); return assetProxyId; }, - decodeProxyData(proxyData: string): ProxyData { - const assetProxyId = assetProxyUtils.decodeProxyDataId(proxyData); + decodeAssetData(assetData: string): ERC20AssetData | ERC721AssetData { + const assetProxyId = assetProxyUtils.decodeAssetDataId(assetData); switch (assetProxyId) { case AssetProxyId.ERC20: - const erc20ProxyData = assetProxyUtils.decodeERC20ProxyData(proxyData); - const generalizedERC20ProxyData = { - assetProxyId, - tokenAddress: erc20ProxyData.tokenAddress, - }; - return generalizedERC20ProxyData; + const erc20AssetData = assetProxyUtils.decodeERC20AssetData(assetData); + return erc20AssetData; case AssetProxyId.ERC721: - const erc721ProxyData = assetProxyUtils.decodeERC721ProxyData(proxyData); - const generaliedERC721ProxyData = { - assetProxyId, - tokenAddress: erc721ProxyData.tokenAddress, - data: erc721ProxyData.tokenId, - }; - return generaliedERC721ProxyData; + const erc721AssetData = assetProxyUtils.decodeERC721AssetData(assetData); + return erc721AssetData; default: throw new Error(`Unrecognized asset proxy id: ${assetProxyId}`); } diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index ec2fe744a..ed5bd8101 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -1,3 +1,8 @@ +import { BigNumber } from '@0xproject/utils'; + export const constants = { NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + // tslint:disable-next-line:custom-no-magic-numbers + UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), + TESTRPC_NETWORK_ID: 50, }; diff --git a/packages/order-utils/src/exchange_transfer_simulator.ts b/packages/order-utils/src/exchange_transfer_simulator.ts new file mode 100644 index 000000000..32d53d6a2 --- /dev/null +++ b/packages/order-utils/src/exchange_transfer_simulator.ts @@ -0,0 +1,113 @@ +import { ExchangeContractErrs } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { AbstractBalanceAndProxyAllowanceLazyStore } from './abstract/abstract_balance_and_proxy_allowance_lazy_store'; +import { constants } from './constants'; +import { TradeSide, TransferType } from './types'; + +enum FailureReason { + Balance = 'balance', + ProxyAllowance = 'proxyAllowance', +} + +const ERR_MSG_MAPPING = { + [FailureReason.Balance]: { + [TradeSide.Maker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance, + }, + [TradeSide.Taker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance, + }, + }, + [FailureReason.ProxyAllowance]: { + [TradeSide.Maker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance, + }, + [TradeSide.Taker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance, + }, + }, +}; + +export class ExchangeTransferSimulator { + private _store: AbstractBalanceAndProxyAllowanceLazyStore; + private static _throwValidationError( + failureReason: FailureReason, + tradeSide: TradeSide, + transferType: TransferType, + ): never { + const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType]; + throw new Error(errMsg); + } + constructor(store: AbstractBalanceAndProxyAllowanceLazyStore) { + this._store = store; + } + /** + * Simulates transferFrom call performed by a proxy + * @param assetData Data of the asset being transferred. Includes + * it's identifying information and assetType, + * e.g address for ERC20, address & tokenId for ERC721 + * @param from Owner of the transferred tokens + * @param to Recipient of the transferred tokens + * @param amountInBaseUnits The amount of tokens being transferred + * @param tradeSide Is Maker/Taker transferring + * @param transferType Is it a fee payment or a value transfer + */ + public async transferFromAsync( + assetData: string, + from: string, + to: string, + amountInBaseUnits: BigNumber, + tradeSide: TradeSide, + transferType: TransferType, + ): Promise<void> { + // HACK: When simulating an open order (e.g taker is NULL_ADDRESS), we don't want to adjust balances/ + // allowances for the taker. We do however, want to increase the balance of the maker since the maker + // might be relying on those funds to fill subsequent orders or pay the order's fees. + if (from === constants.NULL_ADDRESS && tradeSide === TradeSide.Taker) { + await this._increaseBalanceAsync(assetData, to, amountInBaseUnits); + return; + } + const balance = await this._store.getBalanceAsync(assetData, from); + const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, from); + if (proxyAllowance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType); + } + if (balance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.Balance, tradeSide, transferType); + } + await this._decreaseProxyAllowanceAsync(assetData, from, amountInBaseUnits); + await this._decreaseBalanceAsync(assetData, from, amountInBaseUnits); + await this._increaseBalanceAsync(assetData, to, amountInBaseUnits); + } + private async _decreaseProxyAllowanceAsync( + assetData: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const proxyAllowance = await this._store.getProxyAllowanceAsync(assetData, userAddress); + if (!proxyAllowance.eq(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + this._store.setProxyAllowance(assetData, userAddress, proxyAllowance.minus(amountInBaseUnits)); + } + } + private async _increaseBalanceAsync( + assetData: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const balance = await this._store.getBalanceAsync(assetData, userAddress); + this._store.setBalance(assetData, userAddress, balance.plus(amountInBaseUnits)); + } + private async _decreaseBalanceAsync( + assetData: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const balance = await this._store.getBalanceAsync(assetData, userAddress); + this._store.setBalance(assetData, userAddress, balance.minus(amountInBaseUnits)); + } +} diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index cb859dcb9..f9b37df82 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -7,6 +7,7 @@ export { isValidECSignature, ecSignOrderHashAsync, addSignedMessagePrefix, + parseECSignature, } from './signature_utils'; export { orderFactory } from './order_factory'; export { constants } from './constants'; @@ -18,3 +19,5 @@ export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_f export { RemainingFillableCalculator } from './remaining_fillable_calculator'; export { OrderStateUtils } from './order_state_utils'; export { assetProxyUtils } from './asset_proxy_utils'; +export { OrderValidationUtils } from './order_validation_utils'; +export { ExchangeTransferSimulator } from './exchange_transfer_simulator'; diff --git a/packages/order-utils/src/order_state_utils.ts b/packages/order-utils/src/order_state_utils.ts index ca18097c9..40f235da7 100644 --- a/packages/order-utils/src/order_state_utils.ts +++ b/packages/order-utils/src/order_state_utils.ts @@ -79,7 +79,7 @@ export class OrderStateUtils { } public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> { const zrxTokenAddress = this._orderFilledCancelledFetcher.getZRXTokenAddress(); - const makerProxyData = assetProxyUtils.decodeERC20ProxyData(signedOrder.makerAssetData); + const makerProxyData = assetProxyUtils.decodeERC20AssetData(signedOrder.makerAssetData); const makerAssetAddress = makerProxyData.tokenAddress; const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const makerBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( @@ -111,7 +111,7 @@ export class OrderStateUtils { const transferrableMakerAssetAmount = BigNumber.min([makerProxyAllowance, makerBalance]); const transferrableFeeAssetAmount = BigNumber.min([makerFeeProxyAllowance, makerFeeBalance]); - const zrxAssetData = assetProxyUtils.encodeERC20ProxyData(zrxTokenAddress); + const zrxAssetData = assetProxyUtils.encodeERC20AssetData(zrxTokenAddress); const isMakerAssetZRX = signedOrder.makerAssetData === zrxAssetData; const remainingFillableCalculator = new RemainingFillableCalculator( signedOrder.makerFee, diff --git a/packages/order-utils/src/order_validation_utils.ts b/packages/order-utils/src/order_validation_utils.ts new file mode 100644 index 000000000..3a6704f26 --- /dev/null +++ b/packages/order-utils/src/order_validation_utils.ts @@ -0,0 +1,231 @@ +import { ExchangeContractErrs, Order, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Provider } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { OrderError, TradeSide, TransferType } from './types'; + +import { constants } from './constants'; +import { ExchangeTransferSimulator } from './exchange_transfer_simulator'; +import { ExchangeContract } from './generated_contract_wrappers/exchange'; +import { orderHashUtils } from './order_hash'; +import { isValidSignatureAsync } from './signature_utils'; +import { utils } from './utils'; + +export class OrderValidationUtils { + private _exchangeContract: ExchangeContract; + // TODO: Write some tests for the function + // const numerator = new BigNumber(20); + // const denominator = new BigNumber(999); + // const target = new BigNumber(50); + // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% + public static isRoundingError(numerator: BigNumber, denominator: BigNumber, target: BigNumber): boolean { + // Solidity's mulmod() in JS + // Source: https://solidity.readthedocs.io/en/latest/units-and-global-variables.html#mathematical-and-cryptographic-functions + if (denominator.eq(0)) { + throw new Error('denominator cannot be 0'); + } + const remainder = target.mul(numerator).mod(denominator); + if (remainder.eq(0)) { + return false; // no rounding error + } + + // tslint:disable-next-line:custom-no-magic-numbers + const errPercentageTimes1000000 = remainder.mul(1000000).div(numerator.mul(target)); + // tslint:disable-next-line:custom-no-magic-numbers + const isError = errPercentageTimes1000000.gt(1000); + return isError; + } + public static validateCancelOrderThrowIfInvalid( + order: Order, + cancelTakerTokenAmount: BigNumber, + filledTakerTokenAmount: BigNumber, + ): void { + if (cancelTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderCancelAmountZero); + } + if (order.takerAssetAmount.eq(filledTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); + } + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); + if (order.expirationTimeSeconds.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderCancelExpired); + } + } + public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + senderAddress: string, + zrxTokenAddress: string, + ): Promise<void> { + const fillMakerTokenAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + await exchangeTradeEmulator.transferFromAsync( + signedOrder.makerAssetData, + signedOrder.makerAddress, + senderAddress, + fillMakerTokenAmount, + TradeSide.Maker, + TransferType.Trade, + ); + await exchangeTradeEmulator.transferFromAsync( + signedOrder.takerAssetData, + senderAddress, + signedOrder.makerAddress, + fillTakerTokenAmount, + TradeSide.Taker, + TransferType.Trade, + ); + const makerFeeAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerAssetAmount, + signedOrder.makerFee, + ); + await exchangeTradeEmulator.transferFromAsync( + zrxTokenAddress, + signedOrder.makerAddress, + signedOrder.feeRecipientAddress, + makerFeeAmount, + TradeSide.Maker, + TransferType.Fee, + ); + const takerFeeAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerAssetAmount, + signedOrder.takerFee, + ); + await exchangeTradeEmulator.transferFromAsync( + zrxTokenAddress, + senderAddress, + signedOrder.feeRecipientAddress, + takerFeeAmount, + TradeSide.Taker, + TransferType.Fee, + ); + } + private static _validateRemainingFillAmountNotZeroOrThrow( + takerAssetAmount: BigNumber, + filledTakerTokenAmount: BigNumber, + ): void { + if (takerAssetAmount.eq(filledTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); + } + } + private static _validateOrderNotExpiredOrThrow(expirationTimeSeconds: BigNumber): void { + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); + if (expirationTimeSeconds.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderFillExpired); + } + } + private static _getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { + const fillMakerTokenAmount = numerator + .mul(target) + .div(denominator) + .round(0); + return fillMakerTokenAmount; + } + constructor(exchangeContract: ExchangeContract) { + this._exchangeContract = exchangeContract; + } + public async validateOrderFillableOrThrowAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + signedOrder: SignedOrder, + zrxTokenAddress: string, + expectedFillTakerTokenAmount?: BigNumber, + ): Promise<void> { + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash); + OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerAssetAmount, + filledTakerTokenAmount, + ); + OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds); + let fillTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); + if (!_.isUndefined(expectedFillTakerTokenAmount)) { + fillTakerTokenAmount = expectedFillTakerTokenAmount; + } + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount, + signedOrder.takerAddress, + zrxTokenAddress, + ); + } + public async validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + provider: Provider, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + zrxTokenAddress: string, + ): Promise<BigNumber> { + if (fillTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderFillAmountZero); + } + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const isValid = await isValidSignatureAsync( + provider, + orderHash, + signedOrder.signature, + signedOrder.makerAddress, + ); + if (!isValid) { + throw new Error(OrderError.InvalidSignature); + } + const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash); + OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerAssetAmount, + filledTakerTokenAmount, + ); + if (signedOrder.takerAddress !== constants.NULL_ADDRESS && signedOrder.takerAddress !== takerAddress) { + throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); + } + OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds); + const remainingTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); + const desiredFillTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) + ? remainingTakerTokenAmount + : fillTakerTokenAmount; + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + desiredFillTakerTokenAmount, + takerAddress, + zrxTokenAddress, + ); + + const wouldRoundingErrorOccur = OrderValidationUtils.isRoundingError( + filledTakerTokenAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + if (wouldRoundingErrorOccur) { + throw new Error(ExchangeContractErrs.OrderFillRoundingError); + } + return filledTakerTokenAmount; + } + public async validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + provider: Provider, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + zrxTokenAddress: string, + ): Promise<void> { + const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + provider, + signedOrder, + fillTakerTokenAmount, + takerAddress, + zrxTokenAddress, + ); + if (filledTakerTokenAmount !== fillTakerTokenAmount) { + throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount); + } + } +} diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index c3fa0b6a5..44a7203a0 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -90,7 +90,7 @@ export async function isValidPresignedSignatureAsync( data: string, signerAddress: string, ): Promise<boolean> { - const exchangeContract = new ExchangeContract(artifacts.Exchange.abi, signerAddress, provider); + const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider); const isValid = await exchangeContract.preSigned.callAsync(data, signerAddress); return isValid; } @@ -110,7 +110,7 @@ export async function isValidWalletSignatureAsync( ): Promise<boolean> { // tslint:disable-next-line:custom-no-magic-numbers const signatureWithoutType = signature.slice(-2); - const walletContract = new IWalletContract(artifacts.IWallet.abi, signerAddress, provider); + const walletContract = new IWalletContract(artifacts.IWallet.compilerOutput.abi, signerAddress, provider); const isValid = await walletContract.isValidSignature.callAsync(data, signatureWithoutType); return isValid; } @@ -129,7 +129,7 @@ export async function isValidValidatorSignatureAsync( signerAddress: string, ): Promise<boolean> { const validatorSignature = parseValidatorSignature(signature); - const exchangeContract = new ExchangeContract(artifacts.Exchange.abi, signerAddress, provider); + const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider); const isValidatorApproved = await exchangeContract.allowedValidators.callAsync( signerAddress, validatorSignature.validatorAddress, @@ -138,7 +138,7 @@ export async function isValidValidatorSignatureAsync( throw new Error(`Validator ${validatorSignature.validatorAddress} was not pre-approved by ${signerAddress}.`); } - const validatorContract = new IValidatorContract(artifacts.IValidator.abi, signerAddress, provider); + const validatorContract = new IValidatorContract(artifacts.IValidator.compilerOutput.abi, signerAddress, provider); const isValid = await validatorContract.isValidSignature.callAsync( data, signerAddress, @@ -260,12 +260,12 @@ export function addSignedMessagePrefix(message: string, messagePrefixType: Messa } } -function hashTrezorPersonalMessage(message: Buffer): Buffer { - const prefix = ethUtil.toBuffer('\x19Ethereum Signed Message:\n' + String.fromCharCode(message.length)); - return ethUtil.sha3(Buffer.concat([prefix, message])); -} - -function parseECSignature(signature: string): ECSignature { +/** + * Parse a 0x protocol hex-encoded signature string into it's ECSignature components + * @param signature A hex encoded ecSignature 0x Protocol signature + * @return An ECSignature object with r,s,v parameters + */ +export function parseECSignature(signature: string): ECSignature { const ecSignatureTypes = [SignatureType.EthSign, SignatureType.EIP712, SignatureType.Trezor]; assert.isOneOfExpectedSignatureTypes(signature, ecSignatureTypes); @@ -276,6 +276,11 @@ function parseECSignature(signature: string): ECSignature { return ecSignature; } +function hashTrezorPersonalMessage(message: Buffer): Buffer { + const prefix = ethUtil.toBuffer('\x19Ethereum Signed Message:\n' + String.fromCharCode(message.length)); + return ethUtil.sha3(Buffer.concat([prefix, message])); +} + function parseValidatorSignature(signature: string): ValidatorSignature { assert.isOneOfExpectedSignatureTypes(signature, [SignatureType.Validator]); // tslint:disable:custom-no-magic-numbers diff --git a/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts new file mode 100644 index 000000000..08d50b924 --- /dev/null +++ b/packages/order-utils/src/store/balance_and_proxy_allowance_lazy_store.ts @@ -0,0 +1,81 @@ +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { AbstractBalanceAndProxyAllowanceFetcher } from '../abstract/abstract_balance_and_proxy_allowance_fetcher'; +import { AbstractBalanceAndProxyAllowanceLazyStore } from '../abstract/abstract_balance_and_proxy_allowance_lazy_store'; + +/** + * Copy on read store for balances/proxyAllowances of tokens/accounts + */ +export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProxyAllowanceLazyStore { + private _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher; + private _balance: { + [assetData: string]: { + [userAddress: string]: BigNumber; + }; + }; + private _proxyAllowance: { + [assetData: string]: { + [userAddress: string]: BigNumber; + }; + }; + constructor(token: AbstractBalanceAndProxyAllowanceFetcher) { + this._balanceAndProxyAllowanceFetcher = token; + this._balance = {}; + this._proxyAllowance = {}; + } + public async getBalanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + if (_.isUndefined(this._balance[assetData]) || _.isUndefined(this._balance[assetData][userAddress])) { + const balance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync(assetData, userAddress); + this.setBalance(assetData, userAddress, balance); + } + const cachedBalance = this._balance[assetData][userAddress]; + return cachedBalance; + } + public setBalance(assetData: string, userAddress: string, balance: BigNumber): void { + if (_.isUndefined(this._balance[assetData])) { + this._balance[assetData] = {}; + } + this._balance[assetData][userAddress] = balance; + } + public deleteBalance(assetData: string, userAddress: string): void { + if (!_.isUndefined(this._balance[assetData])) { + delete this._balance[assetData][userAddress]; + if (_.isEmpty(this._balance[assetData])) { + delete this._balance[assetData]; + } + } + } + public async getProxyAllowanceAsync(assetData: string, userAddress: string): Promise<BigNumber> { + if ( + _.isUndefined(this._proxyAllowance[assetData]) || + _.isUndefined(this._proxyAllowance[assetData][userAddress]) + ) { + const proxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( + assetData, + userAddress, + ); + this.setProxyAllowance(assetData, userAddress, proxyAllowance); + } + const cachedProxyAllowance = this._proxyAllowance[assetData][userAddress]; + return cachedProxyAllowance; + } + public setProxyAllowance(assetData: string, userAddress: string, proxyAllowance: BigNumber): void { + if (_.isUndefined(this._proxyAllowance[assetData])) { + this._proxyAllowance[assetData] = {}; + } + this._proxyAllowance[assetData][userAddress] = proxyAllowance; + } + public deleteProxyAllowance(assetData: string, userAddress: string): void { + if (!_.isUndefined(this._proxyAllowance[assetData])) { + delete this._proxyAllowance[assetData][userAddress]; + if (_.isEmpty(this._proxyAllowance[assetData])) { + delete this._proxyAllowance[assetData]; + } + } + } + public deleteAll(): void { + this._balance = {}; + this._proxyAllowance = {}; + } +} diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index db0bfb249..3f1fce66d 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -23,3 +23,13 @@ export interface MessagePrefixOpts { prefixType: MessagePrefixType; shouldAddPrefixBeforeCallingEthSign: boolean; } + +export enum TradeSide { + Maker = 'maker', + Taker = 'taker', +} + +export enum TransferType { + Trade = 'trade', + Fee = 'fee', +} diff --git a/packages/order-utils/src/utils.ts b/packages/order-utils/src/utils.ts index 3b465cece..6149316f6 100644 --- a/packages/order-utils/src/utils.ts +++ b/packages/order-utils/src/utils.ts @@ -1,3 +1,5 @@ +import { BigNumber } from '@0xproject/utils'; + export const utils = { getSignatureTypeIndexIfExists(signature: string): number { // tslint:disable-next-line:custom-no-magic-numbers @@ -6,4 +8,8 @@ export const utils = { const signatureTypeInt = parseInt(signatureTypeHex, base); return signatureTypeInt; }, + getCurrentUnixTimestampSec(): BigNumber { + const milisecondsInSecond = 1000; + return new BigNumber(Date.now() / milisecondsInSecond).round(); + }, }; diff --git a/packages/order-utils/test/exchange_transfer_simulator_test.ts b/packages/order-utils/test/exchange_transfer_simulator_test.ts new file mode 100644 index 000000000..eeae42698 --- /dev/null +++ b/packages/order-utils/test/exchange_transfer_simulator_test.ts @@ -0,0 +1,177 @@ +import { BlockchainLifecycle, devConstants } from '@0xproject/dev-utils'; +import { ExchangeContractErrs } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'make-promises-safe'; + +import { artifacts } from '../src/artifacts'; +import { constants } from '../src/constants'; +import { ExchangeTransferSimulator } from '../src/exchange_transfer_simulator'; +import { DummyERC20TokenContract } from '../src/generated_contract_wrappers/dummy_e_r_c20_token'; +import { ERC20ProxyContract } from '../src/generated_contract_wrappers/e_r_c20_proxy'; +import { ERC20TokenContract } from '../src/generated_contract_wrappers/e_r_c20_token'; +import { BalanceAndProxyAllowanceLazyStore } from '../src/store/balance_and_proxy_allowance_lazy_store'; +import { TradeSide, TransferType } from '../src/types'; + +import { chaiSetup } from './utils/chai_setup'; +import { SimpleERC20BalanceAndProxyAllowanceFetcher } from './utils/simple_erc20_balance_and_proxy_allowance_fetcher'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('ExchangeTransferSimulator', async () => { + const transferAmount = new BigNumber(5); + let userAddresses: string[]; + let dummyERC20Token: DummyERC20TokenContract; + let coinbase: string; + let sender: string; + let recipient: string; + let exampleTokenAddress: string; + let exchangeTransferSimulator: ExchangeTransferSimulator; + let txHash: string; + let erc20ProxyAddress: string; + before(async function(): Promise<void> { + const mochaTestTimeoutMs = 20000; + this.timeout(mochaTestTimeoutMs); + + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + [coinbase, sender, recipient] = userAddresses; + + const txDefaults = { + gas: devConstants.GAS_LIMIT, + from: devConstants.TESTRPC_FIRST_ADDRESS, + }; + + const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync( + artifacts.ERC20Proxy, + provider, + txDefaults, + ); + erc20ProxyAddress = erc20Proxy.address; + + const totalSupply = new BigNumber(100000000000000000000); + const name = 'Test'; + const symbol = 'TST'; + const decimals = new BigNumber(18); + // tslint:disable-next-line:no-unused-variable + dummyERC20Token = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.DummyERC20Token, + provider, + txDefaults, + name, + symbol, + decimals, + totalSupply, + ); + + exampleTokenAddress = dummyERC20Token.address; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#transferFromAsync', function(): void { + // HACK: For some reason these tests need a slightly longer timeout + const mochaTestTimeoutMs = 3000; + this.timeout(mochaTestTimeoutMs); + + beforeEach(() => { + const simpleERC20BalanceAndProxyAllowanceFetcher = new SimpleERC20BalanceAndProxyAllowanceFetcher( + (dummyERC20Token as any) as ERC20TokenContract, + erc20ProxyAddress, + ); + const balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( + simpleERC20BalanceAndProxyAllowanceFetcher, + ); + exchangeTransferSimulator = new ExchangeTransferSimulator(balanceAndProxyAllowanceLazyStore); + }); + it("throws if the user doesn't have enough allowance", async () => { + return expect( + exchangeTransferSimulator.transferFromAsync( + exampleTokenAddress, + sender, + recipient, + transferAmount, + TradeSide.Taker, + TransferType.Trade, + ), + ).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance); + }); + it("throws if the user doesn't have enough balance", async () => { + txHash = await dummyERC20Token.approve.sendTransactionAsync(erc20ProxyAddress, transferAmount, { + from: sender, + }); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + return expect( + exchangeTransferSimulator.transferFromAsync( + exampleTokenAddress, + sender, + recipient, + transferAmount, + TradeSide.Maker, + TransferType.Trade, + ), + ).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance); + }); + it('updates balances and proxyAllowance after transfer', async () => { + txHash = await dummyERC20Token.transfer.sendTransactionAsync(sender, transferAmount, { + from: coinbase, + }); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + + txHash = await dummyERC20Token.approve.sendTransactionAsync(erc20ProxyAddress, transferAmount, { + from: sender, + }); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + + await exchangeTransferSimulator.transferFromAsync( + exampleTokenAddress, + sender, + recipient, + transferAmount, + TradeSide.Taker, + TransferType.Trade, + ); + const store = (exchangeTransferSimulator as any)._store; + const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender); + const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient); + const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender); + expect(senderBalance).to.be.bignumber.equal(0); + expect(recipientBalance).to.be.bignumber.equal(transferAmount); + expect(senderProxyAllowance).to.be.bignumber.equal(0); + }); + it("doesn't update proxyAllowance after transfer if unlimited", async () => { + txHash = await dummyERC20Token.transfer.sendTransactionAsync(sender, transferAmount, { + from: coinbase, + }); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + txHash = await dummyERC20Token.approve.sendTransactionAsync( + erc20ProxyAddress, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + { + from: sender, + }, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + await exchangeTransferSimulator.transferFromAsync( + exampleTokenAddress, + sender, + recipient, + transferAmount, + TradeSide.Taker, + TransferType.Trade, + ); + const store = (exchangeTransferSimulator as any)._store; + const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender); + const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient); + const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender); + expect(senderBalance).to.be.bignumber.equal(0); + expect(recipientBalance).to.be.bignumber.equal(transferAmount); + expect(senderProxyAllowance).to.be.bignumber.equal(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); + }); + }); +}); diff --git a/packages/order-utils/test/order_validation_utils_test.ts b/packages/order-utils/test/order_validation_utils_test.ts new file mode 100644 index 000000000..d3ff867d7 --- /dev/null +++ b/packages/order-utils/test/order_validation_utils_test.ts @@ -0,0 +1,70 @@ +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { OrderValidationUtils } from '../src/order_validation_utils'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('OrderValidationUtils', () => { + describe('#isRoundingError', () => { + it('should return false if there is a rounding error of 0.1%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(999); + const target = new BigNumber(50); + // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% + const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + + it('should return false if there is a rounding of 0.09%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(9991); + const target = new BigNumber(500); + // rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09% + const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + + it('should return true if there is a rounding error of 0.11%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(9989); + const target = new BigNumber(500); + // rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011% + const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target); + expect(isRoundingError).to.be.true(); + }); + + it('should return true if there is a rounding error > 0.1%', async () => { + const numerator = new BigNumber(3); + const denominator = new BigNumber(7); + const target = new BigNumber(10); + // rounding error = ((3*10/7) - floor(3*10/7)) / (3*10/7) = 6.67% + const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target); + expect(isRoundingError).to.be.true(); + }); + + it('should return false when there is no rounding error', async () => { + const numerator = new BigNumber(1); + const denominator = new BigNumber(2); + const target = new BigNumber(10); + + const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + + it('should return false when there is rounding error <= 0.1%', async () => { + // randomly generated numbers + const numerator = new BigNumber(76564); + const denominator = new BigNumber(676373677); + const target = new BigNumber(105762562); + // rounding error = ((76564*105762562/676373677) - floor(76564*105762562/676373677)) / + // (76564*105762562/676373677) = 0.0007% + const isRoundingError = OrderValidationUtils.isRoundingError(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + }); +}); diff --git a/packages/order-utils/test/utils/simple_erc20_balance_and_proxy_allowance_fetcher.ts b/packages/order-utils/test/utils/simple_erc20_balance_and_proxy_allowance_fetcher.ts new file mode 100644 index 000000000..d41eaca40 --- /dev/null +++ b/packages/order-utils/test/utils/simple_erc20_balance_and_proxy_allowance_fetcher.ts @@ -0,0 +1,26 @@ +import { BigNumber } from '@0xproject/utils'; + +import { AbstractBalanceAndProxyAllowanceFetcher } from '../../src/abstract/abstract_balance_and_proxy_allowance_fetcher'; + +import { ERC20TokenContract } from '../../src/generated_contract_wrappers/e_r_c20_token'; + +export class SimpleERC20BalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher { + private _erc20TokenContract: ERC20TokenContract; + private _erc20ProxyAddress: string; + constructor(erc20TokenWrapper: ERC20TokenContract, erc20ProxyAddress: string) { + this._erc20TokenContract = erc20TokenWrapper; + this._erc20ProxyAddress = erc20ProxyAddress; + } + public async getBalanceAsync(_assetData: string, userAddress: string): Promise<BigNumber> { + // HACK: We cheat and don't pass in the assetData since it's always the same token used + // in our tests. + const balance = await this._erc20TokenContract.balanceOf.callAsync(userAddress); + return balance; + } + public async getProxyAllowanceAsync(_assetData: string, userAddress: string): Promise<BigNumber> { + // HACK: We cheat and don't pass in the assetData since it's always the same token used + // in our tests. + const proxyAllowance = await this._erc20TokenContract.allowance.callAsync(userAddress, this._erc20ProxyAddress); + return proxyAllowance; + } +} |