Merge pull request #2610 from 0xProject/feat/zero-ex/meta-transactions

EP: MetaTransactions
This commit is contained in:
Lawrence Forman 2020-07-10 11:12:58 -04:00 committed by GitHub
commit 18bc701e8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2781 additions and 141 deletions

View File

@ -9,6 +9,10 @@
{
"note": "Export `AffiliateFeeTransformerContract`",
"pr": 2622
},
{
"note": "Add `MetaTransactions` and `SignatureValidator` features",
"pr": 2610
}
]
},

View File

@ -34,11 +34,12 @@ contract ZeroEx {
/// @dev Construct this contract and register the `Bootstrap` feature.
/// After constructing this contract, `bootstrap()` should be called
/// to seed the initial feature set.
constructor() public {
/// by `bootstrap()` to seed the initial feature set.
/// @param bootstrapper Who can call `bootstrap()`.
constructor(address bootstrapper) public {
// Temporarily create and register the bootstrap feature.
// It will deregister itself after `bootstrap()` has been called.
Bootstrap bootstrap = new Bootstrap(msg.sender);
Bootstrap bootstrap = new Bootstrap(bootstrapper);
LibProxyStorage.getStorage().impls[bootstrap.bootstrap.selector] =
address(bootstrap);
}

View File

@ -0,0 +1,174 @@
/*
Copyright 2020 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.6.5;
library LibMetaTransactionsRichErrors {
// solhint-disable func-name-mixedcase
function InvalidMetaTransactionsArrayLengthsError(
uint256 mtxCount,
uint256 signatureCount
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("InvalidMetaTransactionsArrayLengthsError(uint256,uint256)")),
mtxCount,
signatureCount
);
}
function MetaTransactionUnsupportedFunctionError(
bytes32 mtxHash,
bytes4 selector
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("MetaTransactionUnsupportedFunctionError(bytes32,bytes4)")),
mtxHash,
selector
);
}
function MetaTransactionWrongSenderError(
bytes32 mtxHash,
address sender,
address expectedSender
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("MetaTransactionWrongSenderError(bytes32,address,address)")),
mtxHash,
sender,
expectedSender
);
}
function MetaTransactionExpiredError(
bytes32 mtxHash,
uint256 time,
uint256 expirationTime
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("MetaTransactionExpiredError(bytes32,uint256,uint256)")),
mtxHash,
time,
expirationTime
);
}
function MetaTransactionGasPriceError(
bytes32 mtxHash,
uint256 gasPrice,
uint256 minGasPrice,
uint256 maxGasPrice
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("MetaTransactionGasPriceError(bytes32,uint256,uint256,uint256)")),
mtxHash,
gasPrice,
minGasPrice,
maxGasPrice
);
}
function MetaTransactionInsufficientEthError(
bytes32 mtxHash,
uint256 ethBalance,
uint256 ethRequired
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("MetaTransactionInsufficientEthError(bytes32,uint256,uint256)")),
mtxHash,
ethBalance,
ethRequired
);
}
function MetaTransactionInvalidSignatureError(
bytes32 mtxHash,
bytes memory signature,
bytes memory errData
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("MetaTransactionInvalidSignatureError(bytes32,bytes,bytes)")),
mtxHash,
signature,
errData
);
}
function MetaTransactionAlreadyExecutedError(
bytes32 mtxHash,
uint256 executedBlockNumber
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("MetaTransactionAlreadyExecutedError(bytes32,uint256)")),
mtxHash,
executedBlockNumber
);
}
function MetaTransactionCallFailedError(
bytes32 mtxHash,
bytes memory callData,
bytes memory returnData
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("MetaTransactionCallFailedError(bytes32,bytes,bytes)")),
mtxHash,
callData,
returnData
);
}
}

View File

@ -0,0 +1,52 @@
/*
Copyright 2020 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.6.5;
library LibSignatureRichErrors {
enum SignatureValidationErrorCodes {
ALWAYS_INVALID,
INVALID_LENGTH,
UNSUPPORTED,
ILLEGAL,
WRONG_SIGNER
}
// solhint-disable func-name-mixedcase
function SignatureValidationError(
SignatureValidationErrorCodes code,
bytes32 hash,
address signerAddress,
bytes memory signature
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("SignatureValidationError(uint8,bytes32,address,bytes)")),
code,
hash,
signerAddress,
signature
);
}
}

View File

@ -0,0 +1,127 @@
/*
Copyright 2020 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.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
/// @dev Meta-transactions feature.
interface IMetaTransactions {
/// @dev Describes an exchange proxy meta transaction.
struct MetaTransactionData {
// Signer of meta-transaction. On whose behalf to execute the MTX.
address signer;
// Required sender, or NULL for anyone.
address sender;
// Minimum gas price.
uint256 minGasPrice;
// Maximum gas price.
uint256 maxGasPrice;
// MTX is invalid after this time.
uint256 expirationTimeSeconds;
// Nonce to make this MTX unique.
uint256 salt;
// Encoded call data to a function on the exchange proxy.
bytes callData;
// Amount of ETH to attach to the call.
uint256 value;
// ERC20 fee `signer` pays `sender`.
IERC20TokenV06 feeToken;
// ERC20 fee amount.
uint256 feeAmount;
}
/// @dev Emitted whenever a meta-transaction is executed via
/// `executeMetaTransaction()` or `executeMetaTransactions()`.
/// @param hash The meta-transaction hash.
/// @param selector The selector of the function being executed.
/// @param signer Who to execute the meta-transaction on behalf of.
/// @param sender Who executed the meta-transaction.
event MetaTransactionExecuted(
bytes32 hash,
bytes4 indexed selector,
address signer,
address sender
);
/// @dev Execute a single meta-transaction.
/// @param mtx The meta-transaction.
/// @param signature The signature by `mtx.signer`.
/// @return returnResult The ABI-encoded result of the underlying call.
function executeMetaTransaction(
MetaTransactionData calldata mtx,
bytes calldata signature
)
external
payable
returns (bytes memory returnResult);
/// @dev Execute multiple meta-transactions.
/// @param mtxs The meta-transactions.
/// @param signatures The signature by each respective `mtx.signer`.
/// @return returnResults The ABI-encoded results of the underlying calls.
function batchExecuteMetaTransactions(
MetaTransactionData[] calldata mtxs,
bytes[] calldata signatures
)
external
payable
returns (bytes[] memory returnResults);
/// @dev Execute a meta-transaction via `sender`. Privileged variant.
/// Only callable from within.
/// @param sender Who is executing the meta-transaction..
/// @param mtx The meta-transaction.
/// @param signature The signature by `mtx.signer`.
/// @return returnResult The ABI-encoded result of the underlying call.
function _executeMetaTransaction(
address sender,
MetaTransactionData calldata mtx,
bytes calldata signature
)
external
payable
returns (bytes memory returnResult);
/// @dev Get the block at which a meta-transaction has been executed.
/// @param mtx The meta-transaction.
/// @return blockNumber The block height when the meta-transactioin was executed.
function getMetaTransactionExecutedBlock(MetaTransactionData calldata mtx)
external
view
returns (uint256 blockNumber);
/// @dev Get the block at which a meta-transaction hash has been executed.
/// @param mtxHash The meta-transaction hash.
/// @return blockNumber The block height when the meta-transactioin was executed.
function getMetaTransactionHashExecutedBlock(bytes32 mtxHash)
external
view
returns (uint256 blockNumber);
/// @dev Get the EIP712 hash of a meta-transaction.
/// @param mtx The meta-transaction.
/// @return mtxHash The EIP712 hash of `mtx`.
function getMetaTransactionHash(MetaTransactionData calldata mtx)
external
view
returns (bytes32 mtxHash);
}

View File

@ -0,0 +1,63 @@
/*
Copyright 2020 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.6.5;
pragma experimental ABIEncoderV2;
/// @dev Feature for validating signatures.
interface ISignatureValidator {
/// @dev Allowed signature types.
enum SignatureType {
Illegal, // 0x00, default value
Invalid, // 0x01
EIP712, // 0x02
EthSign, // 0x03
NSignatureTypes // 0x04, number of signature types. Always leave at end.
}
/// @dev Validate that `hash` was signed by `signer` given `signature`.
/// Reverts otherwise.
/// @param hash The hash that was signed.
/// @param signer The signer of the hash.
/// @param signature The signature. The last byte of this signature should
/// be a member of the `SignatureType` enum.
function validateHashSignature(
bytes32 hash,
address signer,
bytes calldata signature
)
external
view;
/// @dev Check that `hash` was signed by `signer` given `signature`.
/// @param hash The hash that was signed.
/// @param signer The signer of the hash.
/// @param signature The signature. The last byte of this signature should
/// be a member of the `SignatureType` enum.
/// @return isValid `true` on success.
function isValidHashSignature(
bytes32 hash,
address signer,
bytes calldata signature
)
external
view
returns (bool isValid);
}

View File

@ -0,0 +1,436 @@
/*
Copyright 2020 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.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol";
import "../errors/LibMetaTransactionsRichErrors.sol";
import "../fixins/FixinCommon.sol";
import "../fixins/FixinEIP712.sol";
import "../migrations/LibMigrate.sol";
import "../storage/LibMetaTransactionsStorage.sol";
import "./IMetaTransactions.sol";
import "./ITransformERC20.sol";
import "./ISignatureValidator.sol";
import "./ITokenSpender.sol";
import "./IFeature.sol";
/// @dev MetaTransactions feature.
contract MetaTransactions is
IFeature,
IMetaTransactions,
FixinCommon,
FixinEIP712
{
using LibBytesV06 for bytes;
using LibRichErrorsV06 for bytes;
/// @dev Intermediate state vars to avoid stack overflows.
struct ExecuteState {
address sender;
bytes32 hash;
MetaTransactionData mtx;
bytes signature;
bytes4 selector;
uint256 selfBalance;
uint256 executedBlockNumber;
}
struct TransformERC20Args {
IERC20TokenV06 inputToken;
IERC20TokenV06 outputToken;
uint256 inputTokenAmount;
uint256 minOutputTokenAmount;
ITransformERC20.Transformation[] transformations;
}
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "MetaTransactions";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev EIP712 typehash of the `MetaTransactionData` struct.
bytes32 public immutable MTX_EIP712_TYPEHASH = keccak256(
"MetaTransactionData("
"address signer,"
"address sender,"
"uint256 minGasPrice,"
"uint256 maxGasPrice,"
"uint256 expirationTimeSeconds,"
"uint256 salt,"
"bytes callData,"
"uint256 value,"
"address feeToken,"
"uint256 feeAmount"
")"
);
constructor(address zeroExAddress)
public
FixinCommon()
FixinEIP712(zeroExAddress)
{
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
/// @return success `LibMigrate.SUCCESS` on success.
function migrate()
external
returns (bytes4 success)
{
_registerFeatureFunction(this.executeMetaTransaction.selector);
_registerFeatureFunction(this.batchExecuteMetaTransactions.selector);
_registerFeatureFunction(this._executeMetaTransaction.selector);
_registerFeatureFunction(this.getMetaTransactionExecutedBlock.selector);
_registerFeatureFunction(this.getMetaTransactionHashExecutedBlock.selector);
_registerFeatureFunction(this.getMetaTransactionHash.selector);
return LibMigrate.MIGRATE_SUCCESS;
}
/// @dev Execute a single meta-transaction.
/// @param mtx The meta-transaction.
/// @param signature The signature by `mtx.signer`.
/// @return returnResult The ABI-encoded result of the underlying call.
function executeMetaTransaction(
MetaTransactionData memory mtx,
bytes memory signature
)
public
payable
override
returns (bytes memory returnResult)
{
return _executeMetaTransactionPrivate(
msg.sender,
mtx,
signature
);
}
/// @dev Execute multiple meta-transactions.
/// @param mtxs The meta-transactions.
/// @param signatures The signature by each respective `mtx.signer`.
/// @return returnResults The ABI-encoded results of the underlying calls.
function batchExecuteMetaTransactions(
MetaTransactionData[] memory mtxs,
bytes[] memory signatures
)
public
payable
override
returns (bytes[] memory returnResults)
{
if (mtxs.length != signatures.length) {
LibMetaTransactionsRichErrors.InvalidMetaTransactionsArrayLengthsError(
mtxs.length,
signatures.length
).rrevert();
}
returnResults = new bytes[](mtxs.length);
for (uint256 i = 0; i < mtxs.length; ++i) {
returnResults[i] = _executeMetaTransactionPrivate(
msg.sender,
mtxs[i],
signatures[i]
);
}
}
/// @dev Execute a meta-transaction via `sender`. Privileged variant.
/// Only callable from within.
/// @param sender Who is executing the meta-transaction..
/// @param mtx The meta-transaction.
/// @param signature The signature by `mtx.signer`.
/// @return returnResult The ABI-encoded result of the underlying call.
function _executeMetaTransaction(
address sender,
MetaTransactionData memory mtx,
bytes memory signature
)
public
payable
override
onlySelf
returns (bytes memory returnResult)
{
return _executeMetaTransactionPrivate(sender, mtx, signature);
}
/// @dev Get the block at which a meta-transaction has been executed.
/// @param mtx The meta-transaction.
/// @return blockNumber The block height when the meta-transactioin was executed.
function getMetaTransactionExecutedBlock(MetaTransactionData memory mtx)
public
override
view
returns (uint256 blockNumber)
{
return getMetaTransactionHashExecutedBlock(getMetaTransactionHash(mtx));
}
/// @dev Get the block at which a meta-transaction hash has been executed.
/// @param mtxHash The meta-transaction hash.
/// @return blockNumber The block height when the meta-transactioin was executed.
function getMetaTransactionHashExecutedBlock(bytes32 mtxHash)
public
override
view
returns (uint256 blockNumber)
{
return LibMetaTransactionsStorage.getStorage().mtxHashToExecutedBlockNumber[mtxHash];
}
/// @dev Get the EIP712 hash of a meta-transaction.
/// @param mtx The meta-transaction.
/// @return mtxHash The EIP712 hash of `mtx`.
function getMetaTransactionHash(MetaTransactionData memory mtx)
public
override
view
returns (bytes32 mtxHash)
{
return _getEIP712Hash(keccak256(abi.encode(
MTX_EIP712_TYPEHASH,
mtx.signer,
mtx.sender,
mtx.minGasPrice,
mtx.maxGasPrice,
mtx.expirationTimeSeconds,
mtx.salt,
keccak256(mtx.callData),
mtx.value,
mtx.feeToken,
mtx.feeAmount
)));
}
/// @dev Execute a meta-transaction by `sender`. Low-level, hidden variant.
/// @param sender Who is executing the meta-transaction..
/// @param mtx The meta-transaction.
/// @param signature The signature by `mtx.signer`.
/// @return returnResult The ABI-encoded result of the underlying call.
function _executeMetaTransactionPrivate(
address sender,
MetaTransactionData memory mtx,
bytes memory signature
)
private
returns (bytes memory returnResult)
{
ExecuteState memory state;
state.sender = sender;
state.hash = getMetaTransactionHash(mtx);
state.mtx = mtx;
state.signature = signature;
_validateMetaTransaction(state);
// Mark the transaction executed.
assert(block.number > 0);
LibMetaTransactionsStorage.getStorage()
.mtxHashToExecutedBlockNumber[state.hash] = block.number;
// Execute the call based on the selector.
state.selector = mtx.callData.readBytes4(0);
if (state.selector == ITransformERC20.transformERC20.selector) {
returnResult = _executeTransformERC20Call(state);
} else {
LibMetaTransactionsRichErrors
.MetaTransactionUnsupportedFunctionError(state.hash, state.selector)
.rrevert();
}
// Pay the fee to the sender.
if (mtx.feeAmount > 0) {
ITokenSpender(address(this))._spendERC20Tokens(
mtx.feeToken,
mtx.signer, // From the signer.
sender, // To the sender.
mtx.feeAmount
);
}
emit MetaTransactionExecuted(
state.hash,
state.selector,
mtx.signer,
mtx.sender
);
}
/// @dev Validate that a meta-transaction is executable.
function _validateMetaTransaction(ExecuteState memory state)
private
view
{
// Must be from the required sender, if set.
if (state.mtx.sender != address(0) && state.mtx.sender != state.sender) {
LibMetaTransactionsRichErrors
.MetaTransactionWrongSenderError(
state.hash,
state.sender,
state.mtx.sender
).rrevert();
}
// Must not be expired.
if (state.mtx.expirationTimeSeconds <= block.timestamp) {
LibMetaTransactionsRichErrors
.MetaTransactionExpiredError(
state.hash,
block.timestamp,
state.mtx.expirationTimeSeconds
).rrevert();
}
// Must have a valid gas price.
if (state.mtx.minGasPrice > tx.gasprice || state.mtx.maxGasPrice < tx.gasprice) {
LibMetaTransactionsRichErrors
.MetaTransactionGasPriceError(
state.hash,
tx.gasprice,
state.mtx.minGasPrice,
state.mtx.maxGasPrice
).rrevert();
}
// Must have enough ETH.
state.selfBalance = address(this).balance;
if (state.mtx.value > state.selfBalance) {
LibMetaTransactionsRichErrors
.MetaTransactionInsufficientEthError(
state.hash,
state.selfBalance,
state.mtx.value
).rrevert();
}
// Must be signed by signer.
try
ISignatureValidator(address(this))
.validateHashSignature(state.hash, state.mtx.signer, state.signature)
{}
catch (bytes memory err) {
LibMetaTransactionsRichErrors
.MetaTransactionInvalidSignatureError(
state.hash,
state.signature,
err
).rrevert();
}
// Transaction must not have been already executed.
state.executedBlockNumber = LibMetaTransactionsStorage
.getStorage().mtxHashToExecutedBlockNumber[state.hash];
if (state.executedBlockNumber != 0) {
LibMetaTransactionsRichErrors
.MetaTransactionAlreadyExecutedError(
state.hash,
state.executedBlockNumber
).rrevert();
}
}
/// @dev Execute a `ITransformERC20.transformERC20()` meta-transaction call
/// by decoding the call args and translating the call to the internal
/// `ITransformERC20._transformERC20()` variant, where we can override
/// the taker address.
function _executeTransformERC20Call(ExecuteState memory state)
private
returns (bytes memory returnResult)
{
// HACK(dorothy-zbornak): `abi.decode()` with the individual args
// will cause a stack overflow. But we can prefix the call data with an
// offset to transform it into the encoding for the equivalent single struct arg,
// since decoding a single struct arg consumes far less stack space than
// decoding multiple struct args.
// Where the encoding for multiple args (with the seleector ommitted)
// would typically look like:
// | argument | offset |
// |--------------------------|---------|
// | inputToken | 0 |
// | outputToken | 32 |
// | inputTokenAmount | 64 |
// | minOutputTokenAmount | 96 |
// | transformations (offset) | 128 | = 32
// | transformations (data) | 160 |
// We will ABI-decode a single struct arg copy with the layout:
// | argument | offset |
// |--------------------------|---------|
// | (arg 1 offset) | 0 | = 32
// | inputToken | 32 |
// | outputToken | 64 |
// | inputTokenAmount | 96 |
// | minOutputTokenAmount | 128 |
// | transformations (offset) | 160 | = 32
// | transformations (data) | 192 |
TransformERC20Args memory args;
{
bytes memory encodedStructArgs = new bytes(state.mtx.callData.length - 4 + 32);
// Copy the args data from the original, after the new struct offset prefix.
bytes memory fromCallData = state.mtx.callData;
assert(fromCallData.length >= 4);
uint256 fromMem;
uint256 toMem;
assembly {
// Prefix the original calldata with a struct offset,
// which is just one word over.
mstore(add(encodedStructArgs, 32), 32)
// Copy everything after the selector.
fromMem := add(fromCallData, 36)
// Start copying after the struct offset.
toMem := add(encodedStructArgs, 64)
}
LibBytesV06.memCopy(toMem, fromMem, fromCallData.length - 4);
// Decode call args for `ITransformERC20.transformERC20()` as a struct.
args = abi.decode(encodedStructArgs, (TransformERC20Args));
}
// Call `ITransformERC20._transformERC20()` (internal variant).
return _callSelf(
state.hash,
abi.encodeWithSelector(
ITransformERC20._transformERC20.selector,
keccak256(state.mtx.callData),
state.mtx.signer, // taker is mtx signer
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations
),
state.mtx.value
);
}
/// @dev Make an arbitrary internal, meta-transaction call.
/// Warning: Do not let unadulerated `callData` into this function.
function _callSelf(bytes32 hash, bytes memory callData, uint256 value)
private
returns (bytes memory returnResult)
{
bool success;
(success, returnResult) = address(this).call{value: value}(callData);
if (!success) {
LibMetaTransactionsRichErrors.MetaTransactionCallFailedError(
hash,
callData,
returnResult
).rrevert();
}
}
}

View File

@ -37,19 +37,15 @@ contract Ownable is
FixinCommon
{
// solhint-disable
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "Ownable";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev The deployed address of this contract.
address immutable private _implementation;
// solhint-enable
using LibRichErrorsV06 for bytes;
constructor() public {
_implementation = address(this);
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initializes this feature. The intial owner will be set to this (ZeroEx)

View File

@ -0,0 +1,260 @@
/*
Copyright 2020 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.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol";
import "../errors/LibSignatureRichErrors.sol";
import "../fixins/FixinCommon.sol";
import "../migrations/LibMigrate.sol";
import "./ISignatureValidator.sol";
import "./IFeature.sol";
/// @dev Feature for validating signatures.
contract SignatureValidator is
IFeature,
ISignatureValidator,
FixinCommon
{
using LibBytesV06 for bytes;
using LibRichErrorsV06 for bytes;
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "SignatureValidator";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
/// @return success `LibMigrate.SUCCESS` on success.
function migrate()
external
returns (bytes4 success)
{
_registerFeatureFunction(this.validateHashSignature.selector);
_registerFeatureFunction(this.isValidHashSignature.selector);
return LibMigrate.MIGRATE_SUCCESS;
}
/// @dev Validate that `hash` was signed by `signer` given `signature`.
/// Reverts otherwise.
/// @param hash The hash that was signed.
/// @param signer The signer of the hash.
/// @param signature The signature. The last byte of this signature should
/// be a member of the `SignatureType` enum.
function validateHashSignature(
bytes32 hash,
address signer,
bytes memory signature
)
public
override
view
{
SignatureType signatureType = _readValidSignatureType(
hash,
signer,
signature
);
// TODO: When we support non-hash signature types, assert that
// `signatureType` is only `EIP712` or `EthSign` here.
_validateHashSignatureTypes(
signatureType,
hash,
signer,
signature
);
}
/// @dev Check that `hash` was signed by `signer` given `signature`.
/// @param hash The hash that was signed.
/// @param signer The signer of the hash.
/// @param signature The signature. The last byte of this signature should
/// be a member of the `SignatureType` enum.
/// @return isValid `true` on success.
function isValidHashSignature(
bytes32 hash,
address signer,
bytes calldata signature
)
external
view
override
returns (bool isValid)
{
try this.validateHashSignature(hash, signer, signature) {
isValid = true;
} catch (bytes memory) {
isValid = false;
}
}
/// @dev Validates a hash-only signature type. Low-level, hidden variant.
/// @param signatureType The type of signature to check.
/// @param hash The hash that was signed.
/// @param signer The signer of the hash.
/// @param signature The signature. The last byte of this signature should
/// be a member of the `SignatureType` enum.
function _validateHashSignatureTypes(
SignatureType signatureType,
bytes32 hash,
address signer,
bytes memory signature
)
private
pure
{
address recovered = address(0);
if (signatureType == SignatureType.Invalid) {
// Always invalid signature.
// Like Illegal, this is always implicitly available and therefore
// offered explicitly. It can be implicitly created by providing
// a correctly formatted but incorrect signature.
LibSignatureRichErrors.SignatureValidationError(
LibSignatureRichErrors.SignatureValidationErrorCodes.ALWAYS_INVALID,
hash,
signer,
signature
).rrevert();
} else if (signatureType == SignatureType.EIP712) {
// Signature using EIP712
if (signature.length != 66) {
LibSignatureRichErrors.SignatureValidationError(
LibSignatureRichErrors.SignatureValidationErrorCodes.INVALID_LENGTH,
hash,
signer,
signature
).rrevert();
}
uint8 v = uint8(signature[0]);
bytes32 r = signature.readBytes32(1);
bytes32 s = signature.readBytes32(33);
recovered = ecrecover(
hash,
v,
r,
s
);
} else if (signatureType == SignatureType.EthSign) {
// Signed using `eth_sign`
if (signature.length != 66) {
LibSignatureRichErrors.SignatureValidationError(
LibSignatureRichErrors.SignatureValidationErrorCodes.INVALID_LENGTH,
hash,
signer,
signature
).rrevert();
}
uint8 v = uint8(signature[0]);
bytes32 r = signature.readBytes32(1);
bytes32 s = signature.readBytes32(33);
recovered = ecrecover(
keccak256(abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
hash
)),
v,
r,
s
);
} else {
// This should never happen.
revert('SignatureValidator/ILLEGAL_CODE_PATH');
}
if (recovered == address(0) || signer != recovered) {
LibSignatureRichErrors.SignatureValidationError(
LibSignatureRichErrors.SignatureValidationErrorCodes.WRONG_SIGNER,
hash,
signer,
signature
).rrevert();
}
}
/// @dev Reads the `SignatureType` from the end of a signature and validates it.
function _readValidSignatureType(
bytes32 hash,
address signer,
bytes memory signature
)
private
pure
returns (SignatureType signatureType)
{
// Read the signatureType from the signature
signatureType = _readSignatureType(
hash,
signer,
signature
);
// Ensure signature is supported
if (uint8(signatureType) >= uint8(SignatureType.NSignatureTypes)) {
LibSignatureRichErrors.SignatureValidationError(
LibSignatureRichErrors.SignatureValidationErrorCodes.UNSUPPORTED,
hash,
signer,
signature
).rrevert();
}
// Always illegal signature.
// This is always an implicit option since a signer can create a
// signature array with invalid type or length. We may as well make
// it an explicit option. This aids testing and analysis. It is
// also the initialization value for the enum type.
if (signatureType == SignatureType.Illegal) {
LibSignatureRichErrors.SignatureValidationError(
LibSignatureRichErrors.SignatureValidationErrorCodes.ILLEGAL,
hash,
signer,
signature
).rrevert();
}
}
/// @dev Reads the `SignatureType` from the end of a signature.
function _readSignatureType(
bytes32 hash,
address signer,
bytes memory signature
)
private
pure
returns (SignatureType sigType)
{
if (signature.length == 0) {
LibSignatureRichErrors.SignatureValidationError(
LibSignatureRichErrors.SignatureValidationErrorCodes.INVALID_LENGTH,
hash,
signer,
signature
).rrevert();
}
return SignatureType(uint8(signature[signature.length - 1]));
}
}

View File

@ -35,19 +35,15 @@ contract SimpleFunctionRegistry is
ISimpleFunctionRegistry,
FixinCommon
{
// solhint-disable
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "SimpleFunctionRegistry";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev The deployed address of this contract.
address private immutable _implementation;
// solhint-enable
using LibRichErrorsV06 for bytes;
constructor() public {
_implementation = address(this);
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initializes this feature, registering its own functions.

View File

@ -44,14 +44,12 @@ contract TokenSpender is
string public constant override FEATURE_NAME = "TokenSpender";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev The implementation address of this feature.
address private immutable _implementation;
// solhint-enable
using LibRichErrorsV06 for bytes;
constructor() public {
_implementation = address(this);
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initialize and register this feature. Should be delegatecalled
@ -59,14 +57,14 @@ contract TokenSpender is
/// @param allowanceTarget An `allowanceTarget` instance, configured to have
/// the ZeroeEx contract as an authority.
/// @return success `MIGRATE_SUCCESS` on success.
function migrate(IAllowanceTarget allowanceTarget) external returns (bytes4 success) {
function migrate(IAllowanceTarget allowanceTarget)
external
returns (bytes4 success)
{
LibTokenSpenderStorage.getStorage().allowanceTarget = allowanceTarget;
ISimpleFunctionRegistry(address(this))
.extend(this.getAllowanceTarget.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this._spendERC20Tokens.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.getSpendableERC20BalanceOf.selector, _implementation);
_registerFeatureFunction(this.getAllowanceTarget.selector);
_registerFeatureFunction(this._spendERC20Tokens.selector);
_registerFeatureFunction(this.getSpendableERC20BalanceOf.selector);
return LibMigrate.MIGRATE_SUCCESS;
}

View File

@ -52,39 +52,32 @@ contract TransformERC20 is
uint256 takerOutputTokenBalanceAfter;
}
// solhint-disable
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "TransformERC20";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 0);
/// @dev The implementation address of this feature.
address private immutable _implementation;
// solhint-enable
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
constructor() public {
_implementation = address(this);
constructor() public FixinCommon() {
// solhint-disable-next-line no-empty-blocks
}
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
/// @param transformerDeployer The trusted deployer for transformers.
/// @return success `LibMigrate.SUCCESS` on success.
function migrate(address transformerDeployer) external returns (bytes4 success) {
ISimpleFunctionRegistry(address(this))
.extend(this.getTransformerDeployer.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.createTransformWallet.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.getTransformWallet.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.setTransformerDeployer.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.transformERC20.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this._transformERC20.selector, _implementation);
function migrate(address transformerDeployer)
external
returns (bytes4 success)
{
_registerFeatureFunction(this.getTransformerDeployer.selector);
_registerFeatureFunction(this.createTransformWallet.selector);
_registerFeatureFunction(this.getTransformWallet.selector);
_registerFeatureFunction(this.setTransformerDeployer.selector);
_registerFeatureFunction(this.transformERC20.selector);
_registerFeatureFunction(this._transformERC20.selector);
this.createTransformWallet();
LibTransformERC20Storage.getStorage().transformerDeployer = transformerDeployer;
return LibMigrate.MIGRATE_SUCCESS;
@ -191,6 +184,7 @@ contract TransformERC20 is
Transformation[] memory transformations
)
public
virtual
override
payable
onlySelf

View File

@ -23,13 +23,17 @@ import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "../errors/LibCommonRichErrors.sol";
import "../errors/LibOwnableRichErrors.sol";
import "../features/IOwnable.sol";
import "../features/ISimpleFunctionRegistry.sol";
/// @dev Common feature utilities.
contract FixinCommon {
abstract contract FixinCommon {
using LibRichErrorsV06 for bytes;
/// @dev The implementation address of this feature.
address internal immutable _implementation;
/// @dev The caller must be this contract.
modifier onlySelf() virtual {
if (msg.sender != address(this)) {
@ -52,6 +56,21 @@ contract FixinCommon {
_;
}
constructor() internal {
// Remember this feature's original address.
_implementation = address(this);
}
/// @dev Registers a function implemented by this feature at `_implementation`.
/// Can and should only be called within a `migrate()`.
/// @param selector The selector of the function whose implementation
/// is at `_implementation`.
function _registerFeatureFunction(bytes4 selector)
internal
{
ISimpleFunctionRegistry(address(this)).extend(selector, _implementation);
}
/// @dev Encode a feature version as a `uint256`.
/// @param major The major version number of the feature.
/// @param minor The minor version number of the feature.

View File

@ -0,0 +1,69 @@
/*
Copyright 2020 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.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "../errors/LibCommonRichErrors.sol";
import "../errors/LibOwnableRichErrors.sol";
import "../features/IOwnable.sol";
/// @dev EIP712 helpers for features.
abstract contract FixinEIP712 {
/// @dev The domain hash separator for the entire exchange proxy.
bytes32 public immutable EIP712_DOMAIN_SEPARATOR;
constructor(address zeroExAddress) internal {
// Compute `EIP712_DOMAIN_SEPARATOR`
{
uint256 chainId;
assembly { chainId := chainid() }
EIP712_DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256(
"EIP712Domain("
"string name,"
"string version,"
"uint256 chainId,"
"address verifyingContract"
")"
),
keccak256("ZeroEx"),
keccak256("1.0.0"),
chainId,
zeroExAddress
)
);
}
}
function _getEIP712Hash(bytes32 structHash)
internal
view
returns (bytes32 eip712Hash)
{
return keccak256(abi.encodePacked(
hex"1901",
EIP712_DOMAIN_SEPARATOR,
structHash
));
}
}

View File

@ -23,6 +23,8 @@ import "../ZeroEx.sol";
import "../features/IOwnable.sol";
import "../features/TokenSpender.sol";
import "../features/TransformERC20.sol";
import "../features/SignatureValidator.sol";
import "../features/MetaTransactions.sol";
import "../external/AllowanceTarget.sol";
import "./InitialMigration.sol";
@ -38,6 +40,8 @@ contract FullMigration {
Ownable ownable;
TokenSpender tokenSpender;
TransformERC20 transformERC20;
SignatureValidator signatureValidator;
MetaTransactions metaTransactions;
}
/// @dev Parameters needed to initialize features.
@ -45,42 +49,56 @@ contract FullMigration {
address transformerDeployer;
}
/// @dev The allowed caller of `deploy()`.
address public immutable deployer;
/// @dev The allowed caller of `initializeZeroEx()`.
address public immutable initializeCaller;
/// @dev The initial migration contract.
InitialMigration private _initialMigration;
/// @dev Instantiate this contract and set the allowed caller of `deploy()`
/// to `deployer`.
/// @param deployer_ The allowed caller of `deploy()`.
constructor(address payable deployer_)
/// @dev Instantiate this contract and set the allowed caller of `initializeZeroEx()`
/// to `initializeCaller`.
/// @param initializeCaller_ The allowed caller of `initializeZeroEx()`.
constructor(address payable initializeCaller_)
public
{
deployer = deployer_;
initializeCaller = initializeCaller_;
// Create an initial migration contract with this contract set to the
// allowed deployer.
// allowed `initializeCaller`.
_initialMigration = new InitialMigration(address(this));
}
/// @dev Deploy the `ZeroEx` contract with the full feature set,
/// @dev Retrieve the bootstrapper address to use when constructing `ZeroEx`.
/// @return bootstrapper The bootstrapper address.
function getBootstrapper()
external
view
returns (address bootstrapper)
{
return address(_initialMigration);
}
/// @dev Initialize the `ZeroEx` contract with the full feature set,
/// transfer ownership to `owner`, then self-destruct.
/// @param owner The owner of the contract.
/// @param zeroEx The instance of the ZeroEx contract. ZeroEx should
/// been constructed with this contract as the bootstrapper.
/// @param features Features to add to the proxy.
/// @return zeroEx The deployed and configured `ZeroEx` contract.
/// @return _zeroEx The configured ZeroEx contract. Same as the `zeroEx` parameter.
/// @param migrateOpts Parameters needed to initialize features.
function deploy(
function initializeZeroEx(
address payable owner,
ZeroEx zeroEx,
Features memory features,
MigrateOpts memory migrateOpts
)
public
returns (ZeroEx zeroEx)
returns (ZeroEx _zeroEx)
{
require(msg.sender == deployer, "FullMigration/INVALID_SENDER");
require(msg.sender == initializeCaller, "FullMigration/INVALID_SENDER");
// Perform the initial migration with the owner set to this contract.
zeroEx = _initialMigration.deploy(
_initialMigration.initializeZeroEx(
address(uint160(address(this))),
zeroEx,
InitialMigration.BootstrapFeatures({
registry: features.registry,
ownable: features.ownable
@ -95,9 +113,11 @@ contract FullMigration {
// Self-destruct.
this.die(owner);
return zeroEx;
}
/// @dev Destroy this contract. Only callable from ourselves (from `deploy()`).
/// @dev Destroy this contract. Only callable from ourselves (from `initializeZeroEx()`).
/// @param ethRecipient Receiver of any ETH in this contract.
function die(address payable ethRecipient)
external
@ -153,5 +173,27 @@ contract FullMigration {
address(this)
);
}
// SignatureValidator
{
// Register the feature.
ownable.migrate(
address(features.signatureValidator),
abi.encodeWithSelector(
SignatureValidator.migrate.selector
),
address(this)
);
}
// MetaTransactions
{
// Register the feature.
ownable.migrate(
address(features.metaTransactions),
abi.encodeWithSelector(
MetaTransactions.migrate.selector
),
address(this)
);
}
}
}

View File

@ -35,36 +35,39 @@ contract InitialMigration {
Ownable ownable;
}
/// @dev The allowed caller of `deploy()`. In production, this would be
/// @dev The allowed caller of `initializeZeroEx()`. In production, this would be
/// the governor.
address public immutable deployer;
address public immutable initializeCaller;
/// @dev The real address of this contract.
address private immutable _implementation;
/// @dev Instantiate this contract and set the allowed caller of `deploy()`
/// to `deployer_`.
/// @param deployer_ The allowed caller of `deploy()`.
constructor(address deployer_) public {
deployer = deployer_;
/// @dev Instantiate this contract and set the allowed caller of `initializeZeroEx()`
/// to `initializeCaller_`.
/// @param initializeCaller_ The allowed caller of `initializeZeroEx()`.
constructor(address initializeCaller_) public {
initializeCaller = initializeCaller_;
_implementation = address(this);
}
/// @dev Deploy the `ZeroEx` contract with the minimum feature set,
/// @dev Initialize the `ZeroEx` contract with the minimum feature set,
/// transfers ownership to `owner`, then self-destructs.
/// Only callable by `deployer` set in the contstructor.
/// Only callable by `initializeCaller` set in the contstructor.
/// @param owner The owner of the contract.
/// @param zeroEx The instance of the ZeroEx contract. ZeroEx should
/// been constructed with this contract as the bootstrapper.
/// @param features Features to bootstrap into the proxy.
/// @return zeroEx The deployed and configured `ZeroEx` contract.
function deploy(address payable owner, BootstrapFeatures memory features)
/// @return _zeroEx The configured ZeroEx contract. Same as the `zeroEx` parameter.
function initializeZeroEx(
address payable owner,
ZeroEx zeroEx,
BootstrapFeatures memory features
)
public
virtual
returns (ZeroEx zeroEx)
returns (ZeroEx _zeroEx)
{
// Must be called by the allowed deployer.
require(msg.sender == deployer, "InitialMigration/INVALID_SENDER");
// Deploy the ZeroEx contract, setting ourselves as the bootstrapper.
zeroEx = new ZeroEx();
// Must be called by the allowed initializeCaller.
require(msg.sender == initializeCaller, "InitialMigration/INVALID_SENDER");
// Bootstrap the initial feature set.
IBootstrap(address(zeroEx)).bootstrap(
@ -75,6 +78,8 @@ contract InitialMigration {
// Self-destruct. This contract should not hold any funds but we send
// them to the owner just in case.
this.die(owner);
return zeroEx;
}
/// @dev Sets up the initial state of the `ZeroEx` contract.

View File

@ -0,0 +1,44 @@
/*
Copyright 2020 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.6.5;
pragma experimental ABIEncoderV2;
import "./LibStorage.sol";
/// @dev Storage helpers for the `MetaTransactions` feature.
library LibMetaTransactionsStorage {
/// @dev Storage bucket for this feature.
struct Storage {
// The block number when a hash was executed.
mapping (bytes32 => uint256) mtxHashToExecutedBlockNumber;
}
/// @dev Get the storage bucket for this contract.
function getStorage() internal pure returns (Storage storage stor) {
uint256 storageSlot = LibStorage.getStorageSlot(
LibStorage.StorageId.MetaTransactions
);
// Dip into assembly to change the slot pointed to by the local
// variable `stor`.
// See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries
assembly { stor_slot := storageSlot }
}
}

View File

@ -34,7 +34,8 @@ library LibStorage {
SimpleFunctionRegistry,
Ownable,
TokenSpender,
TransformERC20
TransformERC20,
MetaTransactions
}
/// @dev Get the storage slot given a storage ID. We assign unique, well-spaced

View File

@ -23,7 +23,7 @@ import "./LibStorage.sol";
import "../external/IFlashWallet.sol";
/// @dev Storage helpers for the `TokenSpender` feature.
/// @dev Storage helpers for the `TransformERC20` feature.
library LibTransformERC20Storage {
/// @dev Storage bucket for this feature.

View File

@ -0,0 +1,71 @@
/*
Copyright 2020 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.6.5;
pragma experimental ABIEncoderV2;
import "../src/features/TransformERC20.sol";
contract TestMetaTransactionsTransformERC20Feature is
TransformERC20
{
event TransformERC20Called(
address sender,
uint256 value,
bytes32 callDataHash,
address taker,
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
uint256 inputTokenAmount,
uint256 minOutputTokenAmount,
Transformation[] transformations
);
function _transformERC20(
bytes32 callDataHash,
address payable taker,
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
uint256 inputTokenAmount,
uint256 minOutputTokenAmount,
Transformation[] memory transformations
)
public
override
payable
returns (uint256 outputTokenAmount)
{
if (msg.value == 666) {
revert('FAIL');
}
emit TransformERC20Called(
msg.sender,
msg.value,
callDataHash,
taker,
inputToken,
outputToken,
inputTokenAmount,
minOutputTokenAmount,
transformations
);
return 1337;
}
}

View File

@ -39,9 +39,9 @@
"publish:private": "yarn build && gitpkg publish"
},
"config": {
"publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer",
"publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer,SignatureValidator,MetaTransactions",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FixinGasToken|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json"
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinGasToken|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IMetaTransactions|IOwnable|ISignatureValidator|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|MetaTransactions|Ownable|PayTakerTransformer|SignatureValidator|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json"
},
"repository": {
"type": "git",

View File

@ -16,8 +16,10 @@ import * as IOwnable from '../generated-artifacts/IOwnable.json';
import * as ISimpleFunctionRegistry from '../generated-artifacts/ISimpleFunctionRegistry.json';
import * as ITokenSpender from '../generated-artifacts/ITokenSpender.json';
import * as ITransformERC20 from '../generated-artifacts/ITransformERC20.json';
import * as MetaTransactions from '../generated-artifacts/MetaTransactions.json';
import * as Ownable from '../generated-artifacts/Ownable.json';
import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json';
import * as SignatureValidator from '../generated-artifacts/SignatureValidator.json';
import * as SimpleFunctionRegistry from '../generated-artifacts/SimpleFunctionRegistry.json';
import * as TokenSpender from '../generated-artifacts/TokenSpender.json';
import * as TransformERC20 from '../generated-artifacts/TransformERC20.json';
@ -42,4 +44,6 @@ export const artifacts = {
TransformERC20: TransformERC20 as ContractArtifact,
TokenSpender: TokenSpender as ContractArtifact,
AffiliateFeeTransformer: AffiliateFeeTransformer as ContractArtifact,
SignatureValidator: SignatureValidator as ContractArtifact,
MetaTransactions: MetaTransactions as ContractArtifact,
};

View File

@ -1,4 +1,3 @@
import { BaseContract } from '@0x/base-contract';
import { SupportedProvider } from '@0x/subproviders';
import { TxData } from 'ethereum-types';
import * as _ from 'lodash';
@ -7,7 +6,9 @@ import { artifacts } from './artifacts';
import {
FullMigrationContract,
InitialMigrationContract,
MetaTransactionsContract,
OwnableContract,
SignatureValidatorContract,
SimpleFunctionRegistryContract,
TokenSpenderContract,
TransformERC20Contract,
@ -16,11 +17,17 @@ import {
// tslint:disable: completed-docs
/**
* Addresses of minimum features for a deployment of the Exchange Proxy.
*/
export interface BootstrapFeatures {
registry: SimpleFunctionRegistryContract;
ownable: OwnableContract;
registry: string;
ownable: string;
}
/**
* Deploy the minimum features of the Exchange Proxy.
*/
export async function deployBootstrapFeaturesAsync(
provider: SupportedProvider,
txDefaults: Partial<TxData>,
@ -34,20 +41,23 @@ export async function deployBootstrapFeaturesAsync(
provider,
txDefaults,
artifacts,
)),
)).address,
ownable:
features.ownable ||
(await OwnableContract.deployFrom0xArtifactAsync(artifacts.Ownable, provider, txDefaults, artifacts)),
(await OwnableContract.deployFrom0xArtifactAsync(artifacts.Ownable, provider, txDefaults, artifacts))
.address,
};
}
/**
* Migrate an instance of the Exchange proxy with minimum viable features.
*/
export async function initialMigrateAsync(
owner: string,
provider: SupportedProvider,
txDefaults: Partial<TxData>,
features: Partial<BootstrapFeatures> = {},
): Promise<ZeroExContract> {
const _features = await deployBootstrapFeaturesAsync(provider, txDefaults, features);
const migrator = await InitialMigrationContract.deployFrom0xArtifactAsync(
artifacts.InitialMigration,
provider,
@ -55,24 +65,42 @@ export async function initialMigrateAsync(
artifacts,
txDefaults.from as string,
);
const deployCall = migrator.deploy(owner, toFeatureAdddresses(_features));
const zeroEx = new ZeroExContract(await deployCall.callAsync(), provider, {});
await deployCall.awaitTransactionSuccessAsync();
const zeroEx = await ZeroExContract.deployFrom0xArtifactAsync(
artifacts.ZeroEx,
provider,
txDefaults,
artifacts,
migrator.address,
);
const _features = await deployBootstrapFeaturesAsync(provider, txDefaults, features);
await migrator.initializeZeroEx(owner, zeroEx.address, _features).awaitTransactionSuccessAsync();
return zeroEx;
}
/**
* Addresses of features for a full deployment of the Exchange Proxy.
*/
export interface FullFeatures extends BootstrapFeatures {
tokenSpender: TokenSpenderContract;
transformERC20: TransformERC20Contract;
tokenSpender: string;
transformERC20: string;
signatureValidator: string;
metaTransactions: string;
}
/**
* Extra configuration options for a full migration of the Exchange Proxy.
*/
export interface FullMigrationOpts {
transformerDeployer: string;
}
/**
* Deploy all the features for a full Exchange Proxy.
*/
export async function deployFullFeaturesAsync(
provider: SupportedProvider,
txDefaults: Partial<TxData>,
zeroExAddress: string,
features: Partial<FullFeatures> = {},
): Promise<FullFeatures> {
return {
@ -84,7 +112,7 @@ export async function deployFullFeaturesAsync(
provider,
txDefaults,
artifacts,
)),
)).address,
transformERC20:
features.transformERC20 ||
(await TransformERC20Contract.deployFrom0xArtifactAsync(
@ -92,10 +120,30 @@ export async function deployFullFeaturesAsync(
provider,
txDefaults,
artifacts,
)),
)).address,
signatureValidator:
features.signatureValidator ||
(await SignatureValidatorContract.deployFrom0xArtifactAsync(
artifacts.SignatureValidator,
provider,
txDefaults,
artifacts,
)).address,
metaTransactions:
features.metaTransactions ||
(await MetaTransactionsContract.deployFrom0xArtifactAsync(
artifacts.MetaTransactions,
provider,
txDefaults,
artifacts,
zeroExAddress,
)).address,
};
}
/**
* Deploy a fully featured instance of the Exchange Proxy.
*/
export async function fullMigrateAsync(
owner: string,
provider: SupportedProvider,
@ -103,7 +151,6 @@ export async function fullMigrateAsync(
features: Partial<FullFeatures> = {},
opts: Partial<FullMigrationOpts> = {},
): Promise<ZeroExContract> {
const _features = await deployFullFeaturesAsync(provider, txDefaults, features);
const migrator = await FullMigrationContract.deployFrom0xArtifactAsync(
artifacts.FullMigration,
provider,
@ -111,20 +158,18 @@ export async function fullMigrateAsync(
artifacts,
txDefaults.from as string,
);
const zeroEx = await ZeroExContract.deployFrom0xArtifactAsync(
artifacts.ZeroEx,
provider,
txDefaults,
artifacts,
await migrator.getBootstrapper().callAsync(),
);
const _features = await deployFullFeaturesAsync(provider, txDefaults, zeroEx.address, features);
const _opts = {
transformerDeployer: txDefaults.from as string,
...opts,
};
const deployCall = migrator.deploy(owner, toFeatureAdddresses(_features), _opts);
const zeroEx = new ZeroExContract(await deployCall.callAsync(), provider, {});
await deployCall.awaitTransactionSuccessAsync();
await migrator.initializeZeroEx(owner, zeroEx.address, _features, _opts).awaitTransactionSuccessAsync();
return zeroEx;
}
// tslint:disable:space-before-function-parent one-line
export function toFeatureAdddresses<T extends BootstrapFeatures | FullFeatures | (BootstrapFeatures & FullFeatures)>(
features: T,
): { [name in keyof T]: string } {
// TS can't figure this out.
return _.mapValues(features, (c: BaseContract) => c.address) as any;
}

View File

@ -14,8 +14,10 @@ export * from '../generated-wrappers/i_simple_function_registry';
export * from '../generated-wrappers/i_token_spender';
export * from '../generated-wrappers/i_transform_erc20';
export * from '../generated-wrappers/initial_migration';
export * from '../generated-wrappers/meta_transactions';
export * from '../generated-wrappers/ownable';
export * from '../generated-wrappers/pay_taker_transformer';
export * from '../generated-wrappers/signature_validator';
export * from '../generated-wrappers/simple_function_registry';
export * from '../generated-wrappers/token_spender';
export * from '../generated-wrappers/transform_erc20';

View File

@ -10,6 +10,7 @@ import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.js
import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json';
import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json';
import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json';
import * as FixinEIP712 from '../test/generated-artifacts/FixinEIP712.json';
import * as FixinGasToken from '../test/generated-artifacts/FixinGasToken.json';
import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json';
import * as FullMigration from '../test/generated-artifacts/FullMigration.json';
@ -21,8 +22,10 @@ import * as IExchange from '../test/generated-artifacts/IExchange.json';
import * as IFeature from '../test/generated-artifacts/IFeature.json';
import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json';
import * as IGasToken from '../test/generated-artifacts/IGasToken.json';
import * as IMetaTransactions from '../test/generated-artifacts/IMetaTransactions.json';
import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json';
import * as IOwnable from '../test/generated-artifacts/IOwnable.json';
import * as ISignatureValidator from '../test/generated-artifacts/ISignatureValidator.json';
import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json';
import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json';
import * as ITokenSpender from '../test/generated-artifacts/ITokenSpender.json';
@ -30,11 +33,14 @@ import * as ITransformERC20 from '../test/generated-artifacts/ITransformERC20.js
import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json';
import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json';
import * as LibERC20Transformer from '../test/generated-artifacts/LibERC20Transformer.json';
import * as LibMetaTransactionsRichErrors from '../test/generated-artifacts/LibMetaTransactionsRichErrors.json';
import * as LibMetaTransactionsStorage from '../test/generated-artifacts/LibMetaTransactionsStorage.json';
import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json';
import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRichErrors.json';
import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json';
import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json';
import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json';
import * as LibSignatureRichErrors from '../test/generated-artifacts/LibSignatureRichErrors.json';
import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json';
import * as LibSimpleFunctionRegistryStorage from '../test/generated-artifacts/LibSimpleFunctionRegistryStorage.json';
import * as LibSpenderRichErrors from '../test/generated-artifacts/LibSpenderRichErrors.json';
@ -43,8 +49,10 @@ import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpe
import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json';
import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json';
import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json';
import * as MetaTransactions from '../test/generated-artifacts/MetaTransactions.json';
import * as Ownable from '../test/generated-artifacts/Ownable.json';
import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json';
import * as SignatureValidator from '../test/generated-artifacts/SignatureValidator.json';
import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json';
import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json';
import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json';
@ -53,6 +61,7 @@ import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/T
import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json';
import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json';
import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMigration.json';
import * as TestMetaTransactionsTransformERC20Feature from '../test/generated-artifacts/TestMetaTransactionsTransformERC20Feature.json';
import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json';
import * as TestMintableERC20Token from '../test/generated-artifacts/TestMintableERC20Token.json';
import * as TestMintTokenERC20Transformer from '../test/generated-artifacts/TestMintTokenERC20Transformer.json';
@ -76,8 +85,10 @@ import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json';
export const artifacts = {
ZeroEx: ZeroEx as ContractArtifact,
LibCommonRichErrors: LibCommonRichErrors as ContractArtifact,
LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact,
LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact,
LibProxyRichErrors: LibProxyRichErrors as ContractArtifact,
LibSignatureRichErrors: LibSignatureRichErrors as ContractArtifact,
LibSimpleFunctionRegistryRichErrors: LibSimpleFunctionRegistryRichErrors as ContractArtifact,
LibSpenderRichErrors: LibSpenderRichErrors as ContractArtifact,
LibTransformERC20RichErrors: LibTransformERC20RichErrors as ContractArtifact,
@ -90,20 +101,26 @@ export const artifacts = {
Bootstrap: Bootstrap as ContractArtifact,
IBootstrap: IBootstrap as ContractArtifact,
IFeature: IFeature as ContractArtifact,
IMetaTransactions: IMetaTransactions as ContractArtifact,
IOwnable: IOwnable as ContractArtifact,
ISignatureValidator: ISignatureValidator as ContractArtifact,
ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact,
ITokenSpender: ITokenSpender as ContractArtifact,
ITransformERC20: ITransformERC20 as ContractArtifact,
MetaTransactions: MetaTransactions as ContractArtifact,
Ownable: Ownable as ContractArtifact,
SignatureValidator: SignatureValidator as ContractArtifact,
SimpleFunctionRegistry: SimpleFunctionRegistry as ContractArtifact,
TokenSpender: TokenSpender as ContractArtifact,
TransformERC20: TransformERC20 as ContractArtifact,
FixinCommon: FixinCommon as ContractArtifact,
FixinEIP712: FixinEIP712 as ContractArtifact,
FixinGasToken: FixinGasToken as ContractArtifact,
FullMigration: FullMigration as ContractArtifact,
InitialMigration: InitialMigration as ContractArtifact,
LibBootstrap: LibBootstrap as ContractArtifact,
LibMigrate: LibMigrate as ContractArtifact,
LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact,
LibOwnableStorage: LibOwnableStorage as ContractArtifact,
LibProxyStorage: LibProxyStorage as ContractArtifact,
LibSimpleFunctionRegistryStorage: LibSimpleFunctionRegistryStorage as ContractArtifact,
@ -128,6 +145,7 @@ export const artifacts = {
TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact,
TestFullMigration: TestFullMigration as ContractArtifact,
TestInitialMigration: TestInitialMigration as ContractArtifact,
TestMetaTransactionsTransformERC20Feature: TestMetaTransactionsTransformERC20Feature as ContractArtifact,
TestMigrator: TestMigrator as ContractArtifact,
TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact,
TestMintableERC20Token: TestMintableERC20Token as ContractArtifact,

View File

@ -0,0 +1,548 @@
import {
blockchainTests,
constants,
expect,
getRandomInteger,
randomAddress,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { getExchangeProxyMetaTransactionHash, signatureUtils } from '@0x/order-utils';
import { ExchangeProxyMetaTransaction } from '@0x/types';
import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
import { MetaTransactionsContract, ZeroExContract } from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { fullMigrateAsync } from '../utils/migration';
import {
ITokenSpenderContract,
TestMetaTransactionsTransformERC20FeatureContract,
TestMetaTransactionsTransformERC20FeatureEvents,
TestMintableERC20TokenContract,
} from '../wrappers';
const { NULL_ADDRESS, ZERO_AMOUNT } = constants;
blockchainTests.resets('MetaTransactions feature', env => {
let owner: string;
let sender: string;
let signers: string[];
let zeroEx: ZeroExContract;
let feature: MetaTransactionsContract;
let feeToken: TestMintableERC20TokenContract;
let transformERC20Feature: TestMetaTransactionsTransformERC20FeatureContract;
let allowanceTarget: string;
const MAX_FEE_AMOUNT = new BigNumber('1e18');
before(async () => {
[owner, sender, ...signers] = await env.getAccountAddressesAsync();
transformERC20Feature = await TestMetaTransactionsTransformERC20FeatureContract.deployFrom0xArtifactAsync(
artifacts.TestMetaTransactionsTransformERC20Feature,
env.provider,
env.txDefaults,
{},
);
zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {
transformERC20: transformERC20Feature.address,
});
feature = new MetaTransactionsContract(zeroEx.address, env.provider, { ...env.txDefaults, from: sender }, abis);
feeToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC20Token,
env.provider,
env.txDefaults,
{},
);
allowanceTarget = await new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults)
.getAllowanceTarget()
.callAsync();
// Fund signers with fee tokens.
await Promise.all(
signers.map(async signer => {
await feeToken.mint(signer, MAX_FEE_AMOUNT).awaitTransactionSuccessAsync();
await feeToken.approve(allowanceTarget, MAX_FEE_AMOUNT).awaitTransactionSuccessAsync({ from: signer });
}),
);
});
function getRandomMetaTransaction(
fields: Partial<ExchangeProxyMetaTransaction> = {},
): ExchangeProxyMetaTransaction {
return {
signer: _.sampleSize(signers)[0],
sender,
minGasPrice: getRandomInteger('2', '1e9'),
maxGasPrice: getRandomInteger('1e9', '100e9'),
expirationTimeSeconds: new BigNumber(Math.floor(_.now() / 1000) + 360),
salt: new BigNumber(hexUtils.random()),
callData: hexUtils.random(4),
value: getRandomInteger(1, '1e18'),
feeToken: feeToken.address,
feeAmount: getRandomInteger(1, MAX_FEE_AMOUNT),
domain: {
chainId: 1, // Ganache's `chainid` opcode is hardcoded as 1
verifyingContract: zeroEx.address,
},
...fields,
};
}
async function signMetaTransactionAsync(mtx: ExchangeProxyMetaTransaction, signer?: string): Promise<string> {
return signatureUtils.ecSignHashAsync(
env.provider,
getExchangeProxyMetaTransactionHash(mtx),
signer || mtx.signer,
);
}
describe('getMetaTransactionHash()', () => {
it('generates the correct hash', async () => {
const mtx = getRandomMetaTransaction();
const expected = getExchangeProxyMetaTransactionHash(mtx);
const actual = await feature.getMetaTransactionHash(mtx).callAsync();
expect(actual).to.eq(expected);
});
});
interface TransformERC20Args {
inputToken: string;
outputToken: string;
inputTokenAmount: BigNumber;
minOutputTokenAmount: BigNumber;
transformations: Array<{ deploymentNonce: BigNumber; data: string }>;
}
function getRandomTransformERC20Args(fields: Partial<TransformERC20Args> = {}): TransformERC20Args {
return {
inputToken: randomAddress(),
outputToken: randomAddress(),
inputTokenAmount: getRandomInteger(1, '1e18'),
minOutputTokenAmount: getRandomInteger(1, '1e18'),
transformations: [{ deploymentNonce: new BigNumber(123), data: hexUtils.random() }],
...fields,
};
}
const RAW_SUCCESS_RESULT = hexUtils.leftPad(1337);
describe('executeMetaTransaction()', () => {
it('can call `TransformERC20.transformERC20()`', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value,
};
const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
expect(rawResult).to.eq(RAW_SUCCESS_RESULT);
const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
verifyEventsFromLogs(
receipt.logs,
[
{
inputToken: args.inputToken,
outputToken: args.outputToken,
inputTokenAmount: args.inputTokenAmount,
minOutputTokenAmount: args.minOutputTokenAmount,
transformations: args.transformations,
sender: zeroEx.address,
value: mtx.value,
callDataHash: hexUtils.hash(mtx.callData),
taker: mtx.signer,
},
],
TestMetaTransactionsTransformERC20FeatureEvents.TransformERC20Called,
);
});
it('can call with any sender if `sender == 0`', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
sender: NULL_ADDRESS,
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value,
from: randomAddress(),
};
const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
expect(rawResult).to.eq(RAW_SUCCESS_RESULT);
});
it('works without fee', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
feeAmount: ZERO_AMOUNT,
feeToken: randomAddress(),
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value,
};
const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
expect(rawResult).to.eq(RAW_SUCCESS_RESULT);
});
it('fails if the translated call fails', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
value: new BigNumber(666),
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
const actualCallData = transformERC20Feature
._transformERC20(
hexUtils.hash(mtx.callData),
mtx.signer,
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
mtxHash,
actualCallData,
new StringRevertError('FAIL').encode(),
),
);
});
it('fails with unsupported function', async () => {
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature.createTransformWallet().getABIEncodedTransactionData(),
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionUnsupportedFunctionError(
mtxHash,
hexUtils.slice(mtx.callData, 0, 4),
),
);
});
it('cannot execute the same mtx twice', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value,
};
const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionAlreadyExecutedError(
mtxHash,
receipt.blockNumber,
),
);
});
it('fails if not enough ETH provided', async () => {
const mtx = getRandomMetaTransaction();
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value.minus(1),
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionInsufficientEthError(
mtxHash,
callOpts.value,
mtx.value,
),
);
});
it('fails if gas price too low', async () => {
const mtx = getRandomMetaTransaction();
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice.minus(1),
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionGasPriceError(
mtxHash,
callOpts.gasPrice,
mtx.minGasPrice,
mtx.maxGasPrice,
),
);
});
it('fails if gas price too high', async () => {
const mtx = getRandomMetaTransaction();
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.maxGasPrice.plus(1),
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionGasPriceError(
mtxHash,
callOpts.gasPrice,
mtx.minGasPrice,
mtx.maxGasPrice,
),
);
});
it('fails if expired', async () => {
const mtx = getRandomMetaTransaction({
expirationTimeSeconds: new BigNumber(Math.floor(_.now() / 1000 - 60)),
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.maxGasPrice,
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionExpiredError(
mtxHash,
undefined,
mtx.expirationTimeSeconds,
),
);
});
it('fails if wrong sender', async () => {
const requiredSender = randomAddress();
const mtx = getRandomMetaTransaction({
sender: requiredSender,
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.maxGasPrice,
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionWrongSenderError(
mtxHash,
sender,
requiredSender,
),
);
});
it('fails if signature is wrong', async () => {
const mtx = getRandomMetaTransaction({ signer: signers[0] });
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx, signers[1]);
const callOpts = {
gasPrice: mtx.maxGasPrice,
value: mtx.value,
};
const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionInvalidSignatureError(
mtxHash,
signature,
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner,
mtxHash,
signers[0],
signature,
).encode(),
),
);
});
});
describe('batchExecuteMetaTransactions()', () => {
it('can execute multiple transactions', async () => {
const mtxs = _.times(2, i => {
const args = getRandomTransformERC20Args();
return getRandomMetaTransaction({
signer: signers[i],
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
});
const signatures = await Promise.all(mtxs.map(async mtx => signMetaTransactionAsync(mtx)));
const callOpts = {
gasPrice: BigNumber.max(...mtxs.map(mtx => mtx.minGasPrice)),
value: BigNumber.sum(...mtxs.map(mtx => mtx.value)),
};
const rawResults = await feature.batchExecuteMetaTransactions(mtxs, signatures).callAsync(callOpts);
expect(rawResults).to.eql(mtxs.map(() => RAW_SUCCESS_RESULT));
});
it('cannot execute the same transaction twice', async () => {
const mtx = (() => {
const args = getRandomTransformERC20Args();
return getRandomMetaTransaction({
signer: _.sampleSize(signers, 1)[0],
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
})();
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const mtxs = _.times(2, () => mtx);
const signatures = await Promise.all(mtxs.map(async m => signMetaTransactionAsync(m)));
const callOpts = {
gasPrice: BigNumber.max(...mtxs.map(m => m.minGasPrice)),
value: BigNumber.sum(...mtxs.map(m => m.value)),
};
const block = await env.web3Wrapper.getBlockNumberAsync();
const tx = feature.batchExecuteMetaTransactions(mtxs, signatures).callAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionAlreadyExecutedError(mtxHash, block),
);
});
});
describe('getMetaTransactionExecutedBlock()', () => {
it('returns zero for an unexecuted mtx', async () => {
const mtx = getRandomMetaTransaction();
const block = await feature.getMetaTransactionExecutedBlock(mtx).callAsync();
expect(block).to.bignumber.eq(0);
});
it('returns the block it was executed in', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value,
};
const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
const block = await feature.getMetaTransactionExecutedBlock(mtx).callAsync();
expect(block).to.bignumber.eq(receipt.blockNumber);
});
});
describe('getMetaTransactionHashExecutedBlock()', () => {
it('returns zero for an unexecuted mtx', async () => {
const mtx = getRandomMetaTransaction();
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const block = await feature.getMetaTransactionHashExecutedBlock(mtxHash).callAsync();
expect(block).to.bignumber.eq(0);
});
it('returns the block it was executed in', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
});
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.minGasPrice,
value: mtx.value,
};
const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const block = await feature.getMetaTransactionHashExecutedBlock(mtxHash).callAsync();
expect(block).to.bignumber.eq(receipt.blockNumber);
});
});
});

