Skip to content

Commit

Permalink
feat: initial stab at Uniswap V3 adaptor
Browse files Browse the repository at this point in the history
  • Loading branch information
kimpers authored and asoong committed May 27, 2021
1 parent aa894d7 commit c25665b
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 54 deletions.
36 changes: 36 additions & 0 deletions contracts/mocks/external/ZeroExMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,42 @@ contract ZeroExMock {
_transferTokens();
}

function sellEthForTokenToUniswapV3(
bytes memory /* encodedPath */,
uint256 /* minBuyAmount */,
address /* recipient */
)
external
payable
returns (uint256)
{
_transferTokens();
}

function sellTokenForEthToUniswapV3(
bytes memory /* encodedPath */,
uint256 /* sellAmount */,
uint256 /* minBuyAmount */,
address payable /* recipient */
)
external
returns (uint256)
{
_transferTokens();
}

function sellTokenForTokenToUniswapV3(
bytes memory /* encodedPath */,
uint256 /* sellAmount */,
uint256 /* minBuyAmount */,
address /* recipient */
)
external
returns (uint256)
{
_transferTokens();
}

function _transferTokens()
private
{
Expand Down
20 changes: 19 additions & 1 deletion contracts/protocol/integration/exchange/ZeroExApiAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,22 @@ contract ZeroExApiAdapter {
bytes data;
}


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

// 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
uint256 private constant UNISWAP_V3_SINGLE_HOP_PATH_SIZE = 20 + 3 + 20;

// Address of the deployed ZeroEx contract.
address public immutable zeroExAddress;

// Returns the address to approve source tokens to for trading. This is the TokenTaker address
address public immutable getSpender;


/* ============ constructor ============ */

constructor(address _zeroExAddress) public {
Expand Down Expand Up @@ -152,7 +157,20 @@ contract ZeroExApiAdapter {
inputToken = fillData.tokens[0];
outputToken = fillData.tokens[fillData.tokens.length - 1];
inputTokenAmount = fillData.sellAmount;
} else {
} else if (selector == 0x6af479b2) {
// sellTokenForTokenToUniswapV3()
{
bytes memory encodedPath;
(encodedPath, inputTokenAmount, minOutputTokenAmount, recipient) =
abi.decode(_data[4:], (bytes, uint256, uint256, address));
require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too short");
}
// TODO(kimpers): Need to decode the path here
supportsRecipient = true;
inputToken = _sourceToken;
outputToken = _destinationToken;
}
else {
revert("Unsupported 0xAPI function selector");
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"license": "MIT",
"homepage": "https://github.com/SetProtocol",
"devDependencies": {
"@0x/utils": "^6.4.3",
"@ethersproject/bignumber": "^5.0.12",
"@ethersproject/providers": "^5.0.17",
"@nomiclabs/hardhat-ethers": "^2.0.1",
Expand Down
120 changes: 76 additions & 44 deletions test/protocol/integration/exchange/zeroExApiAdapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ADDRESS_ZERO, ONE, ZERO, EMPTY_BYTES } from "@utils/constants";
import { ZeroExApiAdapter, ZeroExMock } from "@utils/contracts";
import DeployHelper from "@utils/deploys";
import { addSnapshotBeforeRestoreAfterEach, getAccounts, getWaffleExpect } from "@utils/test/index";
import { hexUtils } from "@0x/utils";

const expect = getWaffleExpect();

Expand All @@ -29,12 +30,7 @@ describe("ZeroExApiAdapter", () => {
deployer = new DeployHelper(owner.wallet);

// Mock OneInch exchange that allows for only fixed exchange amounts
zeroExMock = await deployer.mocks.deployZeroExMock(
ADDRESS_ZERO,
ADDRESS_ZERO,
ZERO,
ZERO,
);
zeroExMock = await deployer.mocks.deployZeroExMock(ADDRESS_ZERO, ADDRESS_ZERO, ZERO, ZERO);
zeroExApiAdapter = await deployer.adapters.deployZeroExApiAdapter(zeroExMock.address);
});

Expand Down Expand Up @@ -417,10 +413,10 @@ describe("ZeroExApiAdapter", () => {
it("validates data", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: sourceToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
inputToken: sourceToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -440,10 +436,10 @@ describe("ZeroExApiAdapter", () => {
it("rejects wrong input token", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: otherToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
inputToken: otherToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -461,10 +457,10 @@ describe("ZeroExApiAdapter", () => {
it("rejects wrong output token", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: sourceToken,
outputToken: otherToken,
sellAmount: sourceQuantity,
calls: [],
inputToken: sourceToken,
outputToken: otherToken,
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -482,10 +478,10 @@ describe("ZeroExApiAdapter", () => {
it("rejects wrong input token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: sourceToken,
outputToken: destToken,
sellAmount: otherQuantity,
calls: [],
inputToken: sourceToken,
outputToken: destToken,
sellAmount: otherQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -503,10 +499,10 @@ describe("ZeroExApiAdapter", () => {
it("rejects wrong output token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("batchFill", [
{
inputToken: sourceToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
inputToken: sourceToken,
outputToken: destToken,
sellAmount: sourceQuantity,
calls: [],
},
otherQuantity,
]);
Expand All @@ -526,9 +522,9 @@ describe("ZeroExApiAdapter", () => {
it("validates data", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken, destToken],
sellAmount: sourceQuantity,
calls: [],
tokens: [sourceToken, destToken],
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -548,9 +544,9 @@ describe("ZeroExApiAdapter", () => {
it("rejects wrong input token", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [otherToken, destToken],
sellAmount: sourceQuantity,
calls: [],
tokens: [otherToken, destToken],
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -568,9 +564,9 @@ describe("ZeroExApiAdapter", () => {
it("rejects went path too short", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken],
sellAmount: sourceQuantity,
calls: [],
tokens: [sourceToken],
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -588,9 +584,9 @@ describe("ZeroExApiAdapter", () => {
it("rejects wrong output token", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken, otherToken],
sellAmount: sourceQuantity,
calls: [],
tokens: [sourceToken, otherToken],
sellAmount: sourceQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -608,9 +604,9 @@ describe("ZeroExApiAdapter", () => {
it("rejects wrong input token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken, destToken],
sellAmount: otherQuantity,
calls: [],
tokens: [sourceToken, destToken],
sellAmount: otherQuantity,
calls: [],
},
minDestinationQuantity,
]);
Expand All @@ -628,9 +624,9 @@ describe("ZeroExApiAdapter", () => {
it("rejects wrong output token quantity", async () => {
const data = zeroExMock.interface.encodeFunctionData("multiHopFill", [
{
tokens: [sourceToken, destToken],
sellAmount: sourceQuantity,
calls: [],
tokens: [sourceToken, destToken],
sellAmount: sourceQuantity,
calls: [],
},
otherQuantity,
]);
Expand All @@ -646,4 +642,40 @@ describe("ZeroExApiAdapter", () => {
});
});
});
describe.only("Uniswap V3", () => {
const POOL_FEE = 1234;
function encodePath(tokens_: string[]): string {
const elems: string[] = [];
tokens_.forEach((t, i) => {
if (i) {
elems.push(hexUtils.leftPad(POOL_FEE, 3));
}
elems.push(hexUtils.leftPad(t, 20));
});
return hexUtils.concat(...elems);
}

describe("sellTokenForTokenToUniswapV3", () => {
it("validates data", async () => {
const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [
encodePath([sourceToken, destToken]),
sourceQuantity,
minDestinationQuantity,
destination,
]);
console.log(data);
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);
});
});
});
});
Loading

0 comments on commit c25665b

Please sign in to comment.