diff options
author | Amir Bandeali <abandeali1@gmail.com> | 2018-04-27 01:59:03 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-04-27 01:59:03 +0800 |
commit | 35ca7d1826c265768fb50fa1304a3c30e29e55b0 (patch) | |
tree | d80e8b1cbf8a0d69452f17e857491df6b644450b /packages | |
parent | e1cab78c09eaa7f8bb67bdbe5a630f398b970897 (diff) | |
parent | 9ddec32260981e714c332651ae292221e06ebc1e (diff) | |
download | dexon-sol-tools-35ca7d1826c265768fb50fa1304a3c30e29e55b0.tar dexon-sol-tools-35ca7d1826c265768fb50fa1304a3c30e29e55b0.tar.gz dexon-sol-tools-35ca7d1826c265768fb50fa1304a3c30e29e55b0.tar.bz2 dexon-sol-tools-35ca7d1826c265768fb50fa1304a3c30e29e55b0.tar.lz dexon-sol-tools-35ca7d1826c265768fb50fa1304a3c30e29e55b0.tar.xz dexon-sol-tools-35ca7d1826c265768fb50fa1304a3c30e29e55b0.tar.zst dexon-sol-tools-35ca7d1826c265768fb50fa1304a3c30e29e55b0.zip |
Merge pull request #498 from 0xProject/feature/contracts/takerAbstraction
Sender abstraction
Diffstat (limited to 'packages')
12 files changed, 489 insertions, 48 deletions
diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol b/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol index 5ccb06b88..caf48bfca 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol @@ -24,13 +24,15 @@ import "./MixinSignatureValidator.sol"; import "./MixinSettlement.sol"; import "./MixinWrapperFunctions.sol"; import "./MixinAssetProxyDispatcher.sol"; +import "./MixinTransactions.sol"; contract Exchange is MixinExchangeCore, MixinSignatureValidator, MixinSettlement, MixinWrapperFunctions, - MixinAssetProxyDispatcher + MixinAssetProxyDispatcher, + MixinTransactions { string constant public VERSION = "2.0.1-alpha"; @@ -42,5 +44,6 @@ contract Exchange is MixinSettlement(_zrxProxyData) MixinWrapperFunctions() MixinAssetProxyDispatcher() + MixinTransactions() {} } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol b/packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol index 61a4a382d..a98d7cbeb 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol @@ -26,6 +26,7 @@ contract LibOrder { "address makerAddress", "address takerAddress", "address feeRecipientAddress", + "address senderAddress", "uint256 makerAssetAmount", "uint256 takerAssetAmount", "uint256 makerFee", @@ -40,6 +41,7 @@ contract LibOrder { address makerAddress; address takerAddress; address feeRecipientAddress; + address senderAddress; uint256 makerAssetAmount; uint256 takerAssetAmount; uint256 makerFee; @@ -66,6 +68,7 @@ contract LibOrder { order.makerAddress, order.takerAddress, order.feeRecipientAddress, + order.senderAddress, order.makerAssetAmount, order.takerAssetAmount, order.makerFee, diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol index 710315515..80120bc74 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol @@ -22,6 +22,7 @@ pragma experimental ABIEncoderV2; import "./mixins/MExchangeCore.sol"; import "./mixins/MSettlement.sol"; import "./mixins/MSignatureValidator.sol"; +import "./mixins/MTransactions.sol"; import "./LibOrder.sol"; import "./LibErrors.sol"; import "./LibPartialAmount.sol"; @@ -35,6 +36,7 @@ contract MixinExchangeCore is MExchangeCore, MSettlement, MSignatureValidator, + MTransactions, SafeMath, LibErrors, LibPartialAmount @@ -113,10 +115,16 @@ contract MixinExchangeCore is require(order.takerAssetAmount > 0); require(isValidSignature(orderHash, order.makerAddress, signature)); } + + // Validate sender is allowed to fill this order + if (order.senderAddress != address(0)) { + require(order.senderAddress == msg.sender); + } - // Validate taker + // Validate taker is allowed to fill this order + address takerAddress = getCurrentContextAddress(); if (order.takerAddress != address(0)) { - require(order.takerAddress == msg.sender); + require(order.takerAddress == takerAddress); } require(takerAssetFillAmount > 0); @@ -146,21 +154,10 @@ contract MixinExchangeCore is // Settle order (fillResults.makerAssetFilledAmount, fillResults.makerFeePaid, fillResults.takerFeePaid) = - settleOrder(order, msg.sender, fillResults.takerAssetFilledAmount); + settleOrder(order, takerAddress, fillResults.takerAssetFilledAmount); // Log order - emit Fill( - order.makerAddress, - msg.sender, - order.feeRecipientAddress, - fillResults.makerAssetFilledAmount, - fillResults.takerAssetFilledAmount, - fillResults.makerFeePaid, - fillResults.takerFeePaid, - orderHash, - order.makerAssetData, - order.takerAssetData - ); + emitFillEvent(order, takerAddress, orderHash, fillResults); return fillResults; } @@ -178,8 +175,16 @@ contract MixinExchangeCore is // Validate the order require(order.makerAssetAmount > 0); require(order.takerAssetAmount > 0); - require(order.makerAddress == msg.sender); + // Validate sender is allowed to cancel this order + if (order.senderAddress != address(0)) { + require(order.senderAddress == msg.sender); + } + + // Validate transaction signed by maker + address makerAddress = getCurrentContextAddress(); + require(order.makerAddress == makerAddress); + if (block.timestamp >= order.expirationTimeSeconds) { emit ExchangeError(uint8(Errors.ORDER_EXPIRED), orderHash); return false; @@ -233,4 +238,27 @@ contract MixinExchangeCore is isError = errPercentageTimes1000000 > 1000; return isError; } + + /// @dev Logs a Fill event with the given arguments. + /// The sole purpose of this function is to get around the stack variable limit. + function emitFillEvent( + Order memory order, + address takerAddress, + bytes32 orderHash, + FillResults memory fillResults) + internal + { + emit Fill( + order.makerAddress, + takerAddress, + order.feeRecipientAddress, + fillResults.makerAssetFilledAmount, + fillResults.takerAssetFilledAmount, + fillResults.makerFeePaid, + fillResults.takerFeePaid, + orderHash, + order.makerAssetData, + order.takerAssetData + ); + } } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol new file mode 100644 index 000000000..9edb1694f --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol @@ -0,0 +1,92 @@ +/* + + 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.4.21; +pragma experimental ABIEncoderV2; + +import "./mixins/MSignatureValidator.sol"; +import "./mixins/MTransactions.sol"; + +contract MixinTransactions is + MSignatureValidator, + MTransactions +{ + + // Mapping of transaction hash => executed + // This prevents transactions from being executed more than once. + mapping (bytes32 => bool) public transactions; + + // Address of current transaction signer + address public currentContextAddress; + + /// @dev Executes an exchange method call in the context of signer. + /// @param salt Arbitrary number to ensure uniqueness of transaction hash. + /// @param signer Address of transaction signer. + /// @param data AbiV2 encoded calldata. + /// @param signature Proof of signer transaction by signer. + function executeTransaction( + uint256 salt, + address signer, + bytes data, + bytes signature) + external + { + // Prevent reentrancy + require(currentContextAddress == address(0)); + + // Calculate transaction hash + bytes32 transactionHash = keccak256( + address(this), + salt, + data + ); + + // Validate transaction has not been executed + require(!transactions[transactionHash]); + + // TODO: is SignatureType.Caller necessary if we make this check? + if (signer != msg.sender) { + // Validate signature + require(isValidSignature(transactionHash, signer, signature)); + + // Set the current transaction signer + currentContextAddress = signer; + } + + // Execute transaction + transactions[transactionHash] = true; + require(address(this).delegatecall(data)); + + // Reset current transaction signer + // TODO: Check if gas is paid when currentContextAddress is already 0. + currentContextAddress = address(0); + } + + /// @dev The current function will be called in the context of this address (either 0x transaction signer or `msg.sender`). + /// If calling a fill function, this address will represent the taker. + /// If calling a cancel function, this address will represent the maker. + /// @return Signer of 0x transaction if entry point is `executeTransaction`. + /// `msg.sender` if entry point is any other function. + function getCurrentContextAddress() + internal + view + returns (address) + { + address contextAddress = currentContextAddress == address(0) ? msg.sender : currentContextAddress; + return contextAddress; + } +} diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol index 2c0fd0751..4d6ba17dd 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol @@ -75,24 +75,25 @@ contract MixinWrapperFunctions is // | | 0x00 | | 1. offset to order (*) | // | | 0x20 | | 2. takerAssetFillAmount | // | | 0x40 | | 3. offset to signature (*) | - // | Data | | 11 * 32 | order: | - // | | 0x000 | | 1. makerAddress | - // | | 0x020 | | 2. takerAddress | - // | | 0x040 | | 3. feeRecipientAddress | - // | | 0x060 | | 4. makerAssetAmount | - // | | 0x080 | | 5. takerAssetAmount | - // | | 0x0A0 | | 6. makerFeeAmount | - // | | 0x0C0 | | 7. takerFeeAmount | - // | | 0x0E0 | | 8. expirationTimeSeconds | - // | | 0x100 | | 9. salt | - // | | 0x120 | | 10. Offset to makerAssetProxyMetadata (*) | - // | | 0x140 | | 11. Offset to takerAssetProxyMetadata (*) | - // | | 0x160 | 32 | makerAssetProxyMetadata Length | - // | | 0x180 | ** | makerAssetProxyMetadata Contents | - // | | 0x1A0 | 32 | takerAssetProxyMetadata Length | - // | | 0x1C0 | ** | takerAssetProxyMetadata Contents | - // | | 0x1E0 | 32 | signature Length | - // | | 0x200 | ** | signature Contents | + // | Data | | 12 * 32 | order: | + // | | 0x000 | | 1. senderAddress | + // | | 0x020 | | 2. makerAddress | + // | | 0x040 | | 3. takerAddress | + // | | 0x060 | | 4. feeRecipientAddress | + // | | 0x080 | | 5. makerAssetAmount | + // | | 0x0A0 | | 6. takerAssetAmount | + // | | 0x0C0 | | 7. makerFeeAmount | + // | | 0x0E0 | | 8. takerFeeAmount | + // | | 0x100 | | 9. expirationTimeSeconds | + // | | 0x120 | | 10. salt | + // | | 0x140 | | 11. Offset to makerAssetProxyMetadata (*) | + // | | 0x160 | | 12. Offset to takerAssetProxyMetadata (*) | + // | | 0x180 | 32 | makerAssetProxyMetadata Length | + // | | 0x1A0 | ** | makerAssetProxyMetadata Contents | + // | | 0x1C0 | 32 | takerAssetProxyMetadata Length | + // | | 0x1E0 | ** | takerAssetProxyMetadata Contents | + // | | 0x200 | 32 | signature Length | + // | | 0x220 | ** | signature Contents | // * Offsets are calculated from the beginning of the current area: Header, Params, Data: // An offset stored in the Params area is calculated from the beginning of the Params section. @@ -150,19 +151,20 @@ contract MixinWrapperFunctions is mstore(dataAreaEnd, mload(sourceOffset)) // makerAddress mstore(add(dataAreaEnd, 0x20), mload(add(sourceOffset, 0x20))) // takerAddress mstore(add(dataAreaEnd, 0x40), mload(add(sourceOffset, 0x40))) // feeRecipientAddress - mstore(add(dataAreaEnd, 0x60), mload(add(sourceOffset, 0x60))) // makerAssetAmount - mstore(add(dataAreaEnd, 0x80), mload(add(sourceOffset, 0x80))) // takerAssetAmount - mstore(add(dataAreaEnd, 0xA0), mload(add(sourceOffset, 0xA0))) // makerFeeAmount - mstore(add(dataAreaEnd, 0xC0), mload(add(sourceOffset, 0xC0))) // takerFeeAmount - mstore(add(dataAreaEnd, 0xE0), mload(add(sourceOffset, 0xE0))) // expirationTimeSeconds - mstore(add(dataAreaEnd, 0x100), mload(add(sourceOffset, 0x100))) // salt - mstore(add(dataAreaEnd, 0x120), mload(add(sourceOffset, 0x120))) // Offset to makerAssetProxyMetadata - mstore(add(dataAreaEnd, 0x140), mload(add(sourceOffset, 0x140))) // Offset to takerAssetProxyMetadata - dataAreaEnd := add(dataAreaEnd, 0x160) - sourceOffset := add(sourceOffset, 0x160) + mstore(add(dataAreaEnd, 0x60), mload(add(sourceOffset, 0x60))) // senderAddress + mstore(add(dataAreaEnd, 0x80), mload(add(sourceOffset, 0x80))) // makerAssetAmount + mstore(add(dataAreaEnd, 0xA0), mload(add(sourceOffset, 0xA0))) // takerAssetAmount + mstore(add(dataAreaEnd, 0xC0), mload(add(sourceOffset, 0xC0))) // makerFeeAmount + mstore(add(dataAreaEnd, 0xE0), mload(add(sourceOffset, 0xE0))) // takerFeeAmount + mstore(add(dataAreaEnd, 0x100), mload(add(sourceOffset, 0x100))) // expirationTimeSeconds + mstore(add(dataAreaEnd, 0x120), mload(add(sourceOffset, 0x120))) // salt + mstore(add(dataAreaEnd, 0x140), mload(add(sourceOffset, 0x140))) // Offset to makerAssetProxyMetadata + mstore(add(dataAreaEnd, 0x160), mload(add(sourceOffset, 0x160))) // Offset to takerAssetProxyMetadata + dataAreaEnd := add(dataAreaEnd, 0x180) + sourceOffset := add(sourceOffset, 0x180) // Write offset to <order.makerAssetProxyMetadata> - mstore(add(dataAreaStart, mul(9, 0x20)), sub(dataAreaEnd, dataAreaStart)) + mstore(add(dataAreaStart, mul(10, 0x20)), sub(dataAreaEnd, dataAreaStart)) // Calculate length of <order.makerAssetProxyMetadata> arrayLenBytes := mload(sourceOffset) @@ -181,7 +183,7 @@ contract MixinWrapperFunctions is } // Write offset to <order.takerAssetProxyMetadata> - mstore(add(dataAreaStart, mul(10, 0x20)), sub(dataAreaEnd, dataAreaStart)) + mstore(add(dataAreaStart, mul(11, 0x20)), sub(dataAreaEnd, dataAreaStart)) // Calculate length of <order.takerAssetProxyMetadata> arrayLenBytes := mload(sourceOffset) diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MTransactions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MTransactions.sol new file mode 100644 index 000000000..10bfcb035 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MTransactions.sol @@ -0,0 +1,46 @@ +/* + + 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.4.21; +pragma experimental ABIEncoderV2; + +import "./MSignatureValidator.sol"; + +contract MTransactions is MSignatureValidator { + + /// @dev Executes an exchange method call in the context of signer. + /// @param salt Arbitrary number to ensure uniqueness of transaction hash. + /// @param signer Address of transaction signer. + /// @param data AbiV2 encoded calldata. + /// @param signature Proof of signer transaction by signer. + function executeTransaction( + uint256 salt, + address signer, + bytes data, + bytes signature) + external; + + /// @dev The current function will be called in the context of this address (either 0x transaction signer or `msg.sender`). + /// If calling a fill function, this address will represent the taker. + /// If calling a cancel function, this address will represent the maker. + /// @return Signer of 0x transaction if entry point is `executeTransaction`. + /// `msg.sender` if entry point is any other function. + function getCurrentContextAddress() + internal + view + returns (address); +} diff --git a/packages/contracts/src/utils/exchange_wrapper.ts b/packages/contracts/src/utils/exchange_wrapper.ts index 7d06c321b..000905854 100644 --- a/packages/contracts/src/utils/exchange_wrapper.ts +++ b/packages/contracts/src/utils/exchange_wrapper.ts @@ -9,7 +9,7 @@ import { constants } from './constants'; import { formatters } from './formatters'; import { LogDecoder } from './log_decoder'; import { orderUtils } from './order_utils'; -import { AssetProxyId, SignedOrder } from './types'; +import { AssetProxyId, SignedOrder, SignedTransaction } from './types'; export class ExchangeWrapper { private _exchange: ExchangeContract; @@ -207,6 +207,20 @@ export class ExchangeWrapper { const tx = await this._getTxWithDecodedExchangeLogsAsync(txHash); return tx; } + public async executeTransactionAsync( + signedTx: SignedTransaction, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const txHash = await this._exchange.executeTransaction.sendTransactionAsync( + signedTx.salt, + signedTx.signer, + signedTx.data, + signedTx.signature, + { from }, + ); + const tx = await this._getTxWithDecodedExchangeLogsAsync(txHash); + return tx; + } public async getOrderHashAsync(signedOrder: SignedOrder): Promise<string> { const order = orderUtils.getOrderStruct(signedOrder); const orderHash = await this._exchange.getOrderHash.callAsync(order); diff --git a/packages/contracts/src/utils/order_factory.ts b/packages/contracts/src/utils/order_factory.ts index 08853054d..044e9b865 100644 --- a/packages/contracts/src/utils/order_factory.ts +++ b/packages/contracts/src/utils/order_factory.ts @@ -19,6 +19,7 @@ export class OrderFactory { ): SignedOrder { const randomExpiration = new BigNumber(Math.floor((Date.now() + Math.random() * 100000000000) / 1000)); const order = ({ + senderAddress: ZeroEx.NULL_ADDRESS, expirationTimeSeconds: randomExpiration, salt: ZeroEx.generatePseudoRandomSalt(), takerAddress: ZeroEx.NULL_ADDRESS, diff --git a/packages/contracts/src/utils/order_utils.ts b/packages/contracts/src/utils/order_utils.ts index 52eb44941..10bbf4f7c 100644 --- a/packages/contracts/src/utils/order_utils.ts +++ b/packages/contracts/src/utils/order_utils.ts @@ -24,6 +24,7 @@ export const orderUtils = { }, getOrderStruct(signedOrder: SignedOrder): OrderStruct { const orderStruct = { + senderAddress: signedOrder.senderAddress, makerAddress: signedOrder.makerAddress, takerAddress: signedOrder.takerAddress, feeRecipientAddress: signedOrder.feeRecipientAddress, @@ -44,6 +45,7 @@ export const orderUtils = { 'address makerAddress', 'address takerAddress', 'address feeRecipientAddress', + 'address senderAddress', 'uint256 makerAssetAmount', 'uint256 takerAssetAmount', 'uint256 makerFee', @@ -58,6 +60,7 @@ export const orderUtils = { order.makerAddress, order.takerAddress, order.feeRecipientAddress, + order.senderAddress, order.makerAssetAmount, order.takerAssetAmount, order.makerFee, diff --git a/packages/contracts/src/utils/transaction_factory.ts b/packages/contracts/src/utils/transaction_factory.ts new file mode 100644 index 000000000..3a4f48330 --- /dev/null +++ b/packages/contracts/src/utils/transaction_factory.ts @@ -0,0 +1,35 @@ +import { ZeroEx } from '0x.js'; +import { BigNumber } from '@0xproject/utils'; +import * as ethUtil from 'ethereumjs-util'; + +import { crypto } from './crypto'; +import { signingUtils } from './signing_utils'; +import { SignatureType, SignedTransaction } from './types'; + +export class TransactionFactory { + private _signer: string; + private _exchangeAddress: string; + private _privateKey: Buffer; + constructor(privateKey: Buffer, exchangeAddress: string) { + this._privateKey = privateKey; + this._exchangeAddress = exchangeAddress; + const signerBuff = ethUtil.privateToAddress(this._privateKey); + this._signer = `0x${signerBuff.toString('hex')}`; + } + public newSignedTransaction( + data: string, + signatureType: SignatureType = SignatureType.Ecrecover, + ): SignedTransaction { + const salt = ZeroEx.generatePseudoRandomSalt(); + const txHash = crypto.solSHA3([this._exchangeAddress, salt, ethUtil.toBuffer(data)]); + const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType); + const signedTx = { + exchangeAddress: this._exchangeAddress, + salt, + signer: this._signer, + data, + signature: `0x${signature.toString('hex')}`, + }; + return signedTx; + } +} diff --git a/packages/contracts/src/utils/types.ts b/packages/contracts/src/utils/types.ts index a23b6b876..934dc52fa 100644 --- a/packages/contracts/src/utils/types.ts +++ b/packages/contracts/src/utils/types.ts @@ -122,6 +122,7 @@ export interface SignedOrder extends UnsignedOrder { } export interface OrderStruct { + senderAddress: string; makerAddress: string; takerAddress: string; feeRecipientAddress: string; @@ -148,3 +149,11 @@ export enum SignatureType { Trezor, Contract, } + +export interface SignedTransaction { + exchangeAddress: string; + salt: BigNumber; + signer: string; + data: string; + signature: string; +} diff --git a/packages/contracts/test/exchange/transactions.ts b/packages/contracts/test/exchange/transactions.ts new file mode 100644 index 000000000..2bc078153 --- /dev/null +++ b/packages/contracts/test/exchange/transactions.ts @@ -0,0 +1,205 @@ +import { ZeroEx } from '0x.js'; + +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import * as ethUtil from 'ethereumjs-util'; +import * as Web3 from 'web3'; + +import { DummyERC20TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c20_token'; +import { ERC20ProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_proxy'; +import { ExchangeContract } from '../../src/contract_wrappers/generated/exchange'; +import { assetProxyUtils } from '../../src/utils/asset_proxy_utils'; +import { constants } from '../../src/utils/constants'; +import { ERC20Wrapper } from '../../src/utils/erc20_wrapper'; +import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; +import { OrderFactory } from '../../src/utils/order_factory'; +import { orderUtils } from '../../src/utils/order_utils'; +import { TransactionFactory } from '../../src/utils/transaction_factory'; +import { + AssetProxyId, + ContractName, + ERC20BalancesByOwner, + ExchangeContractErrs, + OrderStruct, + SignatureType, + SignedOrder, + SignedTransaction, +} from '../../src/utils/types'; +import { chaiSetup } from '../utils/chai_setup'; +import { deployer } from '../utils/deployer'; +import { provider, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('Exchange transactions', () => { + let senderAddress: string; + let owner: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipientAddress: string; + + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let exchange: ExchangeContract; + let erc20Proxy: ERC20ProxyContract; + + let erc20Balances: ERC20BalancesByOwner; + let signedOrder: SignedOrder; + let signedTx: SignedTransaction; + let order: OrderStruct; + let orderFactory: OrderFactory; + let makerTransactionFactory: TransactionFactory; + let takerTransactionFactory: TransactionFactory; + let exchangeWrapper: ExchangeWrapper; + let erc20Wrapper: ERC20Wrapper; + + let defaultMakerTokenAddress: string; + let defaultTakerTokenAddress: string; + + let zeroEx: ZeroEx; + + before(async () => { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([owner, senderAddress, makerAddress, takerAddress, feeRecipientAddress] = accounts); + + erc20Wrapper = new ERC20Wrapper(deployer, provider, usedAddresses, owner); + + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + const exchangeInstance = await deployer.deployAsync(ContractName.Exchange, [ + assetProxyUtils.encodeERC20ProxyData(zrxToken.address), + ]); + exchange = new ExchangeContract(exchangeInstance.abi, exchangeInstance.address, provider); + zeroEx = new ZeroEx(provider, { + exchangeContractAddress: exchange.address, + networkId: constants.TESTRPC_NETWORK_ID, + }); + exchangeWrapper = new ExchangeWrapper(exchange, zeroEx); + await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC20, erc20Proxy.address, owner); + + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { from: owner }); + + defaultMakerTokenAddress = erc20TokenA.address; + defaultTakerTokenAddress = erc20TokenB.address; + + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + senderAddress, + exchangeAddress: exchange.address, + makerAddress, + feeRecipientAddress, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultMakerTokenAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultTakerTokenAddress), + }; + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddress)]; + const takerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(takerAddress)]; + orderFactory = new OrderFactory(makerPrivateKey, defaultOrderParams); + makerTransactionFactory = new TransactionFactory(makerPrivateKey, exchange.address); + takerTransactionFactory = new TransactionFactory(takerPrivateKey, exchange.address); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + + describe('executeTransaction', () => { + describe('fillOrder', () => { + let takerAssetFillAmount: BigNumber; + beforeEach(async () => { + erc20Balances = await erc20Wrapper.getBalancesAsync(); + signedOrder = orderFactory.newSignedOrder(); + order = orderUtils.getOrderStruct(signedOrder); + + takerAssetFillAmount = signedOrder.takerAssetAmount.div(2); + const data = exchange.fillOrder.getABIEncodedTransactionData( + order, + takerAssetFillAmount, + signedOrder.signature, + ); + signedTx = takerTransactionFactory.newSignedTransaction(data); + }); + + it('should throw if not called by specified sender', async () => { + return expect(exchangeWrapper.executeTransactionAsync(signedTx, takerAddress)).to.be.rejectedWith( + constants.REVERT, + ); + }); + + it('should transfer the correct amounts when signed by taker and called by sender', async () => { + await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const makerAssetFillAmount = takerAssetFillAmount + .times(signedOrder.makerAssetAmount) + .dividedToIntegerBy(signedOrder.takerAssetAmount); + const makerFeePaid = signedOrder.makerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + const takerFeePaid = signedOrder.takerFee + .times(makerAssetFillAmount) + .dividedToIntegerBy(signedOrder.makerAssetAmount); + expect(newBalances[makerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerTokenAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultTakerTokenAddress].add(takerAssetFillAmount), + ); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerFeePaid), + ); + expect(newBalances[takerAddress][defaultTakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultTakerTokenAddress].minus(takerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerTokenAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerTokenAddress].add(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].minus(takerFeePaid), + ); + expect(newBalances[feeRecipientAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[feeRecipientAddress][zrxToken.address].add(makerFeePaid.add(takerFeePaid)), + ); + }); + + it('should throw if the a 0x transaction with the same transactionHash has already been executed', async () => { + await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); + return expect(exchangeWrapper.executeTransactionAsync(signedTx, senderAddress)).to.be.rejectedWith( + constants.REVERT, + ); + }); + + it('should reset the currentContextAddress', async () => { + await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); + const currentContextAddress = await exchange.currentContextAddress.callAsync(); + expect(currentContextAddress).to.equal(ZeroEx.NULL_ADDRESS); + }); + }); + + describe('cancelOrder', () => { + beforeEach(async () => { + const data = exchange.cancelOrder.getABIEncodedTransactionData(order); + signedTx = makerTransactionFactory.newSignedTransaction(data); + }); + + it('should throw if not called by specified sender', async () => { + return expect(exchangeWrapper.executeTransactionAsync(signedTx, makerAddress)).to.be.rejectedWith( + constants.REVERT, + ); + }); + + it('should cancel the order when signed by maker and called by sender', async () => { + await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); + const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + const newBalances = await erc20Wrapper.getBalancesAsync(); + expect(newBalances).to.deep.equal(erc20Balances); + }); + }); + }); +}); |