View File

@ -0,0 +1,231 @@
import { blockchainTests, constants, expect, randomAddress, signingUtils } from '@0x/contracts-test-utils';
import { signatureUtils } from '@0x/order-utils';
import { SignatureType } from '@0x/types';
import { hexUtils, ZeroExRevertErrors } from '@0x/utils';
import * as ethjs from 'ethereumjs-util';
import * as _ from 'lodash';
import { SignatureValidatorContract, ZeroExContract } from '../../src/wrappers';
import { abis } from '../utils/abis';
import { fullMigrateAsync } from '../utils/migration';
const { NULL_BYTES } = constants;
blockchainTests.resets('SignatureValidator feature', env => {
let owner: string;
let signers: string[];
let zeroEx: ZeroExContract;
let feature: SignatureValidatorContract;
before(async () => {
[owner, ...signers] = await env.getAccountAddressesAsync();
zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults);
feature = new SignatureValidatorContract(zeroEx.address, env.provider, env.txDefaults, abis);
});
describe('validateHashSignature()', () => {
it('can validate an eth_sign signature', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = await signatureUtils.ecSignHashAsync(env.provider, hash, signer);
await feature.validateHashSignature(hash, signer, signature).callAsync();
});
it('rejects a wrong eth_sign signature', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = await signatureUtils.ecSignHashAsync(env.provider, hash, signer);
const notSigner = randomAddress();
const tx = feature.validateHashSignature(hash, notSigner, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner,
hash,
notSigner,
signature,
),
);
});
it('rejects an eth_sign if ecrecover() fails', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = hexUtils.concat(hexUtils.random(65), SignatureType.EthSign);
const tx = feature.validateHashSignature(hash, signer, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner,
hash,
signer,
signature,
),
);
});
it('rejects a too short eth_sign signature', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = hexUtils.slice(await signatureUtils.ecSignHashAsync(env.provider, hash, signer), 1);
const tx = feature.validateHashSignature(hash, signer, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.InvalidLength,
hash,
signer,
signature,
),
);
});
it('can validate an eip712 signature', async () => {
const privateKey = hexUtils.random();
const signer = hexUtils.toHex(ethjs.privateToAddress(ethjs.toBuffer(privateKey)));
const hash = hexUtils.random();
const signature = hexUtils.toHex(
signingUtils.signMessage(ethjs.toBuffer(hash), ethjs.toBuffer(privateKey), SignatureType.EIP712),
);
await feature.validateHashSignature(hash, signer, signature).callAsync();
});
it('rejects a wrong eip712 signature', async () => {
const privateKey = hexUtils.random();
const hash = hexUtils.random();
const signature = hexUtils.toHex(
signingUtils.signMessage(ethjs.toBuffer(hash), ethjs.toBuffer(privateKey), SignatureType.EIP712),
);
const notSigner = randomAddress();
const tx = feature.validateHashSignature(hash, notSigner, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner,
hash,
notSigner,
signature,
),
);
});
it('rejects an eip712 if ecrecover() fails', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = hexUtils.concat(hexUtils.random(65), SignatureType.EIP712);
const tx = feature.validateHashSignature(hash, signer, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner,
hash,
signer,
signature,
),
);
});
it('rejects a too short eip712 signature', async () => {
const privateKey = hexUtils.random();
const signer = hexUtils.toHex(ethjs.privateToAddress(ethjs.toBuffer(privateKey)));
const hash = hexUtils.random();
const signature = hexUtils.slice(
hexUtils.toHex(
signingUtils.signMessage(ethjs.toBuffer(hash), ethjs.toBuffer(privateKey), SignatureType.EIP712),
),
1,
);
const tx = feature.validateHashSignature(hash, signer, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.InvalidLength,
hash,
signer,
signature,
),
);
});
it('rejects an INVALID signature type', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = hexUtils.concat(
hexUtils.slice(await signatureUtils.ecSignHashAsync(env.provider, hash, signer), 0, -1),
SignatureType.Invalid,
);
const tx = feature.validateHashSignature(hash, signer, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.AlwaysInvalid,
hash,
signer,
signature,
),
);
});
it('rejects an ILLEGAL signature type', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = hexUtils.concat(
hexUtils.slice(await signatureUtils.ecSignHashAsync(env.provider, hash, signer), 0, -1),
SignatureType.Illegal,
);
const tx = feature.validateHashSignature(hash, signer, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.Illegal,
hash,
signer,
signature,
),
);
});
it('rejects an unsupported signature type', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = hexUtils.concat(
hexUtils.slice(await signatureUtils.ecSignHashAsync(env.provider, hash, signer), 0, -1),
SignatureType.Wallet,
);
const tx = feature.validateHashSignature(hash, signer, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.Unsupported,
hash,
signer,
signature,
),
);
});
it('rejects an empty signature type', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = NULL_BYTES;
const tx = feature.validateHashSignature(hash, signer, signature).callAsync();
return expect(tx).to.revertWith(
new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.InvalidLength,
hash,
signer,
signature,
),
);
});
});
describe('isValidHashSignature()', () => {
it('returns true on valid signature', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = await signatureUtils.ecSignHashAsync(env.provider, hash, signer);
const r = await feature.isValidHashSignature(hash, signer, signature).callAsync();
expect(r).to.eq(true);
});
it('returns false on invalid signature', async () => {
const hash = hexUtils.random();
const signer = _.sampleSize(signers, 1)[0];
const signature = await signatureUtils.ecSignHashAsync(env.provider, hash, signer);
const r = await feature.isValidHashSignature(hash, randomAddress(), signature).callAsync();
expect(r).to.eq(false);
});
});
});

