@0x/contracts-zero-ex: Introduce transformer contracts.

This commit is contained in:
Lawrence Forman
2020-04-09 13:20:53 -04:00
committed by Lawrence Forman
parent 0e1a5a375a
commit 2ba3818b65
24 changed files with 2392 additions and 37 deletions

View File

@@ -98,6 +98,21 @@ library LibTransformERC20RichErrors {
);
}
// Common Transformer errors ///////////////////////////////////////////////
function InvalidTransformDataError(
bytes memory transformData
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("InvalidTransformDataError(bytes)")),
transformData
);
}
// FillQuoteTransformer errors /////////////////////////////////////////////
function IncompleteFillSellQuoteError(
@@ -177,24 +192,7 @@ library LibTransformERC20RichErrors {
);
}
// WethTransformer errors ////////////////////////////////////////////////////
function WrongNumberOfTokensReceivedError(
uint256 actual,
uint256 expected
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("WrongNumberOfTokensReceivedError(uint256,uint256)")),
actual,
expected
);
}
function InvalidTokenReceivedError(
function InvalidTakerFeeTokenError(
address token
)
internal
@@ -202,8 +200,9 @@ library LibTransformERC20RichErrors {
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("InvalidTokenReceivedError(address)")),
bytes4(keccak256("InvalidTakerFeeTokenError(address)")),
token
);
}
}

View File

