@0x/contracts-zero-ex: Add reentrancy guard to mtx functions

`@0x/contracts-zero-ex`: Add refund mechanism to mtxs
`@0x/contracts-zero-ex`: Pass sender to transfomers.
`@0x/contracts-zero-ex`: Refund protocol fees to `refundReceiver` in FQT.
`@0x/utils`: Add EP flavor of `IllegalReentrancyError`
`@0x/order-utils`: Add `refundReceiver` to FQT transform data.
`@0x/asset-swapper`: Add `refundReceiver` support to EP swap quote consumer.
This commit is contained in:
Lawrence Forman 2020-08-05 13:33:07 -04:00 committed by Lawrence Forman
parent 7b0a1c3630
commit 9cda9f69cd
35 changed files with 747 additions and 129 deletions

View File

@ -1,4 +1,25 @@
[ [
{
"version": "0.3.0",
"changes": [
{
"note": "Internal audit fixes",
"pr": 2657
},
{
"note": "Add refund mechanism to meta-transactions",
"pr": 2657
},
{
"note": "Pass sender address to transformers",
"pr": 2657
},
{
"note": "Refund unused protocol fees to `refundReceiver` in FQT",
"pr": 2657
}
]
},
{ {
"version": "0.2.0", "version": "0.2.0",
"changes": [ "changes": [

View File

@ -34,13 +34,15 @@ library LibCommonRichErrors {
); );
} }
function IllegalReentrancyError() function IllegalReentrancyError(bytes4 selector, uint256 reentrancyFlags)
internal internal
pure pure
returns (bytes memory) returns (bytes memory)
{ {
return abi.encodeWithSelector( return abi.encodeWithSelector(
bytes4(keccak256("IllegalReentrancyError()")) bytes4(keccak256("IllegalReentrancyError(bytes4,uint256)")),
selector,
reentrancyFlags
); );
} }
} }

View File

@ -21,8 +21,10 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../errors/LibMetaTransactionsRichErrors.sol"; import "../errors/LibMetaTransactionsRichErrors.sol";
import "../fixins/FixinCommon.sol"; import "../fixins/FixinCommon.sol";
import "../fixins/FixinReentrancyGuard.sol";
import "../fixins/FixinEIP712.sol"; import "../fixins/FixinEIP712.sol";
import "../migrations/LibMigrate.sol"; import "../migrations/LibMigrate.sol";
import "../storage/LibMetaTransactionsStorage.sol"; import "../storage/LibMetaTransactionsStorage.sol";
@ -39,6 +41,7 @@ contract MetaTransactions is
IFeature, IFeature,
IMetaTransactions, IMetaTransactions,
FixinCommon, FixinCommon,
FixinReentrancyGuard,
FixinEIP712 FixinEIP712
{ {
using LibBytesV06 for bytes; using LibBytesV06 for bytes;
@ -92,6 +95,16 @@ contract MetaTransactions is
")" ")"
); );
/// @dev Refunds up to `msg.value` leftover ETH at the end of the call.
modifier refundsAttachedEth() {
_;
uint256 remainingBalance =
LibSafeMathV06.min256(msg.value, address(this).balance);
if (remainingBalance > 0) {
msg.sender.transfer(remainingBalance);
}
}
constructor(address zeroExAddress) constructor(address zeroExAddress)
public public
FixinCommon() FixinCommon()
@ -127,9 +140,11 @@ contract MetaTransactions is
public public
payable payable
override override
nonReentrant(REENTRANCY_MTX)
refundsAttachedEth
returns (bytes memory returnResult) returns (bytes memory returnResult)
{ {
return _executeMetaTransactionPrivate( returnResult = _executeMetaTransactionPrivate(
msg.sender, msg.sender,
mtx, mtx,
signature signature
@ -147,6 +162,8 @@ contract MetaTransactions is
public public
payable payable
override override
nonReentrant(REENTRANCY_MTX)
refundsAttachedEth
returns (bytes[] memory returnResults) returns (bytes[] memory returnResults)
{ {
if (mtxs.length != signatures.length) { if (mtxs.length != signatures.length) {
@ -255,10 +272,19 @@ contract MetaTransactions is
_validateMetaTransaction(state); _validateMetaTransaction(state);
// Mark the transaction executed. // Mark the transaction executed.
assert(block.number > 0);
LibMetaTransactionsStorage.getStorage() LibMetaTransactionsStorage.getStorage()
.mtxHashToExecutedBlockNumber[state.hash] = block.number; .mtxHashToExecutedBlockNumber[state.hash] = block.number;
// 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
);
}
// Execute the call based on the selector. // Execute the call based on the selector.
state.selector = mtx.callData.readBytes4(0); state.selector = mtx.callData.readBytes4(0);
if (state.selector == ITransformERC20.transformERC20.selector) { if (state.selector == ITransformERC20.transformERC20.selector) {
@ -268,15 +294,6 @@ contract MetaTransactions is
.MetaTransactionUnsupportedFunctionError(state.hash, state.selector) .MetaTransactionUnsupportedFunctionError(state.hash, state.selector)
.rrevert(); .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( emit MetaTransactionExecuted(
state.hash, state.hash,
state.selector, state.selector,
@ -367,7 +384,7 @@ contract MetaTransactions is
// since decoding a single struct arg consumes far less stack space than // since decoding a single struct arg consumes far less stack space than
// decoding multiple struct args. // decoding multiple struct args.
// Where the encoding for multiple args (with the seleector ommitted) // Where the encoding for multiple args (with the selector ommitted)
// would typically look like: // would typically look like:
// | argument | offset | // | argument | offset |
// |--------------------------|---------| // |--------------------------|---------|
@ -394,7 +411,7 @@ contract MetaTransactions is
bytes memory encodedStructArgs = new bytes(state.mtx.callData.length - 4 + 32); bytes memory encodedStructArgs = new bytes(state.mtx.callData.length - 4 + 32);
// Copy the args data from the original, after the new struct offset prefix. // Copy the args data from the original, after the new struct offset prefix.
bytes memory fromCallData = state.mtx.callData; bytes memory fromCallData = state.mtx.callData;
assert(fromCallData.length >= 4); assert(fromCallData.length >= 160);
uint256 fromMem; uint256 fromMem;
uint256 toMem; uint256 toMem;
assembly { assembly {

View File

@ -37,6 +37,13 @@ contract SignatureValidator is
using LibBytesV06 for bytes; using LibBytesV06 for bytes;
using LibRichErrorsV06 for bytes; using LibRichErrorsV06 for bytes;
/// @dev Exclusive upper limit on ECDSA signatures 'R' values.
/// The valid range is given by fig (282) of the yellow paper.
uint256 private constant ECDSA_SIGNATURE_R_LIMIT =
uint256(0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141);
/// @dev Exclusive upper limit on ECDSA signatures 'S' values.
/// The valid range is given by fig (283) of the yellow paper.
uint256 private constant ECDSA_SIGNATURE_S_LIMIT = ECDSA_SIGNATURE_R_LIMIT / 2 + 1;
/// @dev Name of this feature. /// @dev Name of this feature.
string public constant override FEATURE_NAME = "SignatureValidator"; string public constant override FEATURE_NAME = "SignatureValidator";
/// @dev Version of this feature. /// @dev Version of this feature.
@ -160,12 +167,18 @@ contract SignatureValidator is
uint8 v = uint8(signature[0]); uint8 v = uint8(signature[0]);
bytes32 r = signature.readBytes32(1); bytes32 r = signature.readBytes32(1);
bytes32 s = signature.readBytes32(33); bytes32 s = signature.readBytes32(33);
recovered = ecrecover( if (v < 27) {
hash, // Handle clients that encode v as 0 or 1.
v, v += 27;
r, }
s if (uint256(r) < ECDSA_SIGNATURE_R_LIMIT && uint256(s) < ECDSA_SIGNATURE_S_LIMIT) {
); recovered = ecrecover(
hash,
v,
r,
s
);
}
} else if (signatureType == SignatureType.EthSign) { } else if (signatureType == SignatureType.EthSign) {
// Signed using `eth_sign` // Signed using `eth_sign`
if (signature.length != 66) { if (signature.length != 66) {
@ -179,15 +192,21 @@ contract SignatureValidator is
uint8 v = uint8(signature[0]); uint8 v = uint8(signature[0]);
bytes32 r = signature.readBytes32(1); bytes32 r = signature.readBytes32(1);
bytes32 s = signature.readBytes32(33); bytes32 s = signature.readBytes32(33);
recovered = ecrecover( if (v < 27) {
keccak256(abi.encodePacked( // Handle clients that encode v as 0 or 1.
"\x19Ethereum Signed Message:\n32", v += 27;
hash }
)), if (uint256(r) < ECDSA_SIGNATURE_R_LIMIT && uint256(s) < ECDSA_SIGNATURE_S_LIMIT) {
v, recovered = ecrecover(
r, keccak256(abi.encodePacked(
s "\x19Ethereum Signed Message:\n32",
); hash
)),
v,
r,
s
);
}
} else { } else {
// This should never happen. // This should never happen.
revert('SignatureValidator/ILLEGAL_CODE_PATH'); revert('SignatureValidator/ILLEGAL_CODE_PATH');

View File

@ -360,9 +360,12 @@ contract TransformERC20 is
// Call data. // Call data.
abi.encodeWithSelector( abi.encodeWithSelector(
IERC20Transformer.transform.selector, IERC20Transformer.transform.selector,
callDataHash, IERC20Transformer.TransformContext({
taker, callDataHash: callDataHash,
transformation.data sender: msg.sender,
taker: taker,
data: transformation.data
})
) )
); );
// Ensure the transformer returned the magic bytes. // Ensure the transformer returned the magic bytes.

View File

@ -0,0 +1,60 @@
/*
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/LibBytesV06.sol";
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "../errors/LibCommonRichErrors.sol";
import "../storage/LibReentrancyGuardStorage.sol";
/// @dev Common feature utilities.
abstract contract FixinReentrancyGuard {
using LibRichErrorsV06 for bytes;
using LibBytesV06 for bytes;
// Combinable reentrancy flags.
/// @dev Reentrancy guard flag for meta-transaction functions.
uint256 constant internal REENTRANCY_MTX = 0x1;
/// @dev Cannot reenter a function with the same reentrancy guard flags.
modifier nonReentrant(uint256 reentrancyFlags) virtual {
LibReentrancyGuardStorage.Storage storage stor =
LibReentrancyGuardStorage.getStorage();
{
uint256 currentFlags = stor.reentrancyFlags;
// Revert if any bits in `reentrancyFlags` has already been set.
if ((currentFlags & reentrancyFlags) != 0) {
LibCommonRichErrors.IllegalReentrancyError(
msg.data.readBytes4(0),
reentrancyFlags
).rrevert();
}
// Update reentrancy flags.
stor.reentrancyFlags = currentFlags | reentrancyFlags;
}
_;
// Clear reentrancy flags.
stor.reentrancyFlags = stor.reentrancyFlags & (~reentrancyFlags);
}
}

View File

@ -0,0 +1,46 @@
/*
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";
import "../external/IFlashWallet.sol";
/// @dev Storage helpers for the `FixinReentrancyGuard` mixin.
library LibReentrancyGuardStorage {
/// @dev Storage bucket for this feature.
struct Storage {
// Reentrancy flags set whenever a non-reentrant function is entered
// and cleared when it is exited.
uint256 reentrancyFlags;
}
/// @dev Get the storage bucket for this contract.
function getStorage() internal pure returns (Storage storage stor) {
uint256 storageSlot = LibStorage.getStorageSlot(
LibStorage.StorageId.ReentrancyGuard
);
// 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

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

View File

@ -58,18 +58,14 @@ contract AffiliateFeeTransformer is
{} {}
/// @dev Transfers tokens to recipients. /// @dev Transfers tokens to recipients.
/// @param data ABI-encoded `TokenFee[]`, indicating which tokens to transfer. /// @param context Context information.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). /// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform( function transform(TransformContext calldata context)
bytes32, // callDataHash,
address payable, // taker,
bytes calldata data
)
external external
override override
returns (bytes4 success) returns (bytes4 success)
{ {
TokenFee[] memory fees = abi.decode(data, (TokenFee[])); TokenFee[] memory fees = abi.decode(context.data, (TokenFee[]));
// Transfer tokens to recipients. // Transfer tokens to recipients.
for (uint256 i = 0; i < fees.length; ++i) { for (uint256 i = 0; i < fees.length; ++i) {

View File

@ -71,6 +71,12 @@ contract FillQuoteTransformer is
// For sells, this may be `uint256(-1)` to sell the entire balance of // For sells, this may be `uint256(-1)` to sell the entire balance of
// `sellToken`. // `sellToken`.
uint256 fillAmount; uint256 fillAmount;
// Who to transfer unused protocol fees to.
// May be a valid address or one of:
// `address(0)`: Stay in flash wallet.
// `address(1)`: Send to the taker.
// `address(2)`: Send to the sender (caller of `transformERC20()`).
address payable refundReceiver;
} }
/// @dev Results of a call to `_fillOrder()`. /// @dev Results of a call to `_fillOrder()`.
@ -108,6 +114,12 @@ contract FillQuoteTransformer is
bytes4 private constant ERC20_BRIDGE_PROXY_ID = 0xdc1600f3; bytes4 private constant ERC20_BRIDGE_PROXY_ID = 0xdc1600f3;
/// @dev Maximum uint256 value. /// @dev Maximum uint256 value.
uint256 private constant MAX_UINT256 = uint256(-1); uint256 private constant MAX_UINT256 = uint256(-1);
/// @dev If `refundReceiver` is set to this address, unpsent
/// protocol fees will be sent to the taker.
address private constant REFUND_RECEIVER_TAKER = address(1);
/// @dev If `refundReceiver` is set to this address, unpsent
/// protocol fees will be sent to the sender.
address private constant REFUND_RECEIVER_SENDER = address(2);
/// @dev The Exchange contract. /// @dev The Exchange contract.
IExchange public immutable exchange; IExchange public immutable exchange;
@ -130,31 +142,27 @@ contract FillQuoteTransformer is
/// @dev Sell this contract's entire balance of of `sellToken` in exchange /// @dev Sell this contract's entire balance of of `sellToken` in exchange
/// for `buyToken` by filling `orders`. Protocol fees should be attached /// for `buyToken` by filling `orders`. Protocol fees should be attached
/// to this call. `buyToken` and excess ETH will be transferred back to the caller. /// to this call. `buyToken` and excess ETH will be transferred back to the caller.
/// @param data_ ABI-encoded `TransformData`. /// @param context Context information.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). /// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform( function transform(TransformContext calldata context)
bytes32, // callDataHash,
address payable, // taker,
bytes calldata data_
)
external external
override override
returns (bytes4 success) returns (bytes4 success)
{ {
TransformData memory data = abi.decode(data_, (TransformData)); TransformData memory data = abi.decode(context.data, (TransformData));
FillState memory state; FillState memory state;
// Validate data fields. // Validate data fields.
if (data.sellToken.isTokenETH() || data.buyToken.isTokenETH()) { if (data.sellToken.isTokenETH() || data.buyToken.isTokenETH()) {
LibTransformERC20RichErrors.InvalidTransformDataError( LibTransformERC20RichErrors.InvalidTransformDataError(
LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_TOKENS, LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_TOKENS,
data_ context.data
).rrevert(); ).rrevert();
} }
if (data.orders.length != data.signatures.length) { if (data.orders.length != data.signatures.length) {
LibTransformERC20RichErrors.InvalidTransformDataError( LibTransformERC20RichErrors.InvalidTransformDataError(
LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_ARRAY_LENGTH, LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_ARRAY_LENGTH,
data_ context.data
).rrevert(); ).rrevert();
} }
@ -248,6 +256,17 @@ contract FillQuoteTransformer is
).rrevert(); ).rrevert();
} }
} }
// Refund unspent protocol fees.
if (state.ethRemaining > 0 && data.refundReceiver != address(0)) {
if (data.refundReceiver == REFUND_RECEIVER_TAKER) {
context.taker.transfer(state.ethRemaining);
} else if (data.refundReceiver == REFUND_RECEIVER_SENDER) {
context.sender.transfer(state.ethRemaining);
} else {
data.refundReceiver.transfer(state.ethRemaining);
}
}
return LibERC20Transformer.TRANSFORMER_SUCCESS; return LibERC20Transformer.TRANSFORMER_SUCCESS;
} }

View File

@ -25,17 +25,24 @@ import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
/// @dev A transformation callback used in `TransformERC20.transformERC20()`. /// @dev A transformation callback used in `TransformERC20.transformERC20()`.
interface IERC20Transformer { interface IERC20Transformer {
/// @dev Context information to pass into `transform()` by `TransformERC20.transformERC20()`.
struct TransformContext {
// The hash of the `TransformERC20.transformERC20()` calldata.
bytes32 callDataHash;
// The caller of `TransformERC20.transformERC20()`.
address payable sender;
// taker The taker address, which may be distinct from `sender` in the case
// meta-transactions.
address payable taker;
// Arbitrary data to pass to the transformer.
bytes data;
}
/// @dev Called from `TransformERC20.transformERC20()`. This will be /// @dev Called from `TransformERC20.transformERC20()`. This will be
/// delegatecalled in the context of the FlashWallet instance being used. /// delegatecalled in the context of the FlashWallet instance being used.
/// @param callDataHash The hash of the `TransformERC20.transformERC20()` calldata. /// @param context Context information.
/// @param taker The taker address (caller of `TransformERC20.transformERC20()`).
/// @param data Arbitrary data to pass to the transformer.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). /// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform( function transform(TransformContext calldata context)
bytes32 callDataHash,
address payable taker,
bytes calldata data
)
external external
returns (bytes4 success); returns (bytes4 success);
} }

View File

@ -56,19 +56,14 @@ contract PayTakerTransformer is
{} {}
/// @dev Forwards tokens to the taker. /// @dev Forwards tokens to the taker.
/// @param taker The taker address (caller of `TransformERC20.transformERC20()`). /// @param context Context information.
/// @param data_ ABI-encoded `TransformData`, indicating which tokens to transfer.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). /// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform( function transform(TransformContext calldata context)
bytes32, // callDataHash,
address payable taker,
bytes calldata data_
)
external external
override override
returns (bytes4 success) returns (bytes4 success)
{ {
TransformData memory data = abi.decode(data_, (TransformData)); TransformData memory data = abi.decode(context.data, (TransformData));
// Transfer tokens directly to the taker. // Transfer tokens directly to the taker.
for (uint256 i = 0; i < data.tokens.length; ++i) { for (uint256 i = 0; i < data.tokens.length; ++i) {
@ -79,7 +74,7 @@ contract PayTakerTransformer is
amount = data.tokens[i].getTokenBalanceOf(address(this)); amount = data.tokens[i].getTokenBalanceOf(address(this));
} }
if (amount != 0) { if (amount != 0) {
data.tokens[i].transformerTransfer(taker, amount); data.tokens[i].transformerTransfer(context.taker, amount);
} }
} }
return LibERC20Transformer.TRANSFORMER_SUCCESS; return LibERC20Transformer.TRANSFORMER_SUCCESS;

View File

@ -59,22 +59,18 @@ contract WethTransformer is
} }
/// @dev Wraps and unwraps WETH. /// @dev Wraps and unwraps WETH.
/// @param data_ ABI-encoded `TransformData`, indicating which token to wrap/umwrap. /// @param context Context information.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). /// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform( function transform(TransformContext calldata context)
bytes32, // callDataHash,
address payable, // taker,
bytes calldata data_
)
external external
override override
returns (bytes4 success) returns (bytes4 success)
{ {
TransformData memory data = abi.decode(data_, (TransformData)); TransformData memory data = abi.decode(context.data, (TransformData));
if (!data.token.isTokenETH() && data.token != weth) { if (!data.token.isTokenETH() && data.token != weth) {
LibTransformERC20RichErrors.InvalidTransformDataError( LibTransformERC20RichErrors.InvalidTransformDataError(
LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_TOKENS, LibTransformERC20RichErrors.InvalidTransformDataErrorCode.INVALID_TOKENS,
data_ context.data
).rrevert(); ).rrevert();
} }

View File

@ -31,6 +31,8 @@ contract TestFillQuoteTransformerHost is
IERC20Transformer transformer, IERC20Transformer transformer,
TestMintableERC20Token inputToken, TestMintableERC20Token inputToken,
uint256 inputTokenAmount, uint256 inputTokenAmount,
address payable sender,
address payable taker,
bytes calldata data bytes calldata data
) )
external external
@ -40,6 +42,14 @@ contract TestFillQuoteTransformerHost is
inputToken.mint(address(this), inputTokenAmount); inputToken.mint(address(this), inputTokenAmount);
} }
// Have to make this call externally because transformers aren't payable. // Have to make this call externally because transformers aren't payable.
this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); this.rawExecuteTransform(
transformer,
IERC20Transformer.TransformContext({
callDataHash: bytes32(0),
sender: sender,
taker: taker,
data: data
})
);
} }
} }

View File

@ -20,6 +20,7 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "../src/features/TransformERC20.sol"; import "../src/features/TransformERC20.sol";
import "../src/features/IMetaTransactions.sol";
contract TestMetaTransactionsTransformERC20Feature is contract TestMetaTransactionsTransformERC20Feature is
@ -48,6 +49,49 @@ contract TestMetaTransactionsTransformERC20Feature is
revert('FAIL'); revert('FAIL');
} }
if (msg.value == 777) {
// Try to reenter `executeMetaTransaction()`
IMetaTransactions(address(this)).executeMetaTransaction(
IMetaTransactions.MetaTransactionData({
signer: address(0),
sender: address(0),
minGasPrice: 0,
maxGasPrice: 0,
expirationTimeSeconds: 0,
salt: 0,
callData: "",
value: 0,
feeToken: IERC20TokenV06(0),
feeAmount: 0
}),
""
);
}
if (msg.value == 888) {
// Try to reenter `batchExecuteMetaTransactions()`
IMetaTransactions.MetaTransactionData[] memory mtxs =
new IMetaTransactions.MetaTransactionData[](1);
bytes[] memory signatures = new bytes[](1);
mtxs[0] = IMetaTransactions.MetaTransactionData({
signer: address(0),
sender: address(0),
minGasPrice: 0,
maxGasPrice: 0,
expirationTimeSeconds: 0,
salt: 0,
callData: "",
value: 0,
feeToken: IERC20TokenV06(0),
feeAmount: 0
});
signatures[0] = "";
IMetaTransactions(address(this)).batchExecuteMetaTransactions(
mtxs,
signatures
);
}
emit TransformERC20Called( emit TransformERC20Called(
msg.sender, msg.sender,
msg.value, msg.value,

View File

@ -40,28 +40,26 @@ contract TestMintTokenERC20Transformer is
address context, address context,
address caller, address caller,
bytes32 callDataHash, bytes32 callDataHash,
address sender,
address taker, address taker,
bytes data, bytes data,
uint256 inputTokenBalance, uint256 inputTokenBalance,
uint256 ethBalance uint256 ethBalance
); );
function transform( function transform(TransformContext calldata context)
bytes32 callDataHash,
address payable taker,
bytes calldata data_
)
external external
override override
returns (bytes4 success) returns (bytes4 success)
{ {
TransformData memory data = abi.decode(data_, (TransformData)); TransformData memory data = abi.decode(context.data, (TransformData));
emit MintTransform( emit MintTransform(
address(this), address(this),
msg.sender, msg.sender,
callDataHash, context.callDataHash,
taker, context.sender,
data_, context.taker,
context.data,
data.inputToken.balanceOf(address(this)), data.inputToken.balanceOf(address(this)),
address(this).balance address(this).balance
); );
@ -69,14 +67,14 @@ contract TestMintTokenERC20Transformer is
data.inputToken.transfer(address(0), data.burnAmount); data.inputToken.transfer(address(0), data.burnAmount);
// Mint output tokens. // Mint output tokens.
if (LibERC20Transformer.isTokenETH(IERC20TokenV06(address(data.outputToken)))) { if (LibERC20Transformer.isTokenETH(IERC20TokenV06(address(data.outputToken)))) {
taker.transfer(data.mintAmount); context.taker.transfer(data.mintAmount);
} else { } else {
data.outputToken.mint( data.outputToken.mint(
taker, context.taker,
data.mintAmount data.mintAmount
); );
// Burn fees from output. // Burn fees from output.
data.outputToken.burn(taker, data.feeAmount); data.outputToken.burn(context.taker, data.feeAmount);
} }
return LibERC20Transformer.TRANSFORMER_SUCCESS; return LibERC20Transformer.TRANSFORMER_SUCCESS;
} }

View File

@ -20,17 +20,15 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
import "../src/transformers/Transformer.sol"; import "../src/transformers/Transformer.sol";
import "../src/transformers/IERC20Transformer.sol";
import "../src/transformers/LibERC20Transformer.sol"; import "../src/transformers/LibERC20Transformer.sol";
contract TestTransformerBase is contract TestTransformerBase is
IERC20Transformer,
Transformer Transformer
{ {
function transform( function transform(TransformContext calldata context)
bytes32,
address payable,
bytes calldata
)
external external
override override
returns (bytes4 success) returns (bytes4 success)

View File

@ -32,18 +32,14 @@ contract TestTransformerHost {
function rawExecuteTransform( function rawExecuteTransform(
IERC20Transformer transformer, IERC20Transformer transformer,
bytes32 callDataHash, IERC20Transformer.TransformContext calldata context
address taker,
bytes calldata data
) )
external external
{ {
(bool _success, bytes memory resultData) = (bool _success, bytes memory resultData) =
address(transformer).delegatecall(abi.encodeWithSelector( address(transformer).delegatecall(abi.encodeWithSelector(
transformer.transform.selector, transformer.transform.selector,
callDataHash, context
taker,
data
)); ));
if (!_success) { if (!_success) {
resultData.rrevert(); resultData.rrevert();

View File

@ -48,6 +48,14 @@ contract TestWethTransformerHost is
_weth.deposit{value: wethAmount}(); _weth.deposit{value: wethAmount}();
} }
// Have to make this call externally because transformers aren't payable. // Have to make this call externally because transformers aren't payable.
this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data); this.rawExecuteTransform(
transformer,
IERC20Transformer.TransformContext({
callDataHash: bytes32(0),
sender: msg.sender,
taker: msg.sender,
data: data
})
);
} }
} }

View File

@ -42,6 +42,7 @@ import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRic
import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json'; import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json';
import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json'; import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json';
import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json'; import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json';
import * as LibReentrancyGuardStorage from '../test/generated-artifacts/LibReentrancyGuardStorage.json';
import * as LibSignatureRichErrors from '../test/generated-artifacts/LibSignatureRichErrors.json'; import * as LibSignatureRichErrors from '../test/generated-artifacts/LibSignatureRichErrors.json';
import * as LibSignedCallData from '../test/generated-artifacts/LibSignedCallData.json'; import * as LibSignedCallData from '../test/generated-artifacts/LibSignedCallData.json';
import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json'; import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json';
@ -147,6 +148,7 @@ export const artifacts = {
LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact, LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact,
LibOwnableStorage: LibOwnableStorage as ContractArtifact, LibOwnableStorage: LibOwnableStorage as ContractArtifact,
LibProxyStorage: LibProxyStorage as ContractArtifact, LibProxyStorage: LibProxyStorage as ContractArtifact,
LibReentrancyGuardStorage: LibReentrancyGuardStorage as ContractArtifact,
LibSimpleFunctionRegistryStorage: LibSimpleFunctionRegistryStorage as ContractArtifact, LibSimpleFunctionRegistryStorage: LibSimpleFunctionRegistryStorage as ContractArtifact,
LibStorage: LibStorage as ContractArtifact, LibStorage: LibStorage as ContractArtifact,
LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact, LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact,

View File

@ -36,6 +36,10 @@ blockchainTests.resets('MetaTransactions feature', env => {
let allowanceTarget: string; let allowanceTarget: string;
const MAX_FEE_AMOUNT = new BigNumber('1e18'); const MAX_FEE_AMOUNT = new BigNumber('1e18');
const TRANSFORM_ERC20_FAILING_VALUE = new BigNumber(666);
const TRANSFORM_ERC20_REENTER_VALUE = new BigNumber(777);
const TRANSFORM_ERC20_BATCH_REENTER_VALUE = new BigNumber(888);
const REENTRANCY_FLAG_MTX = 0x1;
before(async () => { before(async () => {
[owner, sender, ...signers] = await env.getAccountAddressesAsync(); [owner, sender, ...signers] = await env.getAccountAddressesAsync();
@ -263,7 +267,7 @@ blockchainTests.resets('MetaTransactions feature', env => {
it('fails if the translated call fails', async () => { it('fails if the translated call fails', async () => {
const args = getRandomTransformERC20Args(); const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({ const mtx = getRandomMetaTransaction({
value: new BigNumber(666), value: new BigNumber(TRANSFORM_ERC20_FAILING_VALUE),
callData: transformERC20Feature callData: transformERC20Feature
.transformERC20( .transformERC20(
args.inputToken, args.inputToken,
@ -469,6 +473,72 @@ blockchainTests.resets('MetaTransactions feature', env => {
), ),
); );
}); });
it('cannot reenter `executeMetaTransaction()`', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
value: TRANSFORM_ERC20_REENTER_VALUE,
});
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.MetaTransactionCallFailedError(
mtxHash,
undefined,
new ZeroExRevertErrors.Common.IllegalReentrancyError(
feature.getSelector('executeMetaTransaction'),
REENTRANCY_FLAG_MTX,
).encode(),
),
);
});
it('cannot reenter `batchExecuteMetaTransactions()`', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
value: TRANSFORM_ERC20_BATCH_REENTER_VALUE,
});
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.MetaTransactionCallFailedError(
mtxHash,
undefined,
new ZeroExRevertErrors.Common.IllegalReentrancyError(
feature.getSelector('batchExecuteMetaTransactions'),
REENTRANCY_FLAG_MTX,
).encode(),
),
);
});
}); });
describe('batchExecuteMetaTransactions()', () => { describe('batchExecuteMetaTransactions()', () => {
@ -526,6 +596,102 @@ blockchainTests.resets('MetaTransactions feature', env => {
new ZeroExRevertErrors.MetaTransactions.MetaTransactionAlreadyExecutedError(mtxHash, block), new ZeroExRevertErrors.MetaTransactions.MetaTransactionAlreadyExecutedError(mtxHash, block),
); );
}); });
it('fails if a meta-transaction fails', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
value: new BigNumber(TRANSFORM_ERC20_FAILING_VALUE),
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.batchExecuteMetaTransactions([mtx], [signature]).callAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
mtxHash,
undefined,
new StringRevertError('FAIL').encode(),
),
);
});
it('cannot reenter `executeMetaTransaction()`', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
value: TRANSFORM_ERC20_REENTER_VALUE,
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.maxGasPrice,
value: mtx.value,
};
const tx = feature.batchExecuteMetaTransactions([mtx], [signature]).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
mtxHash,
undefined,
new ZeroExRevertErrors.Common.IllegalReentrancyError(
feature.getSelector('executeMetaTransaction'),
REENTRANCY_FLAG_MTX,
).encode(),
),
);
});
it('cannot reenter `batchExecuteMetaTransactions()`', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
callData: transformERC20Feature
.transformERC20(
args.inputToken,
args.outputToken,
args.inputTokenAmount,
args.minOutputTokenAmount,
args.transformations,
)
.getABIEncodedTransactionData(),
value: TRANSFORM_ERC20_BATCH_REENTER_VALUE,
});
const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
const signature = await signMetaTransactionAsync(mtx);
const callOpts = {
gasPrice: mtx.maxGasPrice,
value: mtx.value,
};
const tx = feature.batchExecuteMetaTransactions([mtx], [signature]).awaitTransactionSuccessAsync(callOpts);
return expect(tx).to.revertWith(
new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
mtxHash,
undefined,
new ZeroExRevertErrors.Common.IllegalReentrancyError(
feature.getSelector('batchExecuteMetaTransactions'),
REENTRANCY_FLAG_MTX,
).encode(),
),
);
});
}); });
describe('getMetaTransactionExecutedBlock()', () => { describe('getMetaTransactionExecutedBlock()', () => {

View File

@ -37,6 +37,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
const callDataSigner = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(callDataSignerKey))); const callDataSigner = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(callDataSignerKey)));
let owner: string; let owner: string;
let taker: string; let taker: string;
let sender: string;
let transformerDeployer: string; let transformerDeployer: string;
let zeroEx: ZeroExContract; let zeroEx: ZeroExContract;
let feature: TransformERC20Contract; let feature: TransformERC20Contract;
@ -44,7 +45,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
let allowanceTarget: string; let allowanceTarget: string;
before(async () => { before(async () => {
[owner, taker, transformerDeployer] = await env.getAccountAddressesAsync(); [owner, taker, sender, transformerDeployer] = await env.getAccountAddressesAsync();
zeroEx = await fullMigrateAsync( zeroEx = await fullMigrateAsync(
owner, owner,
env.provider, env.provider,
@ -59,12 +60,12 @@ blockchainTests.resets('TransformERC20 feature', env => {
}, },
{ transformerDeployer }, { transformerDeployer },
); );
feature = new TransformERC20Contract(zeroEx.address, env.provider, env.txDefaults, abis); feature = new TransformERC20Contract(zeroEx.address, env.provider, { ...env.txDefaults, from: sender }, abis);
wallet = new FlashWalletContract(await feature.getTransformWallet().callAsync(), env.provider, env.txDefaults); wallet = new FlashWalletContract(await feature.getTransformWallet().callAsync(), env.provider, env.txDefaults);
allowanceTarget = await new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults) allowanceTarget = await new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults)
.getAllowanceTarget() .getAllowanceTarget()
.callAsync(); .callAsync();
await feature.setQuoteSigner(callDataSigner).awaitTransactionSuccessAsync(); await feature.setQuoteSigner(callDataSigner).awaitTransactionSuccessAsync({ from: owner });
}); });
const { MAX_UINT256, ZERO_AMOUNT } = constants; const { MAX_UINT256, ZERO_AMOUNT } = constants;
@ -73,7 +74,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
it('createTransformWallet() replaces the current wallet', async () => { it('createTransformWallet() replaces the current wallet', async () => {
const newWalletAddress = await feature.createTransformWallet().callAsync({ from: owner }); const newWalletAddress = await feature.createTransformWallet().callAsync({ from: owner });
expect(newWalletAddress).to.not.eq(wallet.address); expect(newWalletAddress).to.not.eq(wallet.address);
await feature.createTransformWallet().awaitTransactionSuccessAsync(); await feature.createTransformWallet().awaitTransactionSuccessAsync({ from: owner });
return expect(feature.getTransformWallet().callAsync()).to.eventually.eq(newWalletAddress); return expect(feature.getTransformWallet().callAsync()).to.eventually.eq(newWalletAddress);
}); });
@ -264,8 +265,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
receipt.logs, receipt.logs,
[ [
{ {
callDataHash: NULL_BYTES32, sender,
taker, taker,
callDataHash: NULL_BYTES32,
context: wallet.address, context: wallet.address,
caller: zeroEx.address, caller: zeroEx.address,
data: transformation.data, data: transformation.data,
@ -320,8 +322,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
receipt.logs, receipt.logs,
[ [
{ {
callDataHash: NULL_BYTES32,
taker, taker,
sender,
callDataHash: NULL_BYTES32,
context: wallet.address, context: wallet.address,
caller: zeroEx.address, caller: zeroEx.address,
data: transformation.data, data: transformation.data,
@ -379,8 +382,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
receipt.logs, receipt.logs,
[ [
{ {
callDataHash: NULL_BYTES32, sender,
taker, taker,
callDataHash: NULL_BYTES32,
context: wallet.address, context: wallet.address,
caller: zeroEx.address, caller: zeroEx.address,
data: transformation.data, data: transformation.data,
@ -496,8 +500,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
receipt.logs, receipt.logs,
[ [
{ {
callDataHash: NULL_BYTES32, sender,
taker, taker,
callDataHash: NULL_BYTES32,
context: wallet.address, context: wallet.address,
caller: zeroEx.address, caller: zeroEx.address,
data: transformations[0].data, data: transformations[0].data,
@ -505,8 +510,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
ethBalance: callValue, ethBalance: callValue,
}, },
{ {
callDataHash: NULL_BYTES32, sender,
taker, taker,
callDataHash: NULL_BYTES32,
context: wallet.address, context: wallet.address,
caller: zeroEx.address, caller: zeroEx.address,
data: transformations[1].data, data: transformations[1].data,

View File

@ -86,7 +86,12 @@ blockchainTests.resets('AffiliateFeeTransformer', env => {
await mintHostTokensAsync(amounts[0]); await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]); await sendEtherAsync(host.address, amounts[1]);
await host await host
.rawExecuteTransform(transformer.address, hexUtils.random(), randomAddress(), data) .rawExecuteTransform(transformer.address, {
data,
callDataHash: hexUtils.random(),
sender: randomAddress(),
taker: randomAddress(),
})
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
expect(await getBalancesAsync(recipients[0])).to.deep.eq({ expect(await getBalancesAsync(recipients[0])).to.deep.eq({
@ -112,7 +117,12 @@ blockchainTests.resets('AffiliateFeeTransformer', env => {
await mintHostTokensAsync(amounts[0]); await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]); await sendEtherAsync(host.address, amounts[1]);
await host await host
.rawExecuteTransform(transformer.address, hexUtils.random(), randomAddress(), data) .rawExecuteTransform(transformer.address, {
data,
callDataHash: hexUtils.random(),
sender: randomAddress(),
taker: randomAddress(),
})
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
expect(await getBalancesAsync(recipients[0])).to.deep.eq({ expect(await getBalancesAsync(recipients[0])).to.deep.eq({
@ -138,7 +148,12 @@ blockchainTests.resets('AffiliateFeeTransformer', env => {
await mintHostTokensAsync(amounts[0]); await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]); await sendEtherAsync(host.address, amounts[1]);
await host await host
.rawExecuteTransform(transformer.address, hexUtils.random(), randomAddress(), data) .rawExecuteTransform(transformer.address, {
data,
callDataHash: hexUtils.random(),
sender: randomAddress(),
taker: randomAddress(),
})
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq({ expect(await getBalancesAsync(host.address)).to.deep.eq({
tokenBalance: new BigNumber(1), tokenBalance: new BigNumber(1),

View File

@ -32,6 +32,8 @@ const { NULL_ADDRESS, NULL_BYTES, MAX_UINT256, ZERO_AMOUNT } = constants;
blockchainTests.resets('FillQuoteTransformer', env => { blockchainTests.resets('FillQuoteTransformer', env => {
let maker: string; let maker: string;
let feeRecipient: string; let feeRecipient: string;
let sender: string;
let taker: string;
let exchange: TestFillQuoteTransformerExchangeContract; let exchange: TestFillQuoteTransformerExchangeContract;
let bridge: TestFillQuoteTransformerBridgeContract; let bridge: TestFillQuoteTransformerBridgeContract;
let transformer: FillQuoteTransformerContract; let transformer: FillQuoteTransformerContract;
@ -44,7 +46,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
const GAS_PRICE = 1337; const GAS_PRICE = 1337;
before(async () => { before(async () => {
[maker, feeRecipient] = await env.getAccountAddressesAsync(); [maker, feeRecipient, sender, taker] = await env.getAccountAddressesAsync();
exchange = await TestFillQuoteTransformerExchangeContract.deployFrom0xArtifactAsync( exchange = await TestFillQuoteTransformerExchangeContract.deployFrom0xArtifactAsync(
artifacts.TestFillQuoteTransformerExchange, artifacts.TestFillQuoteTransformerExchange,
env.provider, env.provider,
@ -92,7 +94,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
bridge = await TestFillQuoteTransformerBridgeContract.deployFrom0xArtifactAsync( bridge = await TestFillQuoteTransformerBridgeContract.deployFrom0xArtifactAsync(
artifacts.TestFillQuoteTransformerBridge, artifacts.TestFillQuoteTransformerBridge,
env.provider, env.provider,
env.txDefaults, { ...env.txDefaults, from: sender },
artifacts, artifacts,
); );
[makerToken, takerToken, takerFeeToken] = await Promise.all( [makerToken, takerToken, takerFeeToken] = await Promise.all(
@ -270,6 +272,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
signatures: [], signatures: [],
maxOrderFillAmounts: [], maxOrderFillAmounts: [],
fillAmount: MAX_UINT256, fillAmount: MAX_UINT256,
refundReceiver: NULL_ADDRESS,
...fields, ...fields,
}); });
} }
@ -313,6 +316,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -334,6 +339,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -358,6 +365,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -380,6 +389,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -404,6 +415,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -430,6 +443,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -462,6 +477,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -486,6 +503,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -511,6 +530,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
takerTokenBalance, takerTokenBalance,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -535,6 +556,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
takerTokenBalance, takerTokenBalance,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -564,6 +587,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -586,6 +611,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -608,6 +635,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -627,6 +656,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -640,6 +671,80 @@ blockchainTests.resets('FillQuoteTransformer', env => {
makerAssetBalance: qfr.makerAssetBought, makerAssetBalance: qfr.makerAssetBought,
}); });
}); });
it('can refund unspent protocol fee to the `refundReceiver`', async () => {
const orders = _.times(2, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const protocolFee = qfr.protocolFeePaid.plus(1);
const refundReceiver = randomAddress();
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({
orders,
signatures,
refundReceiver,
}),
)
.awaitTransactionSuccessAsync({ value: protocolFee });
const receiverBalancer = await env.web3Wrapper.getBalanceInWeiAsync(refundReceiver);
expect(receiverBalancer).to.bignumber.eq(1);
});
it('can refund unspent protocol fee to the taker', async () => {
const orders = _.times(2, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const protocolFee = qfr.protocolFeePaid.plus(1);
const refundReceiver = randomAddress();
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
sender,
refundReceiver, // taker = refundReceiver
encodeTransformData({
orders,
signatures,
// address(1) indicates taker
refundReceiver: hexUtils.leftPad(1, 20),
}),
)
.awaitTransactionSuccessAsync({ value: protocolFee });
const receiverBalancer = await env.web3Wrapper.getBalanceInWeiAsync(refundReceiver);
expect(receiverBalancer).to.bignumber.eq(1);
});
it('can refund unspent protocol fee to the sender', async () => {
const orders = _.times(2, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const protocolFee = qfr.protocolFeePaid.plus(1);
const refundReceiver = randomAddress();
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
refundReceiver, // sender = refundReceiver
taker,
encodeTransformData({
orders,
signatures,
// address(2) indicates sender
refundReceiver: hexUtils.leftPad(2, 20),
}),
)
.awaitTransactionSuccessAsync({ value: protocolFee });
const receiverBalancer = await env.web3Wrapper.getBalanceInWeiAsync(refundReceiver);
expect(receiverBalancer).to.bignumber.eq(1);
});
}); });
describe('buy quotes', () => { describe('buy quotes', () => {
@ -652,6 +757,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -675,6 +782,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -701,6 +810,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -725,6 +836,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -751,6 +864,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -774,6 +889,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -804,6 +921,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -828,6 +947,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -852,6 +973,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -873,6 +996,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -900,6 +1025,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -926,6 +1053,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -952,6 +1081,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,
@ -980,6 +1111,8 @@ blockchainTests.resets('FillQuoteTransformer', env => {
transformer.address, transformer.address,
takerToken.address, takerToken.address,
qfr.takerAssetSpent, qfr.takerAssetSpent,
sender,
taker,
encodeTransformData({ encodeTransformData({
orders, orders,
signatures, signatures,

View File

@ -78,7 +78,12 @@ blockchainTests.resets('PayTakerTransformer', env => {
await mintHostTokensAsync(amounts[0]); await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]); await sendEtherAsync(host.address, amounts[1]);
await host await host
.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) .rawExecuteTransform(transformer.address, {
data,
callDataHash: hexUtils.random(),
taker,
sender: randomAddress(),
})
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
expect(await getBalancesAsync(taker)).to.deep.eq({ expect(await getBalancesAsync(taker)).to.deep.eq({
@ -96,7 +101,12 @@ blockchainTests.resets('PayTakerTransformer', env => {
await mintHostTokensAsync(amounts[0]); await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]); await sendEtherAsync(host.address, amounts[1]);
await host await host
.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) .rawExecuteTransform(transformer.address, {
data,
callDataHash: hexUtils.random(),
taker,
sender: randomAddress(),
})
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
expect(await getBalancesAsync(taker)).to.deep.eq({ expect(await getBalancesAsync(taker)).to.deep.eq({
@ -114,7 +124,12 @@ blockchainTests.resets('PayTakerTransformer', env => {
await mintHostTokensAsync(amounts[0]); await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]); await sendEtherAsync(host.address, amounts[1]);
await host await host
.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) .rawExecuteTransform(transformer.address, {
data,
callDataHash: hexUtils.random(),
taker,
sender: randomAddress(),
})
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
expect(await getBalancesAsync(taker)).to.deep.eq({ expect(await getBalancesAsync(taker)).to.deep.eq({
@ -132,7 +147,12 @@ blockchainTests.resets('PayTakerTransformer', env => {
await mintHostTokensAsync(amounts[0]); await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]); await sendEtherAsync(host.address, amounts[1]);
await host await host
.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data) .rawExecuteTransform(transformer.address, {
data,
callDataHash: hexUtils.random(),
taker,
sender: randomAddress(),
})
.awaitTransactionSuccessAsync(); .awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq({ expect(await getBalancesAsync(host.address)).to.deep.eq({
tokenBalance: amounts[0].minus(amounts[0].dividedToIntegerBy(2)), tokenBalance: amounts[0].minus(amounts[0].dividedToIntegerBy(2)),

View File

@ -40,6 +40,7 @@ export * from '../test/generated-wrappers/lib_ownable_rich_errors';
export * from '../test/generated-wrappers/lib_ownable_storage'; export * from '../test/generated-wrappers/lib_ownable_storage';
export * from '../test/generated-wrappers/lib_proxy_rich_errors'; export * from '../test/generated-wrappers/lib_proxy_rich_errors';
export * from '../test/generated-wrappers/lib_proxy_storage'; export * from '../test/generated-wrappers/lib_proxy_storage';
export * from '../test/generated-wrappers/lib_reentrancy_guard_storage';
export * from '../test/generated-wrappers/lib_signature_rich_errors'; export * from '../test/generated-wrappers/lib_signature_rich_errors';
export * from '../test/generated-wrappers/lib_signed_call_data'; export * from '../test/generated-wrappers/lib_signed_call_data';
export * from '../test/generated-wrappers/lib_simple_function_registry_rich_errors'; export * from '../test/generated-wrappers/lib_simple_function_registry_rich_errors';

View File

@ -32,6 +32,7 @@
"test/generated-artifacts/FillQuoteTransformer.json", "test/generated-artifacts/FillQuoteTransformer.json",
"test/generated-artifacts/FixinCommon.json", "test/generated-artifacts/FixinCommon.json",
"test/generated-artifacts/FixinEIP712.json", "test/generated-artifacts/FixinEIP712.json",
"test/generated-artifacts/FixinReentrancyGuard.json",
"test/generated-artifacts/FlashWallet.json", "test/generated-artifacts/FlashWallet.json",
"test/generated-artifacts/FullMigration.json", "test/generated-artifacts/FullMigration.json",
"test/generated-artifacts/IAllowanceTarget.json", "test/generated-artifacts/IAllowanceTarget.json",
@ -62,6 +63,7 @@
"test/generated-artifacts/LibOwnableStorage.json", "test/generated-artifacts/LibOwnableStorage.json",
"test/generated-artifacts/LibProxyRichErrors.json", "test/generated-artifacts/LibProxyRichErrors.json",
"test/generated-artifacts/LibProxyStorage.json", "test/generated-artifacts/LibProxyStorage.json",
"test/generated-artifacts/LibReentrancyGuardStorage.json",
"test/generated-artifacts/LibSignatureRichErrors.json", "test/generated-artifacts/LibSignatureRichErrors.json",
"test/generated-artifacts/LibSignedCallData.json", "test/generated-artifacts/LibSignedCallData.json",
"test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json", "test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json",

View File

@ -85,6 +85,10 @@
{ {
"note": "Enable Quote Report to be generated with an option `shouldGenerateQuoteReport`. Default is `false`", "note": "Enable Quote Report to be generated with an option `shouldGenerateQuoteReport`. Default is `false`",
"pr": 2687 "pr": 2687
},
{
"note": "Add `refundReceiver` to `ExchangeProxySwapQuoteConsumer` options.",
"pr": 2657
} }
] ]
}, },

View File

@ -89,6 +89,7 @@ export {
AffiliateFee, AffiliateFee,
CalldataInfo, CalldataInfo,
ExchangeProxyContractOpts, ExchangeProxyContractOpts,
ExchangeProxyRefundReceiver,
ExtensionContractType, ExtensionContractType,
ForwarderExtensionContractOpts, ForwarderExtensionContractOpts,
GetExtensionContractTypeOpts, GetExtensionContractTypeOpts,

View File

@ -84,7 +84,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
): Promise<CalldataInfo> { ): Promise<CalldataInfo> {
assert.isValidSwapQuote('quote', quote); assert.isValidSwapQuote('quote', quote);
// tslint:disable-next-line:no-object-literal-type-assertion // tslint:disable-next-line:no-object-literal-type-assertion
const { affiliateFee, isFromETH, isToETH } = { const { refundReceiver, affiliateFee, isFromETH, isToETH } = {
...constants.DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS, ...constants.DEFAULT_EXCHANGE_PROXY_EXTENSION_CONTRACT_OPTS,
...opts.extensionContractOpts, ...opts.extensionContractOpts,
} as ExchangeProxyContractOpts; } as ExchangeProxyContractOpts;
@ -116,6 +116,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
sellToken, sellToken,
buyToken: intermediateToken, buyToken: intermediateToken,
side: FillQuoteTransformerSide.Sell, side: FillQuoteTransformerSide.Sell,
refundReceiver: refundReceiver || NULL_ADDRESS,
fillAmount: firstHopOrder.takerAssetAmount, fillAmount: firstHopOrder.takerAssetAmount,
maxOrderFillAmounts: [], maxOrderFillAmounts: [],
orders: [firstHopOrder], orders: [firstHopOrder],
@ -125,8 +126,9 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
transforms.push({ transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer, deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({ data: encodeFillQuoteTransformerData({
sellToken: intermediateToken,
buyToken, buyToken,
sellToken: intermediateToken,
refundReceiver: refundReceiver || NULL_ADDRESS,
side: FillQuoteTransformerSide.Sell, side: FillQuoteTransformerSide.Sell,
fillAmount: MAX_UINT256, fillAmount: MAX_UINT256,
maxOrderFillAmounts: [], maxOrderFillAmounts: [],
@ -140,6 +142,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
data: encodeFillQuoteTransformerData({ data: encodeFillQuoteTransformerData({
sellToken, sellToken,
buyToken, buyToken,
refundReceiver: refundReceiver || NULL_ADDRESS,
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell, side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount, fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount,
maxOrderFillAmounts: [], maxOrderFillAmounts: [],

View File

@ -135,15 +135,31 @@ export interface AffiliateFee {
sellTokenFeeAmount: BigNumber; sellTokenFeeAmount: BigNumber;
} }
/**
* Automatically resolved protocol fee refund receiver addresses.
*/
export enum ExchangeProxyRefundReceiver {
// Refund to the taker address.
Taker = '0x0000000000000000000000000000000000000001',
// Refund to the sender address.
Sender = '0x0000000000000000000000000000000000000002',
}
/** /**
* @param isFromETH Whether the input token is ETH. * @param isFromETH Whether the input token is ETH.
* @param isToETH Whether the output token is ETH. * @param isToETH Whether the output token is ETH.
* @param affiliateFee Fee denominated in taker or maker asset to send to specified recipient. * @param affiliateFee Fee denominated in taker or maker asset to send to specified recipient.
* @param refundReceiver The receiver of unspent protocol fees.
* May be a valid address or one of:
* `address(0)`: Stay in flash wallet.
* `address(1)`: Send to the taker.
* `address(2)`: Send to the sender (caller of `transformERC20()`).
*/ */
export interface ExchangeProxyContractOpts { export interface ExchangeProxyContractOpts {
isFromETH: boolean; isFromETH: boolean;
isToETH: boolean; isToETH: boolean;
affiliateFee: AffiliateFee; affiliateFee: AffiliateFee;
refundReceiver: string | ExchangeProxyRefundReceiver;
} }
export interface GetExtensionContractTypeOpts { export interface GetExtensionContractTypeOpts {

View File

@ -1,6 +1,6 @@
[ [
{ {
"version": "10.3.1", "version": "10.4.0",
"changes": [ "changes": [
{ {
"note": "Add gitpkg.", "note": "Add gitpkg.",
@ -9,6 +9,10 @@
{ {
"note": "Fix `decodeAffiliateFeeTransformerData`", "note": "Fix `decodeAffiliateFeeTransformerData`",
"pr": 2658 "pr": 2658
},
{
"note": "Add `refundReceiver` field to `FillQuoteTransformer.TransformData`.",
"pr": 2657
} }
] ]
}, },

View File

@ -37,6 +37,7 @@ export const fillQuoteTransformerDataEncoder = AbiEncoder.create([
{ name: 'signatures', type: 'bytes[]' }, { name: 'signatures', type: 'bytes[]' },
{ name: 'maxOrderFillAmounts', type: 'uint256[]' }, { name: 'maxOrderFillAmounts', type: 'uint256[]' },
{ name: 'fillAmount', type: 'uint256' }, { name: 'fillAmount', type: 'uint256' },
{ name: 'refundReceiver', type: 'address' },
], ],
}, },
]); ]);
@ -60,6 +61,7 @@ export interface FillQuoteTransformerData {
signatures: string[]; signatures: string[];
maxOrderFillAmounts: BigNumber[]; maxOrderFillAmounts: BigNumber[];
fillAmount: BigNumber; fillAmount: BigNumber;
refundReceiver: string;
} }
/** /**

View File

@ -5,6 +5,10 @@
{ {
"note": "Added support for nested rich revert decoding", "note": "Added support for nested rich revert decoding",
"pr": 2668 "pr": 2668
},
{
"note": "Add EP flavor of `IllegalReentrancyError`.",
"pr": 2657
} }
] ]
}, },

View File

@ -1,4 +1,5 @@
import { RevertError } from '../../revert_error'; import { RevertError } from '../../revert_error';
import { Numberish } from '../../types';
// tslint:disable:max-classes-per-file // tslint:disable:max-classes-per-file
export class OnlyCallableBySelfError extends RevertError { export class OnlyCallableBySelfError extends RevertError {
@ -9,14 +10,16 @@ export class OnlyCallableBySelfError extends RevertError {
} }
} }
// This is identical to the one in utils. export class IllegalReentrancyError extends RevertError {
// export class IllegalReentrancyError extends RevertError { constructor(selector?: string, reentrancyFlags?: Numberish) {
// constructor() { super('IllegalReentrancyError', 'IllegalReentrancyError(bytes4 selector, uint256 reentrancyFlags)', {
// super('IllegalReentrancyError', 'IllegalReentrancyError()', {}); selector,
// } reentrancyFlags,
// } });
}
}
const types = [OnlyCallableBySelfError]; const types = [OnlyCallableBySelfError, IllegalReentrancyError];
// Register the types we've defined. // Register the types we've defined.
for (const type of types) { for (const type of types) {