View File

@ -22,12 +22,12 @@ blockchainTests.resets('TokenSpender feature', env => {
before(async () => {
const [owner] = await env.getAccountAddressesAsync();
zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {
tokenSpender: await TokenSpenderContract.deployFrom0xArtifactAsync(
tokenSpender: (await TokenSpenderContract.deployFrom0xArtifactAsync(
artifacts.TestTokenSpender,
env.provider,
env.txDefaults,
artifacts,
),
)).address,
});
feature = new TokenSpenderContract(zeroEx.address, env.provider, env.txDefaults, abis);
token = await TestTokenSpenderERC20TokenContract.deployFrom0xArtifactAsync(

View File

@ -40,12 +40,12 @@ blockchainTests.resets('TransformERC20 feature', env => {
env.provider,
env.txDefaults,
{
transformERC20: await TransformERC20Contract.deployFrom0xArtifactAsync(
transformERC20: (await TransformERC20Contract.deployFrom0xArtifactAsync(
artifacts.TestTransformERC20,
env.provider,
env.txDefaults,
artifacts,
),
)).address,
},
{ transformerDeployer },
);

View File

@ -6,10 +6,12 @@ import * as _ from 'lodash';
import { artifacts } from './artifacts';
import { abis } from './utils/abis';
import { deployFullFeaturesAsync, FullFeatures, toFeatureAdddresses } from './utils/migration';
import { deployFullFeaturesAsync, FullFeatures } from './utils/migration';
import {
AllowanceTargetContract,
IMetaTransactionsContract,
IOwnableContract,
ISignatureValidatorContract,
ITokenSpenderContract,
ITransformERC20Contract,
TestFullMigrationContract,
@ -27,7 +29,6 @@ blockchainTests.resets('Full migration', env => {
before(async () => {
[owner] = await env.getAccountAddressesAsync();
features = await deployFullFeaturesAsync(env.provider, env.txDefaults);
migrator = await TestFullMigrationContract.deployFrom0xArtifactAsync(
artifacts.TestFullMigration,
env.provider,
@ -35,9 +36,17 @@ blockchainTests.resets('Full migration', env => {
artifacts,
env.txDefaults.from as string,
);
const deployCall = migrator.deploy(owner, toFeatureAdddresses(features), { transformerDeployer });
zeroEx = new ZeroExContract(await deployCall.callAsync(), env.provider, env.txDefaults);
await deployCall.awaitTransactionSuccessAsync();
zeroEx = await ZeroExContract.deployFrom0xArtifactAsync(
artifacts.ZeroEx,
env.provider,
env.txDefaults,
artifacts,
await migrator.getBootstrapper().callAsync(),
);
features = await deployFullFeaturesAsync(env.provider, env.txDefaults, zeroEx.address);
await migrator
.initializeZeroEx(owner, zeroEx.address, features, { transformerDeployer })
.awaitTransactionSuccessAsync();
});
it('ZeroEx has the correct owner', async () => {
@ -51,10 +60,10 @@ blockchainTests.resets('Full migration', env => {
expect(dieRecipient).to.eq(owner);
});
it('Non-deployer cannot call deploy()', async () => {
it('Non-deployer cannot call initializeZeroEx()', async () => {
const notDeployer = randomAddress();
const tx = migrator
.deploy(owner, toFeatureAdddresses(features), { transformerDeployer })
.initializeZeroEx(owner, zeroEx.address, features, { transformerDeployer })
.callAsync({ from: notDeployer });
return expect(tx).to.revertWith('FullMigration/INVALID_SENDER');
});
@ -74,6 +83,21 @@ blockchainTests.resets('Full migration', env => {
'setTransformerDeployer',
],
},
SignatureValidator: {
contractType: ISignatureValidatorContract,
fns: ['isValidHashSignature', 'validateHashSignature'],
},
MetaTransactions: {
contractType: IMetaTransactionsContract,
fns: [
'executeMetaTransaction',
'batchExecuteMetaTransactions',
'_executeMetaTransaction',
'getMetaTransactionExecutedBlock',
'getMetaTransactionHashExecutedBlock',
'getMetaTransactionHash',
],
},
};
function createFakeInputs(inputs: DataItem[] | DataItem): any | any[] {

View File

@ -2,7 +2,7 @@ import { blockchainTests, expect, randomAddress } from '@0x/contracts-test-utils
import { hexUtils, ZeroExRevertErrors } from '@0x/utils';
import { artifacts } from './artifacts';
import { BootstrapFeatures, deployBootstrapFeaturesAsync, toFeatureAdddresses } from './utils/migration';
import { BootstrapFeatures, deployBootstrapFeaturesAsync } from './utils/migration';
import {
IBootstrapContract,
InitialMigrationContract,
@ -35,9 +35,14 @@ blockchainTests.resets('Initial migration', env => {
env.txDefaults,
{},
);
const deployCall = migrator.deploy(owner, toFeatureAdddresses(features));
zeroEx = new ZeroExContract(await deployCall.callAsync(), env.provider, env.txDefaults);
await deployCall.awaitTransactionSuccessAsync();
zeroEx = await ZeroExContract.deployFrom0xArtifactAsync(
artifacts.ZeroEx,
env.provider,
env.txDefaults,
artifacts,
migrator.address,
);
await migrator.initializeZeroEx(owner, zeroEx.address, features).awaitTransactionSuccessAsync();
});
it('Self-destructs after deployment', async () => {
@ -45,9 +50,9 @@ blockchainTests.resets('Initial migration', env => {
expect(dieRecipient).to.eq(owner);
});
it('Non-deployer cannot call deploy()', async () => {
it('Non-deployer cannot call initializeZeroEx()', async () => {
const notDeployer = randomAddress();
const tx = migrator.deploy(owner, toFeatureAdddresses(features)).callAsync({ from: notDeployer });
const tx = migrator.initializeZeroEx(owner, zeroEx.address, features).callAsync({ from: notDeployer });
return expect(tx).to.revertWith('InitialMigration/INVALID_SENDER');
});

View File

@ -4,7 +4,6 @@ export {
deployFullFeaturesAsync,
initialMigrateAsync,
fullMigrateAsync,
toFeatureAdddresses,
FullMigrationOpts,
FullFeatures,
} from '../../src/migration';

View File

@ -8,6 +8,7 @@ export * from '../test/generated-wrappers/allowance_target';
export * from '../test/generated-wrappers/bootstrap';
export * from '../test/generated-wrappers/fill_quote_transformer';
export * from '../test/generated-wrappers/fixin_common';
export * from '../test/generated-wrappers/fixin_e_i_p712';
export * from '../test/generated-wrappers/fixin_gas_token';
export * from '../test/generated-wrappers/flash_wallet';
export * from '../test/generated-wrappers/full_migration';
@ -19,7 +20,9 @@ export * from '../test/generated-wrappers/i_exchange';
export * from '../test/generated-wrappers/i_feature';
export * from '../test/generated-wrappers/i_flash_wallet';
export * from '../test/generated-wrappers/i_gas_token';
export * from '../test/generated-wrappers/i_meta_transactions';
export * from '../test/generated-wrappers/i_ownable';
export * from '../test/generated-wrappers/i_signature_validator';
export * from '../test/generated-wrappers/i_simple_function_registry';
export * from '../test/generated-wrappers/i_test_simple_function_registry_feature';
export * from '../test/generated-wrappers/i_token_spender';
@ -28,11 +31,14 @@ export * from '../test/generated-wrappers/initial_migration';
export * from '../test/generated-wrappers/lib_bootstrap';
export * from '../test/generated-wrappers/lib_common_rich_errors';
export * from '../test/generated-wrappers/lib_erc20_transformer';
export * from '../test/generated-wrappers/lib_meta_transactions_rich_errors';
export * from '../test/generated-wrappers/lib_meta_transactions_storage';
export * from '../test/generated-wrappers/lib_migrate';
export * from '../test/generated-wrappers/lib_ownable_rich_errors';
export * from '../test/generated-wrappers/lib_ownable_storage';
export * from '../test/generated-wrappers/lib_proxy_rich_errors';
export * from '../test/generated-wrappers/lib_proxy_storage';
export * from '../test/generated-wrappers/lib_signature_rich_errors';
export * from '../test/generated-wrappers/lib_simple_function_registry_rich_errors';
export * from '../test/generated-wrappers/lib_simple_function_registry_storage';
export * from '../test/generated-wrappers/lib_spender_rich_errors';
@ -41,8 +47,10 @@ export * from '../test/generated-wrappers/lib_token_spender_storage';
export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors';
export * from '../test/generated-wrappers/lib_transform_erc20_storage';
export * from '../test/generated-wrappers/lib_wallet_rich_errors';
export * from '../test/generated-wrappers/meta_transactions';
export * from '../test/generated-wrappers/ownable';
export * from '../test/generated-wrappers/pay_taker_transformer';
export * from '../test/generated-wrappers/signature_validator';
export * from '../test/generated-wrappers/simple_function_registry';
export * from '../test/generated-wrappers/test_call_target';
export * from '../test/generated-wrappers/test_delegate_caller';
@ -51,6 +59,7 @@ export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange';
export * from '../test/generated-wrappers/test_fill_quote_transformer_host';
export * from '../test/generated-wrappers/test_full_migration';
export * from '../test/generated-wrappers/test_initial_migration';
export * from '../test/generated-wrappers/test_meta_transactions_transform_erc20_feature';
export * from '../test/generated-wrappers/test_migrator';
export * from '../test/generated-wrappers/test_mint_token_erc20_transformer';
export * from '../test/generated-wrappers/test_mintable_erc20_token';

View File

@ -14,8 +14,10 @@
"generated-artifacts/ITokenSpender.json",
"generated-artifacts/ITransformERC20.json",
"generated-artifacts/InitialMigration.json",
"generated-artifacts/MetaTransactions.json",
"generated-artifacts/Ownable.json",
"generated-artifacts/PayTakerTransformer.json",
"generated-artifacts/SignatureValidator.json",
"generated-artifacts/SimpleFunctionRegistry.json",
"generated-artifacts/TokenSpender.json",
"generated-artifacts/TransformERC20.json",
@ -26,6 +28,7 @@
"test/generated-artifacts/Bootstrap.json",
"test/generated-artifacts/FillQuoteTransformer.json",
"test/generated-artifacts/FixinCommon.json",
"test/generated-artifacts/FixinEIP712.json",
"test/generated-artifacts/FixinGasToken.json",
"test/generated-artifacts/FlashWallet.json",
"test/generated-artifacts/FullMigration.json",
@ -37,7 +40,9 @@
"test/generated-artifacts/IFeature.json",
"test/generated-artifacts/IFlashWallet.json",
"test/generated-artifacts/IGasToken.json",
"test/generated-artifacts/IMetaTransactions.json",
"test/generated-artifacts/IOwnable.json",
"test/generated-artifacts/ISignatureValidator.json",
"test/generated-artifacts/ISimpleFunctionRegistry.json",
"test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json",
"test/generated-artifacts/ITokenSpender.json",
@ -46,11 +51,14 @@
"test/generated-artifacts/LibBootstrap.json",
"test/generated-artifacts/LibCommonRichErrors.json",
"test/generated-artifacts/LibERC20Transformer.json",
"test/generated-artifacts/LibMetaTransactionsRichErrors.json",
"test/generated-artifacts/LibMetaTransactionsStorage.json",
"test/generated-artifacts/LibMigrate.json",
"test/generated-artifacts/LibOwnableRichErrors.json",
"test/generated-artifacts/LibOwnableStorage.json",
"test/generated-artifacts/LibProxyRichErrors.json",
"test/generated-artifacts/LibProxyStorage.json",
"test/generated-artifacts/LibSignatureRichErrors.json",
"test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json",
"test/generated-artifacts/LibSimpleFunctionRegistryStorage.json",
"test/generated-artifacts/LibSpenderRichErrors.json",
@ -59,8 +67,10 @@
"test/generated-artifacts/LibTransformERC20RichErrors.json",
"test/generated-artifacts/LibTransformERC20Storage.json",
"test/generated-artifacts/LibWalletRichErrors.json",
"test/generated-artifacts/MetaTransactions.json",
"test/generated-artifacts/Ownable.json",
"test/generated-artifacts/PayTakerTransformer.json",
"test/generated-artifacts/SignatureValidator.json",
"test/generated-artifacts/SimpleFunctionRegistry.json",
"test/generated-artifacts/TestCallTarget.json",
"test/generated-artifacts/TestDelegateCaller.json",
@ -69,6 +79,7 @@
"test/generated-artifacts/TestFillQuoteTransformerHost.json",
"test/generated-artifacts/TestFullMigration.json",
"test/generated-artifacts/TestInitialMigration.json",
"test/generated-artifacts/TestMetaTransactionsTransformERC20Feature.json",
"test/generated-artifacts/TestMigrator.json",
"test/generated-artifacts/TestMintTokenERC20Transformer.json",
"test/generated-artifacts/TestMintableERC20Token.json",

View File

@ -5,6 +5,10 @@
{
"note": "Export `GethCallOverrides` type",
"pr": 2620
},
{
"note": "Export `ExchangeProxyMetaTransaction` and `SignedExchangeProxyMetaTransaction`",
"pr": 2610
}
]
},

View File

@ -82,6 +82,8 @@ export {
EventCallback,
IndexedFilterValues,
DecodedLogEvent,
ExchangeProxyMetaTransaction,
SignedExchangeProxyMetaTransaction,
} from '@0x/types';
export {

View File

@ -21,6 +21,10 @@
{
"note": "Redeploy DFB on kovan",
"pr": 2628
},
{
"note": "Update ganache snapshot Exchange Proxy addresses for MetaTransactions",
"pr": 2610
}
]
},

View File

@ -218,15 +218,15 @@
"dexForwarderBridge": "0x0000000000000000000000000000000000000000",
"multiBridge": "0x0000000000000000000000000000000000000000",
"exchangeProxyGovernor": "0x0000000000000000000000000000000000000000",
"exchangeProxy": "0x4b8ce0fa221284de4aaa09be3e7bf6193444b786",
"exchangeProxyAllowanceTarget": "0xd6724bf180441a89d08ea3aeded2c995180b9a04",
"exchangeProxy": "0x2ebb94cc79d7d0f1195300aaf191d118f53292a8",
"exchangeProxyAllowanceTarget": "0x3eab3df72fd584b50184ff7d988a0d8f9328c866",
"exchangeProxyTransformerDeployer": "0x5409ed021d9299bf6814279a6a1411a7e866a631",
"exchangeProxyFlashWallet": "0xdec8629610e2f4087bd9cc441d10ca8be0c6f6c5",
"exchangeProxyFlashWallet": "0x8362c3ebd90041b30ec45908332e592721642637",
"transformers": {
"wethTransformer": "0xb125995f5a4766c451cd8c34c4f5cac89b724571",
"payTakerTransformer": "0x10a736a7b223f1fe1050264249d1abb975741e75",
"fillQuoteTransformer": "0x33def1aa867be09809f3a01ce41d5ec1888846c9",
"affiliateFeeTransformer": "0xc7124963ab16c33e5bf421d4c0090116622b3074"
"wethTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3",
"payTakerTransformer": "0xc6b0d3c45a6b5092808196cb00df5c357d55e1d5",
"fillQuoteTransformer": "0xc7124963ab16c33e5bf421d4c0090116622b3074",
"affiliateFeeTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a"
}
}
}

View File

@ -15,7 +15,7 @@
"fix": "tslint --fix --format stylish --project .--exclude **/lib/**/*",
"test:circleci": "run-s test:coverage",
"test": "yarn run_mocha",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js lib/test/global_hooks.js --timeout 20000 --bail --exit",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js lib/test/global_hooks.js --timeout 30000 --bail --exit",
"test:coverage": "nyc npm run test --all && yarn coverage:report:lcov",
"coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info",
"prettier": "prettier --write **/* --config ../../.prettierrc",

View File

@ -5,6 +5,10 @@
{
"note": "Add ERC20 Transformer utils and export useful constants.",
"pr": 2604
},
{
"note": "Add `getOrderHash()`, `getExchangeTransactionHash()`, `getExchangeProxyTransactionHash()`",
"pr": 2610
}
]
},

View File

@ -64,6 +64,7 @@
},
"dependencies": {
"@0x/assert": "^3.0.8",
"@0x/contract-addresses": "^4.10.0",
"@0x/contract-wrappers": "^13.7.0",
"@0x/json-schemas": "^5.0.8",
"@0x/utils": "^5.5.0",

View File

@ -1,3 +1,4 @@
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { BigNumber, NULL_ADDRESS, NULL_BYTES } from '@0x/utils';
import { MethodAbi } from 'ethereum-types';
@ -150,6 +151,27 @@ export const constants = {
{ name: 'transactionSignature', type: 'bytes' },
],
},
MAINNET_EXCHANGE_PROXY_DOMAIN: {
name: 'ZeroEx',
version: '1.0.0',
chainId: 1,
verifyingContract: getContractAddressesForChainOrThrow(1).exchangeProxy,
},
EXCHANGE_PROXY_MTX_SCEHMA: {
name: 'MetaTransactionData',
parameters: [
{ name: 'signer', type: 'address' },
{ name: 'sender', type: 'address' },
{ name: 'minGasPrice', type: 'uint256' },
{ name: 'maxGasPrice', type: 'uint256' },
{ name: 'expirationTimeSeconds', type: 'uint256' },
{ name: 'salt', type: 'uint256' },
{ name: 'callData', type: 'bytes' },
{ name: 'value', type: 'uint256' },
{ name: 'feeToken', type: 'address' },
{ name: 'feeAmount', type: 'uint256' },
],
},
ERC20_METHOD_ABI,
ERC721_METHOD_ABI,
MULTI_ASSET_METHOD_ABI,

View File

@ -5,11 +5,12 @@ import {
EIP712Object,
EIP712TypedData,
EIP712Types,
ExchangeProxyMetaTransaction,
Order,
SignedZeroExTransaction,
ZeroExTransaction,
} from '@0x/types';
import { hexUtils, signTypedDataUtils } from '@0x/utils';
import { BigNumber, hexUtils, signTypedDataUtils } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from './constants';
@ -131,4 +132,21 @@ export const eip712Utils = {
);
return typedData;
},
createExchangeProxyMetaTransactionTypedData(mtx: ExchangeProxyMetaTransaction): EIP712TypedData {
return eip712Utils.createTypedData(
constants.EXCHANGE_PROXY_MTX_SCEHMA.name,
{
MetaTransactionData: constants.EXCHANGE_PROXY_MTX_SCEHMA.parameters,
},
_.mapValues(
_.omit(mtx, 'domain'),
// tslint:disable-next-line: custom-no-magic-numbers
v => (BigNumber.isBigNumber(v) ? v.toString(10) : v),
) as EIP712Object,
{
...constants.MAINNET_EXCHANGE_PROXY_DOMAIN,
...mtx.domain,
},
);
},
};

View File

@ -0,0 +1,29 @@
import { ExchangeProxyMetaTransaction, Order, ZeroExTransaction } from '@0x/types';
import { hexUtils, signTypedDataUtils } from '@0x/utils';
import { eip712Utils } from './eip712_utils';
import { orderHashUtils } from './order_hash_utils';
import { transactionHashUtils } from './transaction_hash_utils';
/**
* Compute the EIP712 hash of an order.
*/
export function getOrderHash(order: Order): string {
return orderHashUtils.getOrderHash(order);
}
/**
* Compute the EIP712 hash of an Exchange meta-transaction.
*/
export function getExchangeMetaTransactionHash(tx: ZeroExTransaction): string {
return transactionHashUtils.getTransactionHash(tx);
}
/**
* Compute the EIP712 hash of an Exchange Proxy meta-transaction.
*/
export function getExchangeProxyMetaTransactionHash(mtx: ExchangeProxyMetaTransaction): string {
return hexUtils.toHex(
signTypedDataUtils.generateTypedDataHash(eip712Utils.createExchangeProxyMetaTransactionTypedData(mtx)),
);
}

View File

@ -49,6 +49,8 @@ export {
ZeroExTransaction,
SignedZeroExTransaction,
ValidatorSignature,
ExchangeProxyMetaTransaction,
SignedExchangeProxyMetaTransaction,
} from '@0x/types';
export {
@ -77,6 +79,8 @@ export {
decodeAffiliateFeeTransformerData,
} from './transformer_data_encoders';
export { getOrderHash, getExchangeMetaTransactionHash, getExchangeProxyMetaTransactionHash } from './hash_utils';
import { constants } from './constants';
export const NULL_ADDRESS = constants.NULL_ADDRESS;
export const NULL_BYTES = constants.NULL_BYTES;

View File

@ -1,14 +1,16 @@
import { schemas } from '@0x/json-schemas';
import {
ECSignature,
ExchangeProxyMetaTransaction,
Order,
SignatureType,
SignedExchangeProxyMetaTransaction,
SignedOrder,
SignedZeroExTransaction,
ValidatorSignature,
ZeroExTransaction,
} from '@0x/types';
import { providerUtils } from '@0x/utils';
import { hexUtils, providerUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { SupportedProvider } from 'ethereum-types';
import * as ethUtil from 'ethereumjs-util';
@ -16,6 +18,7 @@ import * as _ from 'lodash';
import { assert } from './assert';
import { eip712Utils } from './eip712_utils';
import { getExchangeProxyMetaTransactionHash } from './hash_utils';
import { orderHashUtils } from './order_hash_utils';
import { transactionHashUtils } from './transaction_hash_utils';
import { TypedDataError } from './types';
@ -187,6 +190,96 @@ export const signatureUtils = {
}
}
},
/**
* Signs an Exchange Proxy meta-transaction and returns a SignedExchangeProxyMetaTransaction.
* First `eth_signTypedData` is requested then a fallback to `eth_sign` if not
* available on the supplied provider.
* @param supportedProvider Web3 provider to use for all JSON RPC requests
* @param transaction The ExchangeProxyMetaTransaction to sign.
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
* must be available via the supplied Provider.
* @return A SignedExchangeProxyMetaTransaction containing the order and
* elliptic curve signature with Signature Type.
*/
async ecSignExchangeProxyMetaTransactionAsync(
supportedProvider: SupportedProvider,
transaction: ExchangeProxyMetaTransaction,
signerAddress: string,
): Promise<SignedExchangeProxyMetaTransaction> {
assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]);
try {
const signedTransaction = await signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync(
supportedProvider,
transaction,
signerAddress,
);
return signedTransaction;
} catch (err) {
// HACK: We are unable to handle specific errors thrown since provider is not an object
// under our control. It could be Metamask Web3, Ethers, or any general RPC provider.
// We check for a user denying the signature request in a way that supports Metamask and
// Coinbase Wallet. Unfortunately for signers with a different error message,
// they will receive two signature requests.
if (err.message.includes('User denied message signature')) {
throw err;
}
const transactionHash = getExchangeProxyMetaTransactionHash(transaction);
const signatureHex = await signatureUtils.ecSignHashAsync(
supportedProvider,
transactionHash,
signerAddress,
);
const signedTransaction = {
...transaction,
signature: signatureHex,
};
return signedTransaction;
}
},
/**
* Signs an Exchange Proxy meta-transaction using `eth_signTypedData` and
* returns a SignedZeroExTransaction.
* @param supportedProvider Web3 provider to use for all JSON RPC requests
* @param transaction The Exchange Proxy transaction to sign.
* @param signerAddress The hex encoded Ethereum address you wish
* to sign it with. This address must be available via the supplied Provider.
* @return A SignedExchangeProxyMetaTransaction containing the
* ExchangeProxyMetaTransaction and elliptic curve signature with Signature Type.
*/
async ecSignTypedDataExchangeProxyMetaTransactionAsync(
supportedProvider: SupportedProvider,
transaction: ExchangeProxyMetaTransaction,
signerAddress: string,
): Promise<SignedExchangeProxyMetaTransaction> {
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isETHAddressHex('signerAddress', signerAddress);
assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]);
const web3Wrapper = new Web3Wrapper(provider);
await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
const normalizedSignerAddress = signerAddress.toLowerCase();
const typedData = eip712Utils.createExchangeProxyMetaTransactionTypedData(transaction);
try {
const signature = await web3Wrapper.signTypedDataAsync(normalizedSignerAddress, typedData);
const ecSignatureRSV = parseSignatureHexAsRSV(signature);
const signatureHex = hexUtils.concat(
ecSignatureRSV.v,
ecSignatureRSV.r,
ecSignatureRSV.s,
SignatureType.EIP712,
);
return {
...transaction,
signature: signatureHex,
};
} catch (err) {
// Detect if Metamask to transition users to the MetamaskSubprovider
if ((provider as any).isMetaMask) {
throw new Error(TypedDataError.InvalidMetamaskSigner);
} else {
throw err;
}
}
},
/**
* Signs a hash using `eth_sign` and returns its elliptic curve signature and signature type.
* @param supportedProvider Web3 provider to use for all JSON RPC requests
@ -245,12 +338,7 @@ export const signatureUtils = {
* @return Hex encoded string of signature (v,r,s) with Signature Type
*/
convertECSignatureToSignatureHex(ecSignature: ECSignature): string {
const signatureBuffer = Buffer.concat([
ethUtil.toBuffer(ecSignature.v),
ethUtil.toBuffer(ecSignature.r),
ethUtil.toBuffer(ecSignature.s),
]);
const signatureHex = `0x${signatureBuffer.toString('hex')}`;
const signatureHex = hexUtils.concat(ecSignature.v, ecSignature.r, ecSignature.s);
const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, SignatureType.EthSign);
return signatureWithType;
},

View File

@ -1,4 +1,13 @@
[
{
"version": "3.2.0",
"changes": [
{
"note": "Add `ExchangeProxyMetaTransaction` and `SignedExchangeProxyMetaTransaction`",
"pr": 2610
}
]
},
{
"timestamp": 1592969527,
"version": "3.1.3",

View File

@ -54,6 +54,30 @@ export interface SignedZeroExTransaction extends ZeroExTransaction {
signature: string;
}
/**
* Exchange Proxy meta transaction struct.
*/
export interface ExchangeProxyMetaTransaction {
signer: string;
sender: string;
minGasPrice: BigNumber;
maxGasPrice: BigNumber;
expirationTimeSeconds: BigNumber;
salt: BigNumber;
callData: string;
value: BigNumber;
feeToken: string;
feeAmount: BigNumber;
domain: EIP712DomainWithDefaultSchema;
}
/**
* `ExchangeProxyMetaTransaction` with `signature` field.
*/
export interface SignedExchangeProxyMetaTransaction extends ExchangeProxyMetaTransaction {
signature: string;
}
/**
* Elliptic Curve signature
*/

View File

@ -29,6 +29,10 @@
{
"note": "Update `ZeroExRevertErrors`",
"pr": 2597
},
{
"note": "Add more revert errors to `ZeroExRevertErrors`",
"pr": 2610
}
],
"timestamp": 1592969527

View File

@ -52,4 +52,6 @@ export const ZeroExRevertErrors = {
Spender: require('./revert_errors/zero-ex/spender_revert_errors'),
TransformERC20: require('./revert_errors/zero-ex/transform_erc20_revert_errors'),
Wallet: require('./revert_errors/zero-ex/wallet_revert_errors'),
MetaTransactions: require('./revert_errors/zero-ex/meta_transaction_revert_errors'),
SignatureValidator: require('./revert_errors/zero-ex/signature_validator_revert_errors'),
};

View File

@ -0,0 +1,144 @@
import { RevertError } from '../../revert_error';
import { Numberish } from '../../types';
// tslint:disable:max-classes-per-file
export class InvalidMetaTransactionsArrayLengthsError extends RevertError {
constructor(mtxCount?: Numberish, signatureCount?: Numberish) {
super(
'InvalidMetaTransactionsArrayLengthsError',
'InvalidMetaTransactionsArrayLengthsError(uint256 mtxCount, uint256 signatureCount)',
{
mtxCount,
signatureCount,
},
);
}
}
export class MetaTransactionAlreadyExecutedError extends RevertError {
constructor(mtxHash?: string, executedBlockNumber?: Numberish) {
super(
'MetaTransactionAlreadyExecutedError',
'MetaTransactionAlreadyExecutedError(bytes32 mtxHash, uint256 executedBlockNumber)',
{
mtxHash,
executedBlockNumber,
},
);
}
}
export class MetaTransactionUnsupportedFunctionError extends RevertError {
constructor(mtxHash?: string, selector?: string) {
super(
'MetaTransactionUnsupportedFunctionError',
'MetaTransactionUnsupportedFunctionError(bytes32 mtxHash, bytes4 selector)',
{
mtxHash,
selector,
},
);
}
}
export class MetaTransactionWrongSenderError extends RevertError {
constructor(mtxHash?: string, sender?: string, expectedSender?: string) {
super(
'MetaTransactionWrongSenderError',
'MetaTransactionWrongSenderError(bytes32 mtxHash, address sender, address expectedSender)',
{
mtxHash,
sender,
expectedSender,
},
);
}
}
export class MetaTransactionExpiredError extends RevertError {
constructor(mtxHash?: string, time?: Numberish, expirationTime?: Numberish) {
super(
'MetaTransactionExpiredError',
'MetaTransactionExpiredError(bytes32 mtxHash, uint256 time, uint256 expirationTime)',
{
mtxHash,
time,
expirationTime,
},
);
}
}
export class MetaTransactionGasPriceError extends RevertError {
constructor(mtxHash?: string, gasPrice?: Numberish, minGasPrice?: Numberish, maxGasPrice?: Numberish) {
super(
'MetaTransactionGasPriceError',
'MetaTransactionGasPriceError(bytes32 mtxHash, uint256 gasPrice, uint256 minGasPrice, uint256 maxGasPrice)',
{
mtxHash,
gasPrice,
minGasPrice,
maxGasPrice,
},
);
}
}
export class MetaTransactionInsufficientEthError extends RevertError {
constructor(mtxHash?: string, ethBalance?: Numberish, ethRequired?: Numberish) {
super(
'MetaTransactionInsufficientEthError',
'MetaTransactionInsufficientEthError(bytes32 mtxHash, uint256 ethBalance, uint256 ethRequired)',
{
mtxHash,
ethBalance,
ethRequired,
},
);
}
}
export class MetaTransactionInvalidSignatureError extends RevertError {
constructor(mtxHash?: string, signature?: string, errData?: string) {
super(
'MetaTransactionInvalidSignatureError',
'MetaTransactionInvalidSignatureError(bytes32 mtxHash, bytes signature, bytes errData)',
{
mtxHash,
signature,
errData,
},
);
}
}
export class MetaTransactionCallFailedError extends RevertError {
constructor(mtxHash?: string, callData?: string, returnData?: string) {
super(
'MetaTransactionCallFailedError',
'MetaTransactionCallFailedError(bytes32 mtxHash, bytes callData, bytes returnData)',
{
mtxHash,
callData,
returnData,
},
);
}
}
const types = [
InvalidMetaTransactionsArrayLengthsError,
MetaTransactionAlreadyExecutedError,
MetaTransactionUnsupportedFunctionError,
MetaTransactionWrongSenderError,
MetaTransactionExpiredError,
MetaTransactionGasPriceError,
MetaTransactionInsufficientEthError,
MetaTransactionInvalidSignatureError,
MetaTransactionCallFailedError,
];
// Register the types we've defined.
for (const type of types) {
RevertError.registerType(type);
}

View File

@ -0,0 +1,33 @@
import { RevertError } from '../../revert_error';
// tslint:disable:max-classes-per-file
export enum SignatureValidationErrorCodes {
AlwaysInvalid = 0,
InvalidLength = 1,
Unsupported = 2,
Illegal = 3,
WrongSigner = 4,
}
export class SignatureValidationError extends RevertError {
constructor(code?: SignatureValidationErrorCodes, hash?: string, signerAddress?: string, signature?: string) {
super(
'SignatureValidationError',
'SignatureValidationError(uint8 code, bytes32 hash, address signerAddress, bytes signature)',
{
code,
hash,
signerAddress,
signature,
},
);
}
}
const types = [SignatureValidationError];
// Register the types we've defined.
for (const type of types) {
RevertError.registerType(type);
}