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

Commit

Permalink
add batchFill() and multiHopFill() support to ZeroExApiAdapter (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
dorothy-zbornak committed Mar 20, 2021
1 parent 32f575a commit 0b45ec7
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 5 deletions.
58 changes: 54 additions & 4 deletions contracts/mocks/external/ZeroExMock.sol
Expand Up @@ -29,6 +29,30 @@ contract ZeroExMock {
bytes data;
}

struct BatchFillData {
address inputToken;
address outputToken;
uint256 sellAmount;
WrappedBatchCall[] calls;
}

struct WrappedBatchCall {
bytes4 selector;
uint256 sellAmount;
bytes data;
}

struct MultiHopFillData {
address[] tokens;
uint256 sellAmount;
WrappedMultiHopCall[] calls;
}

struct WrappedMultiHopCall {
bytes4 selector;
bytes data;
}

address public mockReceiveToken;
address public mockSendToken;
uint256 public mockReceiveAmount;
Expand Down Expand Up @@ -64,8 +88,7 @@ contract ZeroExMock {
payable
returns (uint256)
{
require(ERC20(mockSendToken).transferFrom(setTokenAddress, address(this), mockSendAmount), "ERC20 TransferFrom failed");
require(ERC20(mockReceiveToken).transfer(setTokenAddress, mockReceiveAmount), "ERC20 transfer failed");
_transferTokens();
}

function sellToUniswap(
Expand All @@ -78,8 +101,7 @@ contract ZeroExMock {
payable
returns (uint256)
{
require(ERC20(mockSendToken).transferFrom(setTokenAddress, address(this), mockSendAmount), "ERC20 TransferFrom failed");
require(ERC20(mockReceiveToken).transfer(setTokenAddress, mockReceiveAmount), "ERC20 transfer failed");
_transferTokens();
}

function sellToLiquidityProvider(
Expand All @@ -94,6 +116,34 @@ contract ZeroExMock {
external
payable
returns (uint256)
{
_transferTokens();
}

function batchFill(
BatchFillData memory /* fillData */,
uint256 /* minBuyAmount */
)
external
payable
returns (uint256)
{
_transferTokens();
}

function multiHopFill(
MultiHopFillData memory /* fillData */,
uint256 /* minBuyAmount */
)
external
payable
returns (uint256)
{
_transferTokens();
}

function _transferTokens()
private
{
require(ERC20(mockSendToken).transferFrom(setTokenAddress, address(this), mockSendAmount), "ERC20 TransferFrom failed");
require(ERC20(mockReceiveToken).transfer(setTokenAddress, mockReceiveAmount), "ERC20 transfer failed");
Expand Down
42 changes: 41 additions & 1 deletion contracts/protocol/integration/ZeroExApiAdapter.sol
Expand Up @@ -28,6 +28,30 @@ pragma experimental "ABIEncoderV2";

contract ZeroExApiAdapter {

struct BatchFillData {
address inputToken;
address outputToken;
uint256 sellAmount;
WrappedBatchCall[] calls;
}

struct WrappedBatchCall {
bytes4 selector;
uint256 sellAmount;
bytes data;
}

struct MultiHopFillData {
address[] tokens;
uint256 sellAmount;
WrappedMultiHopCall[] calls;
}

struct WrappedMultiHopCall {
bytes4 selector;
bytes data;
}

/* ============ State Variables ============ */

// ETH pseudo-token address used by 0x API.
Expand All @@ -46,7 +70,6 @@ contract ZeroExApiAdapter {
getSpender = _zeroExAddress;
}


/* ============ External Getter Functions ============ */

/**
Expand Down Expand Up @@ -111,6 +134,23 @@ contract ZeroExApiAdapter {
require(path.length > 1, "Uniswap token path too short");
inputToken = path[0];
outputToken = path[path.length - 1];
} else if (selector == 0xafc6728e) {
// batchFill()
BatchFillData memory fillData;
(fillData, minOutputTokenAmount) =
abi.decode(_data[4:], (BatchFillData, uint256));
inputToken = fillData.inputToken;
outputToken = fillData.outputToken;
inputTokenAmount = fillData.sellAmount;
} else if (selector == 0x21c184b6) {
// multiHopFill()
MultiHopFillData memory fillData;
(fillData, minOutputTokenAmount) =
abi.decode(_data[4:], (MultiHopFillData, uint256));
require(fillData.tokens.length > 1, "Multihop token path too short");
inputToken = fillData.tokens[0];
outputToken = fillData.tokens[fillData.tokens.length - 1];
inputTokenAmount = fillData.sellAmount;
} else {
revert("Unsupported 0xAPI function selector");
}
Expand Down
213 changes: 213 additions & 0 deletions test/protocol/integration/zeroExApiAdapter.spec.ts
Expand Up @@ -412,5 +412,218 @@ describe("ZeroExApiAdapter", () => {
await expect(tx).to.be.revertedWith("Mismatched recipient");
});
});

describe("batchFill", () => {
it("validates data", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: sourceToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
expect(target).to.eq(zeroExMock.address);
expect(value).to.deep.eq(ZERO);
expect(_data).to.deep.eq(data);
});

it("rejects wrong input token", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: otherToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched input token");
});

it("rejects wrong output token", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: sourceToken,
outputToken: otherToken,
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched output token");
});

it("rejects wrong input token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: sourceToken,
outputToken: destToken,
sellAmount: otherQuantity,
calls: [],
},
minDestinationQuantity,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched input token quantity");
});

it("rejects wrong output token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: sourceToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
},
otherQuantity,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched output token quantity");
});
});

describe("multiHopFill", () => {
it("validates data", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken, destToken],
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
expect(target).to.eq(zeroExMock.address);
expect(value).to.deep.eq(ZERO);
expect(_data).to.deep.eq(data);
});

it("rejects wrong input token", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [otherToken, destToken],
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched input token");
});

it("rejects wrong output token", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken, otherToken],
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched output token");
});

it("rejects wrong input token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken, destToken],
sellAmount: otherQuantity,
calls: [],
},
minDestinationQuantity,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched input token quantity");
});

it("rejects wrong output token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken, destToken],
sellAmount: sourceQuantity,
calls: [],
},
otherQuantity,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
destToken,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched output token quantity");
});
});
});
});

0 comments on commit 0b45ec7

Please sign in to comment.