Files
protocol/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol
2021-10-25 20:08:58 -04:00

436 lines
16 KiB
Solidity

// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol";
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
import "../errors/LibTransformERC20RichErrors.sol";
import "../fixins/FixinCommon.sol";
import "../fixins/FixinTokenSpender.sol";
import "../migrations/LibMigrate.sol";
import "../external/IFlashWallet.sol";
import "../external/FlashWallet.sol";
import "../storage/LibTransformERC20Storage.sol";
import "../transformers/IERC20Transformer.sol";
import "../transformers/LibERC20Transformer.sol";
import "./interfaces/IFeature.sol";
import "./interfaces/ITransformERC20Feature.sol";
/// @dev Feature to composably transform between ERC20 tokens.
contract TransformERC20Feature is
IFeature,
ITransformERC20Feature,
FixinCommon,
FixinTokenSpender
{
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
/// @dev Stack vars for `_transformERC20Private()`.
struct TransformERC20PrivateState {
IFlashWallet wallet;
address transformerDeployer;
uint256 recipientOutputTokenBalanceBefore;
uint256 recipientOutputTokenBalanceAfter;
}
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "TransformERC20";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 4, 0);
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
/// @param transformerDeployer The trusted deployer for transformers.
/// @return success `LibMigrate.SUCCESS` on success.
function migrate(address transformerDeployer)
external
returns (bytes4 success)
{
_registerFeatureFunction(this.getTransformerDeployer.selector);
_registerFeatureFunction(this.createTransformWallet.selector);
_registerFeatureFunction(this.getTransformWallet.selector);
_registerFeatureFunction(this.setTransformerDeployer.selector);
_registerFeatureFunction(this.setQuoteSigner.selector);
_registerFeatureFunction(this.getQuoteSigner.selector);
_registerFeatureFunction(this.transformERC20.selector);
_registerFeatureFunction(this._transformERC20.selector);
if (this.getTransformWallet() == IFlashWallet(address(0))) {
// Create the transform wallet if it doesn't exist.
this.createTransformWallet();
}
LibTransformERC20Storage.getStorage().transformerDeployer = transformerDeployer;
return LibMigrate.MIGRATE_SUCCESS;
}
/// @dev Replace the allowed deployer for transformers.
/// Only callable by the owner.
/// @param transformerDeployer The address of the trusted deployer for transformers.
function setTransformerDeployer(address transformerDeployer)
external
override
onlyOwner
{
LibTransformERC20Storage.getStorage().transformerDeployer = transformerDeployer;
emit TransformerDeployerUpdated(transformerDeployer);
}
/// @dev Replace the optional signer for `transformERC20()` calldata.
/// Only callable by the owner.
/// @param quoteSigner The address of the new calldata signer.
function setQuoteSigner(address quoteSigner)
external
override
onlyOwner
{
LibTransformERC20Storage.getStorage().quoteSigner = quoteSigner;
emit QuoteSignerUpdated(quoteSigner);
}
/// @dev Return the allowed deployer for transformers.
/// @return deployer The transform deployer address.
function getTransformerDeployer()
public
override
view
returns (address deployer)
{
return LibTransformERC20Storage.getStorage().transformerDeployer;
}
/// @dev Return the optional signer for `transformERC20()` calldata.
/// @return signer The signer address.
function getQuoteSigner()
public
override
view
returns (address signer)
{
return LibTransformERC20Storage.getStorage().quoteSigner;
}
/// @dev Deploy a new wallet instance and replace the current one with it.
/// Useful if we somehow break the current wallet instance.
/// Only callable by the owner.
/// @return wallet The new wallet instance.
function createTransformWallet()
public
override
onlyOwner
returns (IFlashWallet wallet)
{
wallet = new FlashWallet();
LibTransformERC20Storage.getStorage().wallet = wallet;
}
/// @dev Executes a series of transformations to convert an ERC20 `inputToken`
/// to an ERC20 `outputToken`.
/// @param inputToken The token being provided by the sender.
/// If `0xeee...`, ETH is implied and should be provided with the call.`
/// @param outputToken The token to be acquired by the sender.
/// `0xeee...` implies ETH.
/// @param inputTokenAmount The amount of `inputToken` to take from the sender.
/// If set to `uint256(-1)`, the entire spendable balance of the taker
/// will be solt.
/// @param minOutputTokenAmount The minimum amount of `outputToken` the sender
/// must receive for the entire transformation to succeed. If set to zero,
/// the minimum output token transfer will not be asserted.
/// @param transformations The transformations to execute on the token balance(s)
/// in sequence.
/// @return outputTokenAmount The amount of `outputToken` received by the sender.
function transformERC20(
IERC20TokenV06 inputToken,
IERC20TokenV06 outputToken,
uint256 inputTokenAmount,
uint256 minOutputTokenAmount,
Transformation[] memory transformations
)
public
override
payable
returns (uint256 outputTokenAmount)
{
return _transformERC20Private(
TransformERC20Args({
taker: msg.sender,
inputToken: inputToken,
outputToken: outputToken,
inputTokenAmount: inputTokenAmount,
minOutputTokenAmount: minOutputTokenAmount,
transformations: transformations,
useSelfBalance: false,
recipient: msg.sender
})
);
}
/// @dev Internal version of `transformERC20()`. Only callable from within.
/// @param args A `TransformERC20Args` struct.
/// @return outputTokenAmount The amount of `outputToken` received by the taker.
function _transformERC20(TransformERC20Args memory args)
public
virtual
override
payable
onlySelf
returns (uint256 outputTokenAmount)
{
return _transformERC20Private(args);
}
/// @dev Private version of `transformERC20()`.
/// @param args A `TransformERC20Args` struct.
/// @return outputTokenAmount The amount of `outputToken` received by the taker.
function _transformERC20Private(TransformERC20Args memory args)
private
returns (uint256 outputTokenAmount)
{
// If the input token amount is -1 and we are not selling ETH,
// transform the taker's entire spendable balance.
if (!args.useSelfBalance && args.inputTokenAmount == uint256(-1)) {
if (LibERC20Transformer.isTokenETH(args.inputToken)) {
// We can't pull more ETH from the taker, so we just set the
// input token amount to the value attached to the call.
args.inputTokenAmount = msg.value;
} else {
args.inputTokenAmount = _getSpendableERC20BalanceOf(
args.inputToken,
args.taker
);
}
}
TransformERC20PrivateState memory state;
state.wallet = getTransformWallet();
state.transformerDeployer = getTransformerDeployer();
// Remember the initial output token balance of the recipient.
state.recipientOutputTokenBalanceBefore =
LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.recipient);
// Pull input tokens from the taker to the wallet and transfer attached ETH.
_transferInputTokensAndAttachedEth(args, address(state.wallet));
{
// Perform transformations.
for (uint256 i = 0; i < args.transformations.length; ++i) {
_executeTransformation(
state.wallet,
args.transformations[i],
state.transformerDeployer,
args.recipient
);
}
// Transfer output tokens from wallet to recipient
outputTokenAmount = _executeOutputTokenTransfer(
args.outputToken,
state.wallet,
args.recipient
);
}
// Compute how much output token has been transferred to the recipient.
state.recipientOutputTokenBalanceAfter =
LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.recipient);
if (state.recipientOutputTokenBalanceAfter < state.recipientOutputTokenBalanceBefore) {
LibTransformERC20RichErrors.NegativeTransformERC20OutputError(
address(args.outputToken),
state.recipientOutputTokenBalanceBefore - state.recipientOutputTokenBalanceAfter
).rrevert();
}
outputTokenAmount = LibSafeMathV06.min256(
outputTokenAmount,
state.recipientOutputTokenBalanceAfter.safeSub(state.recipientOutputTokenBalanceBefore)
);
// Ensure enough output token has been sent to the taker.
if (outputTokenAmount < args.minOutputTokenAmount) {
LibTransformERC20RichErrors.IncompleteTransformERC20Error(
address(args.outputToken),
outputTokenAmount,
args.minOutputTokenAmount
).rrevert();
}
// Emit an event.
emit TransformedERC20(
args.taker,
address(args.inputToken),
address(args.outputToken),
args.inputTokenAmount,
outputTokenAmount
);
}
/// @dev Return the current wallet instance that will serve as the execution
/// context for transformations.
/// @return wallet The wallet instance.
function getTransformWallet()
public
override
view
returns (IFlashWallet wallet)
{
return LibTransformERC20Storage.getStorage().wallet;
}
/// @dev Transfer input tokens and any attached ETH to `to`
/// @param args A `TransformERC20Args` struct.
/// @param to The recipient of tokens and ETH.
function _transferInputTokensAndAttachedEth(
TransformERC20Args memory args,
address payable to
)
private
{
if (
LibERC20Transformer.isTokenETH(args.inputToken) &&
msg.value < args.inputTokenAmount
) {
// Token is ETH, so the caller must attach enough ETH to the call.
LibTransformERC20RichErrors.InsufficientEthAttachedError(
msg.value,
args.inputTokenAmount
).rrevert();
}
// Transfer any attached ETH.
if (msg.value != 0) {
to.transfer(msg.value);
}
// Transfer input tokens.
if (!LibERC20Transformer.isTokenETH(args.inputToken)) {
if (args.useSelfBalance) {
// Use EP balance input token.
_transferERC20Tokens(
args.inputToken,
to,
args.inputTokenAmount
);
} else {
// Pull ERC20 tokens from taker.
_transferERC20TokensFrom(
args.inputToken,
args.taker,
to,
args.inputTokenAmount
);
}
}
}
/// @dev Executs a transformer in the context of `wallet`.
/// @param wallet The wallet instance.
/// @param transformation The transformation.
/// @param transformerDeployer The address of the transformer deployer.
/// @param recipient The recipient address.
function _executeTransformation(
IFlashWallet wallet,
Transformation memory transformation,
address transformerDeployer,
address payable recipient
)
private
{
// Derive the transformer address from the deployment nonce.
address payable transformer = LibERC20Transformer.getDeployedAddress(
transformerDeployer,
transformation.deploymentNonce
);
// Call `transformer.transform()` as the wallet.
bytes memory resultData = wallet.executeDelegateCall(
// The call target.
transformer,
// Call data.
abi.encodeWithSelector(
IERC20Transformer.transform.selector,
IERC20Transformer.TransformContext({
sender: msg.sender,
recipient: recipient,
data: transformation.data
})
)
);
// Ensure the transformer returned the magic bytes.
if (resultData.length != 32 ||
abi.decode(resultData, (bytes4)) != LibERC20Transformer.TRANSFORMER_SUCCESS
) {
LibTransformERC20RichErrors.TransformerFailedError(
transformer,
transformation.data,
resultData
).rrevert();
}
}
function _executeOutputTokenTransfer(
IERC20TokenV06 outputToken,
IFlashWallet wallet,
address payable recipient
)
private
returns (uint256 transferAmount)
{
transferAmount =
LibERC20Transformer.getTokenBalanceOf(outputToken, address(wallet));
if (LibERC20Transformer.isTokenETH(outputToken)) {
wallet.executeCall(
recipient,
"",
transferAmount
);
} else {
bytes memory resultData = wallet.executeCall(
payable(address(outputToken)),
abi.encodeWithSelector(
IERC20TokenV06.transfer.selector,
recipient,
transferAmount
),
0
);
if (resultData.length == 0) {
// If we get back 0 returndata, this may be a non-standard ERC-20 that
// does not return a boolean. Check that it at least contains code.
uint256 size;
assembly { size := extcodesize(outputToken) }
require(size > 0, "invalid token address, contains no code");
} else if (resultData.length >= 32) {
// If we get back at least 32 bytes, we know the target address
// contains code, and we assume it is a token that returned a boolean
// success value, which must be true.
uint256 result = LibBytesV06.readUint256(resultData, 0);
if (result != 1) {
LibRichErrorsV06.rrevert(resultData);
}
} else {
// If 0 < returndatasize < 32, the target is a contract, but not a
// valid token.
LibRichErrorsV06.rrevert(resultData);
}
}
}
}