@0x/contracts-zero-ex: Add RFQT taker enforcement to FQT.

`@0x/contracts-zero-ex`: Remove redundant event from `BridgeAdapter`.
`@0x/contracts-zero-ex`: Treat all calldata as signed if quote signer is not in `TransformERC20Feature`.

`@0x/contracts-zero-ex`: Update bridge adapters
This commit is contained in:
Lawrence Forman 2020-09-01 22:11:05 -04:00
parent 43810835d7
commit 889b58a914
23 changed files with 423 additions and 167 deletions

View File

@ -106,7 +106,7 @@ blockchainTests.resets('exchange proxy - meta-transactions', env => {
orders: SignedOrder[]; orders: SignedOrder[];
} }
async function generateSwapAsync(orderFields: Partial<Order> = {}): Promise<SwapInfo> { async function generateSwapAsync(orderFields: Partial<Order> = {}, isRfqt: boolean = false): Promise<SwapInfo> {
const order = await signatureUtils.ecSignTypedDataOrderAsync( const order = await signatureUtils.ecSignTypedDataOrderAsync(
env.provider, env.provider,
{ {
@ -116,7 +116,7 @@ blockchainTests.resets('exchange proxy - meta-transactions', env => {
salt: new BigNumber(hexUtils.random()), salt: new BigNumber(hexUtils.random()),
feeRecipientAddress: NULL_ADDRESS, feeRecipientAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS, senderAddress: NULL_ADDRESS,
takerAddress: flashWalletAddress, takerAddress: isRfqt ? flashWalletAddress : NULL_ADDRESS,
makerAddress: maker, makerAddress: maker,
makerAssetData: assetDataUtils.encodeERC20AssetData(outputToken.address), makerAssetData: assetDataUtils.encodeERC20AssetData(outputToken.address),
takerAssetData: assetDataUtils.encodeERC20AssetData(inputToken.address), takerAssetData: assetDataUtils.encodeERC20AssetData(inputToken.address),
@ -144,6 +144,7 @@ blockchainTests.resets('exchange proxy - meta-transactions', env => {
fillAmount: order.takerAssetAmount, fillAmount: order.takerAssetAmount,
maxOrderFillAmounts: [], maxOrderFillAmounts: [],
refundReceiver: hexUtils.leftPad(2, 20), // Send refund to sender. refundReceiver: hexUtils.leftPad(2, 20), // Send refund to sender.
rfqtTakerAddress: isRfqt ? taker : NULL_ADDRESS,
side: FillQuoteTransformerSide.Sell, side: FillQuoteTransformerSide.Sell,
}), }),
}, },
@ -205,6 +206,7 @@ blockchainTests.resets('exchange proxy - meta-transactions', env => {
async function createMetaTransactionAsync( async function createMetaTransactionAsync(
data: string, data: string,
value: BigNumber, value: BigNumber,
fee?: BigNumber | number,
): Promise<SignedExchangeProxyMetaTransaction> { ): Promise<SignedExchangeProxyMetaTransaction> {
return signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync( return signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync(
env.provider, env.provider,
@ -218,7 +220,7 @@ blockchainTests.resets('exchange proxy - meta-transactions', env => {
salt: new BigNumber(hexUtils.random()), salt: new BigNumber(hexUtils.random()),
callData: data, callData: data,
feeToken: feeToken.address, feeToken: feeToken.address,
feeAmount: getRandomPortion(TAKER_FEE_BALANCE), feeAmount: fee !== undefined ? new BigNumber(fee) : getRandomPortion(TAKER_FEE_BALANCE),
domain: { domain: {
chainId: 1, chainId: 1,
name: 'ZeroEx', name: 'ZeroEx',
@ -230,6 +232,42 @@ blockchainTests.resets('exchange proxy - meta-transactions', env => {
); );
} }
it('can call `transformERC20()` with signed calldata and no relayer fee', async () => {
const swap = await generateSwapAsync();
const callDataHash = hexUtils.hash(getSwapData(swap));
const signedSwapData = getSignedSwapData(swap);
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
const mtx = await createMetaTransactionAsync(signedSwapData, _protocolFee, 0);
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
const receipt = await zeroEx
.executeMetaTransaction(mtx, mtx.signature)
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
const relayerEthRefund = relayerEthBalanceBefore
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
.minus(GAS_PRICE.times(receipt.gasUsed));
// Ensure the relayer got back the unused protocol fees.
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
// Ensure the relayer got paid no mtx fees.
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0);
// Ensure the taker got output tokens.
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
// Ensure the maker got input tokens.
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
// Check events.
verifyEventsFromLogs(
receipt.logs,
[
{
taker,
callDataHash,
sender: zeroEx.address,
data: NULL_BYTES,
},
],
'TransformerMetadata',
);
});
it('can call `transformERC20()` with signed calldata and a relayer fee', async () => { it('can call `transformERC20()` with signed calldata and a relayer fee', async () => {
const swap = await generateSwapAsync(); const swap = await generateSwapAsync();
const callDataHash = hexUtils.hash(getSwapData(swap)); const callDataHash = hexUtils.hash(getSwapData(swap));
@ -301,4 +339,40 @@ blockchainTests.resets('exchange proxy - meta-transactions', env => {
'TransformerMetadata', 'TransformerMetadata',
); );
}); });
it('`transformERC20()` can fill RFQT order if calldata is signed', async () => {
const swap = await generateSwapAsync({}, true);
const callDataHash = hexUtils.hash(getSwapData(swap));
const signedSwapData = getSignedSwapData(swap);
const _protocolFee = protocolFee.times(GAS_PRICE).times(swap.orders.length + 1); // Pay a little more fee than needed.
const mtx = await createMetaTransactionAsync(signedSwapData, _protocolFee, 0);
const relayerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(relayer);
const receipt = await zeroEx
.executeMetaTransaction(mtx, mtx.signature)
.awaitTransactionSuccessAsync({ from: relayer, value: mtx.value, gasPrice: GAS_PRICE });
const relayerEthRefund = relayerEthBalanceBefore
.minus(await env.web3Wrapper.getBalanceInWeiAsync(relayer))
.minus(GAS_PRICE.times(receipt.gasUsed));
// Ensure the relayer got back the unused protocol fees.
expect(relayerEthRefund).to.bignumber.eq(protocolFee.times(GAS_PRICE));
// Ensure the relayer got paid no mtx fees.
expect(await feeToken.balanceOf(relayer).callAsync()).to.bignumber.eq(0);
// Ensure the taker got output tokens.
expect(await outputToken.balanceOf(taker).callAsync()).to.bignumber.eq(swap.minOutputTokenAmount);
// Ensure the maker got input tokens.
expect(await inputToken.balanceOf(maker).callAsync()).to.bignumber.eq(swap.inputTokenAmount);
// Check events.
verifyEventsFromLogs(
receipt.logs,
[
{
taker,
callDataHash,
sender: zeroEx.address,
data: NULL_BYTES,
},
],
'TransformerMetadata',
);
});
}); });

View File

@ -30,10 +30,6 @@
"note": "Add `LogMetadataTransformer`", "note": "Add `LogMetadataTransformer`",
"pr": 2657 "pr": 2657
}, },
{
"note": "Add `IUniswapV2Feature`",
"pr": 2657
},
{ {
"note": "Rename all feature contracts to have `Feature` suffix", "note": "Rename all feature contracts to have `Feature` suffix",
"pr": 2657 "pr": 2657
@ -41,6 +37,18 @@
{ {
"note": "Return `IZeroExContract` in `fullMigrateAsync()`", "note": "Return `IZeroExContract` in `fullMigrateAsync()`",
"pr": 2657 "pr": 2657
},
{
"note": "Add taker address enforcement to RFQT orders in FQT",
"pr": 2692
},
{
"note": "All calldata is valid if quote signer is unset in `TransformERC20`",
"pr": 2692
},
{
"note": "Add updated Kyber and Mooniswap rollup to FQT",
"pr": 2692
} }
] ]
}, },

View File

@ -386,13 +386,19 @@ contract TransformERC20Feature is
view view
returns (bytes32 validCallDataHash) returns (bytes32 validCallDataHash)
{ {
address quoteSigner = getQuoteSigner();
if (quoteSigner == address(0)) {
// If no quote signer is configured, then all calldata hashes are
// valid.
return callDataHash;
}
if (signature.length == 0) { if (signature.length == 0) {
return bytes32(0); return bytes32(0);
} }
if (ISignatureValidatorFeature(address(this)).isValidHashSignature( if (ISignatureValidatorFeature(address(this)).isValidHashSignature(
callDataHash, callDataHash,
getQuoteSigner(), quoteSigner,
signature signature
)) { )) {
return callDataHash; return callDataHash;

View File

@ -27,7 +27,7 @@ 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/LibMathV06.sol";
import "../errors/LibTransformERC20RichErrors.sol"; import "../errors/LibTransformERC20RichErrors.sol";
import "../vendor/v3/IExchange.sol"; import "../vendor/v3/IExchange.sol";
import "../bridges/IBridgeAdapter.sol"; import "./bridges/IBridgeAdapter.sol";
import "./Transformer.sol"; import "./Transformer.sol";
import "./LibERC20Transformer.sol"; import "./LibERC20Transformer.sol";
@ -50,7 +50,7 @@ contract FillQuoteTransformer is
/// @dev Transform data to ABI-encode and pass into `transform()`. /// @dev Transform data to ABI-encode and pass into `transform()`.
struct TransformData { struct TransformData {
// Whether we aer performing a market sell or buy. // Whether we are performing a market sell or buy.
Side side; Side side;
// The token being sold. // The token being sold.
// This should be an actual token, not the ETH pseudo-token. // This should be an actual token, not the ETH pseudo-token.
@ -77,6 +77,9 @@ contract FillQuoteTransformer is
// `address(1)`: Send to the taker. // `address(1)`: Send to the taker.
// `address(2)`: Send to the sender (caller of `transformERC20()`). // `address(2)`: Send to the sender (caller of `transformERC20()`).
address payable refundReceiver; address payable refundReceiver;
// Required taker address for RFQT orders.
// Null means any taker can fill it.
address rfqtTakerAddress;
} }
/// @dev Results of a call to `_fillOrder()`. /// @dev Results of a call to `_fillOrder()`.
@ -96,6 +99,7 @@ contract FillQuoteTransformer is
uint256 soldAmount; uint256 soldAmount;
uint256 protocolFee; uint256 protocolFee;
uint256 takerTokenBalanceRemaining; uint256 takerTokenBalanceRemaining;
bool isRfqtAllowed;
} }
/// @dev Emitted when a trade is skipped due to a lack of funds /// @dev Emitted when a trade is skipped due to a lack of funds
@ -178,9 +182,14 @@ contract FillQuoteTransformer is
// Approve the ERC20 proxy to spend `sellToken`. // Approve the ERC20 proxy to spend `sellToken`.
data.sellToken.approveIfBelow(erc20Proxy, data.fillAmount); data.sellToken.approveIfBelow(erc20Proxy, data.fillAmount);
// Fill the orders.
state.protocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice); state.protocolFee = exchange.protocolFeeMultiplier().safeMul(tx.gasprice);
state.ethRemaining = address(this).balance; state.ethRemaining = address(this).balance;
// RFQT orders can only be filled if we have a valid calldata hash
// (calldata was signed), and the actual taker matches the RFQT taker (if set).
state.isRfqtAllowed = context.callDataHash != bytes32(0)
&& (data.rfqtTakerAddress == address(0) || context.taker == data.rfqtTakerAddress);
// Fill the orders.
for (uint256 i = 0; i < data.orders.length; ++i) { for (uint256 i = 0; i < data.orders.length; ++i) {
// Check if we've hit our targets. // Check if we've hit our targets.
if (data.side == Side.Sell) { if (data.side == Side.Sell) {
@ -434,6 +443,11 @@ contract FillQuoteTransformer is
} }
return results; return results;
} else { } else {
// If the order taker address is set to this contract's address then
// this is an RFQT order, and we will only fill it if allowed to.
if (order.takerAddress == address(this) && !state.isRfqtAllowed) {
return results; // Empty results.
}
// Emit an event if we do not have sufficient ETH to cover the protocol fee. // Emit an event if we do not have sufficient ETH to cover the protocol fee.
if (state.ethRemaining < state.protocolFee) { if (state.ethRemaining < state.protocolFee) {
emit ProtocolFeeUnfunded(state.ethRemaining, state.protocolFee); emit ProtocolFeeUnfunded(state.ethRemaining, state.protocolFee);

View File

@ -28,6 +28,7 @@ interface IERC20Transformer {
/// @dev Context information to pass into `transform()` by `TransformERC20.transformERC20()`. /// @dev Context information to pass into `transform()` by `TransformERC20.transformERC20()`.
struct TransformContext { struct TransformContext {
// The hash of the `TransformERC20.transformERC20()` calldata. // The hash of the `TransformERC20.transformERC20()` calldata.
// Will be null if the calldata is not signed.
bytes32 callDataHash; bytes32 callDataHash;
// The caller of `TransformERC20.transformERC20()`. // The caller of `TransformERC20.transformERC20()`.
address payable sender; address payable sender;

View File

@ -23,6 +23,7 @@ import "./mixins/MixinAdapterAddresses.sol";
import "./mixins/MixinBalancer.sol"; import "./mixins/MixinBalancer.sol";
import "./mixins/MixinCurve.sol"; import "./mixins/MixinCurve.sol";
import "./mixins/MixinKyber.sol"; import "./mixins/MixinKyber.sol";
import "./mixins/MixinMooniswap.sol";
import "./mixins/MixinMStable.sol"; import "./mixins/MixinMStable.sol";
import "./mixins/MixinOasis.sol"; import "./mixins/MixinOasis.sol";
import "./mixins/MixinUniswap.sol"; import "./mixins/MixinUniswap.sol";
@ -34,6 +35,7 @@ contract BridgeAdapter is
MixinBalancer, MixinBalancer,
MixinCurve, MixinCurve,
MixinKyber, MixinKyber,
MixinMooniswap,
MixinMStable, MixinMStable,
MixinOasis, MixinOasis,
MixinUniswap, MixinUniswap,
@ -44,6 +46,7 @@ contract BridgeAdapter is
address private immutable BALANCER_BRIDGE_ADDRESS; address private immutable BALANCER_BRIDGE_ADDRESS;
address private immutable CURVE_BRIDGE_ADDRESS; address private immutable CURVE_BRIDGE_ADDRESS;
address private immutable KYBER_BRIDGE_ADDRESS; address private immutable KYBER_BRIDGE_ADDRESS;
address private immutable MOONISWAP_BRIDGE_ADDRESS;
address private immutable MSTABLE_BRIDGE_ADDRESS; address private immutable MSTABLE_BRIDGE_ADDRESS;
address private immutable OASIS_BRIDGE_ADDRESS; address private immutable OASIS_BRIDGE_ADDRESS;
address private immutable UNISWAP_BRIDGE_ADDRESS; address private immutable UNISWAP_BRIDGE_ADDRESS;
@ -57,8 +60,8 @@ contract BridgeAdapter is
/// @param from The bridge address, indicating the underlying source of the fill. /// @param from The bridge address, indicating the underlying source of the fill.
/// @param to The `to` address, currrently `address(this)` /// @param to The `to` address, currrently `address(this)`
event ERC20BridgeTransfer( event ERC20BridgeTransfer(
address inputToken, IERC20TokenV06 inputToken,
address outputToken, IERC20TokenV06 outputToken,
uint256 inputTokenAmount, uint256 inputTokenAmount,
uint256 outputTokenAmount, uint256 outputTokenAmount,
address from, address from,
@ -70,6 +73,7 @@ contract BridgeAdapter is
MixinBalancer() MixinBalancer()
MixinCurve() MixinCurve()
MixinKyber(addresses) MixinKyber(addresses)
MixinMooniswap(addresses)
MixinMStable(addresses) MixinMStable(addresses)
MixinOasis(addresses) MixinOasis(addresses)
MixinUniswap(addresses) MixinUniswap(addresses)
@ -79,6 +83,7 @@ contract BridgeAdapter is
BALANCER_BRIDGE_ADDRESS = addresses.balancerBridge; BALANCER_BRIDGE_ADDRESS = addresses.balancerBridge;
CURVE_BRIDGE_ADDRESS = addresses.curveBridge; CURVE_BRIDGE_ADDRESS = addresses.curveBridge;
KYBER_BRIDGE_ADDRESS = addresses.kyberBridge; KYBER_BRIDGE_ADDRESS = addresses.kyberBridge;
MOONISWAP_BRIDGE_ADDRESS = addresses.mooniswapBridge;
MSTABLE_BRIDGE_ADDRESS = addresses.mStableBridge; MSTABLE_BRIDGE_ADDRESS = addresses.mStableBridge;
OASIS_BRIDGE_ADDRESS = addresses.oasisBridge; OASIS_BRIDGE_ADDRESS = addresses.oasisBridge;
UNISWAP_BRIDGE_ADDRESS = addresses.uniswapBridge; UNISWAP_BRIDGE_ADDRESS = addresses.uniswapBridge;
@ -87,19 +92,19 @@ contract BridgeAdapter is
function trade( function trade(
bytes calldata makerAssetData, bytes calldata makerAssetData,
address fromTokenAddress, IERC20TokenV06 sellToken,
uint256 sellAmount uint256 sellAmount
) )
external external
returns (uint256 boughtAmount) returns (uint256 boughtAmount)
{ {
( (
address toTokenAddress, IERC20TokenV06 buyToken,
address bridgeAddress, address bridgeAddress,
bytes memory bridgeData bytes memory bridgeData
) = abi.decode( ) = abi.decode(
makerAssetData[4:], makerAssetData[4:],
(address, address, bytes) (IERC20TokenV06, address, bytes)
); );
require( require(
bridgeAddress != address(this) && bridgeAddress != address(0), bridgeAddress != address(this) && bridgeAddress != address(0),
@ -108,65 +113,71 @@ contract BridgeAdapter is
if (bridgeAddress == CURVE_BRIDGE_ADDRESS) { if (bridgeAddress == CURVE_BRIDGE_ADDRESS) {
boughtAmount = _tradeCurve( boughtAmount = _tradeCurve(
toTokenAddress, buyToken,
sellAmount, sellAmount,
bridgeData bridgeData
); );
} else if (bridgeAddress == UNISWAP_V2_BRIDGE_ADDRESS) { } else if (bridgeAddress == UNISWAP_V2_BRIDGE_ADDRESS) {
boughtAmount = _tradeUniswapV2( boughtAmount = _tradeUniswapV2(
toTokenAddress, buyToken,
sellAmount, sellAmount,
bridgeData bridgeData
); );
} else if (bridgeAddress == UNISWAP_BRIDGE_ADDRESS) { } else if (bridgeAddress == UNISWAP_BRIDGE_ADDRESS) {
boughtAmount = _tradeUniswap( boughtAmount = _tradeUniswap(
toTokenAddress, buyToken,
sellAmount, sellAmount,
bridgeData bridgeData
); );
} else if (bridgeAddress == BALANCER_BRIDGE_ADDRESS) { } else if (bridgeAddress == BALANCER_BRIDGE_ADDRESS) {
boughtAmount = _tradeBalancer( boughtAmount = _tradeBalancer(
toTokenAddress, buyToken,
sellAmount, sellAmount,
bridgeData bridgeData
); );
} else if (bridgeAddress == KYBER_BRIDGE_ADDRESS) { } else if (bridgeAddress == KYBER_BRIDGE_ADDRESS) {
boughtAmount = _tradeKyber( boughtAmount = _tradeKyber(
toTokenAddress, buyToken,
sellAmount,
bridgeData
);
} else if (bridgeAddress == MOONISWAP_BRIDGE_ADDRESS) {
boughtAmount = _tradeMooniswap(
buyToken,
sellAmount, sellAmount,
bridgeData bridgeData
); );
} else if (bridgeAddress == MSTABLE_BRIDGE_ADDRESS) { } else if (bridgeAddress == MSTABLE_BRIDGE_ADDRESS) {
boughtAmount = _tradeMStable( boughtAmount = _tradeMStable(
toTokenAddress, buyToken,
sellAmount, sellAmount,
bridgeData bridgeData
); );
} else if (bridgeAddress == OASIS_BRIDGE_ADDRESS) { } else if (bridgeAddress == OASIS_BRIDGE_ADDRESS) {
boughtAmount = _tradeOasis( boughtAmount = _tradeOasis(
toTokenAddress, buyToken,
sellAmount, sellAmount,
bridgeData bridgeData
); );
} else { } else {
boughtAmount = _tradeZeroExBridge( boughtAmount = _tradeZeroExBridge(
bridgeAddress, bridgeAddress,
fromTokenAddress, sellToken,
toTokenAddress, buyToken,
sellAmount, sellAmount,
bridgeData bridgeData
); );
// Do not emit an event. The bridge contract should emit one itself.
return boughtAmount;
} }
emit ERC20BridgeTransfer( emit ERC20BridgeTransfer(
fromTokenAddress, sellToken,
toTokenAddress, buyToken,
sellAmount, sellAmount,
boughtAmount, boughtAmount,
bridgeAddress, bridgeAddress,
address(this) address(this)
); );
return boughtAmount;
} }
} }

View File

@ -26,6 +26,7 @@ contract MixinAdapterAddresses
address balancerBridge; address balancerBridge;
address curveBridge; address curveBridge;
address kyberBridge; address kyberBridge;
address mooniswapBridge;
address mStableBridge; address mStableBridge;
address oasisBridge; address oasisBridge;
address uniswapBridge; address uniswapBridge;
@ -36,6 +37,7 @@ contract MixinAdapterAddresses
address uniswapV2Router; address uniswapV2Router;
address uniswapExchangeFactory; address uniswapExchangeFactory;
address mStable; address mStable;
address mooniswapRegistry;
// Other // Other
address weth; address weth;
} }

View File

@ -32,9 +32,9 @@ interface IBalancerPool {
/// @return spotPriceAfter The new marginal spot price of the given /// @return spotPriceAfter The new marginal spot price of the given
/// token pair for this pool. /// token pair for this pool.
function swapExactAmountIn( function swapExactAmountIn(
address tokenIn, IERC20TokenV06 tokenIn,
uint tokenAmountIn, uint tokenAmountIn,
address tokenOut, IERC20TokenV06 tokenOut,
uint minAmountOut, uint minAmountOut,
uint maxPrice uint maxPrice
) external returns (uint tokenAmountOut, uint spotPriceAfter); ) external returns (uint tokenAmountOut, uint spotPriceAfter);
@ -45,7 +45,7 @@ contract MixinBalancer {
using LibERC20TokenV06 for IERC20TokenV06; using LibERC20TokenV06 for IERC20TokenV06;
function _tradeBalancer( function _tradeBalancer(
address toTokenAddress, IERC20TokenV06 buyToken,
uint256 sellAmount, uint256 sellAmount,
bytes memory bridgeData bytes memory bridgeData
) )
@ -53,21 +53,21 @@ contract MixinBalancer {
returns (uint256 boughtAmount) returns (uint256 boughtAmount)
{ {
// Decode the bridge data. // Decode the bridge data.
(address fromTokenAddress, address poolAddress) = abi.decode( (IERC20TokenV06 sellToken, IBalancerPool pool) = abi.decode(
bridgeData, bridgeData,
(address, address) (IERC20TokenV06, IBalancerPool)
); );
IERC20TokenV06(fromTokenAddress).approveIfBelow( sellToken.approveIfBelow(
poolAddress, address(pool),
sellAmount sellAmount
); );
// Sell all of this contract's `fromTokenAddress` token balance. // Sell all of this contract's `sellToken` token balance.
(boughtAmount,) = IBalancerPool(poolAddress).swapExactAmountIn( (boughtAmount,) = pool.swapExactAmountIn(
fromTokenAddress, // tokenIn sellToken, // tokenIn
sellAmount, // tokenAmountIn sellAmount, // tokenAmountIn
toTokenAddress, // tokenOut buyToken, // tokenOut
1, // minAmountOut 1, // minAmountOut
uint256(-1) // maxPrice uint256(-1) // maxPrice
); );
return boughtAmount; return boughtAmount;
} }

View File

@ -34,13 +34,13 @@ contract MixinCurve {
struct CurveBridgeData { struct CurveBridgeData {
address curveAddress; address curveAddress;
bytes4 exchangeFunctionSelector; bytes4 exchangeFunctionSelector;
address fromTokenAddress; IERC20TokenV06 sellToken;
int128 fromCoinIdx; int128 fromCoinIdx;
int128 toCoinIdx; int128 toCoinIdx;
} }
function _tradeCurve( function _tradeCurve(
address toTokenAddress, IERC20TokenV06 buyToken,
uint256 sellAmount, uint256 sellAmount,
bytes memory bridgeData bytes memory bridgeData
) )
@ -49,8 +49,8 @@ contract MixinCurve {
{ {
// Decode the bridge data to get the Curve metadata. // Decode the bridge data to get the Curve metadata.
CurveBridgeData memory data = abi.decode(bridgeData, (CurveBridgeData)); CurveBridgeData memory data = abi.decode(bridgeData, (CurveBridgeData));
IERC20TokenV06(data.fromTokenAddress).approveIfBelow(data.curveAddress, sellAmount); data.sellToken.approveIfBelow(data.curveAddress, sellAmount);
uint256 beforeBalance = IERC20TokenV06(toTokenAddress).balanceOf(address(this)); uint256 beforeBalance = buyToken.balanceOf(address(this));
(bool success, bytes memory resultData) = (bool success, bytes memory resultData) =
data.curveAddress.call(abi.encodeWithSelector( data.curveAddress.call(abi.encodeWithSelector(
data.exchangeFunctionSelector, data.exchangeFunctionSelector,
@ -64,7 +64,6 @@ contract MixinCurve {
if (!success) { if (!success) {
resultData.rrevert(); resultData.rrevert();
} }
return buyToken.balanceOf(address(this)).safeSub(beforeBalance);
return IERC20TokenV06(toTokenAddress).balanceOf(address(this)).safeSub(beforeBalance);
} }
} }

View File

@ -26,34 +26,36 @@ import "./MixinAdapterAddresses.sol";
interface IKyberNetworkProxy { interface IKyberNetworkProxy {
/// @dev Sells `sellTokenAddress` tokens for `buyTokenAddress` tokens. /// @dev Sells `sellTokenAddress` tokens for `buyTokenAddress` tokens
/// @param sellTokenAddress Token to sell. /// using a hint for the reserve.
/// @param sellToken Token to sell.
/// @param sellAmount Amount of tokens to sell. /// @param sellAmount Amount of tokens to sell.
/// @param buyTokenAddress Token to buy. /// @param buyToken Token to buy.
/// @param recipientAddress Address to send bought tokens to. /// @param recipientAddress Address to send bought tokens to.
/// @param maxBuyTokenAmount A limit on the amount of tokens to buy. /// @param maxBuyTokenAmount A limit on the amount of tokens to buy.
/// @param minConversionRate The minimal conversion rate. If actual rate /// @param minConversionRate The minimal conversion rate. If actual rate
/// is lower, trade is canceled. /// is lower, trade is canceled.
/// @param walletId The wallet ID to send part of the fees /// @param walletId The wallet ID to send part of the fees
/// @param hint The hint for the selective inclusion (or exclusion) of reserves
/// @return boughtAmount Amount of tokens bought. /// @return boughtAmount Amount of tokens bought.
function trade( function tradeWithHint(
address sellTokenAddress, IERC20TokenV06 sellToken,
uint256 sellAmount, uint256 sellAmount,
address buyTokenAddress, IERC20TokenV06 buyToken,
address payable recipientAddress, address payable recipientAddress,
uint256 maxBuyTokenAmount, uint256 maxBuyTokenAmount,
uint256 minConversionRate, uint256 minConversionRate,
address walletId address payable walletId,
bytes calldata hint
) )
external external
payable payable
returns(uint256 boughtAmount); returns (uint256 boughtAmount);
} }
contract MixinKyber is contract MixinKyber is
MixinAdapterAddresses MixinAdapterAddresses
{ {
using LibERC20TokenV06 for IERC20TokenV06; using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Address indicating the trade is using ETH /// @dev Address indicating the trade is using ETH
@ -71,41 +73,39 @@ contract MixinKyber is
} }
function _tradeKyber( function _tradeKyber(
address toTokenAddress, IERC20TokenV06 buyToken,
uint256 sellAmount, uint256 sellAmount,
bytes memory bridgeData bytes memory bridgeData
) )
internal internal
returns (uint256 boughtAmount) returns (uint256 boughtAmount)
{ {
// Decode the bridge data to get the `fromTokenAddress`. (IERC20TokenV06 sellToken, bytes memory hint) =
address fromTokenAddress = abi.decode(bridgeData, (address)); abi.decode(bridgeData, (IERC20TokenV06, bytes));
uint256 payableAmount;
if (fromTokenAddress != address(WETH)) { uint256 payableAmount = 0;
if (sellToken != WETH) {
// If the input token is not WETH, grant an allowance to the exchange // If the input token is not WETH, grant an allowance to the exchange
// to spend them. // to spend them.
IERC20TokenV06(fromTokenAddress).approveIfBelow( sellToken.approveIfBelow(
address(KYBER_NETWORK_PROXY), address(KYBER_NETWORK_PROXY),
sellAmount sellAmount
); );
} else { } else {
// If the input token is WETH, unwrap it and attach it to the call. // If the input token is WETH, unwrap it and attach it to the call.
fromTokenAddress = KYBER_ETH_ADDRESS;
payableAmount = sellAmount; payableAmount = sellAmount;
WETH.withdraw(payableAmount); WETH.withdraw(payableAmount);
} }
bool isToTokenWeth = toTokenAddress == address(WETH);
// Try to sell all of this contract's input token balance through // Try to sell all of this contract's input token balance through
// `KyberNetworkProxy.trade()`. // `KyberNetworkProxy.trade()`.
boughtAmount = KYBER_NETWORK_PROXY.trade{ value: payableAmount }( boughtAmount = KYBER_NETWORK_PROXY.tradeWithHint{ value: payableAmount }(
// Input token. // Input token.
fromTokenAddress, sellToken == WETH ? IERC20TokenV06(KYBER_ETH_ADDRESS) : sellToken,
// Sell amount. // Sell amount.
sellAmount, sellAmount,
// Output token. // Output token.
isToTokenWeth ? KYBER_ETH_ADDRESS : toTokenAddress, buyToken == WETH ? IERC20TokenV06(KYBER_ETH_ADDRESS) : buyToken,
// Transfer to this contract // Transfer to this contract
address(uint160(address(this))), address(uint160(address(this))),
// Buy as much as possible. // Buy as much as possible.
@ -113,9 +113,11 @@ contract MixinKyber is
// Lowest minimum conversion rate // Lowest minimum conversion rate
1, 1,
// No affiliate address. // No affiliate address.
address(0) address(0),
hint
); );
if (isToTokenWeth) { // If receving ETH, wrap it to WETH.
if (buyToken == WETH) {
WETH.deposit{ value: boughtAmount }(); WETH.deposit{ value: boughtAmount }();
} }
return boughtAmount; return boughtAmount;

View File

@ -27,19 +27,18 @@ import "./MixinAdapterAddresses.sol";
interface IMStable { interface IMStable {
function swap( function swap(
address _input, IERC20TokenV06 sellToken,
address _output, IERC20TokenV06 buyToken,
uint256 _quantity, uint256 sellAmount,
address _recipient address recipient
) )
external external
returns (uint256 output); returns (uint256 boughtAmount);
} }
contract MixinMStable is contract MixinMStable is
MixinAdapterAddresses MixinAdapterAddresses
{ {
using LibERC20TokenV06 for IERC20TokenV06; using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the mStable mUSD contract. /// @dev Mainnet address of the mStable mUSD contract.
@ -52,21 +51,21 @@ contract MixinMStable is
} }
function _tradeMStable( function _tradeMStable(
address toTokenAddress, IERC20TokenV06 buyToken,
uint256 sellAmount, uint256 sellAmount,
bytes memory bridgeData bytes memory bridgeData
) )
internal internal
returns (uint256 boughtAmount) returns (uint256 boughtAmount)
{ {
// Decode the bridge data to get the `fromTokenAddress`. // Decode the bridge data to get the `sellToken`.
(address fromTokenAddress) = abi.decode(bridgeData, (address)); (IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06));
// Grant an allowance to the exchange to spend `fromTokenAddress` token. // Grant an allowance to the exchange to spend `sellToken` token.
IERC20TokenV06(fromTokenAddress).approveIfBelow(address(MSTABLE), sellAmount); sellToken.approveIfBelow(address(MSTABLE), sellAmount);
boughtAmount = MSTABLE.swap( boughtAmount = MSTABLE.swap(
fromTokenAddress, sellToken,
toTokenAddress, buyToken,
sellAmount, sellAmount,
address(this) address(this)
); );

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;
import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
import "./MixinAdapterAddresses.sol";
/// @dev Moooniswap pool interface.
interface IMooniswapPool {
function swap(
IERC20TokenV06 sellToken,
IERC20TokenV06 buyToken,
uint256 sellAmount,
uint256 minBoughtAmount,
address referrer
)
external
payable
returns (uint256 boughtAmount);
}
/// @dev Moooniswap registry interface.
interface IMooniswapRegistry {
function pools(
IERC20TokenV06 token1,
IERC20TokenV06 token2
)
external
view
returns (IMooniswapPool);
}
/// @dev BridgeAdapter mixin for mooniswap.
contract MixinMooniswap is
MixinAdapterAddresses
{
using LibERC20TokenV06 for IERC20TokenV06;
using LibERC20TokenV06 for IEtherTokenV06;
/// @dev Mooniswap registry contract.
IMooniswapRegistry private immutable REGISTRY;
/// @dev WETH token.
IEtherTokenV06 private immutable WETH;
constructor(AdapterAddresses memory addresses)
public
{
REGISTRY = IMooniswapRegistry(addresses.mooniswapRegistry);
WETH = IEtherTokenV06(addresses.weth);
}
function _tradeMooniswap(
IERC20TokenV06 buyToken,
uint256 sellAmount,
bytes memory bridgeData
)
internal
returns (uint256 boughtAmount)
{
IERC20TokenV06 sellToken = abi.decode(bridgeData, (IERC20TokenV06));
IMooniswapPool pool = REGISTRY.pools(sellToken, buyToken);
// Convert WETH to ETH.
uint256 ethValue = 0;
if (sellToken == WETH) {
WETH.withdraw(sellAmount);
ethValue = sellAmount;
} else {
// Grant the pool an allowance.
sellToken.approveIfBelow(
address(pool),
sellAmount
);
}
boughtAmount = pool.swap{value: ethValue}(
sellToken == WETH ? IERC20TokenV06(0) : sellToken,
buyToken == WETH ? IERC20TokenV06(0) : buyToken,
sellAmount,
1,
address(0)
);
}
}

View File

@ -25,26 +25,25 @@ import "./MixinAdapterAddresses.sol";
interface IOasis { interface IOasis {
/// @dev Sell `sellAmount` of `fromToken` token and receive `toToken` token. /// @dev Sell `sellAmount` of `sellToken` token and receive `buyToken` token.
/// @param fromToken The token being sold. /// @param sellToken The token being sold.
/// @param sellAmount The amount of `fromToken` token being sold. /// @param sellAmount The amount of `sellToken` token being sold.
/// @param toToken The token being bought. /// @param buyToken The token being bought.
/// @param minFillAmount Minimum amount of `toToken` token to buy. /// @param minBoughtAmount Minimum amount of `buyToken` token to buy.
/// @return fillAmount Amount of `toToken` bought. /// @return boughtAmount Amount of `buyToken` bought.
function sellAllAmount( function sellAllAmount(
address fromToken, IERC20TokenV06 sellToken,
uint256 sellAmount, uint256 sellAmount,
address toToken, IERC20TokenV06 buyToken,
uint256 minFillAmount uint256 minBoughtAmount
) )
external external
returns (uint256 fillAmount); returns (uint256 boughtAmount);
} }
contract MixinOasis is contract MixinOasis is
MixinAdapterAddresses MixinAdapterAddresses
{ {
using LibERC20TokenV06 for IERC20TokenV06; using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the Oasis `MatchingMarket` contract. /// @dev Mainnet address of the Oasis `MatchingMarket` contract.
@ -57,25 +56,25 @@ contract MixinOasis is
} }
function _tradeOasis( function _tradeOasis(
address toTokenAddress, IERC20TokenV06 buyToken,
uint256 sellAmount, uint256 sellAmount,
bytes memory bridgeData bytes memory bridgeData
) )
internal internal
returns (uint256 boughtAmount) returns (uint256 boughtAmount)
{ {
// Decode the bridge data to get the `fromTokenAddress`. // Decode the bridge data to get the `sellToken`.
(address fromTokenAddress) = abi.decode(bridgeData, (address)); (IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06));
// Grant an allowance to the exchange to spend `fromTokenAddress` token. // Grant an allowance to the exchange to spend `sellToken` token.
IERC20TokenV06(fromTokenAddress).approveIfBelow( sellToken.approveIfBelow(
address(OASIS), address(OASIS),
sellAmount sellAmount
); );
// Try to sell all of this contract's `fromTokenAddress` token balance. // Try to sell all of this contract's `sellToken` token balance.
boughtAmount = OASIS.sellAllAmount( boughtAmount = OASIS.sellAllAmount(
fromTokenAddress, sellToken,
sellAmount, sellAmount,
toTokenAddress, buyToken,
// min fill amount // min fill amount
1 1
); );

View File

@ -27,11 +27,11 @@ import "./MixinAdapterAddresses.sol";
interface IUniswapExchangeFactory { interface IUniswapExchangeFactory {
/// @dev Get the exchange for a token. /// @dev Get the exchange for a token.
/// @param tokenAddress The address of the token contract. /// @param token The token contract.
function getExchange(address tokenAddress) function getExchange(IERC20TokenV06 token)
external external
view view
returns (address); returns (IUniswapExchange exchange);
} }
interface IUniswapExchange { interface IUniswapExchange {
@ -71,7 +71,7 @@ interface IUniswapExchange {
/// @param minEthBought The minimum amount of intermediate ETH to buy. /// @param minEthBought The minimum amount of intermediate ETH to buy.
/// @param deadline Time when this order expires. /// @param deadline Time when this order expires.
/// @param recipient Who to transfer the tokens to. /// @param recipient Who to transfer the tokens to.
/// @param toTokenAddress The token being bought. /// @param buyToken The token being bought.
/// @return tokensBought Amount of tokens bought. /// @return tokensBought Amount of tokens bought.
function tokenToTokenTransferInput( function tokenToTokenTransferInput(
uint256 tokensSold, uint256 tokensSold,
@ -79,7 +79,7 @@ interface IUniswapExchange {
uint256 minEthBought, uint256 minEthBought,
uint256 deadline, uint256 deadline,
address recipient, address recipient,
address toTokenAddress IERC20TokenV06 buyToken
) )
external external
returns (uint256 tokensBought); returns (uint256 tokensBought);
@ -89,14 +89,14 @@ interface IUniswapExchange {
/// @param minTokensBought The minimum number of tokens to buy. /// @param minTokensBought The minimum number of tokens to buy.
/// @param minEthBought The minimum amount of intermediate ETH to buy. /// @param minEthBought The minimum amount of intermediate ETH to buy.
/// @param deadline Time when this order expires. /// @param deadline Time when this order expires.
/// @param toTokenAddress The token being bought. /// @param buyToken The token being bought.
/// @return tokensBought Amount of tokens bought. /// @return tokensBought Amount of tokens bought.
function tokenToTokenSwapInput( function tokenToTokenSwapInput(
uint256 tokensSold, uint256 tokensSold,
uint256 minTokensBought, uint256 minTokensBought,
uint256 minEthBought, uint256 minEthBought,
uint256 deadline, uint256 deadline,
address toTokenAddress IERC20TokenV06 buyToken
) )
external external
returns (uint256 tokensBought); returns (uint256 tokensBought);
@ -105,7 +105,6 @@ interface IUniswapExchange {
contract MixinUniswap is contract MixinUniswap is
MixinAdapterAddresses MixinAdapterAddresses
{ {
using LibERC20TokenV06 for IERC20TokenV06; using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the WETH contract. /// @dev Mainnet address of the WETH contract.
@ -121,27 +120,27 @@ contract MixinUniswap is
} }
function _tradeUniswap( function _tradeUniswap(
address toTokenAddress, IERC20TokenV06 buyToken,
uint256 sellAmount, uint256 sellAmount,
bytes memory bridgeData bytes memory bridgeData
) )
internal internal
returns (uint256 boughtAmount) returns (uint256 boughtAmount)
{ {
// Decode the bridge data to get the `fromTokenAddress`. // Decode the bridge data to get the `sellToken`.
(address fromTokenAddress) = abi.decode(bridgeData, (address)); (IERC20TokenV06 sellToken) = abi.decode(bridgeData, (IERC20TokenV06));
// Get the exchange for the token pair. // Get the exchange for the token pair.
IUniswapExchange exchange = _getUniswapExchangeForTokenPair( IUniswapExchange exchange = _getUniswapExchangeForTokenPair(
fromTokenAddress, sellToken,
toTokenAddress buyToken
); );
// Convert from WETH to a token. // Convert from WETH to a token.
if (fromTokenAddress == address(WETH)) { if (sellToken == WETH) {
// Unwrap the WETH. // Unwrap the WETH.
WETH.withdraw(sellAmount); WETH.withdraw(sellAmount);
// Buy as much of `toTokenAddress` token with ETH as possible // Buy as much of `buyToken` token with ETH as possible
boughtAmount = exchange.ethToTokenTransferInput{ value: sellAmount }( boughtAmount = exchange.ethToTokenTransferInput{ value: sellAmount }(
// Minimum buy amount. // Minimum buy amount.
1, 1,
@ -152,13 +151,13 @@ contract MixinUniswap is
); );
// Convert from a token to WETH. // Convert from a token to WETH.
} else if (toTokenAddress == address(WETH)) { } else if (buyToken == WETH) {
// Grant the exchange an allowance. // Grant the exchange an allowance.
IERC20TokenV06(fromTokenAddress).approveIfBelow( sellToken.approveIfBelow(
address(exchange), address(exchange),
sellAmount sellAmount
); );
// Buy as much ETH with `fromTokenAddress` token as possible. // Buy as much ETH with `sellToken` token as possible.
boughtAmount = exchange.tokenToEthSwapInput( boughtAmount = exchange.tokenToEthSwapInput(
// Sell all tokens we hold. // Sell all tokens we hold.
sellAmount, sellAmount,
@ -172,11 +171,11 @@ contract MixinUniswap is
// Convert from one token to another. // Convert from one token to another.
} else { } else {
// Grant the exchange an allowance. // Grant the exchange an allowance.
IERC20TokenV06(fromTokenAddress).approveIfBelow( sellToken.approveIfBelow(
address(exchange), address(exchange),
sellAmount sellAmount
); );
// Buy as much `toTokenAddress` token with `fromTokenAddress` token // Buy as much `buyToken` token with `sellToken` token
boughtAmount = exchange.tokenToTokenSwapInput( boughtAmount = exchange.tokenToTokenSwapInput(
// Sell all tokens we hold. // Sell all tokens we hold.
sellAmount, sellAmount,
@ -186,8 +185,8 @@ contract MixinUniswap is
1, 1,
// Expires after this block. // Expires after this block.
block.timestamp, block.timestamp,
// Convert to `toTokenAddress`. // Convert to `buyToken`.
toTokenAddress buyToken
); );
} }
@ -197,24 +196,21 @@ contract MixinUniswap is
/// @dev Retrieves the uniswap exchange for a given token pair. /// @dev Retrieves the uniswap exchange for a given token pair.
/// In the case of a WETH-token exchange, this will be the non-WETH token. /// In the case of a WETH-token exchange, this will be the non-WETH token.
/// In th ecase of a token-token exchange, this will be the first token. /// In th ecase of a token-token exchange, this will be the first token.
/// @param fromTokenAddress The address of the token we are converting from. /// @param sellToken The address of the token we are converting from.
/// @param toTokenAddress The address of the token we are converting to. /// @param buyToken The address of the token we are converting to.
/// @return exchange The uniswap exchange. /// @return exchange The uniswap exchange.
function _getUniswapExchangeForTokenPair( function _getUniswapExchangeForTokenPair(
address fromTokenAddress, IERC20TokenV06 sellToken,
address toTokenAddress IERC20TokenV06 buyToken
) )
private private
view view
returns (IUniswapExchange exchange) returns (IUniswapExchange exchange)
{ {
address exchangeTokenAddress = fromTokenAddress;
// Whichever isn't WETH is the exchange token. // Whichever isn't WETH is the exchange token.
if (fromTokenAddress == address(WETH)) { exchange = sellToken == WETH
exchangeTokenAddress = toTokenAddress; ? UNISWAP_EXCHANGE_FACTORY.getExchange(buyToken)
} : UNISWAP_EXCHANGE_FACTORY.getExchange(sellToken);
exchange = IUniswapExchange(UNISWAP_EXCHANGE_FACTORY.getExchange(exchangeTokenAddress));
require(address(exchange) != address(0), "NO_UNISWAP_EXCHANGE_FOR_TOKEN"); require(address(exchange) != address(0), "NO_UNISWAP_EXCHANGE_FOR_TOKEN");
return exchange;
} }
} }

View File

@ -50,7 +50,6 @@ interface IUniswapV2Router02 {
contract MixinUniswapV2 is contract MixinUniswapV2 is
MixinAdapterAddresses MixinAdapterAddresses
{ {
using LibERC20TokenV06 for IERC20TokenV06; using LibERC20TokenV06 for IERC20TokenV06;
/// @dev Mainnet address of the `UniswapV2Router02` contract. /// @dev Mainnet address of the `UniswapV2Router02` contract.
@ -63,21 +62,23 @@ contract MixinUniswapV2 is
} }
function _tradeUniswapV2( function _tradeUniswapV2(
address toTokenAddress, IERC20TokenV06 buyToken,
uint256 sellAmount, uint256 sellAmount,
bytes memory bridgeData bytes memory bridgeData
) )
internal internal
returns (uint256) returns (uint256 boughtAmount)
{ {
// Decode the bridge data to get the `fromTokenAddress`.
// solhint-disable indent // solhint-disable indent
address[] memory path = abi.decode(bridgeData, (address[])); address[] memory path = abi.decode(bridgeData, (address[]));
// solhint-enable indent // solhint-enable indent
require(path.length >= 2, "UniswapV2Bridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO"); require(path.length >= 2, "UniswapV2Bridge/PATH_LENGTH_MUST_BE_AT_LEAST_TWO");
require(path[path.length - 1] == toTokenAddress, "UniswapV2Bridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"); require(
// Grant the Uniswap router an allowance. path[path.length - 1] == address(buyToken),
"UniswapV2Bridge/LAST_ELEMENT_OF_PATH_MUST_MATCH_OUTPUT_TOKEN"
);
// Grant the Uniswap router an allowance to sell the first token.
IERC20TokenV06(path[0]).approveIfBelow( IERC20TokenV06(path[0]).approveIfBelow(
address(UNISWAP_V2_ROUTER), address(UNISWAP_V2_ROUTER),
sellAmount sellAmount
@ -88,7 +89,7 @@ contract MixinUniswapV2 is
sellAmount, sellAmount,
// Minimum buy amount. // Minimum buy amount.
1, 1,
// Convert `fromTokenAddress` to `toTokenAddress`. // Convert to `buyToken` along this path.
path, path,
// Recipient is `this`. // Recipient is `this`.
address(this), address(this),

View File

@ -24,15 +24,15 @@ import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol";
interface IERC20Bridge { interface IERC20Bridge {
/// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. /// @dev Transfers `amount` of the ERC20 `buyToken` from `from` to `to`.
/// @param tokenAddress The address of the ERC20 token to transfer. /// @param buyToken The address of the ERC20 token to transfer.
/// @param from Address to transfer asset from. /// @param from Address to transfer asset from.
/// @param to Address to transfer asset to. /// @param to Address to transfer asset to.
/// @param amount Amount of asset to transfer. /// @param amount Amount of asset to transfer.
/// @param bridgeData Arbitrary asset data needed by the bridge contract. /// @param bridgeData Arbitrary asset data needed by the bridge contract.
/// @return success The magic bytes `0xdc1600f3` if successful. /// @return success The magic bytes `0xdc1600f3` if successful.
function bridgeTransferFrom( function bridgeTransferFrom(
address tokenAddress, IERC20TokenV06 buyToken,
address from, address from,
address to, address to,
uint256 amount, uint256 amount,
@ -49,28 +49,27 @@ contract MixinZeroExBridge {
function _tradeZeroExBridge( function _tradeZeroExBridge(
address bridgeAddress, address bridgeAddress,
address fromTokenAddress, IERC20TokenV06 sellToken,
address toTokenAddress, IERC20TokenV06 buyToken,
uint256 sellAmount, uint256 sellAmount,
bytes memory bridgeData bytes memory bridgeData
) )
internal internal
returns (uint256 boughtAmount) returns (uint256 boughtAmount)
{ {
uint256 balanceBefore = IERC20TokenV06(toTokenAddress).balanceOf(address(this)); uint256 balanceBefore = buyToken.balanceOf(address(this));
// Trade the good old fashioned way // Trade the good old fashioned way
IERC20TokenV06(fromTokenAddress).compatTransfer( sellToken.compatTransfer(
bridgeAddress, bridgeAddress,
sellAmount sellAmount
); );
IERC20Bridge(bridgeAddress).bridgeTransferFrom( IERC20Bridge(bridgeAddress).bridgeTransferFrom(
toTokenAddress, buyToken,
bridgeAddress, address(bridgeAddress),
address(this), address(this),
1, // amount to transfer back from the bridge 1, // amount to transfer back from the bridge
bridgeData bridgeData
); );
boughtAmount = buyToken.balanceOf(address(this)).safeSub(balanceBefore);
boughtAmount = IERC20TokenV06(toTokenAddress).balanceOf(address(this)).safeSub(balanceBefore);
} }
} }

View File

@ -41,7 +41,7 @@
"config": { "config": {
"publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter", "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json" "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -60,6 +60,7 @@ import * as MixinAdapterAddresses from '../test/generated-artifacts/MixinAdapter
import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.json'; import * as MixinBalancer from '../test/generated-artifacts/MixinBalancer.json';
import * as MixinCurve from '../test/generated-artifacts/MixinCurve.json'; import * as MixinCurve from '../test/generated-artifacts/MixinCurve.json';
import * as MixinKyber from '../test/generated-artifacts/MixinKyber.json'; import * as MixinKyber from '../test/generated-artifacts/MixinKyber.json';
import * as MixinMooniswap from '../test/generated-artifacts/MixinMooniswap.json';
import * as MixinMStable from '../test/generated-artifacts/MixinMStable.json'; import * as MixinMStable from '../test/generated-artifacts/MixinMStable.json';
import * as MixinOasis from '../test/generated-artifacts/MixinOasis.json'; import * as MixinOasis from '../test/generated-artifacts/MixinOasis.json';
import * as MixinUniswap from '../test/generated-artifacts/MixinUniswap.json'; import * as MixinUniswap from '../test/generated-artifacts/MixinUniswap.json';
@ -100,17 +101,6 @@ import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json';
export const artifacts = { export const artifacts = {
IZeroEx: IZeroEx as ContractArtifact, IZeroEx: IZeroEx as ContractArtifact,
ZeroEx: ZeroEx as ContractArtifact, ZeroEx: ZeroEx as ContractArtifact,
BridgeAdapter: BridgeAdapter as ContractArtifact,
IBridgeAdapter: IBridgeAdapter as ContractArtifact,
MixinAdapterAddresses: MixinAdapterAddresses as ContractArtifact,
MixinBalancer: MixinBalancer as ContractArtifact,
MixinCurve: MixinCurve as ContractArtifact,
MixinKyber: MixinKyber as ContractArtifact,
MixinMStable: MixinMStable as ContractArtifact,
MixinOasis: MixinOasis as ContractArtifact,
MixinUniswap: MixinUniswap as ContractArtifact,
MixinUniswapV2: MixinUniswapV2 as ContractArtifact,
MixinZeroExBridge: MixinZeroExBridge as ContractArtifact,
LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, LibCommonRichErrors: LibCommonRichErrors as ContractArtifact,
LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact, LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact,
LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact,
@ -164,6 +154,18 @@ export const artifacts = {
PayTakerTransformer: PayTakerTransformer as ContractArtifact, PayTakerTransformer: PayTakerTransformer as ContractArtifact,
Transformer: Transformer as ContractArtifact, Transformer: Transformer as ContractArtifact,
WethTransformer: WethTransformer as ContractArtifact, WethTransformer: WethTransformer as ContractArtifact,
BridgeAdapter: BridgeAdapter as ContractArtifact,
IBridgeAdapter: IBridgeAdapter as ContractArtifact,
MixinAdapterAddresses: MixinAdapterAddresses as ContractArtifact,
MixinBalancer: MixinBalancer as ContractArtifact,
MixinCurve: MixinCurve as ContractArtifact,
MixinKyber: MixinKyber as ContractArtifact,
MixinMStable: MixinMStable as ContractArtifact,
MixinMooniswap: MixinMooniswap as ContractArtifact,
MixinOasis: MixinOasis as ContractArtifact,
MixinUniswap: MixinUniswap as ContractArtifact,
MixinUniswapV2: MixinUniswapV2 as ContractArtifact,
MixinZeroExBridge: MixinZeroExBridge as ContractArtifact,
IERC20Bridge: IERC20Bridge as ContractArtifact, IERC20Bridge: IERC20Bridge as ContractArtifact,
IExchange: IExchange as ContractArtifact, IExchange: IExchange as ContractArtifact,
IGasToken: IGasToken as ContractArtifact, IGasToken: IGasToken as ContractArtifact,

View File

@ -28,11 +28,11 @@ import {
TransformERC20FeatureEvents, TransformERC20FeatureEvents,
} from '../wrappers'; } from '../wrappers';
const { NULL_BYTES, NULL_BYTES32 } = constants; const { NULL_ADDRESS, NULL_BYTES, NULL_BYTES32 } = constants;
type MintTokenTransformerEvent = DecodedLogEntry<TestMintTokenERC20TransformerMintTransformEventArgs>; type MintTokenTransformerEvent = DecodedLogEntry<TestMintTokenERC20TransformerMintTransformEventArgs>;
blockchainTests.resets('TransformERC20 feature', env => { blockchainTests.resets.only('TransformERC20 feature', env => {
const callDataSignerKey = hexUtils.random(); const callDataSignerKey = hexUtils.random();
const callDataSigner = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(callDataSignerKey))); const callDataSigner = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(callDataSignerKey)));
let owner: string; let owner: string;
@ -687,6 +687,37 @@ blockchainTests.resets('TransformERC20 feature', env => {
expect(actualCallDataHash).to.eq(hexUtils.hash(callData)); expect(actualCallDataHash).to.eq(hexUtils.hash(callData));
}); });
it('passes the calldata hash to transformer when no quote signer configured', async () => {
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
const startingInputTokenBalance = getRandomInteger(0, '100e18');
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
const minOutputTokenAmount = getRandomInteger(1, '1e18');
const outputTokenMintAmount = minOutputTokenAmount;
const callValue = getRandomInteger(1, '1e18');
const transformation = createMintTokenTransformation({
outputTokenMintAmount,
inputTokenBurnAmunt: inputTokenAmount,
});
const bakedCall = feature.transformERC20(
inputToken.address,
outputToken.address,
inputTokenAmount,
minOutputTokenAmount,
[transformation],
);
const callData = bakedCall.getABIEncodedTransactionData();
await feature.setQuoteSigner(NULL_ADDRESS).awaitTransactionSuccessAsync({ from: owner });
const receipt = await bakedCall.awaitTransactionSuccessAsync({
from: taker,
value: callValue,
data: callData,
});
const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
expect(actualCallDataHash).to.eq(hexUtils.hash(callData));
});
it('passes empty calldata hash to transformer with improperly signed calldata', async () => { it('passes empty calldata hash to transformer with improperly signed calldata', async () => {
const startingOutputTokenBalance = getRandomInteger(0, '100e18'); const startingOutputTokenBalance = getRandomInteger(0, '100e18');
const startingInputTokenBalance = getRandomInteger(0, '100e18'); const startingInputTokenBalance = getRandomInteger(0, '100e18');

View File

@ -62,6 +62,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
balancerBridge: NULL_ADDRESS, balancerBridge: NULL_ADDRESS,
curveBridge: NULL_ADDRESS, curveBridge: NULL_ADDRESS,
kyberBridge: NULL_ADDRESS, kyberBridge: NULL_ADDRESS,
mooniswapBridge: NULL_ADDRESS,
mStableBridge: NULL_ADDRESS, mStableBridge: NULL_ADDRESS,
oasisBridge: NULL_ADDRESS, oasisBridge: NULL_ADDRESS,
uniswapBridge: NULL_ADDRESS, uniswapBridge: NULL_ADDRESS,
@ -71,6 +72,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
uniswapV2Router: NULL_ADDRESS, uniswapV2Router: NULL_ADDRESS,
uniswapExchangeFactory: NULL_ADDRESS, uniswapExchangeFactory: NULL_ADDRESS,
mStable: NULL_ADDRESS, mStable: NULL_ADDRESS,
mooniswapRegistry: NULL_ADDRESS,
weth: NULL_ADDRESS, weth: NULL_ADDRESS,
}, },
); );
@ -273,6 +275,7 @@ blockchainTests.resets('FillQuoteTransformer', env => {
maxOrderFillAmounts: [], maxOrderFillAmounts: [],
fillAmount: MAX_UINT256, fillAmount: MAX_UINT256,
refundReceiver: NULL_ADDRESS, refundReceiver: NULL_ADDRESS,
rfqtTakerAddress: NULL_ADDRESS,
...fields, ...fields,
}); });
} }

View File

@ -59,6 +59,7 @@ export * from '../test/generated-wrappers/mixin_balancer';
export * from '../test/generated-wrappers/mixin_curve'; export * from '../test/generated-wrappers/mixin_curve';
export * from '../test/generated-wrappers/mixin_kyber'; export * from '../test/generated-wrappers/mixin_kyber';
export * from '../test/generated-wrappers/mixin_m_stable'; export * from '../test/generated-wrappers/mixin_m_stable';
export * from '../test/generated-wrappers/mixin_mooniswap';
export * from '../test/generated-wrappers/mixin_oasis'; export * from '../test/generated-wrappers/mixin_oasis';
export * from '../test/generated-wrappers/mixin_uniswap'; export * from '../test/generated-wrappers/mixin_uniswap';
export * from '../test/generated-wrappers/mixin_uniswap_v2'; export * from '../test/generated-wrappers/mixin_uniswap_v2';

View File

@ -82,6 +82,7 @@
"test/generated-artifacts/MixinCurve.json", "test/generated-artifacts/MixinCurve.json",
"test/generated-artifacts/MixinKyber.json", "test/generated-artifacts/MixinKyber.json",
"test/generated-artifacts/MixinMStable.json", "test/generated-artifacts/MixinMStable.json",
"test/generated-artifacts/MixinMooniswap.json",
"test/generated-artifacts/MixinOasis.json", "test/generated-artifacts/MixinOasis.json",
"test/generated-artifacts/MixinUniswap.json", "test/generated-artifacts/MixinUniswap.json",
"test/generated-artifacts/MixinUniswapV2.json", "test/generated-artifacts/MixinUniswapV2.json",