@@ -0,0 +1,418 @@
/*
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-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol";
import "../errors/LibTransformERC20RichErrors.sol";
import "../vendor/v3/IExchange.sol";
import "./IERC20Transformer.sol";
import "./LibERC20Transformer.sol";
/// @dev A transformer that fills an ERC20 market sell/buy quote.
contract FillQuoteTransformer is
IERC20Transformer
{
// solhint-disable indent,no-empty-blocks,no-unused-vars
/// @dev Transform data to ABI-encode and pass into `transform()`.
struct TransformData {
// The token being sold.
// This should be an actual token, not the ETH pseudo-token.
IERC20TokenV06 sellToken;
// The token being bought.
// This should be an actual token, not the ETH pseudo-token.
IERC20TokenV06 buyToken;
// The orders to fill.
IExchange.Order[] orders;
// Signatures for each respective order in `orders`.
bytes[] signatures;
// Maximum fill amount for each order. This may be shorter than the
// number of orders, where missing entries will be treated as `uint256(-1)`.
// For sells, this will be the maximum sell amount (taker asset).
// For buys, this will be the maximum buy amount (maker asset).
uint256[] maxOrderFillAmounts;
// Amount of `sellToken` to sell. May be `uint256(-1)` to sell entire
// amount of `sellToken` received. Zero if performing a market buy.
uint256 sellAmount;
// Amount of `buyToken` to buy. Zero if performing a market sell.
uint256 buyAmount;
}
/// @dev Results of a call to `_fillOrder()`.
struct FillOrderResults {
// The amount of taker tokens sold, according to balance checks.
uint256 takerTokenSoldAmount;
// The amount of maker tokens sold, according to balance checks.
uint256 makerTokenBoughtAmount;
// The amount of protocol fee paid.
uint256 protocolFeePaid;
}
/// @dev The Exchange ERC20Proxy ID.
bytes4 constant private ERC20_ASSET_PROXY_ID = 0xf47261b0;
/// @dev The Exchange contract.
IExchange public immutable exchange;
/// @dev The ERC20Proxy address.
address public immutable erc20Proxy;
using LibERC20TokenV06 for IERC20TokenV06;
using LibERC20Transformer for IERC20TokenV06;
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
constructor(IExchange exchange_) public {
exchange = exchange_;
erc20Proxy = exchange_.getAssetProxy(ERC20_ASSET_PROXY_ID);
}
/// @dev Sell this contract's entire balance of of `sellToken` in exchange
/// for `buyToken` by filling `orders`. Protocol fees should be attached
/// to this call. `buyToken` and excess ETH will be transferred back to the caller.
/// This function cannot be re-entered.
/// @param data_ ABI-encoded `TransformData`.
/// @return success `TRANSFORMER_SUCCESS` on success.
function transform(
bytes32, // callDataHash,
address payable, // taker,
bytes calldata data_
)
external
override
returns (bytes4 success)
{
TransformData memory data = abi.decode(data_, (TransformData));
// Validate data fields.
if (data.sellToken.isTokenETH() ||
data.buyToken.isTokenETH() ||
data.orders.length != data.signatures.length)
{
LibTransformERC20RichErrors.InvalidTransformDataError(data_).rrevert();
}
// If `sellAmount == -1` and `buyAmount == 0` then we are selling
// the entire balance of `sellToken`. This is useful in cases where
// the exact sell amount is not exactly known in advance, like when
// unwrapping Chai/cUSDC/cDAI.
if (data.sellAmount == uint256(-1) && data.buyAmount == 0) {
data.sellAmount = data.sellToken.getTokenBalanceOf(address(this));
}
// Approve the ERC20 proxy to spend `sellToken`.
data.sellToken.approveIfBelow(erc20Proxy, data.sellAmount);
// Fill the orders.
uint256 singleProtocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice);
uint256 ethRemaining = address(this).balance;
uint256 boughtAmount = 0;
uint256 soldAmount = 0;
for (uint256 i = 0; i < data.orders.length; ++i) {
// Check if we've hit our targets.
if (data.buyAmount == 0) {
// Market sell check.
if (soldAmount >= data.sellAmount) {
break;
}
} else {
// Market buy check.
if (boughtAmount >= data.buyAmount) {
break;
}
}
// Ensure we have enough ETH to cover the protocol fee.
if (ethRemaining < singleProtocolFee) {
LibTransformERC20RichErrors
.InsufficientProtocolFeeError(ethRemaining, singleProtocolFee)
.rrevert();
}
// Fill the order.
FillOrderResults memory results;
if (data.buyAmount == 0) {
// Market sell.
results = _sellToOrder(
data.buyToken,
data.sellToken,
data.orders[i],
data.signatures[i],
data.sellAmount.safeSub(soldAmount).min256(
data.maxOrderFillAmounts.length > i
? data.maxOrderFillAmounts[i]
: uint256(-1)
),
singleProtocolFee
);
} else {
// Market buy.
results = _buyFromOrder(
data.buyToken,
data.sellToken,
data.orders[i],
data.signatures[i],
data.buyAmount.safeSub(boughtAmount).min256(
data.maxOrderFillAmounts.length > i
? data.maxOrderFillAmounts[i]
: uint256(-1)
),
singleProtocolFee
);
}
// Accumulate totals.
soldAmount = soldAmount.safeAdd(results.takerTokenSoldAmount);
boughtAmount = boughtAmount.safeAdd(results.makerTokenBoughtAmount);
ethRemaining = ethRemaining.safeSub(results.protocolFeePaid);
}
// Ensure we hit our targets.
if (data.buyAmount == 0) {
// Market sell check.
if (soldAmount < data.sellAmount) {
LibTransformERC20RichErrors
.IncompleteFillSellQuoteError(
address(data.sellToken),
soldAmount,
data.sellAmount
).rrevert();
}
} else {
// Market buy check.
if (boughtAmount < data.buyAmount) {
LibTransformERC20RichErrors
.IncompleteFillBuyQuoteError(
address(data.buyToken),
boughtAmount,
data.buyAmount
).rrevert();
}
}
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
/// @dev Try to sell up to `sellAmount` from an order.
/// @param makerToken The maker/buy token.
/// @param takerToken The taker/sell token.
/// @param order The order to fill.
/// @param signature The signature for `order`.
/// @param sellAmount Amount of taker token to sell.
/// @param protocolFee The protocol fee needed to fill `order`.
function _sellToOrder(
IERC20TokenV06 makerToken,
IERC20TokenV06 takerToken,
IExchange.Order memory order,
bytes memory signature,
uint256 sellAmount,
uint256 protocolFee
)
private
returns (FillOrderResults memory results)
{
IERC20TokenV06 takerFeeToken = order.takerFeeAssetData.length == 0
? IERC20TokenV06(address(0))
: _getTokenFromERC20AssetData(order.takerFeeAssetData);
uint256 takerTokenFillAmount = sellAmount;
if (order.takerFee != 0) {
if (takerFeeToken == makerToken) {
// Taker fee is payable in the maker token, so we need to
// approve the proxy to spend the maker token.
// It isn't worth computing the actual taker fee
// since `approveIfBelow()` will set the allowance to infinite. We
// just need a reasonable upper bound to avoid unnecessarily re-approving.
takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee);
} else if (takerFeeToken == takerToken){
// Taker fee is payable in the taker token, so we need to
// reduce the fill amount to cover the fee.
// takerTokenFillAmount' =
// (takerTokenFillAmount * order.takerAssetAmount) /
// (order.takerAssetAmount + order.takerFee)
takerTokenFillAmount = LibMathV06.getPartialAmountCeil(
order.takerAssetAmount,
order.takerAssetAmount.safeAdd(order.takerFee),
takerTokenFillAmount
);
} else {
// Only support taker or maker asset denominated taker fees.
LibTransformERC20RichErrors.InvalidTakerFeeTokenError(
address(takerFeeToken)
).rrevert();
}
}
// Clamp fill amount to order size.
takerTokenFillAmount = LibSafeMathV06.min256(
takerTokenFillAmount,
order.takerAssetAmount
);
// Perform the fill.
return _fillOrder(
order,
signature,
takerTokenFillAmount,
protocolFee,
makerToken,
takerFeeToken == takerToken
);
}
/// @dev Try to buy up to `buyAmount` from an order.
/// @param makerToken The maker/buy token.
/// @param takerToken The taker/sell token.
/// @param order The order to fill.
/// @param signature The signature for `order`.
/// @param buyAmount Amount of maker token to buy.
/// @param protocolFee The protocol fee needed to fill `order`.
function _buyFromOrder(
IERC20TokenV06 makerToken,
IERC20TokenV06 takerToken,
IExchange.Order memory order,
bytes memory signature,
uint256 buyAmount,
uint256 protocolFee
)
private
returns (FillOrderResults memory results)
{
IERC20TokenV06 takerFeeToken = order.takerFeeAssetData.length == 0
? IERC20TokenV06(address(0))
: _getTokenFromERC20AssetData(order.takerFeeAssetData);
uint256 makerTokenFillAmount = buyAmount;
if (order.takerFee != 0) {
if (takerFeeToken == makerToken) {
// Taker fee is payable in the maker token.
// Increase the fill amount to account for maker tokens being
// lost to the taker fee.
// makerTokenFillAmount' =
// (order.makerAssetAmount * makerTokenFillAmount) /
// (order.makerAssetAmount - order.takerFee)
makerTokenFillAmount = LibMathV06.getPartialAmountCeil(
order.makerAssetAmount,
order.makerAssetAmount.safeSub(order.takerFee),
makerTokenFillAmount
);
// Approve the proxy to spend the maker token.
// It isn't worth computing the actual taker fee
// since `approveIfBelow()` will set the allowance to infinite. We
// just need a reasonable upper bound to avoid unnecessarily re-approving.
takerFeeToken.approveIfBelow(erc20Proxy, order.takerFee);
} else if (takerFeeToken != takerToken) {
// Only support taker or maker asset denominated taker fees.
LibTransformERC20RichErrors.InvalidTakerFeeTokenError(
address(takerFeeToken)
).rrevert();
}
}
// Convert maker fill amount to taker fill amount.
uint256 takerTokenFillAmount = LibSafeMathV06.min256(
order.takerAssetAmount,
LibMathV06.getPartialAmountCeil(
makerTokenFillAmount,
order.makerAssetAmount,
order.takerAssetAmount
)
);
// Perform the fill.
return _fillOrder(
order,
signature,
takerTokenFillAmount,
protocolFee,
makerToken,
takerFeeToken == takerToken
);
}
/// @dev Attempt to fill an order. If the fill reverts, the revert will be
/// swallowed and `results` will be zeroed out.
/// @param order The order to fill.
/// @param signature The order signature.
/// @param takerAssetFillAmount How much taker asset to fill.
/// @param protocolFee The protocol fee needed to fill this order.
/// @param makerToken The maker token.
/// @param isTakerFeeInTakerToken Whether the taker fee token is the same as the
/// taker token.
function _fillOrder(
IExchange.Order memory order,
bytes memory signature,
uint256 takerAssetFillAmount,
uint256 protocolFee,
IERC20TokenV06 makerToken,
bool isTakerFeeInTakerToken
)
private
returns (FillOrderResults memory results)
{
// Track changes in the maker token balance.
results.makerTokenBoughtAmount = makerToken.balanceOf(address(this));
try
exchange.fillOrder
{value: protocolFee}
(order, takerAssetFillAmount, signature)
returns (IExchange.FillResults memory fillResults)
{
// Update maker quantity based on changes in token balances.
results.makerTokenBoughtAmount = makerToken.balanceOf(address(this))
.safeSub(results.makerTokenBoughtAmount);
// We can trust the other fill result quantities.
results.protocolFeePaid = fillResults.protocolFeePaid;
results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount;
// If the taker fee is payable in the taker asset, include the
// taker fee in the total amount sold.
if (isTakerFeeInTakerToken) {
results.takerTokenSoldAmount =
results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid);
}
} catch (bytes memory) {
// If the fill fails, zero out fill quantities.
results.makerTokenBoughtAmount = 0;
}
}
/// @dev Extract the token from plain ERC20 asset data.
/// @param assetData The order asset data.
function _getTokenFromERC20AssetData(bytes memory assetData)
private
pure
returns (IERC20TokenV06 token)
{
if (assetData.length != 36 ||
LibBytesV06.readBytes4(assetData, 0) != ERC20_ASSET_PROXY_ID)
{
LibTransformERC20RichErrors
.InvalidERC20AssetDataError(assetData)
.rrevert();
}
return IERC20TokenV06(LibBytesV06.readAddress(assetData, 16));
}
}

View File

@@ -0,0 +1,77 @@
/*
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/LibSafeMathV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "../errors/LibTransformERC20RichErrors.sol";
import "./IERC20Transformer.sol";
import "./LibERC20Transformer.sol";
/// @dev A transformer that transfers tokens to the taker.
contract PayTakerTransformer is
IERC20Transformer
{
/// @dev Transform data to ABI-encode and pass into `transform()`.
struct TransformData {
// The tokens to transfer to the taker.
IERC20TokenV06[] tokens;
// Amount of each token in `tokens` to transfer to the taker.
// `uint(-1)` will transfer the entire balance.
uint256[] amounts;
}
using LibRichErrorsV06 for bytes;
using LibSafeMathV06 for uint256;
using LibERC20Transformer for IERC20TokenV06;
/// @dev Forwards tokens to the taker.
/// @param taker The taker address (caller of `TransformERC20.transformERC20()`).
/// @param data_ ABI-encoded `TransformData`, indicating which tokens to transfer.
/// @return success `TRANSFORMER_SUCCESS` on success.
function transform(
bytes32, // callDataHash,
address payable taker,
bytes calldata data_
)
external
override
returns (bytes4 success)
{
TransformData memory data = abi.decode(data_, (TransformData));
// Transfer tokens directly to the taker.
for (uint256 i = 0; i < data.tokens.length; ++i) {
// The `amounts` array can be shorter than the `tokens` array.
// Missing elements are treated as `uint256(-1)`.
uint256 amount = data.amounts.length > i ? data.amounts[i] : uint256(-1);
if (amount == uint256(-1)) {
amount = data.tokens[i].getTokenBalanceOf(address(this));
}
if (amount != 0) {
data.tokens[i].transformerTransfer(taker, amount);
}
}
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
}

View File

@@ -0,0 +1,91 @@
/*
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/LibSafeMathV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "../errors/LibTransformERC20RichErrors.sol";
import "./IERC20Transformer.sol";
import "./LibERC20Transformer.sol";
/// @dev A transformer that wraps or unwraps WETH.
contract WethTransformer is
IERC20Transformer
{
/// @dev Transform data to ABI-encode and pass into `transform()`.
struct TransformData {
// The token to wrap/unwrap. Must be either ETH or WETH.
IERC20TokenV06 token;
// Amount of `token` to wrap or unwrap.
// `uint(-1)` will unwrap the entire balance.
uint256 amount;
}
// solhint-disable
/// @dev The WETH contract address.
IEtherTokenV06 public immutable weth;
// solhint-enable
using LibRichErrorsV06 for bytes;
using LibSafeMathV06 for uint256;
using LibERC20Transformer for IERC20TokenV06;
/// @dev Construct the transformer and store the WETH address in an immutable.
/// @param weth_ The weth token.
constructor(IEtherTokenV06 weth_) public {
weth = weth_;
}
/// @dev Wraps and unwraps WETH.
/// @param data_ ABI-encoded `TransformData`, indicating which token to wrap/umwrap.
/// @return success `TRANSFORMER_SUCCESS` on success.
function transform(
bytes32, // callDataHash,
address payable, // taker,
bytes calldata data_
)
external
override
returns (bytes4 success)
{
TransformData memory data = abi.decode(data_, (TransformData));
if (!data.token.isTokenETH() && data.token != weth) {
LibTransformERC20RichErrors.InvalidTransformDataError(data_).rrevert();
}
uint256 amount = data.amount;
if (amount == uint256(-1)) {
amount = data.token.getTokenBalanceOf(address(this));
}
if (amount != 0) {
if (data.token.isTokenETH()) {
// Wrap ETH.
weth.deposit{value: amount}();
} else {
// Unwrap WETH.
weth.withdraw(amount);
}
}
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
}

View File

@@ -0,0 +1,107 @@
/*
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 Interface to the V3 Exchange.
interface IExchange {
/// @dev V3 Order structure.
struct Order {
// Address that created the order.
address makerAddress;
// Address that is allowed to fill the order.
// If set to 0, any address is allowed to fill the order.
address takerAddress;
// Address that will recieve fees when order is filled.
address feeRecipientAddress;
// Address that is allowed to call Exchange contract methods that affect this order.
// If set to 0, any address is allowed to call these methods.
address senderAddress;
// Amount of makerAsset being offered by maker. Must be greater than 0.
uint256 makerAssetAmount;
// Amount of takerAsset being bid on by maker. Must be greater than 0.
uint256 takerAssetAmount;
// Fee paid to feeRecipient by maker when order is filled.
uint256 makerFee;
// Fee paid to feeRecipient by taker when order is filled.
uint256 takerFee;
// Timestamp in seconds at which order expires.
uint256 expirationTimeSeconds;
// Arbitrary number to facilitate uniqueness of the order's hash.
uint256 salt;
// Encoded data that can be decoded by a specified proxy contract when transferring makerAsset.
// The leading bytes4 references the id of the asset proxy.
bytes makerAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring takerAsset.
// The leading bytes4 references the id of the asset proxy.
bytes takerAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring makerFeeAsset.
// The leading bytes4 references the id of the asset proxy.
bytes makerFeeAssetData;
// Encoded data that can be decoded by a specified proxy contract when transferring takerFeeAsset.
// The leading bytes4 references the id of the asset proxy.
bytes takerFeeAssetData;
}
/// @dev V3 `fillOrder()` results.`
struct FillResults {
// Total amount of makerAsset(s) filled.
uint256 makerAssetFilledAmount;
// Total amount of takerAsset(s) filled.
uint256 takerAssetFilledAmount;
// Total amount of fees paid by maker(s) to feeRecipient(s).
uint256 makerFeePaid;
// Total amount of fees paid by taker to feeRecipients(s).
uint256 takerFeePaid;
// Total amount of fees paid by taker to the staking contract.
uint256 protocolFeePaid;
}
/// @dev Fills the input order.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker.
/// @return fillResults Amounts filled and fees paid by maker and taker.
function fillOrder(
Order calldata order,
uint256 takerAssetFillAmount,
bytes calldata signature
)
external
payable
returns (FillResults memory fillResults);
/// @dev Returns the protocolFeeMultiplier
/// @return multiplier The multiplier for protocol fees.
function protocolFeeMultiplier()
external
view
returns (uint256 multiplier);
/// @dev Gets an asset proxy.
/// @param assetProxyId Id of the asset proxy.
/// @return proxyAddress The asset proxy registered to assetProxyId.
/// Returns 0x0 if no proxy is registered.
function getAssetProxy(bytes4 assetProxyId)
external
view
returns (address proxyAddress);
}

View File

@@ -0,0 +1,145 @@
/*
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/LibMathV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../src/vendor/v3/IExchange.sol";
import "./TestMintableERC20Token.sol";
contract TestFillQuoteTransformerExchange {
struct FillBehavior {
// How much of the order is filled, in taker asset amount.
uint256 filledTakerAssetAmount;
// Scaling for maker assets minted, in 1e18.
uint256 makerAssetMintRatio;
}
uint256 private constant PROTOCOL_FEE_MULTIPLIER = 1337;
using LibSafeMathV06 for uint256;
function fillOrder(
IExchange.Order calldata order,
uint256 takerAssetFillAmount,
bytes calldata signature
)
external
payable
returns (IExchange.FillResults memory fillResults)
{
require(
signature.length != 0,
"TestFillQuoteTransformerExchange/INVALID_SIGNATURE"
);
// The signature is the ABI-encoded FillBehavior data.
FillBehavior memory behavior = abi.decode(signature, (FillBehavior));
uint256 protocolFee = PROTOCOL_FEE_MULTIPLIER * tx.gasprice;
require(
msg.value == protocolFee,
"TestFillQuoteTransformerExchange/INSUFFICIENT_PROTOCOL_FEE"
);
// Return excess protocol fee.
msg.sender.transfer(msg.value - protocolFee);
// Take taker tokens.
TestMintableERC20Token takerToken = _getTokenFromAssetData(order.takerAssetData);
takerAssetFillAmount = LibSafeMathV06.min256(
order.takerAssetAmount.safeSub(behavior.filledTakerAssetAmount),
takerAssetFillAmount
);
require(
takerToken.getSpendableAmount(msg.sender, address(this)) >= takerAssetFillAmount,
"TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FUNDS"
);
takerToken.transferFrom(msg.sender, order.makerAddress, takerAssetFillAmount);
// Mint maker tokens.
uint256 makerAssetFilledAmount = LibMathV06.getPartialAmountFloor(
takerAssetFillAmount,
order.takerAssetAmount,
order.makerAssetAmount
);
TestMintableERC20Token makerToken = _getTokenFromAssetData(order.makerAssetData);
makerToken.mint(
msg.sender,
LibMathV06.getPartialAmountFloor(
behavior.makerAssetMintRatio,
1e18,
makerAssetFilledAmount
)
);
// Take taker fee.
TestMintableERC20Token takerFeeToken = _getTokenFromAssetData(order.takerFeeAssetData);
uint256 takerFee = LibMathV06.getPartialAmountFloor(
takerAssetFillAmount,
order.takerAssetAmount,
order.takerFee
);
require(
takerFeeToken.getSpendableAmount(msg.sender, address(this)) >= takerFee,
"TestFillQuoteTransformerExchange/INSUFFICIENT_TAKER_FEE_FUNDS"
);
takerFeeToken.transferFrom(msg.sender, order.feeRecipientAddress, takerFee);
fillResults.makerAssetFilledAmount = makerAssetFilledAmount;
fillResults.takerAssetFilledAmount = takerAssetFillAmount;
fillResults.makerFeePaid = uint256(-1);
fillResults.takerFeePaid = takerFee;
fillResults.protocolFeePaid = protocolFee;
}
function encodeBehaviorData(FillBehavior calldata behavior)
external
pure
returns (bytes memory encoded)
{
return abi.encode(behavior);
}
function protocolFeeMultiplier()
external
pure
returns (uint256)
{
return PROTOCOL_FEE_MULTIPLIER;
}
function getAssetProxy(bytes4)
external
view
returns (address)
{
return address(this);
}
function _getTokenFromAssetData(bytes memory assetData)
private
pure
returns (TestMintableERC20Token token)
{
return TestMintableERC20Token(LibBytesV06.readAddress(assetData, 16));
}
}

View File

@@ -0,0 +1,45 @@
/*
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/transformers/IERC20Transformer.sol";
import "./TestMintableERC20Token.sol";
import "./TestTransformerHost.sol";
contract TestFillQuoteTransformerHost is
TestTransformerHost
{
function executeTransform(
IERC20Transformer transformer,
TestMintableERC20Token inputToken,
uint256 inputTokenAmount,
bytes calldata data
)
external
payable
{
if (inputTokenAmount != 0) {
inputToken.mint(address(this), inputTokenAmount);
}
// Have to make this call externally because transformers aren't payable.
this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data);
}
}

View File

@@ -74,4 +74,14 @@ contract TestMintableERC20Token {
balanceOf[to] += amount;
return true;
}
function getSpendableAmount(address owner, address spender)
external
view
returns (uint256)
{
return balanceOf[owner] < allowance[owner][spender]
? balanceOf[owner]
: allowance[owner][spender];
}
}

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/errors/LibRichErrorsV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "../src/transformers/IERC20Transformer.sol";
import "../src/transformers/LibERC20Transformer.sol";
contract TestTransformerHost {
using LibERC20Transformer for IERC20TokenV06;
using LibRichErrorsV06 for bytes;
function rawExecuteTransform(
IERC20Transformer transformer,
bytes32 callDataHash,
address taker,
bytes calldata data
)
external
{
(bool success, bytes memory resultData) =
address(transformer).delegatecall(abi.encodeWithSelector(
transformer.transform.selector,
callDataHash,
taker,
data
));
if (!success) {
resultData.rrevert();
}
require(
abi.decode(resultData, (bytes4)) == LibERC20Transformer.TRANSFORMER_SUCCESS,
"TestFillQuoteTransformerTaker/UNSUCCESSFUL_RESULT"
);
}
// solhint-disable
receive() external payable {}
// solhint-enable
}

View File

@@ -0,0 +1,42 @@
/*
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 "./TestMintableERC20Token.sol";
contract TestWeth is
TestMintableERC20Token
{
function deposit()
external
payable
{
this.mint(msg.sender, msg.value);
}
function withdraw(uint256 amount)
external
{
require(balanceOf[msg.sender] >= amount, "TestWeth/INSUFFICIENT_FUNDS");
balanceOf[msg.sender] -= amount;
msg.sender.transfer(amount);
}
}

View File

@@ -0,0 +1,53 @@
/*
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/transformers/IERC20Transformer.sol";
import "./TestMintableERC20Token.sol";
import "./TestTransformerHost.sol";
import "./TestWeth.sol";
contract TestWethTransformerHost is
TestTransformerHost
{
// solhint-disable
TestWeth private immutable _weth;
// solhint-enable
constructor(TestWeth weth) public {
_weth = weth;
}
function executeTransform(
uint256 wethAmount,
IERC20Transformer transformer,
bytes calldata data
)
external
payable
{
if (wethAmount != 0) {
_weth.deposit{value: wethAmount}();
}
// Have to make this call externally because transformers aren't payable.
this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data);
}
}

View File

@@ -38,7 +38,7 @@
"docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES"
},
"config": {
"publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20",
"publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AllowanceTarget|Bootstrap|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IFeature|IFlashWallet|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|SimpleFunctionRegistry|TestCallTarget|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestZeroExFeature|TokenSpender|TransformERC20|ZeroEx).json"
},
@@ -56,6 +56,7 @@
"@0x/contracts-gen": "^2.0.8",
"@0x/contracts-test-utils": "^5.3.2",
"@0x/dev-utils": "^3.2.1",
"@0x/order-utils": "^10.2.4",
"@0x/sol-compiler": "^4.0.8",
"@0x/subproviders": "^6.0.8",
"@0x/ts-doc-gen": "^0.0.22",

View File

@@ -5,25 +5,31 @@
*/
import { ContractArtifact } from 'ethereum-types';
import * as FillQuoteTransformer from '../generated-artifacts/FillQuoteTransformer.json';
import * as FullMigration from '../generated-artifacts/FullMigration.json';
import * as IAllowanceTarget from '../generated-artifacts/IAllowanceTarget.json';
import * as IERC20Transformer from '../generated-artifacts/IERC20Transformer.json';
import * as IFlashWallet from '../generated-artifacts/IFlashWallet.json';
import * as InitialMigration from '../generated-artifacts/InitialMigration.json';
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 LibERC20Transformer from '../generated-artifacts/LibERC20Transformer.json';
import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json';
import * as Puppet from '../generated-artifacts/Puppet.json';
import * as WethTransformer from '../generated-artifacts/WethTransformer.json';
import * as ZeroEx from '../generated-artifacts/ZeroEx.json';
export const artifacts = {
ZeroEx: ZeroEx as ContractArtifact,
FullMigration: FullMigration as ContractArtifact,
InitialMigration: InitialMigration as ContractArtifact,
IFlashWallet: IFlashWallet as ContractArtifact,
IAllowanceTarget: IAllowanceTarget as ContractArtifact,
Puppet: Puppet as ContractArtifact,
IERC20Transformer: IERC20Transformer as ContractArtifact,
IOwnable: IOwnable as ContractArtifact,
ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact,
ITokenSpender: ITokenSpender as ContractArtifact,
ITransformERC20: ITransformERC20 as ContractArtifact,
LibERC20Transformer: LibERC20Transformer as ContractArtifact,
PayTakerTransformer: PayTakerTransformer as ContractArtifact,
WethTransformer: WethTransformer as ContractArtifact,
FillQuoteTransformer: FillQuoteTransformer as ContractArtifact,
};

