Skip to content

Commit

Permalink
feat: implement sellTokenForEthToUniswapV3 and sellEthForTokenToUnisw…
Browse files Browse the repository at this point in the history
…apV3
  • Loading branch information
kimpers authored and asoong committed May 27, 2021
1 parent 54aa3fe commit 2296f6e
Show file tree
Hide file tree
Showing 2 changed files with 271 additions and 6 deletions.
24 changes: 20 additions & 4 deletions contracts/protocol/integration/exchange/ZeroExApiAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ contract ZeroExApiAdapter {
// ETH pseudo-token address used by 0x API.
address private constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

// Minimum byte size of a single hop Uniswap V3 encoded path
// Minimum byte size of a single hop Uniswap V3 encoded path (token address + fee + token adress)
uint256 private constant UNISWAP_V3_SINGLE_HOP_PATH_SIZE = 20 + 3 + 20;
// Byte size of one hop in the Uniswap V3 encoded path (token address + fee)
uint256 private constant UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE = 20 + 3;

// Address of the deployed ZeroEx contract.
Expand Down Expand Up @@ -163,8 +164,23 @@ contract ZeroExApiAdapter {
abi.decode(_data[4:], (bytes, uint256, uint256, address));
supportsRecipient = true;
(inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath);
}
else {
} else if (selector == 0x803ba26d) {
// sellTokenForEthToUniswapV3()
bytes memory encodedPath;
(encodedPath, inputTokenAmount, minOutputTokenAmount, recipient) =
abi.decode(_data[4:], (bytes, uint256, uint256, address));
supportsRecipient = true;
(inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath);
} else if (selector == 0x3598d8ab) {
// sellEthForTokenToUniswapV3()
// TODO(kimpers): is this correct?
inputTokenAmount = 0;
bytes memory encodedPath;
(encodedPath, minOutputTokenAmount, recipient) =
abi.decode(_data[4:], (bytes, uint256, address));
supportsRecipient = true;
(inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath);
} else {
revert("Unsupported 0xAPI function selector");
}
}
Expand Down Expand Up @@ -215,7 +231,7 @@ contract ZeroExApiAdapter {
address outputToken
)
{
require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too shor too shortt");
require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "UniswapV3 token path too short");
assembly {
let p := add(encodedPath, 32)
p := add(p, offset)
Expand Down
253 changes: 251 additions & 2 deletions test/protocol/integration/exchange/zeroExApiAdapter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "module-alias/register";

import { Account } from "@utils/test/types";
import { ADDRESS_ZERO, ONE, ZERO, EMPTY_BYTES } from "@utils/constants";
import { ADDRESS_ZERO, ONE, ZERO, EMPTY_BYTES, ETH_ADDRESS } from "@utils/constants";
import { ZeroExApiAdapter, ZeroExMock } from "@utils/contracts";
import DeployHelper from "@utils/deploys";
import { addSnapshotBeforeRestoreAfterEach, getAccounts, getWaffleExpect } from "@utils/test/index";
Expand Down Expand Up @@ -644,7 +644,7 @@ describe("ZeroExApiAdapter", () => {
});
});
});
describe.only("Uniswap V3", () => {
describe("Uniswap V3", () => {
const POOL_FEE = 1234;
function encodePath(tokens_: string[]): string {
const elems: string[] = [];
Expand Down Expand Up @@ -792,5 +792,254 @@ describe("ZeroExApiAdapter", () => {
await expect(tx).to.be.revertedWith("Mismatched recipient");
});
});

describe("sellTokenForEthToUniswapV3", () => {
const additionalHops = [otherToken, extraHopToken];
for (let i = 0; i <= additionalHops.length; i++) {
const hops = take(additionalHops, i);
it(`validates data for ${i + 1} hops`, async () => {
const path = [sourceToken, ...hops, ETH_ADDRESS];

const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
encodePath(path),
sourceQuantity,
minDestinationQuantity,
destination,
]);
const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata(
sourceToken,
ETH_ADDRESS,
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("sellTokenForEthToUniswapV3", [
encodePath([otherToken, ETH_ADDRESS]),
sourceQuantity,
minDestinationQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
ETH_ADDRESS,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched input token");
});

it("rejects wrong output token", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
encodePath([sourceToken, otherToken]),
sourceQuantity,
minDestinationQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
ETH_ADDRESS,
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("sellTokenForEthToUniswapV3", [
encodePath([sourceToken, ETH_ADDRESS]),
otherQuantity,
minDestinationQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
ETH_ADDRESS,
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("sellTokenForEthToUniswapV3", [
encodePath([sourceToken, ETH_ADDRESS]),
sourceQuantity,
otherQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
ETH_ADDRESS,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched output token quantity");
});

it("rejects invalid uniswap path", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
encodePath([sourceToken]),
sourceQuantity,
minDestinationQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
ETH_ADDRESS,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("UniswapV3 token path too short");
});

it("rejects wrong destination", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
encodePath([sourceToken, ETH_ADDRESS]),
sourceQuantity,
minDestinationQuantity,
ADDRESS_ZERO,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
sourceToken,
ETH_ADDRESS,
destination,
sourceQuantity,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched recipient");
});
});

describe("sellEthForTokenToUniswapV3", () => {
const additionalHops = [otherToken, extraHopToken];
for (let i = 0; i <= additionalHops.length; i++) {
const hops = take(additionalHops, i);
it(`validates data for ${i + 1} hops`, async () => {
const path = [ETH_ADDRESS, ...hops, destToken];

const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
encodePath(path),
minDestinationQuantity,
destination,
]);
const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata(
ETH_ADDRESS,
destToken,
destination,
ZERO,
minDestinationQuantity,
data,
);
expect(target).to.eq(zeroExMock.address);
// TODO(kimpers): is value 0 correct here?
expect(value).to.deep.eq(ZERO);
expect(_data).to.deep.eq(data);
});
}

it("rejects wrong input token", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
encodePath([otherToken, destToken]),
minDestinationQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
ETH_ADDRESS,
destToken,
destination,
ZERO,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched input token");
});

it("rejects wrong output token", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
encodePath([ETH_ADDRESS, otherToken]),
minDestinationQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
ETH_ADDRESS,
destToken,
destination,
ZERO,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched output token");
});

it("rejects wrong output token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
encodePath([ETH_ADDRESS, destToken]),
otherQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
ETH_ADDRESS,
destToken,
destination,
ZERO,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched output token quantity");
});

it("rejects invalid uniswap path", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
encodePath([ETH_ADDRESS]),
minDestinationQuantity,
destination,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
ETH_ADDRESS,
destToken,
destination,
ZERO,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("UniswapV3 token path too short");
});

it("rejects wrong destination", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
encodePath([ETH_ADDRESS, destToken]),
minDestinationQuantity,
ADDRESS_ZERO,
]);
const tx = zeroExApiAdapter.getTradeCalldata(
ETH_ADDRESS,
destToken,
destination,
ZERO,
minDestinationQuantity,
data,
);
await expect(tx).to.be.revertedWith("Mismatched recipient");
});
});
});
});

0 comments on commit 2296f6e

Please sign in to comment.