Merge pull request #1714 from 0xProject/feature/order-utils/one-sided-transfer-validation

Simulate Maker transfer in order validation
This commit is contained in:
Jacob Evans
2019-03-28 15:10:00 +01:00
committed by GitHub
26 changed files with 1730 additions and 103 deletions

View File

@@ -1,4 +1,13 @@
[
{
"version": "2.2.0",
"changes": [
{
"note": "Added UntransferrableDummyERC20Token",
"pr": 1714
}
]
},
{
"version": "2.1.0",
"changes": [

View File

@@ -32,6 +32,7 @@
"src/interfaces/IEtherToken.sol",
"test/DummyERC20Token.sol",
"test/DummyMultipleReturnERC20Token.sol",
"test/DummyNoReturnERC20Token.sol"
"test/DummyNoReturnERC20Token.sol",
"test/UntransferrableDummyERC20Token.sol"
]
}

View File

@@ -0,0 +1,61 @@
/*
Copyright 2018 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.5;
import "./DummyERC20Token.sol";
// solhint-disable no-empty-blocks
contract UntransferrableDummyERC20Token is
DummyERC20Token
{
constructor (
string memory _name,
string memory _symbol,
uint256 _decimals,
uint256 _totalSupply
)
public
DummyERC20Token(
_name,
_symbol,
_decimals,
_totalSupply
)
{}
/// @dev send `value` token to `to` from `from` on the condition it is approved by `from`
/// @param _from The address of the sender
/// @param _to The address of the recipient
/// @param _value The amount of token to be transferred
function transferFrom(
address _from,
address _to,
uint256 _value
)
external
returns (bool)
{
require(
false,
"TRANSFER_DISABLED"
);
}
}

View File

@@ -1,4 +1,13 @@
[
{
"version": "4.2.0",
"changes": [
{
"note": "Added IAssetProxy wrapper",
"pr": 1714
}
]
},
{
"version": "4.1.0",
"changes": [

View File

@@ -18,7 +18,7 @@
"generate_contract_wrappers": "abi-gen --abis ${npm_package_config_abis} --template ../../node_modules/@0x/abi-gen-templates/contract.handlebars --partials '../../node_modules/@0x/abi-gen-templates/partials/**/*.handlebars' --output src/generated-wrappers --backend ethers"
},
"config": {
"abis": "../contract-artifacts/artifacts/@(AssetProxyOwner|DutchAuction|DummyERC20Token|DummyERC721Token|ERC20Proxy|ERC20Token|ERC721Proxy|ERC721Token|Exchange|Forwarder|IValidator|IWallet|MultiAssetProxy|OrderValidator|WETH9|ZRXToken|Coordinator|CoordinatorRegistry).json"
"abis": "../contract-artifacts/artifacts/@(AssetProxyOwner|DutchAuction|DummyERC20Token|DummyERC721Token|ERC20Proxy|ERC20Token|ERC721Proxy|ERC721Token|Exchange|Forwarder|IAssetProxy|IValidator|IWallet|MultiAssetProxy|OrderValidator|WETH9|ZRXToken|Coordinator|CoordinatorRegistry).json"
},
"repository": {
"type": "git",

View File

@@ -0,0 +1,534 @@
// tslint:disable:no-consecutive-blank-lines ordered-imports align trailing-comma whitespace class-name
// tslint:disable:no-unused-variable
// tslint:disable:no-unbound-method
import { BaseContract } from '@0x/base-contract';
import { BlockParam, BlockParamLiteral, CallData, ContractAbi, ContractArtifact, DecodedLogArgs, MethodAbi, TxData, TxDataPayable, SupportedProvider } from 'ethereum-types';
import { BigNumber, classUtils, logUtils, providerUtils } from '@0x/utils';
import { SimpleContractArtifact } from '@0x/types';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as ethers from 'ethers';
import * as _ from 'lodash';
// tslint:enable:no-unused-variable
/* istanbul ignore next */
// tslint:disable:no-parameter-reassignment
// tslint:disable-next-line:class-name
export class IAssetProxyContract extends BaseContract {
public addAuthorizedAddress = {
async sendTransactionAsync(
target: string,
txData: Partial<TxData> = {},
): Promise<string> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('addAuthorizedAddress(address)', [target
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
self.addAuthorizedAddress.estimateGasAsync.bind(
self,
target
),
);
const txHash = await self._web3Wrapper.sendTransactionAsync(txDataWithDefaults);
return txHash;
},
async estimateGasAsync(
target: string,
txData: Partial<TxData> = {},
): Promise<number> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('addAuthorizedAddress(address)', [target
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const gas = await self._web3Wrapper.estimateGasAsync(txDataWithDefaults);
return gas;
},
getABIEncodedTransactionData(
target: string,
): string {
const self = this as any as IAssetProxyContract;
const abiEncodedTransactionData = self._strictEncodeArguments('addAuthorizedAddress(address)', [target
]);
return abiEncodedTransactionData;
},
async callAsync(
target: string,
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<void
> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('addAuthorizedAddress(address)', [target
]);
const callDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...callData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const rawCallResult = await self._web3Wrapper.callAsync(callDataWithDefaults, defaultBlock);
BaseContract._throwIfRevertWithReasonCallResult(rawCallResult);
const abiEncoder = self._lookupAbiEncoder('addAuthorizedAddress(address)');
// tslint:disable boolean-naming
const result = abiEncoder.strictDecodeReturnValue<void
>(rawCallResult);
// tslint:enable boolean-naming
return result;
},
};
public removeAuthorizedAddress = {
async sendTransactionAsync(
target: string,
txData: Partial<TxData> = {},
): Promise<string> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('removeAuthorizedAddress(address)', [target
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
self.removeAuthorizedAddress.estimateGasAsync.bind(
self,
target
),
);
const txHash = await self._web3Wrapper.sendTransactionAsync(txDataWithDefaults);
return txHash;
},
async estimateGasAsync(
target: string,
txData: Partial<TxData> = {},
): Promise<number> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('removeAuthorizedAddress(address)', [target
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const gas = await self._web3Wrapper.estimateGasAsync(txDataWithDefaults);
return gas;
},
getABIEncodedTransactionData(
target: string,
): string {
const self = this as any as IAssetProxyContract;
const abiEncodedTransactionData = self._strictEncodeArguments('removeAuthorizedAddress(address)', [target
]);
return abiEncodedTransactionData;
},
async callAsync(
target: string,
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<void
> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('removeAuthorizedAddress(address)', [target
]);
const callDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...callData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const rawCallResult = await self._web3Wrapper.callAsync(callDataWithDefaults, defaultBlock);
BaseContract._throwIfRevertWithReasonCallResult(rawCallResult);
const abiEncoder = self._lookupAbiEncoder('removeAuthorizedAddress(address)');
// tslint:disable boolean-naming
const result = abiEncoder.strictDecodeReturnValue<void
>(rawCallResult);
// tslint:enable boolean-naming
return result;
},
};
public removeAuthorizedAddressAtIndex = {
async sendTransactionAsync(
target: string,
index: BigNumber,
txData: Partial<TxData> = {},
): Promise<string> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('removeAuthorizedAddressAtIndex(address,uint256)', [target,
index
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
self.removeAuthorizedAddressAtIndex.estimateGasAsync.bind(
self,
target,
index
),
);
const txHash = await self._web3Wrapper.sendTransactionAsync(txDataWithDefaults);
return txHash;
},
async estimateGasAsync(
target: string,
index: BigNumber,
txData: Partial<TxData> = {},
): Promise<number> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('removeAuthorizedAddressAtIndex(address,uint256)', [target,
index
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const gas = await self._web3Wrapper.estimateGasAsync(txDataWithDefaults);
return gas;
},
getABIEncodedTransactionData(
target: string,
index: BigNumber,
): string {
const self = this as any as IAssetProxyContract;
const abiEncodedTransactionData = self._strictEncodeArguments('removeAuthorizedAddressAtIndex(address,uint256)', [target,
index
]);
return abiEncodedTransactionData;
},
async callAsync(
target: string,
index: BigNumber,
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<void
> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('removeAuthorizedAddressAtIndex(address,uint256)', [target,
index
]);
const callDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...callData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const rawCallResult = await self._web3Wrapper.callAsync(callDataWithDefaults, defaultBlock);
BaseContract._throwIfRevertWithReasonCallResult(rawCallResult);
const abiEncoder = self._lookupAbiEncoder('removeAuthorizedAddressAtIndex(address,uint256)');
// tslint:disable boolean-naming
const result = abiEncoder.strictDecodeReturnValue<void
>(rawCallResult);
// tslint:enable boolean-naming
return result;
},
};
public transferFrom = {
async sendTransactionAsync(
assetData: string,
from: string,
to: string,
amount: BigNumber,
txData: Partial<TxData> = {},
): Promise<string> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('transferFrom(bytes,address,address,uint256)', [assetData,
from,
to,
amount
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
self.transferFrom.estimateGasAsync.bind(
self,
assetData,
from,
to,
amount
),
);
const txHash = await self._web3Wrapper.sendTransactionAsync(txDataWithDefaults);
return txHash;
},
async estimateGasAsync(
assetData: string,
from: string,
to: string,
amount: BigNumber,
txData: Partial<TxData> = {},
): Promise<number> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('transferFrom(bytes,address,address,uint256)', [assetData,
from,
to,
amount
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const gas = await self._web3Wrapper.estimateGasAsync(txDataWithDefaults);
return gas;
},
getABIEncodedTransactionData(
assetData: string,
from: string,
to: string,
amount: BigNumber,
): string {
const self = this as any as IAssetProxyContract;
const abiEncodedTransactionData = self._strictEncodeArguments('transferFrom(bytes,address,address,uint256)', [assetData,
from,
to,
amount
]);
return abiEncodedTransactionData;
},
async callAsync(
assetData: string,
from: string,
to: string,
amount: BigNumber,
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<void
> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('transferFrom(bytes,address,address,uint256)', [assetData,
from,
to,
amount
]);
const callDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...callData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const rawCallResult = await self._web3Wrapper.callAsync(callDataWithDefaults, defaultBlock);
BaseContract._throwIfRevertWithReasonCallResult(rawCallResult);
const abiEncoder = self._lookupAbiEncoder('transferFrom(bytes,address,address,uint256)');
// tslint:disable boolean-naming
const result = abiEncoder.strictDecodeReturnValue<void
>(rawCallResult);
// tslint:enable boolean-naming
return result;
},
};
public getProxyId = {
async callAsync(
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<string
> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('getProxyId()', []);
const callDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...callData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const rawCallResult = await self._web3Wrapper.callAsync(callDataWithDefaults, defaultBlock);
BaseContract._throwIfRevertWithReasonCallResult(rawCallResult);
const abiEncoder = self._lookupAbiEncoder('getProxyId()');
// tslint:disable boolean-naming
const result = abiEncoder.strictDecodeReturnValue<string
>(rawCallResult);
// tslint:enable boolean-naming
return result;
},
};
public getAuthorizedAddresses = {
async callAsync(
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<string[]
> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('getAuthorizedAddresses()', []);
const callDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...callData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const rawCallResult = await self._web3Wrapper.callAsync(callDataWithDefaults, defaultBlock);
BaseContract._throwIfRevertWithReasonCallResult(rawCallResult);
const abiEncoder = self._lookupAbiEncoder('getAuthorizedAddresses()');
// tslint:disable boolean-naming
const result = abiEncoder.strictDecodeReturnValue<string[]
>(rawCallResult);
// tslint:enable boolean-naming
return result;
},
};
public transferOwnership = {
async sendTransactionAsync(
newOwner: string,
txData: Partial<TxData> = {},
): Promise<string> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('transferOwnership(address)', [newOwner
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
self.transferOwnership.estimateGasAsync.bind(
self,
newOwner
),
);
const txHash = await self._web3Wrapper.sendTransactionAsync(txDataWithDefaults);
return txHash;
},
async estimateGasAsync(
newOwner: string,
txData: Partial<TxData> = {},
): Promise<number> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('transferOwnership(address)', [newOwner
]);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...txData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const gas = await self._web3Wrapper.estimateGasAsync(txDataWithDefaults);
return gas;
},
getABIEncodedTransactionData(
newOwner: string,
): string {
const self = this as any as IAssetProxyContract;
const abiEncodedTransactionData = self._strictEncodeArguments('transferOwnership(address)', [newOwner
]);
return abiEncodedTransactionData;
},
async callAsync(
newOwner: string,
callData: Partial<CallData> = {},
defaultBlock?: BlockParam,
): Promise<void
> {
const self = this as any as IAssetProxyContract;
const encodedData = self._strictEncodeArguments('transferOwnership(address)', [newOwner
]);
const callDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{
to: self.address,
...callData,
data: encodedData,
},
self._web3Wrapper.getContractDefaults(),
);
const rawCallResult = await self._web3Wrapper.callAsync(callDataWithDefaults, defaultBlock);
BaseContract._throwIfRevertWithReasonCallResult(rawCallResult);
const abiEncoder = self._lookupAbiEncoder('transferOwnership(address)');
// tslint:disable boolean-naming
const result = abiEncoder.strictDecodeReturnValue<void
>(rawCallResult);
// tslint:enable boolean-naming
return result;
},
};
public static async deployFrom0xArtifactAsync(
artifact: ContractArtifact | SimpleContractArtifact,
supportedProvider: SupportedProvider,
txDefaults: Partial<TxData>,
): Promise<IAssetProxyContract> {
if (_.isUndefined(artifact.compilerOutput)) {
throw new Error('Compiler output not found in the artifact file');
}
const provider = providerUtils.standardizeOrThrow(supportedProvider);
const bytecode = artifact.compilerOutput.evm.bytecode.object;
const abi = artifact.compilerOutput.abi;
return IAssetProxyContract.deployAsync(bytecode, abi, provider, txDefaults, );
}
public static async deployAsync(
bytecode: string,
abi: ContractAbi,
supportedProvider: SupportedProvider,
txDefaults: Partial<TxData>,
): Promise<IAssetProxyContract> {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
const constructorAbi = BaseContract._lookupConstructorAbi(abi);
[] = BaseContract._formatABIDataItemList(
constructorAbi.inputs,
[],
BaseContract._bigNumberToString,
);
const iface = new ethers.utils.Interface(abi);
const deployInfo = iface.deployFunction;
const txData = deployInfo.encode(bytecode, []);
const web3Wrapper = new Web3Wrapper(provider);
const txDataWithDefaults = await BaseContract._applyDefaultsToTxDataAsync(
{data: txData},
txDefaults,
web3Wrapper.estimateGasAsync.bind(web3Wrapper),
);
const txHash = await web3Wrapper.sendTransactionAsync(txDataWithDefaults);
logUtils.log(`transactionHash: ${txHash}`);
const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash);
logUtils.log(`IAssetProxy successfully deployed at ${txReceipt.contractAddress}`);
const contractInstance = new IAssetProxyContract(abi, txReceipt.contractAddress as string, provider, txDefaults);
contractInstance.constructorArgs = [];
return contractInstance;
}
constructor(abi: ContractAbi, address: string, supportedProvider: SupportedProvider, txDefaults?: Partial<TxData>) {
super('IAssetProxy', abi, address, supportedProvider, txDefaults);
classUtils.bindAll(this, ['_abiEncoderByFunctionSignature', 'address', 'abi', '_web3Wrapper']);
}
} // tslint:disable:max-file-line-count
// tslint:enable:no-unbound-method

View File

@@ -8,6 +8,7 @@ export * from './generated-wrappers/erc721_proxy';
export * from './generated-wrappers/erc721_token';
export * from './generated-wrappers/exchange';
export * from './generated-wrappers/forwarder';
export * from './generated-wrappers/i_asset_proxy';
export * from './generated-wrappers/i_validator';
export * from './generated-wrappers/i_wallet';
export * from './generated-wrappers/multi_asset_proxy';

View File

@@ -1,4 +1,13 @@
[
{
"version": "6.1.0",
"changes": [
{
"note": "Moved order_utils into `@0x/order-utils` package as `orderCalculationUtils`",
"pr": 1714
}
]
},
{
"timestamp": 1553183790,
"version": "6.0.5",

View File

@@ -1,4 +1,5 @@
import { HttpClient } from '@0x/connect';
import { orderCalculationUtils } from '@0x/order-utils';
import { APIOrder, AssetPairsResponse, OrderbookResponse } from '@0x/types';
import * as _ from 'lodash';
@@ -10,7 +11,6 @@ import {
SignedOrderWithRemainingFillableMakerAssetAmount,
} from '../types';
import { assert } from '../utils/assert';
import { orderUtils } from '../utils/order_utils';
export class StandardRelayerAPIOrderProvider implements OrderProvider {
public readonly apiUrl: string;
@@ -31,7 +31,7 @@ export class StandardRelayerAPIOrderProvider implements OrderProvider {
'remainingTakerAssetAmount',
order.takerAssetAmount,
);
const remainingFillableMakerAssetAmount = orderUtils.getRemainingMakerAmount(
const remainingFillableMakerAssetAmount = orderCalculationUtils.getMakerFillAmount(
order,
remainingFillableTakerAssetAmount,
);

View File

@@ -1,4 +1,4 @@
import { marketUtils, SignedOrder } from '@0x/order-utils';
import { marketUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
@@ -6,8 +6,6 @@ import { constants } from '../constants';
import { InsufficientAssetLiquidityError } from '../errors';
import { AssetBuyerError, BuyQuote, BuyQuoteInfo, OrdersAndFillableAmounts } from '../types';
import { orderUtils } from './order_utils';
// Calculates a buy quote for orders that have WETH as the takerAsset
export const buyQuoteCalculator = {
calculate(
@@ -166,7 +164,7 @@ function findEthAmountNeededToBuyZrx(
const { totalEthAmount, remainingZrxBuyAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const makerFillAmount = BigNumber.min(remainingZrxBuyAmount, remainingFillableMakerAssetAmount);
const [takerFillAmount, adjustedMakerFillAmount] = orderUtils.getTakerFillAmountForFeeOrder(
const [takerFillAmount, adjustedMakerFillAmount] = orderCalculationUtils.getTakerFillAmountForFeeOrder(
order,
makerFillAmount,
);
@@ -200,8 +198,8 @@ function findEthAndZrxAmountNeededToBuyAsset(
const { totalEthAmount, totalZrxAmount, remainingAssetBuyAmount } = acc;
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const makerFillAmount = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount);
const takerFillAmount = orderUtils.getTakerFillAmount(order, makerFillAmount);
const takerFeeAmount = orderUtils.getTakerFeeAmount(order, takerFillAmount);
const takerFillAmount = orderCalculationUtils.getTakerFillAmount(order, makerFillAmount);
const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerFillAmount);
return {
totalEthAmount: totalEthAmount.plus(takerFillAmount),
totalZrxAmount: totalZrxAmount.plus(takerFeeAmount),

View File

@@ -1,9 +1,8 @@
import { orderCalculationUtils } from '@0x/order-utils';
import { BigNumber } from '@0x/utils';
import { LiquidityForAssetData, OrdersAndFillableAmounts } from '../types';
import { orderUtils } from './order_utils';
export const calculateLiquidity = (ordersAndFillableAmounts: OrdersAndFillableAmounts): LiquidityForAssetData => {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
const liquidityInBigNumbers = orders.reduce(
@@ -14,7 +13,10 @@ export const calculateLiquidity = (ordersAndFillableAmounts: OrdersAndFillableAm
}
const tokensAvailableForCurrentOrder = availableMakerAssetAmount;
const ethValueAvailableForCurrentOrder = orderUtils.getTakerFillAmount(order, availableMakerAssetAmount);
const ethValueAvailableForCurrentOrder = orderCalculationUtils.getTakerFillAmount(
order,
availableMakerAssetAmount,
);
return {
tokensAvailableInBaseUnits: acc.tokensAvailableInBaseUnits.plus(tokensAvailableForCurrentOrder),
ethValueAvailableInWei: acc.ethValueAvailableInWei.plus(ethValueAvailableForCurrentOrder),

View File

@@ -1,5 +1,5 @@
import { OrderAndTraderInfo, OrderStatus, OrderValidatorWrapper } from '@0x/contract-wrappers';
import { sortingUtils } from '@0x/order-utils';
import { orderCalculationUtils, sortingUtils } from '@0x/order-utils';
import { RemainingFillableCalculator } from '@0x/order-utils/lib/src/remaining_fillable_calculator';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
@@ -14,8 +14,6 @@ import {
SignedOrderWithRemainingFillableMakerAssetAmount,
} from '../types';
import { orderUtils } from './order_utils';
export const orderProviderResponseProcessor = {
throwIfInvalidResponse(response: OrderProviderResponse, request: OrderProviderRequest): void {
const { makerAssetData, takerAssetData } = request;
@@ -83,7 +81,10 @@ function filterOutExpiredAndNonOpenOrders(
expiryBufferSeconds: number,
): SignedOrderWithRemainingFillableMakerAssetAmount[] {
const result = _.filter(orders, order => {
return orderUtils.isOpenOrder(order) && !orderUtils.willOrderExpire(order, expiryBufferSeconds);
return (
orderCalculationUtils.isOpenOrder(order) &&
!orderCalculationUtils.willOrderExpire(order, expiryBufferSeconds)
);
});
return result;
}
@@ -112,7 +113,10 @@ function getValidOrdersWithRemainingFillableMakerAssetAmountsFromOnChain(
const transferrableAssetAmount = BigNumber.min(traderInfo.makerAllowance, traderInfo.makerBalance);
const transferrableFeeAssetAmount = BigNumber.min(traderInfo.makerZrxAllowance, traderInfo.makerZrxBalance);
const remainingTakerAssetAmount = order.takerAssetAmount.minus(orderInfo.orderTakerAssetFilledAmount);
const remainingMakerAssetAmount = orderUtils.getRemainingMakerAmount(order, remainingTakerAssetAmount);
const remainingMakerAssetAmount = orderCalculationUtils.getMakerFillAmount(
order,
remainingTakerAssetAmount,
);
const remainingFillableCalculator = new RemainingFillableCalculator(
order.makerFee,
order.makerAssetAmount,

View File

@@ -1,74 +0,0 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { constants } from '../constants';
export const orderUtils = {
isOrderExpired(order: SignedOrder): boolean {
return orderUtils.willOrderExpire(order, 0);
},
willOrderExpire(order: SignedOrder, secondsFromNow: number): boolean {
const millisecondsInSecond = 1000;
const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).integerValue();
return order.expirationTimeSeconds.isLessThan(currentUnixTimestampSec.plus(secondsFromNow));
},
isOpenOrder(order: SignedOrder): boolean {
return order.takerAddress === constants.NULL_ADDRESS;
},
// given a remaining amount of takerAsset, calculate how much makerAsset is available
getRemainingMakerAmount(order: SignedOrder, remainingTakerAmount: BigNumber): BigNumber {
const remainingMakerAmount = remainingTakerAmount
.times(order.makerAssetAmount)
.div(order.takerAssetAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return remainingMakerAmount;
},
// given a desired amount of makerAsset, calculate how much takerAsset is required to fill that amount
getTakerFillAmount(order: SignedOrder, makerFillAmount: BigNumber): BigNumber {
// Round up because exchange rate favors Maker
const takerFillAmount = makerFillAmount
.multipliedBy(order.takerAssetAmount)
.div(order.makerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
return takerFillAmount;
},
// given a desired amount of takerAsset to fill, calculate how much fee is required by the taker to fill that amount
getTakerFeeAmount(order: SignedOrder, takerFillAmount: BigNumber): BigNumber {
// Round down because Taker fee rate favors Taker
const takerFeeAmount = takerFillAmount
.multipliedBy(order.takerFee)
.div(order.takerAssetAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return takerFeeAmount;
},
// given a desired amount of takerAsset to fill, calculate how much makerAsset will be filled
getMakerFillAmount(order: SignedOrder, takerFillAmount: BigNumber): BigNumber {
// Round down because exchange rate favors Maker
const makerFillAmount = takerFillAmount
.multipliedBy(order.makerAssetAmount)
.div(order.takerAssetAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return makerFillAmount;
},
// given a desired amount of makerAsset, calculate how much fee is required by the maker to fill that amount
getMakerFeeAmount(order: SignedOrder, makerFillAmount: BigNumber): BigNumber {
// Round down because Maker fee rate favors Maker
const makerFeeAmount = makerFillAmount
.multipliedBy(order.makerFee)
.div(order.makerAssetAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return makerFeeAmount;
},
// given a desired amount of ZRX from a fee order, calculate how much takerAsset is required to fill that amount
// also calculate how much ZRX needs to be bought in order fill the desired amount + takerFee
getTakerFillAmountForFeeOrder(order: SignedOrder, makerFillAmount: BigNumber): [BigNumber, BigNumber] {
// For each unit of TakerAsset we buy (MakerAsset - TakerFee)
const adjustedTakerFillAmount = makerFillAmount
.multipliedBy(order.takerAssetAmount)
.div(order.makerAssetAmount.minus(order.takerFee))
.integerValue(BigNumber.ROUND_CEIL);
// The amount that we buy will be greater than makerFillAmount, since we buy some amount for fees.
const adjustedMakerFillAmount = orderUtils.getMakerFillAmount(order, adjustedTakerFillAmount);
return [adjustedTakerFillAmount, adjustedMakerFillAmount];
},
};

View File

@@ -1,4 +1,13 @@
[
{
"version": "1.5.0",
"changes": [
{
"note": "Added artifact for `IAssetProxy`",
"pr": 1714
}
]
},
{
"version": "1.4.0",
"changes": [

View File

@@ -0,0 +1,185 @@
{
"schemaVersion": "2.0.0",
"contractName": "IAssetProxy",
"compilerOutput": {
"abi": [
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
}
],
"name": "addAuthorizedAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
}
],
"name": "removeAuthorizedAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
},
{
"name": "index",
"type": "uint256"
}
],
"name": "removeAuthorizedAddressAtIndex",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "assetData",
"type": "bytes"
},
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getProxyId",
"outputs": [
{
"name": "",
"type": "bytes4"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getAuthorizedAddresses",
"outputs": [
{
"name": "",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
],
"evm": {
"bytecode": {
"linkReferences": {},
"object": "0x",
"opcodes": "",
"sourceMap": ""
},
"deployedBytecode": {
"linkReferences": {},
"object": "0x",
"opcodes": "",
"sourceMap": ""
}
}
},
"sources": {
"src/interfaces/IAssetProxy.sol": {
"id": 2
},
"src/interfaces/IAuthorizable.sol": {
"id": 3
},
"@0x/contracts-utils/contracts/src/interfaces/IOwnable.sol": {
"id": 6
}
},
"sourceCodes": {
"src/interfaces/IAssetProxy.sol": "/*\n\n Copyright 2018 ZeroEx Intl.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n*/\n\npragma solidity ^0.4.24;\n\nimport \"./IAuthorizable.sol\";\n\n\ncontract IAssetProxy is\n IAuthorizable\n{\n /// @dev Transfers assets. Either succeeds or throws.\n /// @param assetData Byte array encoded for the respective asset proxy.\n /// @param from Address to transfer asset from.\n /// @param to Address to transfer asset to.\n /// @param amount Amount of asset to transfer.\n function transferFrom(\n bytes assetData,\n address from,\n address to,\n uint256 amount\n )\n external;\n \n /// @dev Gets the proxy id associated with the proxy address.\n /// @return Proxy id.\n function getProxyId()\n external\n pure\n returns (bytes4);\n}\n",
"src/interfaces/IAuthorizable.sol": "/*\n\n Copyright 2018 ZeroEx Intl.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n*/\n\npragma solidity ^0.4.24;\n\nimport \"@0x/contracts-utils/contracts/src/interfaces/IOwnable.sol\";\n\n\ncontract IAuthorizable is\n IOwnable\n{\n /// @dev Authorizes an address.\n /// @param target Address to authorize.\n function addAuthorizedAddress(address target)\n external;\n\n /// @dev Removes authorizion of an address.\n /// @param target Address to remove authorization from.\n function removeAuthorizedAddress(address target)\n external;\n\n /// @dev Removes authorizion of an address.\n /// @param target Address to remove authorization from.\n /// @param index Index of target in authorities array.\n function removeAuthorizedAddressAtIndex(\n address target,\n uint256 index\n )\n external;\n \n /// @dev Gets all authorized addresses.\n /// @return Array of authorized addresses.\n function getAuthorizedAddresses()\n external\n view\n returns (address[] memory);\n}\n",
"@0x/contracts-utils/contracts/src/interfaces/IOwnable.sol": "pragma solidity ^0.4.24;\n\n\ncontract IOwnable {\n\n function transferOwnership(address newOwner)\n public;\n}\n"
},
"sourceTreeHashHex": "0x4fe33d3b00ae85e5c1d5478eeb1bf17fdc637fa0501dc1ff6fd1f871f87c83ca",
"compiler": {
"name": "solc",
"version": "0.4.25+commit.59dbf8f1.Linux.g++",
"settings": {
"optimizer": {
"enabled": true,
"runs": 1000000,
"details": {
"yul": true,
"deduplicate": true,
"cse": true,
"constantOptimizer": true
}
},
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
},
"evmVersion": "byzantium",
"remappings": [
"@0x/contracts-utils=/Users/jacob/projects/ethdev/0x/0x.js/contracts/asset-proxy/node_modules/@0x/contracts-utils"
]
}
},
"networks": {}
}

View File

@@ -10,6 +10,7 @@ import * as ERC721Proxy from '../artifacts/ERC721Proxy.json';
import * as ERC721Token from '../artifacts/ERC721Token.json';
import * as Exchange from '../artifacts/Exchange.json';
import * as Forwarder from '../artifacts/Forwarder.json';
import * as IAssetProxy from '../artifacts/IAssetProxy.json';
import * as IValidator from '../artifacts/IValidator.json';
import * as IWallet from '../artifacts/IWallet.json';
import * as MultiAssetProxy from '../artifacts/MultiAssetProxy.json';
@@ -28,6 +29,7 @@ export {
ERC721Token,
Exchange,
Forwarder,
IAssetProxy,
IValidator,
IWallet,
MultiAssetProxy,

View File

@@ -17,6 +17,7 @@
"./artifacts/ERC721Token.json",
"./artifacts/Exchange.json",
"./artifacts/Forwarder.json",
"./artifacts/IAssetProxy.json",
"./artifacts/IValidator.json",
"./artifacts/IWallet.json",
"./artifacts/MultiAssetProxy.json",

View File

@@ -1,4 +1,17 @@
[
{
"version": "9.0.0",
"changes": [
{
"note": "Added a simulation to transfer from maker to taker during `exchange.validateOrderFillableOrThrowAsync`",
"pr": 1714
},
{
"note": "Added additional properties to `ValidateOrderFillableOpts`. An order can now be validated to fill a non-zero amount by specifying `validateRemainingOrderAmountIsFillable` as `false`. The default `true` will continue to validate the entire remaining balance is fillable.",
"pr": 1714
}
]
},
{
"timestamp": 1553183790,
"version": "8.0.5",

View File

@@ -1,10 +1,13 @@
import { ExchangeContract, ExchangeEventArgs, ExchangeEvents } from '@0x/abi-gen-wrappers';
import { Exchange } from '@0x/contract-artifacts';
import { ExchangeContract, ExchangeEventArgs, ExchangeEvents, IAssetProxyContract } from '@0x/abi-gen-wrappers';
import { Exchange, IAssetProxy } from '@0x/contract-artifacts';
import { schemas } from '@0x/json-schemas';
import {
assetDataUtils,
BalanceAndProxyAllowanceLazyStore,
ExchangeTransferSimulator,
orderCalculationUtils,
orderHashUtils,
OrderStateUtils,
OrderValidationUtils,
} from '@0x/order-utils';
import { AssetProxyId, Order, SignedOrder } from '@0x/types';
@@ -1139,7 +1142,8 @@ export class ExchangeWrapper extends ContractWrapper {
* Validate if the supplied order is fillable, and throw if it isn't
* @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 a non-zero amount)
* 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 validateOrderFillableOrThrowAsync(
signedOrder: SignedOrder,
@@ -1155,16 +1159,77 @@ export class ExchangeWrapper extends ContractWrapper {
);
const balanceAllowanceStore = new BalanceAndProxyAllowanceLazyStore(balanceAllowanceFetcher);
const exchangeTradeSimulator = new ExchangeTransferSimulator(balanceAllowanceStore);
const expectedFillTakerTokenAmountIfExists = opts.expectedFillTakerTokenAmount;
const filledCancelledFetcher = new OrderFilledCancelledFetcher(this, BlockParamLiteral.Latest);
let fillableTakerAssetAmount;
const shouldValidateRemainingOrderAmountIsFillable = _.isUndefined(opts.validateRemainingOrderAmountIsFillable)
? 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 this.getFilledTakerAssetAmountAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
fillableTakerAssetAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount);
} else {
const orderStateUtils = new OrderStateUtils(balanceAllowanceStore, filledCancelledFetcher);
// Calculate the taker amount fillable given the maker balance and allowance
const orderRelevantState = await orderStateUtils.getOpenOrderRelevantStateAsync(signedOrder);
fillableTakerAssetAmount = orderRelevantState.remainingFillableTakerAssetAmount;
}
const orderValidationUtils = new OrderValidationUtils(filledCancelledFetcher, this._web3Wrapper.getProvider());
await orderValidationUtils.validateOrderFillableOrThrowAsync(
exchangeTradeSimulator,
signedOrder,
this.getZRXAssetData(),
expectedFillTakerTokenAmountIfExists,
fillableTakerAssetAmount,
);
const makerTransferAmount = orderCalculationUtils.getMakerFillAmount(signedOrder, fillableTakerAssetAmount);
await this.validateMakerTransferThrowIfInvalidAsync(
signedOrder,
makerTransferAmount,
opts.simulationTakerAddress,
);
}
/**
* Validate the transfer from the maker to the taker. This is simulated on-chain
* via an eth_call. If this call fails, the asset is currently nontransferable.
* @param signedOrder SignedOrder of interest
* @param makerAssetAmount Amount to transfer from the maker
* @param takerAddress The address to transfer to, defaults to signedOrder.takerAddress
*/
public async validateMakerTransferThrowIfInvalidAsync(
signedOrder: SignedOrder,
makerAssetAmount: BigNumber,
takerAddress?: string,
): Promise<void> {
const toAddress = _.isUndefined(takerAddress) ? signedOrder.takerAddress : takerAddress;
const exchangeInstance = await this._getExchangeContractAsync();
const makerAssetData = signedOrder.makerAssetData;
const makerAssetDataProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData);
const assetProxyAddress = await exchangeInstance.assetProxies.callAsync(makerAssetDataProxyId);
const assetProxy = new IAssetProxyContract(
IAssetProxy.compilerOutput.abi,
assetProxyAddress,
this._web3Wrapper.getProvider(),
);
const result = await assetProxy.transferFrom.callAsync(
makerAssetData,
signedOrder.makerAddress,
toAddress,
makerAssetAmount,
{
from: this.address,
},
);
if (result !== undefined) {
throw new Error(`Error during maker transfer simulation: ${result}`);
}
}
/**
* Validate a call to FillOrder and throw if it wouldn't succeed

View File

@@ -120,13 +120,22 @@ export interface ContractWrappersConfig {
}
/**
* expectedFillTakerTokenAmount: If specified, the validation method will ensure that the
* supplied order maker has a sufficient allowance/balance to fill this amount of the order's
* takerTokenAmount. If not specified, the validation method ensures that the maker has a sufficient
* allowance/balance to fill the entire remaining order amount.
* `expectedFillTakerTokenAmount`: If specified, the validation method will ensure that the supplied order maker has a sufficient
* allowance/balance to fill this amount of the order's takerTokenAmount.
*
* `validateRemainingOrderAmountIsFillable`: The validation method ensures that the maker has sufficient allowance/balance to fill
* the entire remaining order amount. If this option is set to false, the balances
* and allowances are calculated to determine the order is fillable for a
* non-zero amount (some value less than or equal to the order remaining amount).
* We call such orders "partially fillable orders". Default is `true`.
*
* `simulationTakerAddress`: During the maker transfer simulation, tokens are sent from the maker to the `simulationTakerAddress`. This defaults
* to the `takerAddress` specified in the order. Some tokens prevent transfer to the NULL address so this address can be specified.
*/
export interface ValidateOrderFillableOpts {
expectedFillTakerTokenAmount?: BigNumber;
validateRemainingOrderAmountIsFillable?: boolean;
simulationTakerAddress?: string;
}
/**

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,4 @@
import { DummyERC20TokenContract } from '@0x/abi-gen-wrappers';
import { BlockchainLifecycle, callbackErrorReporter } from '@0x/dev-utils';
import { FillScenarios } from '@0x/fill-scenarios';
import { assetDataUtils, orderHashUtils, signatureUtils } from '@0x/order-utils';
@@ -10,6 +11,7 @@ import 'mocha';
import { ContractWrappers, ExchangeCancelEventArgs, ExchangeEvents, ExchangeFillEventArgs, OrderStatus } from '../src';
import { DecodedLogEvent } from '../src/types';
import { UntransferrableDummyERC20Token } from './artifacts/UntransferrableDummyERC20Token';
import { chaiSetup } from './utils/chai_setup';
import { constants } from './utils/constants';
import { migrateOnceAsync } from './utils/migrate';
@@ -290,10 +292,94 @@ describe('ExchangeWrapper', () => {
'0x1b61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403',
};
expect(
return expect(
contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrderWithInvalidSignature),
).to.eventually.to.be.rejectedWith(RevertReason.InvalidOrderSignature);
});
it('should validate the order with the current balances and allowances for the maker', async () => {
await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder, {
validateRemainingOrderAmountIsFillable: false,
});
});
it('should validate the order with remaining fillable amount for the order', async () => {
await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder);
});
it('should validate the order with specified amount', async () => {
await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder, {
expectedFillTakerTokenAmount: signedOrder.takerAssetAmount,
});
});
it('should throw if the amount is greater than the allowance/balance', async () => {
return expect(
contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder, {
// tslint:disable-next-line:custom-no-magic-numbers
expectedFillTakerTokenAmount: new BigNumber(2).pow(256).minus(1),
}),
).to.eventually.to.be.rejected();
});
it('should throw when the maker does not have enough balance for the remaining order amount', async () => {
const makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
// Change maker balance to have less than the order amount
const remainingBalance = makerBalance.minus(signedOrder.makerAssetAmount.minus(1));
await web3Wrapper.awaitTransactionSuccessAsync(
await contractWrappers.erc20Token.transferAsync(
makerTokenAddress,
makerAddress,
constants.NULL_ADDRESS,
remainingBalance,
),
);
return expect(
contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder),
).to.eventually.to.be.rejected();
});
it('should validate the order when remaining order amount has some fillable amount', async () => {
const makerBalance = await contractWrappers.erc20Token.getBalanceAsync(makerTokenAddress, makerAddress);
// Change maker balance to have less than the order amount
const remainingBalance = makerBalance.minus(signedOrder.makerAssetAmount.minus(1));
await web3Wrapper.awaitTransactionSuccessAsync(
await contractWrappers.erc20Token.transferAsync(
makerTokenAddress,
makerAddress,
constants.NULL_ADDRESS,
remainingBalance,
),
);
// An amount is still transferrable
await contractWrappers.exchange.validateOrderFillableOrThrowAsync(signedOrder, {
validateRemainingOrderAmountIsFillable: false,
});
});
it('should throw when the ERC20 token has transfer restrictions', async () => {
const untransferrableToken = await DummyERC20TokenContract.deployFrom0xArtifactAsync(
UntransferrableDummyERC20Token,
provider,
{ from: userAddresses[0] },
'UntransferrableToken',
'UTT',
new BigNumber(constants.ZRX_DECIMALS),
// tslint:disable-next-line:custom-no-magic-numbers
new BigNumber(2).pow(20).minus(1),
);
const untransferrableMakerAssetData = assetDataUtils.encodeERC20AssetData(untransferrableToken.address);
const invalidSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
untransferrableMakerAssetData,
takerAssetData,
makerAddress,
takerAddress,
fillableAmount,
);
await web3Wrapper.awaitTransactionSuccessAsync(
await contractWrappers.erc20Token.setProxyAllowanceAsync(
untransferrableToken.address,
makerAddress,
signedOrder.makerAssetAmount,
),
);
return expect(
contractWrappers.exchange.validateOrderFillableOrThrowAsync(invalidSignedOrder),
).to.eventually.to.be.rejectedWith('TRANSFER_FAILED');
});
});
describe('#isValidSignature', () => {
it('should check if the signature is valid', async () => {

View File

@@ -1,4 +1,13 @@
[
{
"version": "7.2.0",
"changes": [
{
"note": "Added `orderCalculationUtils`",
"pr": 1714
}
]
},
{
"timestamp": 1553183790,
"version": "7.1.1",

View File

@@ -7,6 +7,7 @@ export { transactionHashUtils } from './transaction_hash';
export { rateUtils } from './rate_utils';
export { sortingUtils } from './sorting_utils';
export { orderParsingUtils } from './parsing_utils';
export { orderCalculationUtils } from './order_calculation_utils';
export { OrderStateUtils } from './order_state_utils';
export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';

View File

@@ -0,0 +1,99 @@
import { Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { constants } from './constants';
export const orderCalculationUtils = {
/**
* Determines if the order is expired given the current time
* @param order The order for expiry calculation
*/
isOrderExpired(order: Order): boolean {
return orderCalculationUtils.willOrderExpire(order, 0);
},
/**
* Calculates if the order will expire in the future.
* @param order The order for expiry calculation
* @param secondsFromNow The amount of seconds from current time
*/
willOrderExpire(order: Order, secondsFromNow: number): boolean {
const millisecondsInSecond = 1000;
const currentUnixTimestampSec = new BigNumber(Date.now() / millisecondsInSecond).integerValue();
return order.expirationTimeSeconds.isLessThan(currentUnixTimestampSec.plus(secondsFromNow));
},
/**
* Determines if the order is open and fillable by any taker.
* @param order The order
*/
isOpenOrder(order: Order): boolean {
return order.takerAddress === constants.NULL_ADDRESS;
},
/**
* Given an amount of taker asset, calculate the the amount of maker asset
* @param order The order
* @param makerFillAmount the amount of taker asset
*/
getMakerFillAmount(order: Order, takerFillAmount: BigNumber): BigNumber {
// Round down because exchange rate favors Maker
const makerFillAmount = takerFillAmount
.multipliedBy(order.makerAssetAmount)
.div(order.takerAssetAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return makerFillAmount;
},
/**
* Given an amount of maker asset, calculate the equivalent amount in taker asset
* @param order The order
* @param makerFillAmount the amount of maker asset
*/
getTakerFillAmount(order: Order, makerFillAmount: BigNumber): BigNumber {
// Round up because exchange rate favors Maker
const takerFillAmount = makerFillAmount
.multipliedBy(order.takerAssetAmount)
.div(order.makerAssetAmount)
.integerValue(BigNumber.ROUND_CEIL);
return takerFillAmount;
},
/**
* Given an amount of taker asset, calculate the fee amount required for the taker
* @param order The order
* @param takerFillAmount the amount of taker asset
*/
getTakerFeeAmount(order: Order, takerFillAmount: BigNumber): BigNumber {
// Round down because Taker fee rate favors Taker
const takerFeeAmount = takerFillAmount
.multipliedBy(order.takerFee)
.div(order.takerAssetAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return takerFeeAmount;
},
/**
* Given an amount of maker asset, calculate the fee amount required for the maker
* @param order The order
* @param makerFillAmount the amount of maker asset
*/
getMakerFeeAmount(order: Order, makerFillAmount: BigNumber): BigNumber {
// Round down because Maker fee rate favors Maker
const makerFeeAmount = makerFillAmount
.multipliedBy(order.makerFee)
.div(order.makerAssetAmount)
.integerValue(BigNumber.ROUND_FLOOR);
return makerFeeAmount;
},
/**
* Given a desired amount of ZRX from a fee order, calculate the amount of taker asset required to fill.
* Also calculate how much ZRX needs to be purchased in order to fill the desired amount plus the taker fee amount
* @param order The order
* @param makerFillAmount the amount of maker asset
*/
getTakerFillAmountForFeeOrder(order: Order, makerFillAmount: BigNumber): [BigNumber, BigNumber] {
// For each unit of TakerAsset we buy (MakerAsset - TakerFee)
const adjustedTakerFillAmount = makerFillAmount
.multipliedBy(order.takerAssetAmount)
.div(order.makerAssetAmount.minus(order.takerFee))
.integerValue(BigNumber.ROUND_CEIL);
// The amount that we buy will be greater than makerFillAmount, since we buy some amount for fees.
const adjustedMakerFillAmount = orderCalculationUtils.getMakerFillAmount(order, adjustedTakerFillAmount);
return [adjustedTakerFillAmount, adjustedMakerFillAmount];
},
};

View File

@@ -0,0 +1,185 @@
{
"schemaVersion": "2.0.0",
"contractName": "IAssetProxy",
"compilerOutput": {
"abi": [
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
}
],
"name": "addAuthorizedAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
}
],
"name": "removeAuthorizedAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "target",
"type": "address"
},
{
"name": "index",
"type": "uint256"
}
],
"name": "removeAuthorizedAddressAtIndex",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "assetData",
"type": "bytes"
},
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getProxyId",
"outputs": [
{
"name": "",
"type": "bytes4"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getAuthorizedAddresses",
"outputs": [
{
"name": "",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
],
"evm": {
"bytecode": {
"linkReferences": {},
"object": "0x",
"opcodes": "",
"sourceMap": ""
},
"deployedBytecode": {
"linkReferences": {},
"object": "0x",
"opcodes": "",
"sourceMap": ""
}
}
},
"sources": {
"src/interfaces/IAssetProxy.sol": {
"id": 2
},
"src/interfaces/IAuthorizable.sol": {
"id": 3
},
"@0x/contracts-utils/contracts/src/interfaces/IOwnable.sol": {
"id": 6
}
},
"sourceCodes": {
"src/interfaces/IAssetProxy.sol": "/*\n\n Copyright 2018 ZeroEx Intl.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n*/\n\npragma solidity ^0.4.24;\n\nimport \"./IAuthorizable.sol\";\n\n\ncontract IAssetProxy is\n IAuthorizable\n{\n /// @dev Transfers assets. Either succeeds or throws.\n /// @param assetData Byte array encoded for the respective asset proxy.\n /// @param from Address to transfer asset from.\n /// @param to Address to transfer asset to.\n /// @param amount Amount of asset to transfer.\n function transferFrom(\n bytes assetData,\n address from,\n address to,\n uint256 amount\n )\n external;\n \n /// @dev Gets the proxy id associated with the proxy address.\n /// @return Proxy id.\n function getProxyId()\n external\n pure\n returns (bytes4);\n}\n",
"src/interfaces/IAuthorizable.sol": "/*\n\n Copyright 2018 ZeroEx Intl.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n*/\n\npragma solidity ^0.4.24;\n\nimport \"@0x/contracts-utils/contracts/src/interfaces/IOwnable.sol\";\n\n\ncontract IAuthorizable is\n IOwnable\n{\n /// @dev Authorizes an address.\n /// @param target Address to authorize.\n function addAuthorizedAddress(address target)\n external;\n\n /// @dev Removes authorizion of an address.\n /// @param target Address to remove authorization from.\n function removeAuthorizedAddress(address target)\n external;\n\n /// @dev Removes authorizion of an address.\n /// @param target Address to remove authorization from.\n /// @param index Index of target in authorities array.\n function removeAuthorizedAddressAtIndex(\n address target,\n uint256 index\n )\n external;\n \n /// @dev Gets all authorized addresses.\n /// @return Array of authorized addresses.\n function getAuthorizedAddresses()\n external\n view\n returns (address[] memory);\n}\n",
"@0x/contracts-utils/contracts/src/interfaces/IOwnable.sol": "pragma solidity ^0.4.24;\n\n\ncontract IOwnable {\n\n function transferOwnership(address newOwner)\n public;\n}\n"
},
"sourceTreeHashHex": "0x4fe33d3b00ae85e5c1d5478eeb1bf17fdc637fa0501dc1ff6fd1f871f87c83ca",
"compiler": {
"name": "solc",
"version": "0.4.25+commit.59dbf8f1.Linux.g++",
"settings": {
"optimizer": {
"enabled": true,
"runs": 1000000,
"details": {
"yul": true,
"deduplicate": true,
"cse": true,
"constantOptimizer": true
}
},
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
},
"evmVersion": "byzantium",
"remappings": [
"@0x/contracts-utils=/Users/jacob/projects/ethdev/0x/0x.js/contracts/asset-proxy/node_modules/@0x/contracts-utils"
]
}
},
"networks": {}
}