View File

@@ -0,0 +1,4 @@
/*
* The pseudo-token address for ETH used by `tranformERC20()`.
*/
export const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';

View File

@@ -1,9 +1,14 @@
export { artifacts } from './artifacts';
export {
FillQuoteTransformerContract,
IOwnableContract,
IOwnableEvents,
ISimpleFunctionRegistryContract,
ISimpleFunctionRegistryEvents,
ITokenSpenderContract,
ITransformERC20Contract,
PayTakerTransformerContract,
WethTransformerContract,
ZeroExContract,
} from './wrappers';
export { ZeroExRevertErrors } from '@0x/utils';
@@ -36,4 +41,6 @@ export {
TupleDataItem,
StateMutability,
} from 'ethereum-types';
export { rlpEncodeNonce } from './nonce_utils';
export * from './constants';
export * from './transformer_data_encoders';

View File

@@ -0,0 +1,114 @@
import { Order } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
const ORDER_ABI_COMPONENTS = [
{ name: 'makerAddress', type: 'address' },
{ name: 'takerAddress', type: 'address' },
{ name: 'feeRecipientAddress', type: 'address' },
{ name: 'senderAddress', type: 'address' },
{ name: 'makerAssetAmount', type: 'uint256' },
{ name: 'takerAssetAmount', type: 'uint256' },
{ name: 'makerFee', type: 'uint256' },
{ name: 'takerFee', type: 'uint256' },
{ name: 'expirationTimeSeconds', type: 'uint256' },
{ name: 'salt', type: 'uint256' },
{ name: 'makerAssetData', type: 'bytes' },
{ name: 'takerAssetData', type: 'bytes' },
{ name: 'makerFeeAssetData', type: 'bytes' },
{ name: 'takerFeeAssetData', type: 'bytes' },
];
/**
* ABI encoder for `FillQuoteTransformer.TransformData`
*/
export const fillQuoteTransformerDataEncoder = AbiEncoder.create([
{
name: 'data',
type: 'tuple',
components: [
{ name: 'sellToken', type: 'address' },
{ name: 'buyToken', type: 'address' },
{
name: 'orders',
type: 'tuple[]',
components: ORDER_ABI_COMPONENTS,
},
{ name: 'signatures', type: 'bytes[]' },
{ name: 'maxOrderFillAmounts', type: 'uint256[]' },
{ name: 'sellAmount', type: 'uint256' },
{ name: 'buyAmount', type: 'uint256' },
],
},
]);
/**
* `FillQuoteTransformer.TransformData`
*/
export interface FillQuoteTransformerData {
sellToken: string;
buyToken: string;
orders: Array<Exclude<Order, ['signature', 'exchangeAddress', 'chainId']>>;
signatures: string[];
maxOrderFillAmounts: BigNumber[];
sellAmount: BigNumber;
buyAmount: BigNumber;
}
/**
* ABI-encode a `FillQuoteTransformer.TransformData` type.
*/
export function encodeFillQuoteTransformerData(data: FillQuoteTransformerData): string {
return fillQuoteTransformerDataEncoder.encode([data]);
}
/**
* ABI encoder for `WethTransformer.TransformData`
*/
export const wethTransformerDataEncoder = AbiEncoder.create([
{
name: 'data',
type: 'tuple',
components: [{ name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }],
},
]);
/**
* `WethTransformer.TransformData`
*/
export interface WethTransformerData {
token: string;
amount: BigNumber;
}
/**
* ABI-encode a `WethTransformer.TransformData` type.
*/
export function encodeWethTransformerData(data: WethTransformerData): string {
return wethTransformerDataEncoder.encode([data]);
}
/**
* ABI encoder for `PayTakerTransformer.TransformData`
*/
export const payTakerTransformerDataEncoder = AbiEncoder.create([
{
name: 'data',
type: 'tuple',
components: [{ name: 'tokens', type: 'address[]' }, { name: 'amounts', type: 'uint256[]' }],
},
]);
/**
* `PayTakerTransformer.TransformData`
*/
export interface PayTakerTransformerData {
tokens: string[];
amounts: BigNumber[];
}
/**
* ABI-encode a `PayTakerTransformer.TransformData` type.
*/
export function encodePayTakerTransformerData(data: PayTakerTransformerData): string {
return payTakerTransformerDataEncoder.encode([data]);
}

