aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils
diff options
context:
space:
mode:
authorLeonid Logvinov <logvinov.leon@gmail.com>2017-10-09 18:07:48 +0800
committerLeonid Logvinov <logvinov.leon@gmail.com>2017-10-09 18:07:48 +0800
commit1424a7302a1d1b3a9c36b3a6ce5a344f8045400d (patch)
tree5266fdd4b0f167fc1fa5131fc422f2196a4bf2ff /src/utils
parent54ac354809c493cedfec5cd19b4707341b2c9f93 (diff)
downloaddexon-sol-tools-1424a7302a1d1b3a9c36b3a6ce5a344f8045400d.tar
dexon-sol-tools-1424a7302a1d1b3a9c36b3a6ce5a344f8045400d.tar.gz
dexon-sol-tools-1424a7302a1d1b3a9c36b3a6ce5a344f8045400d.tar.bz2
dexon-sol-tools-1424a7302a1d1b3a9c36b3a6ce5a344f8045400d.tar.lz
dexon-sol-tools-1424a7302a1d1b3a9c36b3a6ce5a344f8045400d.tar.xz
dexon-sol-tools-1424a7302a1d1b3a9c36b3a6ce5a344f8045400d.tar.zst
dexon-sol-tools-1424a7302a1d1b3a9c36b3a6ce5a344f8045400d.zip
Implement transfer Emulator and rewrite tests
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/assert.ts2
-rw-r--r--src/utils/exchange_transfer_simulator.ts136
-rw-r--r--src/utils/order_validation_utils.ts153
3 files changed, 189 insertions, 102 deletions
diff --git a/src/utils/assert.ts b/src/utils/assert.ts
index 0b7a11939..099f4490f 100644
--- a/src/utils/assert.ts
+++ b/src/utils/assert.ts
@@ -28,7 +28,7 @@ export const assert = {
const web3 = new Web3();
this.assert(web3.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value));
this.assert(
- web3.isAddress(value) && !web3.isChecksumAddress(value),
+ web3.isAddress(value) && value.toLowerCase() === value,
`Checksummed addresses are not supported. Convert ${variableName} to lower case before passing`,
);
},
diff --git a/src/utils/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts
new file mode 100644
index 000000000..f79dce242
--- /dev/null
+++ b/src/utils/exchange_transfer_simulator.ts
@@ -0,0 +1,136 @@
+import * as _ from 'lodash';
+import {ExchangeContractErrs, TradeSide, TransferType} from '../types';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+
+enum FailureReason {
+ Balance = 'balance',
+ ProxyAllowance = 'proxy allowance',
+}
+
+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,
+ },
+ },
+};
+
+/**
+ * Copy on read store for balances/proxyAllowances of tokens/accounts touched in trades
+ * @param {TokenWrapper} token [description]
+ * @return {[type]} [description]
+ */
+export class BalanceAndProxyAllowanceLazyStore {
+ protected _token: TokenWrapper;
+ private _balance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber.BigNumber,
+ },
+ };
+ private _proxyAllowance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber.BigNumber,
+ },
+ };
+ constructor(token: TokenWrapper) {
+ this._token = token;
+ this._balance = {};
+ this._proxyAllowance = {};
+ }
+ protected async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber.BigNumber> {
+ if (_.isUndefined(this._balance[tokenAddress]) || _.isUndefined(this._balance[tokenAddress][userAddress])) {
+ const balance = await this._token.getBalanceAsync(tokenAddress, userAddress);
+ this.setBalance(tokenAddress, userAddress, balance);
+ }
+ return this._balance[tokenAddress][userAddress];
+ }
+ protected setBalance(tokenAddress: string, userAddress: string, balance: BigNumber.BigNumber): void {
+ if (_.isUndefined(this._balance[tokenAddress])) {
+ this._balance[tokenAddress] = {};
+ }
+ this._balance[tokenAddress][userAddress] = balance;
+ }
+ protected async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber.BigNumber> {
+ if (_.isUndefined(this._proxyAllowance[tokenAddress]) ||
+ _.isUndefined(this._proxyAllowance[tokenAddress][userAddress])) {
+ const proxyAllowance = await this._token.getProxyAllowanceAsync(tokenAddress, userAddress);
+ this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
+ }
+ return this._proxyAllowance[tokenAddress][userAddress];
+ }
+ protected setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber.BigNumber): void {
+ if (_.isUndefined(this._proxyAllowance[tokenAddress])) {
+ this._proxyAllowance[tokenAddress] = {};
+ }
+ this._proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
+ }
+}
+
+export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore {
+ constructor(token: TokenWrapper) {
+ super(token);
+ }
+ /**
+ * Simulates transferFrom call performed by a proxy
+ * @param tokenAddress Address of a token to be transfered
+ * @param from Owner of the transfered tokens
+ * @param to Recipient of the transfered tokens
+ * @param amountInBaseUnits The amount of tokens being transfered
+ * @param tradeSide Is Maker/Taker transferring
+ * @param transferType Is it a fee payment or a value transfer
+ */
+ public async transferFromAsync(tokenAddress: string, from: string, to: string,
+ amountInBaseUnits: BigNumber.BigNumber, tradeSide: TradeSide,
+ transferType: TransferType): Promise<void> {
+ const balance = await this.getBalanceAsync(tokenAddress, from);
+ const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, from);
+ if (balance.lessThan(amountInBaseUnits)) {
+ this.throwValidationError(FailureReason.Balance, tradeSide, transferType);
+ }
+ if (proxyAllowance.lessThan(amountInBaseUnits)) {
+ this.throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType);
+ }
+ await this.decreaseProxyAllowanceAsync(tokenAddress, from, amountInBaseUnits);
+ await this.decreaseBalanceAsync(tokenAddress, from, amountInBaseUnits);
+ if (!_.isUndefined(to)) {
+ await this.increaseBalanceAsync(tokenAddress, to, amountInBaseUnits);
+ }
+ }
+ private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber.BigNumber): Promise<void> {
+ const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, userAddress);
+ if (!proxyAllowance.eq(this._token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
+ this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
+ }
+ }
+ private async increaseBalanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber.BigNumber): Promise<void> {
+ const balance = await this.getBalanceAsync(tokenAddress, userAddress);
+ this.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
+ }
+ private async decreaseBalanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber.BigNumber): Promise<void> {
+ const balance = await this.getBalanceAsync(tokenAddress, userAddress);
+ this.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
+ }
+ private throwValidationError(failureReason: FailureReason, tradeSide: TradeSide,
+ transferType: TransferType): Promise<never> {
+ const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType];
+ throw new Error(errMsg);
+ }
+}
diff --git a/src/utils/order_validation_utils.ts b/src/utils/order_validation_utils.ts
index 0450d26a6..5d14602db 100644
--- a/src/utils/order_validation_utils.ts
+++ b/src/utils/order_validation_utils.ts
@@ -1,10 +1,11 @@
import * as _ from 'lodash';
-import {ExchangeContractErrs, SignedOrder, Order, ZeroExError} from '../types';
+import {ExchangeContractErrs, SignedOrder, Order, ZeroExError, TradeSide, TransferType} from '../types';
import {ZeroEx} from '../0x';
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
import {utils} from '../utils/utils';
import {constants} from '../utils/constants';
+import {ExchangeTransferSimulator} from './exchange_transfer_simulator';
export class OrderValidationUtils {
private tokenWrapper: TokenWrapper;
@@ -14,8 +15,8 @@ export class OrderValidationUtils {
this.exchangeWrapper = exchangeWrapper;
}
public async validateOrderFillableOrThrowAsync(
- signedOrder: SignedOrder, zrxTokenAddress: string, expectedFillTakerTokenAmount?: BigNumber.BigNumber,
- ): Promise<void> {
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, zrxTokenAddress: string,
+ expectedFillTakerTokenAmount?: BigNumber.BigNumber): Promise<void> {
const orderHash = utils.getOrderHashHex(signedOrder);
const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
this.validateRemainingFillAmountNotZeroOrThrow(
@@ -26,14 +27,20 @@ export class OrderValidationUtils {
if (!_.isUndefined(expectedFillTakerTokenAmount)) {
fillTakerTokenAmount = expectedFillTakerTokenAmount;
}
- await this.validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, zrxTokenAddress,
+ const fillMakerTokenAmount = this.getFillMakerTokenAmount(signedOrder, fillTakerTokenAmount);
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker, signedOrder.taker, fillMakerTokenAmount,
+ TradeSide.Maker, TransferType.Trade,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, signedOrder.makerFee,
+ TradeSide.Maker, TransferType.Fee,
);
}
- public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
- fillTakerTokenAmount: BigNumber.BigNumber,
- takerAddress: string,
- zrxTokenAddress: string): Promise<void> {
+ public async validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber.BigNumber, takerAddress: string,
+ zrxTokenAddress: string): Promise<BigNumber.BigNumber> {
if (fillTakerTokenAmount.eq(0)) {
throw new Error(ExchangeContractErrs.OrderFillAmountZero);
}
@@ -49,29 +56,29 @@ export class OrderValidationUtils {
throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
}
this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
+ const remainingTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
+ const filledTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) ?
+ remainingTakerTokenAmount :
+ fillTakerTokenAmount;
await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
+ exchangeTradeEmulator, signedOrder, filledTakerTokenAmount, takerAddress, zrxTokenAddress,
);
const wouldRoundingErrorOccur = await this.exchangeWrapper.isRoundingErrorAsync(
- fillTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount,
+ filledTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount,
);
if (wouldRoundingErrorOccur) {
throw new Error(ExchangeContractErrs.OrderFillRoundingError);
}
+ return filledTakerTokenAmount;
}
- public async validateFillOrKillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
- fillTakerTokenAmount: BigNumber.BigNumber,
- takerAddress: string,
- zrxTokenAddress: string): Promise<void> {
- await this.validateFillOrderThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
+ public async validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber.BigNumber, takerAddress: string, zrxTokenAddress: string): Promise<void> {
+ const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
);
- // Check that fillValue available >= fillTakerAmount
- const orderHashHex = utils.getOrderHashHex(signedOrder);
- const unavailableTakerAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHashHex);
- const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount);
- if (remainingTakerAmount < fillTakerTokenAmount) {
+ if (filledTakerTokenAmount !== fillTakerTokenAmount) {
throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount);
}
}
@@ -91,87 +98,25 @@ export class OrderValidationUtils {
}
}
public async validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
- signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, senderAddress: string, zrxTokenAddress: string,
- ): Promise<void> {
- await this.validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, zrxTokenAddress,
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber.BigNumber, senderAddress: string, zrxTokenAddress: string): Promise<void> {
+ const fillMakerTokenAmount = this.getFillMakerTokenAmount(signedOrder, fillTakerTokenAmount);
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker, signedOrder.taker, fillMakerTokenAmount,
+ TradeSide.Maker, TransferType.Trade,
);
- await this.validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, senderAddress, zrxTokenAddress,
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.takerTokenAddress, signedOrder.taker, signedOrder.maker, fillTakerTokenAmount,
+ TradeSide.Taker, TransferType.Trade,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, signedOrder.makerFee, TradeSide.Maker,
+ TransferType.Fee,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, signedOrder.taker, signedOrder.feeRecipient, signedOrder.takerFee, TradeSide.Taker,
+ TransferType.Fee,
);
- }
- private async validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, zrxTokenAddress: string,
- ): Promise<void> {
- const makerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.makerTokenAddress, signedOrder.maker);
- const makerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(
- signedOrder.makerTokenAddress, signedOrder.maker);
-
- const isMakerTokenZRX = signedOrder.makerTokenAddress === zrxTokenAddress;
- // exchangeRate is the price of one maker token denominated in taker tokens
- const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
- const fillMakerAmount = fillTakerAmount.div(exchangeRate);
-
- const requiredMakerAmount = isMakerTokenZRX ? fillMakerAmount.plus(signedOrder.makerFee) : fillMakerAmount;
- if (requiredMakerAmount.greaterThan(makerBalance)) {
- throw new Error(ExchangeContractErrs.InsufficientMakerBalance);
- }
- if (requiredMakerAmount.greaterThan(makerAllowance)) {
- throw new Error(ExchangeContractErrs.InsufficientMakerAllowance);
- }
-
- if (!isMakerTokenZRX) {
- let makerZRXBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress, signedOrder.maker);
- const isTakerTokenZRX = signedOrder.takerTokenAddress === zrxTokenAddress;
- if (isTakerTokenZRX) {
- makerZRXBalance = makerZRXBalance.plus(fillTakerAmount);
- }
- const makerZRXAllowance = await this.tokenWrapper.getProxyAllowanceAsync(
- zrxTokenAddress, signedOrder.maker);
-
- if (signedOrder.makerFee.greaterThan(makerZRXBalance)) {
- throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance);
- }
- if (signedOrder.makerFee.greaterThan(makerZRXAllowance)) {
- throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance);
- }
- }
- }
- private async validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, senderAddress: string, zrxTokenAddress: string,
- ): Promise<void> {
- const takerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.takerTokenAddress, senderAddress);
- const takerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(
- signedOrder.takerTokenAddress, senderAddress);
-
- const isTakerTokenZRX = signedOrder.takerTokenAddress === zrxTokenAddress;
- // exchangeRate is the price of one maker token denominated in taker tokens
- const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
- const fillMakerAmount = fillTakerAmount.div(exchangeRate);
-
- const requiredTakerAmount = isTakerTokenZRX ? fillTakerAmount.plus(signedOrder.takerFee) : fillTakerAmount;
- if (requiredTakerAmount.greaterThan(takerBalance)) {
- throw new Error(ExchangeContractErrs.InsufficientTakerBalance);
- }
- if (requiredTakerAmount.greaterThan(takerAllowance)) {
- throw new Error(ExchangeContractErrs.InsufficientTakerAllowance);
- }
-
- if (!isTakerTokenZRX) {
- const isMakerTokenZRX = signedOrder.makerTokenAddress === zrxTokenAddress;
- let takerZRXBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress, senderAddress);
- if (isMakerTokenZRX) {
- takerZRXBalance = takerZRXBalance.plus(fillMakerAmount);
- }
- const takerZRXAllowance = await this.tokenWrapper.getProxyAllowanceAsync(zrxTokenAddress, senderAddress);
-
- if (signedOrder.takerFee.greaterThan(takerZRXBalance)) {
- throw new Error(ExchangeContractErrs.InsufficientTakerFeeBalance);
- }
- if (signedOrder.takerFee.greaterThan(takerZRXAllowance)) {
- throw new Error(ExchangeContractErrs.InsufficientTakerFeeAllowance);
- }
- }
}
private validateRemainingFillAmountNotZeroOrThrow(
takerTokenAmount: BigNumber.BigNumber, unavailableTakerTokenAmount: BigNumber.BigNumber,
@@ -186,4 +131,10 @@ export class OrderValidationUtils {
throw new Error(ExchangeContractErrs.OrderFillExpired);
}
}
+ private getFillMakerTokenAmount(signedOrder: Order,
+ fillTakerTokenAmount: BigNumber.BigNumber): BigNumber.BigNumber {
+ const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
+ const fillMakerTokenAmount = fillTakerTokenAmount.div(exchangeRate);
+ return fillMakerTokenAmount;
+ }
}