aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorAmir Bandeali <abandeali1@gmail.com>2018-04-27 01:59:03 +0800
committerGitHub <noreply@github.com>2018-04-27 01:59:03 +0800
commit35ca7d1826c265768fb50fa1304a3c30e29e55b0 (patch)
treed80e8b1cbf8a0d69452f17e857491df6b644450b /packages
parente1cab78c09eaa7f8bb67bdbe5a630f398b970897 (diff)
parent9ddec32260981e714c332651ae292221e06ebc1e (diff)
downloaddexon-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')
-rw-r--r--packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol5
-rw-r--r--packages/contracts/src/contracts/current/protocol/Exchange/LibOrder.sol3
-rw-r--r--packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol60
-rw-r--r--packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol92
-rw-r--r--packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol62
-rw-r--r--packages/contracts/src/contracts/current/protocol/Exchange/mixins/MTransactions.sol46
-rw-r--r--packages/contracts/src/utils/exchange_wrapper.ts16
-rw-r--r--packages/contracts/src/utils/order_factory.ts1
-rw-r--r--packages/contracts/src/utils/order_utils.ts3
-rw-r--r--packages/contracts/src/utils/transaction_factory.ts35
-rw-r--r--packages/contracts/src/utils/types.ts9
-rw-r--r--packages/contracts/test/exchange/transactions.ts205
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);
+ });
+ });
+ });
+});