View File

@@ -3,13 +3,16 @@
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
* -----------------------------------------------------------------------------
*/
export * from '../generated-wrappers/fill_quote_transformer';
export * from '../generated-wrappers/full_migration';
export * from '../generated-wrappers/i_allowance_target';
export * from '../generated-wrappers/i_erc20_transformer';
export * from '../generated-wrappers/i_flash_wallet';
export * from '../generated-wrappers/i_ownable';
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/lib_erc20_transformer';
export * from '../generated-wrappers/pay_taker_transformer';
export * from '../generated-wrappers/puppet';
export * from '../generated-wrappers/weth_transformer';
export * from '../generated-wrappers/zero_ex';

View File

@@ -7,16 +7,17 @@ import { ContractArtifact } from 'ethereum-types';
import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json';
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 FlashWallet from '../test/generated-artifacts/FlashWallet.json';
import * as FullMigration from '../test/generated-artifacts/FullMigration.json';
import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json';
import * as IBootstrap from '../test/generated-artifacts/IBootstrap.json';
import * as IERC20Transformer from '../test/generated-artifacts/IERC20Transformer.json';
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 InitialMigration from '../test/generated-artifacts/InitialMigration.json';
import * as IOwnable from '../test/generated-artifacts/IOwnable.json';
import * as IPuppet from '../test/generated-artifacts/IPuppet.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';
@@ -29,6 +30,7 @@ import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRic
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 LibPuppetRichErrors from '../test/generated-artifacts/LibPuppetRichErrors.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';
@@ -36,37 +38,44 @@ import * as LibStorage from '../test/generated-artifacts/LibStorage.json';
import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpenderStorage.json';
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 Ownable from '../test/generated-artifacts/Ownable.json';
import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json';
import * as Puppet from '../test/generated-artifacts/Puppet.json';
import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json';
import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json';
import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json';
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 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';
import * as TestPuppetTarget from '../test/generated-artifacts/TestPuppetTarget.json';
import * as TestSimpleFunctionRegistryFeatureImpl1 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json';
import * as TestSimpleFunctionRegistryFeatureImpl2 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json';
import * as TestTokenSpender from '../test/generated-artifacts/TestTokenSpender.json';
import * as TestTokenSpenderERC20Token from '../test/generated-artifacts/TestTokenSpenderERC20Token.json';
import * as TestTransformERC20 from '../test/generated-artifacts/TestTransformERC20.json';
import * as TestTransformerHost from '../test/generated-artifacts/TestTransformerHost.json';
import * as TestWeth from '../test/generated-artifacts/TestWeth.json';
import * as TestWethTransformerHost from '../test/generated-artifacts/TestWethTransformerHost.json';
import * as TestZeroExFeature from '../test/generated-artifacts/TestZeroExFeature.json';
import * as TokenSpender from '../test/generated-artifacts/TokenSpender.json';
import * as TransformERC20 from '../test/generated-artifacts/TransformERC20.json';
import * as WethTransformer from '../test/generated-artifacts/WethTransformer.json';
import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json';
export const artifacts = {
ZeroEx: ZeroEx as ContractArtifact,
LibCommonRichErrors: LibCommonRichErrors as ContractArtifact,
LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact,
LibProxyRichErrors: LibProxyRichErrors as ContractArtifact,
LibPuppetRichErrors: LibPuppetRichErrors as ContractArtifact,
LibSimpleFunctionRegistryRichErrors: LibSimpleFunctionRegistryRichErrors as ContractArtifact,
LibSpenderRichErrors: LibSpenderRichErrors as ContractArtifact,
LibTransformERC20RichErrors: LibTransformERC20RichErrors as ContractArtifact,
LibWalletRichErrors: LibWalletRichErrors as ContractArtifact,
AllowanceTarget: AllowanceTarget as ContractArtifact,
FlashWallet: FlashWallet as ContractArtifact,
IAllowanceTarget: IAllowanceTarget as ContractArtifact,
IFlashWallet: IFlashWallet as ContractArtifact,
IPuppet: IPuppet as ContractArtifact,
Puppet: Puppet as ContractArtifact,
Bootstrap: Bootstrap as ContractArtifact,
IBootstrap: IBootstrap as ContractArtifact,
IFeature: IFeature as ContractArtifact,
@@ -89,19 +98,28 @@ export const artifacts = {
LibStorage: LibStorage as ContractArtifact,
LibTokenSpenderStorage: LibTokenSpenderStorage as ContractArtifact,
LibTransformERC20Storage: LibTransformERC20Storage as ContractArtifact,
FillQuoteTransformer: FillQuoteTransformer as ContractArtifact,
IERC20Transformer: IERC20Transformer as ContractArtifact,
LibERC20Transformer: LibERC20Transformer as ContractArtifact,
PayTakerTransformer: PayTakerTransformer as ContractArtifact,
WethTransformer: WethTransformer as ContractArtifact,
IExchange: IExchange as ContractArtifact,
ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact,
TestCallTarget: TestCallTarget as ContractArtifact,
TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact,
TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact,
TestFullMigration: TestFullMigration as ContractArtifact,
TestInitialMigration: TestInitialMigration as ContractArtifact,
TestMigrator: TestMigrator as ContractArtifact,
TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact,
TestMintableERC20Token: TestMintableERC20Token as ContractArtifact,
TestPuppetTarget: TestPuppetTarget as ContractArtifact,
TestSimpleFunctionRegistryFeatureImpl1: TestSimpleFunctionRegistryFeatureImpl1 as ContractArtifact,
TestSimpleFunctionRegistryFeatureImpl2: TestSimpleFunctionRegistryFeatureImpl2 as ContractArtifact,
TestTokenSpender: TestTokenSpender as ContractArtifact,
TestTokenSpenderERC20Token: TestTokenSpenderERC20Token as ContractArtifact,
TestTransformERC20: TestTransformERC20 as ContractArtifact,
TestTransformerHost: TestTransformerHost as ContractArtifact,
TestWeth: TestWeth as ContractArtifact,
TestWethTransformerHost: TestWethTransformerHost as ContractArtifact,
TestZeroExFeature: TestZeroExFeature as ContractArtifact,
};

View File

@@ -10,6 +10,7 @@ import {
} from '@0x/contracts-test-utils';
import { AbiEncoder, hexUtils, ZeroExRevertErrors } from '@0x/utils';
import { ETH_TOKEN_ADDRESS } from '../../src/constants';
import { getRLPEncodedAccountNonceAsync } from '../../src/nonce_utils';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
@@ -206,8 +207,6 @@ blockchainTests.resets('TransformERC20 feature', env => {
);
});
const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount, with ETH", async () => {
const startingInputTokenBalance = getRandomInteger(0, '100e18');
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();

