Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Commit

Permalink
@0x/contracts-zero-ex: Introduce the TransformERC20 feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
dorothy-zbornak authored and merklejerk committed May 6, 2020
1 parent b83875a commit 94d0da9
Show file tree
Hide file tree
Showing 13 changed files with 1,007 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@ library LibTransformERC20RichErrors {
);
}

function InvalidTokensReceivedError(
address[] memory tokens
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("InvalidTokensReceivedError(address[])")),
tokens
);
}

// WethTransformer rors ////////////////////////////////////////////////////

function WrongNumberOfTokensReceivedError(
Expand Down
331 changes: 331 additions & 0 deletions contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
/*
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 "@0x/contracts-utils/contracts/src/v06/ReentrancyGuardV06.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,
ReentrancyGuardV06
{
// solhint-disable indent,no-empty-blocks,no-unused-vars

/// @dev Data to encode and pass to `transform()`.
struct FillQuoteTransformData {
// 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.
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 ERC20Proxy ID.
bytes4 constant private ERC20_ASSET_PROXY_ID = 0xf47261b0;
/// @dev Received tokens index of the sell token.
uint256 constant private SELL_TOKEN_IDX = 0;
/// @dev Received tokens index of the ETH "token" (protocol fees).
uint256 constant private ETH_TOKEN_IDX = 1;

/// @dev The Exchange contract.
IExchange public immutable exchange;
/// @dev The ERC20Proxy address.
address public immutable erc20Proxy;

using LibERC20TokenV06 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 `FillQuoteTransformData`.
/// @return success `TRANSFORMER_SUCCESS` on success.
function transform(
bytes32, // callDataHash,
address payable, // taker,
IERC20TokenV06[] calldata tokens,
uint256[] calldata amounts,
bytes calldata data_
)
external
override
payable
nonReentrant
returns (bytes4 success)
{
FillQuoteTransformData memory data =
abi.decode(data_, (FillQuoteTransformData));

// We expect to receive two tokens: The sell token and ETH for the protocol fee.
if (tokens.length != 2 ||
tokens[SELL_TOKEN_IDX] != data.sellToken ||
!LibERC20Transformer.isTokenETH(tokens[ETH_TOKEN_IDX]))
{
LibTransformERC20RichErrors
.InvalidTokensReceivedError(_asAddressArray(tokens))
.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 known in advance, like when unwrapping
// Chai/cUSDC/cDAI.
if (data.sellAmount == uint256(-1) && data.buyAmount == 0) {
data.sellAmount = amounts[SELL_TOKEN_IDX];
}

// Approve the ERC20 proxy to spend `sellToken`.
data.sellToken.approveIfBelow(erc20Proxy, data.sellAmount);

// Fill the orders.
uint256 singleProtocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice);
uint256 boughtAmount = 0;
uint256 soldAmount = 0;
uint256 protocolFeesPaid = 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.
uint256 remainingETH = amounts[ETH_TOKEN_IDX].safeSub(protocolFeesPaid);
if (remainingETH < singleProtocolFee) {
LibTransformERC20RichErrors
.InsufficientProtocolFeeError(remainingETH, singleProtocolFee)
.rrevert();
}
}

// Compute the remaining fill amount.
uint256 fillAmount = (data.buyAmount == 0)
// Market sell.
? data.sellAmount.safeSub(soldAmount)
// Market buy, so compute the fill amount from the bought amount.
: data.sellAmount.safeSub(
LibMathV06.getPartialAmountFloor(
boughtAmount,
data.buyAmount,
data.sellAmount
)
);
if (data.maxOrderFillAmounts.length > i) {
fillAmount = LibSafeMathV06.min256(fillAmount, data.maxOrderFillAmounts[i]);
}

// Fill the order.
FillOrderResults memory results = _fillOrder(
data.buyToken,
data.orders[i],
fillAmount,
data.signatures[i],
singleProtocolFee
);

// Accumulate totals.
soldAmount = soldAmount.safeAdd(results.takerTokenSoldAmount);
boughtAmount = boughtAmount.safeAdd(results.makerTokenBoughtAmount);
protocolFeesPaid = protocolFeesPaid.safeAdd(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();
}
}

// Transfer buy tokens.
data.buyToken.compatTransfer(msg.sender, boughtAmount);
{
// Return unused sell tokens.
uint256 remainingSellToken = amounts[SELL_TOKEN_IDX].safeSub(soldAmount);
if (remainingSellToken != 0) {
data.sellToken.compatTransfer(msg.sender, remainingSellToken);
}
}
{
// Return unused ETH.
uint256 remainingETH = amounts[ETH_TOKEN_IDX].safeSub(protocolFeesPaid);
if (remainingETH != 0) {
msg.sender.transfer(remainingETH);
}
}
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}

// solhint-disable
/// @dev Allow this contract to receive protocol fee refunds.
receive() external payable {}
// solhint-enable

function _fillOrder(
IERC20TokenV06 makerToken,
IExchange.Order memory order,
uint256 takerTokenFillAmount,
bytes memory signature,
uint256 protocolFee
)
private
returns (FillOrderResults memory results)
{
takerTokenFillAmount = LibSafeMathV06.min256(takerTokenFillAmount, order.takerAssetAmount);
IERC20TokenV06 takerToken = _getTokenFromERC20AssetData(order.takerAssetData);
IERC20TokenV06 takerFeeToken = _getTokenFromERC20AssetData(order.takerFeeAssetData);

if (order.takerFee != 0) {
if (takerFeeToken != takerToken) {
// Taker fee is not payable in the taker token, so we need to
// approve the proxy to spend the fee 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 {
// Taker fee is payable in the taker token, so we need to
// reduce the fill amount by the fee to ensure we have enough
// to complete the fill.
uint256 takerFee = LibMathV06.getPartialAmountFloor(
takerTokenFillAmount,
order.takerAssetAmount,
order.takerFee
);
takerTokenFillAmount = takerTokenFillAmount.safeSub(takerFee);
}
}

// Track changes in the maker token balance.
results.makerTokenBoughtAmount = makerToken.balanceOf(address(this));

// Perform the fill.
try
exchange.fillOrder
{value: protocolFee}
(order, takerTokenFillAmount, signature)
returns (IExchange.FillResults memory fillResults)
{
results.protocolFeePaid = fillResults.protocolFeePaid;
// Update maker quantity based on changes in token balances.
results.makerTokenBoughtAmount = makerToken.balanceOf(address(this))
.safeSub(results.makerTokenBoughtAmount);
results.takerTokenSoldAmount = fillResults.takerAssetFilledAmount;
// If the taker fee is payable in the taker asset, include the
// taker fee in the total amount sold.
if (takerFeeToken == takerToken) {
results.takerTokenSoldAmount =
results.takerTokenSoldAmount.safeAdd(fillResults.takerFeePaid);
}
} catch (bytes memory r) {
LibRichErrorsV06.rrevert(r);
// If the fill fails, zero out fill quantities.
results.makerTokenBoughtAmount = 0;
}
}

/// @dev Extract the token from plain ERC20 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));
}

/// @dev Cast an array of tokens to an array of addresses.
function _asAddressArray(IERC20TokenV06[] memory tokens)
private
pure
returns (address[] memory addrs)
{
assembly { addrs := tokens }
}
}
Loading

0 comments on commit 94d0da9

Please sign in to comment.