diff options
author | Leonid Logvinov <logvinov.leon@gmail.com> | 2017-10-09 18:07:48 +0800 |
---|---|---|
committer | Leonid Logvinov <logvinov.leon@gmail.com> | 2017-10-09 18:07:48 +0800 |
commit | 1424a7302a1d1b3a9c36b3a6ce5a344f8045400d (patch) | |
tree | 5266fdd4b0f167fc1fa5131fc422f2196a4bf2ff /src/utils | |
parent | 54ac354809c493cedfec5cd19b4707341b2c9f93 (diff) | |
download | dexon-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.ts | 2 | ||||
-rw-r--r-- | src/utils/exchange_transfer_simulator.ts | 136 | ||||
-rw-r--r-- | src/utils/order_validation_utils.ts | 153 |
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; + } } |