View File

@@ -0,0 +1,849 @@
import {
assertIntegerRoughlyEquals,
blockchainTests,
constants,
expect,
getRandomInteger,
Numberish,
randomAddress,
} from '@0x/contracts-test-utils';
import { assetDataUtils } from '@0x/order-utils';
import { Order } from '@0x/types';
import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
import { encodeFillQuoteTransformerData, FillQuoteTransformerData } from '../../src/transformer_data_encoders';
import { artifacts } from '../artifacts';
import {
FillQuoteTransformerContract,
TestFillQuoteTransformerExchangeContract,
TestFillQuoteTransformerHostContract,
TestMintableERC20TokenContract,
} from '../wrappers';
const { NULL_ADDRESS, NULL_BYTES, MAX_UINT256, ZERO_AMOUNT } = constants;
blockchainTests.resets('FillQuoteTransformer', env => {
let maker: string;
let feeRecipient: string;
let exchange: TestFillQuoteTransformerExchangeContract;
let transformer: FillQuoteTransformerContract;
let host: TestFillQuoteTransformerHostContract;
let makerToken: TestMintableERC20TokenContract;
let takerToken: TestMintableERC20TokenContract;
let takerFeeToken: TestMintableERC20TokenContract;
let singleProtocolFee: BigNumber;
const GAS_PRICE = 1337;
before(async () => {
[maker, feeRecipient] = await env.getAccountAddressesAsync();
exchange = await TestFillQuoteTransformerExchangeContract.deployFrom0xArtifactAsync(
artifacts.TestFillQuoteTransformerExchange,
env.provider,
env.txDefaults,
artifacts,
);
transformer = await FillQuoteTransformerContract.deployFrom0xArtifactAsync(
artifacts.FillQuoteTransformer,
env.provider,
env.txDefaults,
artifacts,
exchange.address,
);
host = await TestFillQuoteTransformerHostContract.deployFrom0xArtifactAsync(
artifacts.TestFillQuoteTransformerHost,
env.provider,
{
...env.txDefaults,
gasPrice: GAS_PRICE,
},
artifacts,
);
[makerToken, takerToken, takerFeeToken] = await Promise.all(
_.times(3, async () =>
TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC20Token,
env.provider,
env.txDefaults,
artifacts,
),
),
);
singleProtocolFee = (await exchange.protocolFeeMultiplier().callAsync()).times(GAS_PRICE);
});
type FilledOrder = Order & { filledTakerAssetAmount: BigNumber };
function createOrder(fields: Partial<Order> = {}): FilledOrder {
return {
chainId: 1,
exchangeAddress: exchange.address,
expirationTimeSeconds: ZERO_AMOUNT,
salt: ZERO_AMOUNT,
senderAddress: NULL_ADDRESS,
takerAddress: NULL_ADDRESS,
makerAddress: maker,
feeRecipientAddress: feeRecipient,
makerAssetAmount: getRandomInteger('0.1e18', '1e18'),
takerAssetAmount: getRandomInteger('0.1e18', '1e18'),
makerFee: ZERO_AMOUNT,
takerFee: getRandomInteger('0.001e18', '0.1e18'),
makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address),
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address),
filledTakerAssetAmount: ZERO_AMOUNT,
...fields,
};
}
interface QuoteFillResults {
makerAssetBought: BigNumber;
takerAssetSpent: BigNumber;
protocolFeePaid: BigNumber;
}
const ZERO_QUOTE_FILL_RESULTS = {
makerAssetBought: ZERO_AMOUNT,
takerAssetSpent: ZERO_AMOUNT,
protocolFeePaid: ZERO_AMOUNT,
};
function getExpectedSellQuoteFillResults(
orders: FilledOrder[],
takerAssetFillAmount: BigNumber = constants.MAX_UINT256,
): QuoteFillResults {
const qfr = { ...ZERO_QUOTE_FILL_RESULTS };
for (const order of orders) {
if (qfr.takerAssetSpent.gte(takerAssetFillAmount)) {
break;
}
const singleFillAmount = BigNumber.min(
takerAssetFillAmount.minus(qfr.takerAssetSpent),
order.takerAssetAmount.minus(order.filledTakerAssetAmount),
);
const fillRatio = singleFillAmount.div(order.takerAssetAmount);
qfr.takerAssetSpent = qfr.takerAssetSpent.plus(singleFillAmount);
qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee);
qfr.makerAssetBought = qfr.makerAssetBought.plus(
fillRatio.times(order.makerAssetAmount).integerValue(BigNumber.ROUND_DOWN),
);
const takerFee = fillRatio.times(order.takerFee).integerValue(BigNumber.ROUND_DOWN);
if (order.takerAssetData === order.takerFeeAssetData) {
// Taker fee is in taker asset.
qfr.takerAssetSpent = qfr.takerAssetSpent.plus(takerFee);
} else if (order.makerAssetData === order.takerFeeAssetData) {
// Taker fee is in maker asset.
qfr.makerAssetBought = qfr.makerAssetBought.minus(takerFee);
}
}
return qfr;
}
function getExpectedBuyQuoteFillResults(
orders: FilledOrder[],
makerAssetFillAmount: BigNumber = constants.MAX_UINT256,
): QuoteFillResults {
const qfr = { ...ZERO_QUOTE_FILL_RESULTS };
for (const order of orders) {
if (qfr.makerAssetBought.gte(makerAssetFillAmount)) {
break;
}
const filledMakerAssetAmount = order.filledTakerAssetAmount
.times(order.makerAssetAmount.div(order.takerAssetAmount))
.integerValue(BigNumber.ROUND_DOWN);
const singleFillAmount = BigNumber.min(
makerAssetFillAmount.minus(qfr.makerAssetBought),
order.makerAssetAmount.minus(filledMakerAssetAmount),
);
const fillRatio = singleFillAmount.div(order.makerAssetAmount);
qfr.takerAssetSpent = qfr.takerAssetSpent.plus(
fillRatio.times(order.takerAssetAmount).integerValue(BigNumber.ROUND_UP),
);
qfr.protocolFeePaid = qfr.protocolFeePaid.plus(singleProtocolFee);
qfr.makerAssetBought = qfr.makerAssetBought.plus(singleFillAmount);
const takerFee = fillRatio.times(order.takerFee).integerValue(BigNumber.ROUND_UP);
if (order.takerAssetData === order.takerFeeAssetData) {
// Taker fee is in taker asset.
qfr.takerAssetSpent = qfr.takerAssetSpent.plus(takerFee);
} else if (order.makerAssetData === order.takerFeeAssetData) {
// Taker fee is in maker asset.
qfr.makerAssetBought = qfr.makerAssetBought.minus(takerFee);
}
}
return qfr;
}
interface Balances {
makerAssetBalance: BigNumber;
takerAssetBalance: BigNumber;
takerFeeBalance: BigNumber;
protocolFeeBalance: BigNumber;
}
const ZERO_BALANCES = {
makerAssetBalance: ZERO_AMOUNT,
takerAssetBalance: ZERO_AMOUNT,
takerFeeBalance: ZERO_AMOUNT,
protocolFeeBalance: ZERO_AMOUNT,
};
async function getBalancesAsync(owner: string): Promise<Balances> {
const balances = { ...ZERO_BALANCES };
[
balances.makerAssetBalance,
balances.takerAssetBalance,
balances.takerFeeBalance,
balances.protocolFeeBalance,
] = await Promise.all([
makerToken.balanceOf(owner).callAsync(),
takerToken.balanceOf(owner).callAsync(),
takerFeeToken.balanceOf(owner).callAsync(),
env.web3Wrapper.getBalanceInWeiAsync(owner),
]);
return balances;
}
function assertBalances(actual: Balances, expected: Balances): void {
assertIntegerRoughlyEquals(actual.makerAssetBalance, expected.makerAssetBalance, 10, 'makerAssetBalance');
assertIntegerRoughlyEquals(actual.takerAssetBalance, expected.takerAssetBalance, 10, 'takerAssetBalance');
assertIntegerRoughlyEquals(actual.takerFeeBalance, expected.takerFeeBalance, 10, 'takerFeeBalance');
assertIntegerRoughlyEquals(actual.protocolFeeBalance, expected.protocolFeeBalance, 10, 'protocolFeeBalance');
}
function encodeTransformData(fields: Partial<FillQuoteTransformerData> = {}): string {
return encodeFillQuoteTransformerData({
sellToken: takerToken.address,
buyToken: makerToken.address,
orders: [],
signatures: [],
maxOrderFillAmounts: [],
sellAmount: MAX_UINT256,
buyAmount: ZERO_AMOUNT,
...fields,
});
}
function encodeExchangeBehavior(
filledTakerAssetAmount: Numberish = 0,
makerAssetMintRatio: Numberish = 1.0,
): string {
return hexUtils.slice(
exchange
.encodeBehaviorData({
filledTakerAssetAmount: new BigNumber(filledTakerAssetAmount),
makerAssetMintRatio: new BigNumber(makerAssetMintRatio).times('1e18').integerValue(),
})
.getABIEncodedTransactionData(),
4,
);
}
const ERC20_ASSET_PROXY_ID = '0xf47261b0';
describe('sell quotes', () => {
it('can fully sell to a single order quote', async () => {
const orders = _.times(1, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('can fully sell to multi order quote', async () => {
const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('can partially sell to single order quote', async () => {
const orders = _.times(1, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(
orders,
getExpectedSellQuoteFillResults(orders).takerAssetSpent.dividedToIntegerBy(2),
);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('can partially sell to multi order quote and refund unused protocol fees', async () => {
const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders.slice(0, 2));
const maxProtocolFees = singleProtocolFee.times(orders.length);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: maxProtocolFees });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
protocolFeeBalance: singleProtocolFee,
});
});
it('can sell to multi order quote with a failing order', async () => {
const orders = _.times(3, () => createOrder());
// First order will fail.
const validOrders = orders.slice(1);
const signatures = [NULL_BYTES, ...validOrders.map(() => encodeExchangeBehavior())];
const qfr = getExpectedSellQuoteFillResults(validOrders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('succeeds if an order transfers too few maker tokens', async () => {
const mintScale = 0.5;
const orders = _.times(3, () => createOrder());
// First order mints less than expected.
const signatures = [
encodeExchangeBehavior(0, mintScale),
...orders.slice(1).map(() => encodeExchangeBehavior()),
];
const qfr = getExpectedSellQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought
.minus(orders[0].makerAssetAmount.times(1 - mintScale))
.integerValue(BigNumber.ROUND_DOWN),
});
});
it('can fail if an order is partially filled', async () => {
const orders = _.times(3, () => createOrder());
// First order is partially filled.
const filledOrder = {
...orders[0],
filledTakerAssetAmount: orders[0].takerAssetAmount.dividedToIntegerBy(2),
};
// First order is partially filled.
const signatures = [
encodeExchangeBehavior(filledOrder.filledTakerAssetAmount),
...orders.slice(1).map(() => encodeExchangeBehavior()),
];
const qfr = getExpectedSellQuoteFillResults(orders);
const tx = host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError(
takerToken.address,
getExpectedSellQuoteFillResults([filledOrder, ...orders.slice(1)]).takerAssetSpent,
qfr.takerAssetSpent,
),
);
});
it('fails if not enough protocol fee provided', async () => {
const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const tx = host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid.minus(1) });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.TransformERC20.InsufficientProtocolFeeError(
singleProtocolFee.minus(1),
singleProtocolFee,
),
);
});
it('can sell less than the taker token balance', async () => {
const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const takerTokenBalance = qfr.takerAssetSpent.times(1.01).integerValue();
await host
.executeTransform(
transformer.address,
takerToken.address,
takerTokenBalance,
encodeTransformData({
orders,
signatures,
sellAmount: qfr.takerAssetSpent,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
takerAssetBalance: qfr.takerAssetSpent.times(0.01).integerValue(),
});
});
it('fails to sell more than the taker token balance', async () => {
const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const takerTokenBalance = qfr.takerAssetSpent.times(0.99).integerValue();
const tx = host
.executeTransform(
transformer.address,
takerToken.address,
takerTokenBalance,
encodeTransformData({
orders,
signatures,
sellAmount: qfr.takerAssetSpent,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.TransformERC20.IncompleteFillSellQuoteError(
takerToken.address,
getExpectedSellQuoteFillResults(orders.slice(0, 2)).takerAssetSpent,
qfr.takerAssetSpent,
),
);
});
it('can fully sell to a single order with maker asset taker fees', async () => {
const orders = _.times(1, () =>
createOrder({
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address),
}),
);
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('fails if an order has a non-standard taker fee asset', async () => {
const BAD_ASSET_DATA = hexUtils.random(36);
const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA }));
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const tx = host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.TransformERC20.InvalidERC20AssetDataError(BAD_ASSET_DATA),
);
});
it('fails if an order has a fee asset that is neither maker or taker asset', async () => {
const badToken = randomAddress();
const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken));
const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA }));
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const tx = host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTakerFeeTokenError(badToken));
});
it('respects `maxOrderFillAmounts`', async () => {
const orders = _.times(2, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders.slice(1));
const protocolFee = singleProtocolFee.times(2);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
// Skip the first order.
maxOrderFillAmounts: [ZERO_AMOUNT],
}),
)
.awaitTransactionSuccessAsync({ value: protocolFee });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
});
describe('buy quotes', () => {
it('can fully buy from a single order quote', async () => {
const orders = _.times(1, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedBuyQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('can fully buy from a multi order quote', async () => {
const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedBuyQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('can partially buy from a single order quote', async () => {
const orders = _.times(1, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedBuyQuoteFillResults(
orders,
getExpectedBuyQuoteFillResults(orders).makerAssetBought.dividedToIntegerBy(2),
);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('can partially buy from multi order quote and refund unused protocol fees', async () => {
const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedBuyQuoteFillResults(orders.slice(0, 2));
const maxProtocolFees = singleProtocolFee.times(orders.length);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: maxProtocolFees });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
protocolFeeBalance: singleProtocolFee,
});
});
it('can buy from multi order quote with a failing order', async () => {
const orders = _.times(3, () => createOrder());
// First order will fail.
const validOrders = orders.slice(1);
const signatures = [NULL_BYTES, ...validOrders.map(() => encodeExchangeBehavior())];
const qfr = getExpectedBuyQuoteFillResults(validOrders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('succeeds if an order transfers too many maker tokens', async () => {
const orders = _.times(2, () => createOrder());
// First order will mint its tokens + the maker tokens of the second.
const mintScale = orders[1].makerAssetAmount.div(orders[0].makerAssetAmount.minus(1)).plus(1);
const signatures = [
encodeExchangeBehavior(0, mintScale),
...orders.slice(1).map(() => encodeExchangeBehavior()),
];
const qfr = getExpectedBuyQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: orders[0].makerAssetAmount.times(mintScale).integerValue(BigNumber.ROUND_DOWN),
takerAssetBalance: orders[1].takerAssetAmount.plus(orders[1].takerFee),
protocolFeeBalance: singleProtocolFee,
});
});
it('fails to buy more than available in orders', async () => {
const orders = _.times(3, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedBuyQuoteFillResults(orders);
const tx = host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought.plus(1),
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.TransformERC20.IncompleteFillBuyQuoteError(
makerToken.address,
qfr.makerAssetBought,
qfr.makerAssetBought.plus(1),
),
);
});
it('can fully buy from a single order with maker asset taker fees', async () => {
const orders = _.times(1, () =>
createOrder({
takerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address),
}),
);
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedBuyQuoteFillResults(orders);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
it('fails if an order has a non-standard taker fee asset', async () => {
const BAD_ASSET_DATA = hexUtils.random(36);
const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA }));
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const tx = host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.TransformERC20.InvalidERC20AssetDataError(BAD_ASSET_DATA),
);
});
it('fails if an order has a fee asset that is neither maker or taker asset', async () => {
const badToken = randomAddress();
const BAD_ASSET_DATA = hexUtils.concat(ERC20_ASSET_PROXY_ID, hexUtils.leftPad(badToken));
const orders = _.times(1, () => createOrder({ takerFeeAssetData: BAD_ASSET_DATA }));
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const tx = host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
}),
)
.awaitTransactionSuccessAsync({ value: qfr.protocolFeePaid });
return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTakerFeeTokenError(badToken));
});
it('respects `maxOrderFillAmounts`', async () => {
const orders = _.times(2, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders.slice(1));
const protocolFee = singleProtocolFee.times(2);
await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
buyAmount: qfr.makerAssetBought,
// Skip the first order.
maxOrderFillAmounts: [ZERO_AMOUNT],
}),
)
.awaitTransactionSuccessAsync({ value: protocolFee });
assertBalances(await getBalancesAsync(host.address), {
...ZERO_BALANCES,
makerAssetBalance: qfr.makerAssetBought,
});
});
});
});

