This repository has been archived by the owner on Jul 9, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 465
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
@0x/contracts-zero-ex
: Introduce the TransformERC20
feature.
- Loading branch information
1 parent
b83875a
commit 94d0da9
Showing
13 changed files
with
1,007 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
331 changes: 331 additions & 0 deletions
331
contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} | ||
} |
Oops, something went wrong.