Skip to content

Commit 2296f6e

Browse files
kimpersasoong
authored andcommitted
feat: implement sellTokenForEthToUniswapV3 and sellEthForTokenToUniswapV3
1 parent 54aa3fe commit 2296f6e

File tree

2 files changed

+271
-6
lines changed

2 files changed

+271
-6
lines changed

contracts/protocol/integration/exchange/ZeroExApiAdapter.sol

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ contract ZeroExApiAdapter {
5757
// ETH pseudo-token address used by 0x API.
5858
address private constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
5959

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

6465
// Address of the deployed ZeroEx contract.
@@ -163,8 +164,23 @@ contract ZeroExApiAdapter {
163164
abi.decode(_data[4:], (bytes, uint256, uint256, address));
164165
supportsRecipient = true;
165166
(inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath);
166-
}
167-
else {
167+
} else if (selector == 0x803ba26d) {
168+
// sellTokenForEthToUniswapV3()
169+
bytes memory encodedPath;
170+
(encodedPath, inputTokenAmount, minOutputTokenAmount, recipient) =
171+
abi.decode(_data[4:], (bytes, uint256, uint256, address));
172+
supportsRecipient = true;
173+
(inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath);
174+
} else if (selector == 0x3598d8ab) {
175+
// sellEthForTokenToUniswapV3()
176+
// TODO(kimpers): is this correct?
177+
inputTokenAmount = 0;
178+
bytes memory encodedPath;
179+
(encodedPath, minOutputTokenAmount, recipient) =
180+
abi.decode(_data[4:], (bytes, uint256, address));
181+
supportsRecipient = true;
182+
(inputToken, outputToken) = _decodeTokensFromUniswapV3EncodedPath(encodedPath);
183+
} else {
168184
revert("Unsupported 0xAPI function selector");
169185
}
170186
}
@@ -215,7 +231,7 @@ contract ZeroExApiAdapter {
215231
address outputToken
216232
)
217233
{
218-
require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too shor too shortt");
234+
require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "UniswapV3 token path too short");
219235
assembly {
220236
let p := add(encodedPath, 32)
221237
p := add(p, offset)

test/protocol/integration/exchange/zeroExApiAdapter.spec.ts

Lines changed: 251 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "module-alias/register";
22

33
import { Account } from "@utils/test/types";
4-
import { ADDRESS_ZERO, ONE, ZERO, EMPTY_BYTES } from "@utils/constants";
4+
import { ADDRESS_ZERO, ONE, ZERO, EMPTY_BYTES, ETH_ADDRESS } from "@utils/constants";
55
import { ZeroExApiAdapter, ZeroExMock } from "@utils/contracts";
66
import DeployHelper from "@utils/deploys";
77
import { addSnapshotBeforeRestoreAfterEach, getAccounts, getWaffleExpect } from "@utils/test/index";
@@ -644,7 +644,7 @@ describe("ZeroExApiAdapter", () => {
644644
});
645645
});
646646
});
647-
describe.only("Uniswap V3", () => {
647+
describe("Uniswap V3", () => {
648648
const POOL_FEE = 1234;
649649
function encodePath(tokens_: string[]): string {
650650
const elems: string[] = [];
@@ -792,5 +792,254 @@ describe("ZeroExApiAdapter", () => {
792792
await expect(tx).to.be.revertedWith("Mismatched recipient");
793793
});
794794
});
795+
796+
describe("sellTokenForEthToUniswapV3", () => {
797+
const additionalHops = [otherToken, extraHopToken];
798+
for (let i = 0; i <= additionalHops.length; i++) {
799+
const hops = take(additionalHops, i);
800+
it(`validates data for ${i + 1} hops`, async () => {
801+
const path = [sourceToken, ...hops, ETH_ADDRESS];
802+
803+
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
804+
encodePath(path),
805+
sourceQuantity,
806+
minDestinationQuantity,
807+
destination,
808+
]);
809+
const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata(
810+
sourceToken,
811+
ETH_ADDRESS,
812+
destination,
813+
sourceQuantity,
814+
minDestinationQuantity,
815+
data,
816+
);
817+
expect(target).to.eq(zeroExMock.address);
818+
expect(value).to.deep.eq(ZERO);
819+
expect(_data).to.deep.eq(data);
820+
});
821+
}
822+
823+
it("rejects wrong input token", async () => {
824+
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
825+
encodePath([otherToken, ETH_ADDRESS]),
826+
sourceQuantity,
827+
minDestinationQuantity,
828+
destination,
829+
]);
830+
const tx = zeroExApiAdapter.getTradeCalldata(
831+
sourceToken,
832+
ETH_ADDRESS,
833+
destination,
834+
sourceQuantity,
835+
minDestinationQuantity,
836+
data,
837+
);
838+
await expect(tx).to.be.revertedWith("Mismatched input token");
839+
});
840+
841+
it("rejects wrong output token", async () => {
842+
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
843+
encodePath([sourceToken, otherToken]),
844+
sourceQuantity,
845+
minDestinationQuantity,
846+
destination,
847+
]);
848+
const tx = zeroExApiAdapter.getTradeCalldata(
849+
sourceToken,
850+
ETH_ADDRESS,
851+
destination,
852+
sourceQuantity,
853+
minDestinationQuantity,
854+
data,
855+
);
856+
await expect(tx).to.be.revertedWith("Mismatched output token");
857+
});
858+
859+
it("rejects wrong input token quantity", async () => {
860+
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
861+
encodePath([sourceToken, ETH_ADDRESS]),
862+
otherQuantity,
863+
minDestinationQuantity,
864+
destination,
865+
]);
866+
const tx = zeroExApiAdapter.getTradeCalldata(
867+
sourceToken,
868+
ETH_ADDRESS,
869+
destination,
870+
sourceQuantity,
871+
minDestinationQuantity,
872+
data,
873+
);
874+
await expect(tx).to.be.revertedWith("Mismatched input token quantity");
875+
});
876+
877+
it("rejects wrong output token quantity", async () => {
878+
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
879+
encodePath([sourceToken, ETH_ADDRESS]),
880+
sourceQuantity,
881+
otherQuantity,
882+
destination,
883+
]);
884+
const tx = zeroExApiAdapter.getTradeCalldata(
885+
sourceToken,
886+
ETH_ADDRESS,
887+
destination,
888+
sourceQuantity,
889+
minDestinationQuantity,
890+
data,
891+
);
892+
await expect(tx).to.be.revertedWith("Mismatched output token quantity");
893+
});
894+
895+
it("rejects invalid uniswap path", async () => {
896+
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
897+
encodePath([sourceToken]),
898+
sourceQuantity,
899+
minDestinationQuantity,
900+
destination,
901+
]);
902+
const tx = zeroExApiAdapter.getTradeCalldata(
903+
sourceToken,
904+
ETH_ADDRESS,
905+
destination,
906+
sourceQuantity,
907+
minDestinationQuantity,
908+
data,
909+
);
910+
await expect(tx).to.be.revertedWith("UniswapV3 token path too short");
911+
});
912+
913+
it("rejects wrong destination", async () => {
914+
const data = zeroExMock.interface.encodeFunctionData("sellTokenForEthToUniswapV3", [
915+
encodePath([sourceToken, ETH_ADDRESS]),
916+
sourceQuantity,
917+
minDestinationQuantity,
918+
ADDRESS_ZERO,
919+
]);
920+
const tx = zeroExApiAdapter.getTradeCalldata(
921+
sourceToken,
922+
ETH_ADDRESS,
923+
destination,
924+
sourceQuantity,
925+
minDestinationQuantity,
926+
data,
927+
);
928+
await expect(tx).to.be.revertedWith("Mismatched recipient");
929+
});
930+
});
931+
932+
describe("sellEthForTokenToUniswapV3", () => {
933+
const additionalHops = [otherToken, extraHopToken];
934+
for (let i = 0; i <= additionalHops.length; i++) {
935+
const hops = take(additionalHops, i);
936+
it(`validates data for ${i + 1} hops`, async () => {
937+
const path = [ETH_ADDRESS, ...hops, destToken];
938+
939+
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
940+
encodePath(path),
941+
minDestinationQuantity,
942+
destination,
943+
]);
944+
const [target, value, _data] = await zeroExApiAdapter.getTradeCalldata(
945+
ETH_ADDRESS,
946+
destToken,
947+
destination,
948+
ZERO,
949+
minDestinationQuantity,
950+
data,
951+
);
952+
expect(target).to.eq(zeroExMock.address);
953+
// TODO(kimpers): is value 0 correct here?
954+
expect(value).to.deep.eq(ZERO);
955+
expect(_data).to.deep.eq(data);
956+
});
957+
}
958+
959+
it("rejects wrong input token", async () => {
960+
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
961+
encodePath([otherToken, destToken]),
962+
minDestinationQuantity,
963+
destination,
964+
]);
965+
const tx = zeroExApiAdapter.getTradeCalldata(
966+
ETH_ADDRESS,
967+
destToken,
968+
destination,
969+
ZERO,
970+
minDestinationQuantity,
971+
data,
972+
);
973+
await expect(tx).to.be.revertedWith("Mismatched input token");
974+
});
975+
976+
it("rejects wrong output token", async () => {
977+
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
978+
encodePath([ETH_ADDRESS, otherToken]),
979+
minDestinationQuantity,
980+
destination,
981+
]);
982+
const tx = zeroExApiAdapter.getTradeCalldata(
983+
ETH_ADDRESS,
984+
destToken,
985+
destination,
986+
ZERO,
987+
minDestinationQuantity,
988+
data,
989+
);
990+
await expect(tx).to.be.revertedWith("Mismatched output token");
991+
});
992+
993+
it("rejects wrong output token quantity", async () => {
994+
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
995+
encodePath([ETH_ADDRESS, destToken]),
996+
otherQuantity,
997+
destination,
998+
]);
999+
const tx = zeroExApiAdapter.getTradeCalldata(
1000+
ETH_ADDRESS,
1001+
destToken,
1002+
destination,
1003+
ZERO,
1004+
minDestinationQuantity,
1005+
data,
1006+
);
1007+
await expect(tx).to.be.revertedWith("Mismatched output token quantity");
1008+
});
1009+
1010+
it("rejects invalid uniswap path", async () => {
1011+
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
1012+
encodePath([ETH_ADDRESS]),
1013+
minDestinationQuantity,
1014+
destination,
1015+
]);
1016+
const tx = zeroExApiAdapter.getTradeCalldata(
1017+
ETH_ADDRESS,
1018+
destToken,
1019+
destination,
1020+
ZERO,
1021+
minDestinationQuantity,
1022+
data,
1023+
);
1024+
await expect(tx).to.be.revertedWith("UniswapV3 token path too short");
1025+
});
1026+
1027+
it("rejects wrong destination", async () => {
1028+
const data = zeroExMock.interface.encodeFunctionData("sellEthForTokenToUniswapV3", [
1029+
encodePath([ETH_ADDRESS, destToken]),
1030+
minDestinationQuantity,
1031+
ADDRESS_ZERO,
1032+
]);
1033+
const tx = zeroExApiAdapter.getTradeCalldata(
1034+
ETH_ADDRESS,
1035+
destToken,
1036+
destination,
1037+
ZERO,
1038+
minDestinationQuantity,
1039+
data,
1040+
);
1041+
await expect(tx).to.be.revertedWith("Mismatched recipient");
1042+
});
1043+
});
7951044
});
7961045
});

0 commit comments

Comments
 (0)