View File

@@ -0,0 +1,147 @@
import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash';
import { ETH_TOKEN_ADDRESS } from '../../src/constants';
import { encodePayTakerTransformerData } from '../../src/transformer_data_encoders';
import { artifacts } from '../artifacts';
import { PayTakerTransformerContract, TestMintableERC20TokenContract, TestTransformerHostContract } from '../wrappers';
const { MAX_UINT256, ZERO_AMOUNT } = constants;
blockchainTests.resets('PayTakerTransformer', env => {
let caller: string;
const taker = randomAddress();
let token: TestMintableERC20TokenContract;
let transformer: PayTakerTransformerContract;
let host: TestTransformerHostContract;
before(async () => {
[caller] = await env.getAccountAddressesAsync();
token = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
artifacts.TestMintableERC20Token,
env.provider,
env.txDefaults,
artifacts,
);
transformer = await PayTakerTransformerContract.deployFrom0xArtifactAsync(
artifacts.PayTakerTransformer,
env.provider,
env.txDefaults,
artifacts,
);
host = await TestTransformerHostContract.deployFrom0xArtifactAsync(
artifacts.TestTransformerHost,
env.provider,
{ ...env.txDefaults, from: caller },
artifacts,
);
});
interface Balances {
ethBalance: BigNumber;
tokenBalance: BigNumber;
}
const ZERO_BALANCES = {
ethBalance: ZERO_AMOUNT,
tokenBalance: ZERO_AMOUNT,
};
async function getBalancesAsync(owner: string): Promise<Balances> {
return {
ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(owner),
tokenBalance: await token.balanceOf(owner).callAsync(),
};
}
async function mintHostTokensAsync(amount: BigNumber): Promise<void> {
await token.mint(host.address, amount).awaitTransactionSuccessAsync();
}
async function sendEtherAsync(to: string, amount: BigNumber): Promise<void> {
await env.web3Wrapper.awaitTransactionSuccessAsync(
await env.web3Wrapper.sendTransactionAsync({
...env.txDefaults,
to,
from: caller,
value: amount,
}),
);
}
it('can transfer a token and ETH', async () => {
const amounts = _.times(2, () => getRandomInteger(1, '1e18'));
const data = encodePayTakerTransformerData({
amounts,
tokens: [token.address, ETH_TOKEN_ADDRESS],
});
await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]);
await host
.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data)
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
expect(await getBalancesAsync(taker)).to.deep.eq({
tokenBalance: amounts[0],
ethBalance: amounts[1],
});
});
it('can transfer all of a token and ETH', async () => {
const amounts = _.times(2, () => getRandomInteger(1, '1e18'));
const data = encodePayTakerTransformerData({
amounts: [MAX_UINT256, MAX_UINT256],
tokens: [token.address, ETH_TOKEN_ADDRESS],
});
await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]);
await host
.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data)
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
expect(await getBalancesAsync(taker)).to.deep.eq({
tokenBalance: amounts[0],
ethBalance: amounts[1],
});
});
it('can transfer all of a token and ETH (empty amounts)', async () => {
const amounts = _.times(2, () => getRandomInteger(1, '1e18'));
const data = encodePayTakerTransformerData({
amounts: [],
tokens: [token.address, ETH_TOKEN_ADDRESS],
});
await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]);
await host
.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data)
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES);
expect(await getBalancesAsync(taker)).to.deep.eq({
tokenBalance: amounts[0],
ethBalance: amounts[1],
});
});
it('can transfer less than the balance of a token and ETH', async () => {
const amounts = _.times(2, () => getRandomInteger(1, '1e18'));
const data = encodePayTakerTransformerData({
amounts: amounts.map(a => a.dividedToIntegerBy(2)),
tokens: [token.address, ETH_TOKEN_ADDRESS],
});
await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]);
await host
.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data)
.awaitTransactionSuccessAsync();
expect(await getBalancesAsync(host.address)).to.deep.eq({
tokenBalance: amounts[0].minus(amounts[0].dividedToIntegerBy(2)),
ethBalance: amounts[1].minus(amounts[1].dividedToIntegerBy(2)),
});
expect(await getBalancesAsync(taker)).to.deep.eq({
tokenBalance: amounts[0].dividedToIntegerBy(2),
ethBalance: amounts[1].dividedToIntegerBy(2),
});
});
});

