Skip to content

Commit

Permalink
feat: verify path for multiple hops
Browse files Browse the repository at this point in the history
  • Loading branch information
kimpers authored and asoong committed May 27, 2021
1 parent 7905d73 commit 06ca9b6
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 43 deletions.
18 changes: 14 additions & 4 deletions contracts/protocol/integration/exchange/ZeroExApiAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,21 @@ 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;
uint256 private constant UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE = 20 + 3;

// 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 @@ -164,7 +163,17 @@ contract ZeroExApiAdapter {
abi.decode(_data[4:], (bytes, uint256, uint256, address));
supportsRecipient = true;

(inputToken, outputToken) = _decodePoolInfoFromPath(encodedPath);
uint256 numHops = (encodedPath.length - 20)/UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE;
require(numHops > 0, "At least 1 hop");

if (numHops == 1) {
(inputToken, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, 0);

} else {
uint256 lastPoolOffset = (numHops - 1) * UNISWAP_V3_SINGLE_HOP_OFFSET_SIZE;
(inputToken,) = _decodePoolInfoFromPathWithOffset(encodedPath, 0);
(, outputToken) = _decodePoolInfoFromPathWithOffset(encodedPath, lastPoolOffset);
}
}
else {
revert("Unsupported 0xAPI function selector");
Expand All @@ -186,7 +195,7 @@ contract ZeroExApiAdapter {
}

// Return the first input token, output token, and fee of an encoded uniswap path.
function _decodePoolInfoFromPath(bytes memory encodedPath)
function _decodePoolInfoFromPathWithOffset(bytes memory encodedPath, uint256 offset)
private
pure
returns (
Expand All @@ -197,6 +206,7 @@ contract ZeroExApiAdapter {
require(encodedPath.length >= UNISWAP_V3_SINGLE_HOP_PATH_SIZE, "Uniswap token path too shor too shortt");
assembly {
let p := add(encodedPath, 32)
p := add(p, offset)
inputToken := shr(96, mload(p))
p := add(p, 20)
// account for fee
Expand Down
66 changes: 27 additions & 39 deletions test/protocol/integration/exchange/zeroExApiAdapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ZeroExApiAdapter, ZeroExMock } from "@utils/contracts";
import DeployHelper from "@utils/deploys";
import { addSnapshotBeforeRestoreAfterEach, getAccounts, getWaffleExpect } from "@utils/test/index";
import { hexUtils } from "@0x/utils";
import { take } from "lodash";

const expect = getWaffleExpect();

Expand All @@ -14,6 +15,7 @@ describe("ZeroExApiAdapter", () => {
const sourceToken = "0x6cf5f1d59fddae3a688210953a512b6aee6ea643";
const destToken = "0x5e5d0bea9d4a15db2d0837aff0435faba166190d";
const otherToken = "0xae9902bb655de1a67f334d8661b3ae6a96723d5b";
const extraHopToken = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const destination = "0x89b3515cad4f23c1deacea79fc12445cc21bd0e1";
const otherDestination = "0xdeb100c55cccfd6e39753f78c8b0c3bcbef86157";
const sourceQuantity = ONE;
Expand Down Expand Up @@ -656,45 +658,31 @@ describe("ZeroExApiAdapter", () => {
}

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

const data = zeroExMock.interface.encodeFunctionData("sellTokenForTokenToUniswapV3", [
encodePath(path),
sourceQuantity,
minDestinationQuantity,
destination,
]);
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);
});
}
});
});
});

0 comments on commit 06ca9b6

Please sign in to comment.