View File

@@ -0,0 +1,147 @@
import { blockchainTests, constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
import { BigNumber, ZeroExRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
import { ETH_TOKEN_ADDRESS } from '../../src/constants';
import { encodeWethTransformerData } from '../../src/transformer_data_encoders';
import { artifacts } from '../artifacts';
import { TestWethContract, TestWethTransformerHostContract, WethTransformerContract } from '../wrappers';
const { MAX_UINT256, ZERO_AMOUNT } = constants;
blockchainTests.resets('WethTransformer', env => {
let weth: TestWethContract;
let transformer: WethTransformerContract;
let host: TestWethTransformerHostContract;
before(async () => {
weth = await TestWethContract.deployFrom0xArtifactAsync(
artifacts.TestWeth,
env.provider,
env.txDefaults,
artifacts,
);
transformer = await WethTransformerContract.deployFrom0xArtifactAsync(
artifacts.WethTransformer,
env.provider,
env.txDefaults,
artifacts,
weth.address,
);
host = await TestWethTransformerHostContract.deployFrom0xArtifactAsync(
artifacts.TestWethTransformerHost,
env.provider,
env.txDefaults,
artifacts,
weth.address,
);
});
interface Balances {
ethBalance: BigNumber;
wethBalance: BigNumber;
}
async function getHostBalancesAsync(): Promise<Balances> {
return {
ethBalance: await env.web3Wrapper.getBalanceInWeiAsync(host.address),
wethBalance: await weth.balanceOf(host.address).callAsync(),
};
}
it('fails if the token is neither ETH or WETH', async () => {
const amount = getRandomInteger(1, '1e18');
const data = encodeWethTransformerData({
amount,
token: randomAddress(),
});
const tx = host
.executeTransform(amount, transformer.address, data)
.awaitTransactionSuccessAsync({ value: amount });
return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidTransformDataError(data));
});
it('can unwrap WETH', async () => {
const amount = getRandomInteger(1, '1e18');
const data = encodeWethTransformerData({
amount,
token: weth.address,
});
await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount });
expect(await getHostBalancesAsync()).to.deep.eq({
ethBalance: amount,
wethBalance: ZERO_AMOUNT,
});
});
it('can unwrap all WETH', async () => {
const amount = getRandomInteger(1, '1e18');
const data = encodeWethTransformerData({
amount: MAX_UINT256,
token: weth.address,
});
await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount });
expect(await getHostBalancesAsync()).to.deep.eq({
ethBalance: amount,
wethBalance: ZERO_AMOUNT,
});
});
it('can unwrap some WETH', async () => {
const amount = getRandomInteger(1, '1e18');
const data = encodeWethTransformerData({
amount: amount.dividedToIntegerBy(2),
token: weth.address,
});
await host.executeTransform(amount, transformer.address, data).awaitTransactionSuccessAsync({ value: amount });
expect(await getHostBalancesAsync()).to.deep.eq({
ethBalance: amount.dividedToIntegerBy(2),
wethBalance: amount.minus(amount.dividedToIntegerBy(2)),
});
});
it('can wrap ETH', async () => {
const amount = getRandomInteger(1, '1e18');
const data = encodeWethTransformerData({
amount,
token: ETH_TOKEN_ADDRESS,
});
await host
.executeTransform(ZERO_AMOUNT, transformer.address, data)
.awaitTransactionSuccessAsync({ value: amount });
expect(await getHostBalancesAsync()).to.deep.eq({
ethBalance: ZERO_AMOUNT,
wethBalance: amount,
});
});
it('can wrap all ETH', async () => {
const amount = getRandomInteger(1, '1e18');
const data = encodeWethTransformerData({
amount: MAX_UINT256,
token: ETH_TOKEN_ADDRESS,
});
await host
.executeTransform(ZERO_AMOUNT, transformer.address, data)
.awaitTransactionSuccessAsync({ value: amount });
expect(await getHostBalancesAsync()).to.deep.eq({
ethBalance: ZERO_AMOUNT,
wethBalance: amount,
});
});
it('can wrap some ETH', async () => {
const amount = getRandomInteger(1, '1e18');
const data = encodeWethTransformerData({
amount: amount.dividedToIntegerBy(2),
token: ETH_TOKEN_ADDRESS,
});
await host
.executeTransform(ZERO_AMOUNT, transformer.address, data)
.awaitTransactionSuccessAsync({ value: amount });
expect(await getHostBalancesAsync()).to.deep.eq({
ethBalance: amount.minus(amount.dividedToIntegerBy(2)),
wethBalance: amount.dividedToIntegerBy(2),
});
});
});

View File

@@ -5,12 +5,14 @@
*/
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/flash_wallet';
export * from '../test/generated-wrappers/full_migration';
export * from '../test/generated-wrappers/i_allowance_target';
export * from '../test/generated-wrappers/i_bootstrap';
export * from '../test/generated-wrappers/i_erc20_transformer';
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_ownable';
@@ -49,8 +51,12 @@ export * from '../test/generated-wrappers/test_simple_function_registry_feature_
export * from '../test/generated-wrappers/test_token_spender';
export * from '../test/generated-wrappers/test_token_spender_erc20_token';
export * from '../test/generated-wrappers/test_transform_erc20';
export * from '../test/generated-wrappers/test_transformer_host';
export * from '../test/generated-wrappers/test_weth';
export * from '../test/generated-wrappers/test_weth_transformer_host';
export * from '../test/generated-wrappers/test_zero_ex_feature';
export * from '../test/generated-wrappers/token_spender';
export * from '../test/generated-wrappers/token_spender_puppet';
export * from '../test/generated-wrappers/transform_erc20';
export * from '../test/generated-wrappers/weth_transformer';
export * from '../test/generated-wrappers/zero_ex';

View File

@@ -3,6 +3,7 @@
"compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true },
"include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"],
"files": [
"generated-artifacts/FillQuoteTransformer.json",
"generated-artifacts/FullMigration.json",
"generated-artifacts/IAllowanceTarget.json",
"generated-artifacts/IERC20Transformer.json",
@@ -15,12 +16,14 @@
"generated-artifacts/ZeroEx.json",
"test/generated-artifacts/AllowanceTarget.json",
"test/generated-artifacts/Bootstrap.json",
"test/generated-artifacts/FillQuoteTransformer.json",
"test/generated-artifacts/FixinCommon.json",
"test/generated-artifacts/FlashWallet.json",
"test/generated-artifacts/FullMigration.json",
"test/generated-artifacts/IAllowanceTarget.json",
"test/generated-artifacts/IBootstrap.json",
"test/generated-artifacts/IERC20Transformer.json",
"test/generated-artifacts/IExchange.json",
"test/generated-artifacts/IFeature.json",
"test/generated-artifacts/IFlashWallet.json",
"test/generated-artifacts/IOwnable.json",
@@ -59,10 +62,14 @@
"test/generated-artifacts/TestTokenSpender.json",
"test/generated-artifacts/TestTokenSpenderERC20Token.json",
"test/generated-artifacts/TestTransformERC20.json",
"test/generated-artifacts/TestTransformerHost.json",
"test/generated-artifacts/TestWeth.json",
"test/generated-artifacts/TestWethTransformerHost.json",
"test/generated-artifacts/TestZeroExFeature.json",
"test/generated-artifacts/TokenSpender.json",
"test/generated-artifacts/TokenSpenderPuppet.json",
"test/generated-artifacts/TransformERC20.json",
"test/generated-artifacts/WethTransformer.json",
"test/generated-artifacts/ZeroEx.json"
],
"exclude": ["./deploy/solc/solc_bin"]