Skip to content

Commit 31d67e5

Browse files
authored
Uniswap V3 Exchange Adapter (#96)
* add basic Uniswap V3 fixture * add createPool to fixture * add quoter * add getTokenOrder helper * add addLiquidityWide helper function * add external contracts * sort imports * fix imports * improve comments * use exact values in tests * split uniswap contracts into v2/v3 folders * add UniswapV3ExchangeAdapter * remove stray console log * fix comments * cleanup * add revert on path mismatch * add uniswap v3 tests to trade module integration tests * improve revert messages
1 parent 9a7a0b3 commit 31d67e5

File tree

8 files changed

+579
-5
lines changed

8 files changed

+579
-5
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity 0.6.10;
3+
pragma experimental ABIEncoderV2;
4+
5+
6+
/// @title Router token swapping functionality
7+
/// @notice Functions for swapping tokens via Uniswap V3
8+
interface ISwapRouter {
9+
struct ExactInputSingleParams {
10+
address tokenIn;
11+
address tokenOut;
12+
uint24 fee;
13+
address recipient;
14+
uint256 deadline;
15+
uint256 amountIn;
16+
uint256 amountOutMinimum;
17+
uint160 sqrtPriceLimitX96;
18+
}
19+
20+
/// @notice Swaps `amountIn` of one token for as much as possible of another token
21+
/// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata
22+
/// @return amountOut The amount of the received token
23+
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
24+
25+
struct ExactInputParams {
26+
bytes path;
27+
address recipient;
28+
uint256 deadline;
29+
uint256 amountIn;
30+
uint256 amountOutMinimum;
31+
}
32+
33+
/// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path
34+
/// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata
35+
/// @return amountOut The amount of the received token
36+
function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
37+
38+
struct ExactOutputSingleParams {
39+
address tokenIn;
40+
address tokenOut;
41+
uint24 fee;
42+
address recipient;
43+
uint256 deadline;
44+
uint256 amountOut;
45+
uint256 amountInMaximum;
46+
uint160 sqrtPriceLimitX96;
47+
}
48+
49+
/// @notice Swaps as little as possible of one token for `amountOut` of another token
50+
/// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata
51+
/// @return amountIn The amount of the input token
52+
function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);
53+
54+
struct ExactOutputParams {
55+
bytes path;
56+
address recipient;
57+
uint256 deadline;
58+
uint256 amountOut;
59+
uint256 amountInMaximum;
60+
}
61+
62+
/// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed)
63+
/// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata
64+
/// @return amountIn The amount of the input token
65+
function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn);
66+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
Copyright 2021 Set Labs Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache License, Version 2.0
17+
*/
18+
19+
pragma solidity 0.6.10;
20+
pragma experimental "ABIEncoderV2";
21+
22+
import { BytesLib } from "../../../../external/contracts/uniswap/v3/lib/BytesLib.sol";
23+
import { ISwapRouter } from "../../../interfaces/external/ISwapRouter.sol";
24+
25+
26+
/**
27+
* @title UniswapV3TradeAdapter
28+
* @author Set Protocol
29+
*
30+
* Exchange adapter for Uniswap V3 SwapRouter that encodes trade data
31+
*/
32+
contract UniswapV3ExchangeAdapter {
33+
34+
using BytesLib for bytes;
35+
36+
/* ============= Constants ================= */
37+
38+
// signature of exactInput SwapRouter function
39+
string internal constant EXACT_INPUT = "exactInput((bytes,address,uint256,uint256,uint256))";
40+
41+
/* ============ State Variables ============ */
42+
43+
// Address of Uniswap V3 SwapRouter contract
44+
address public immutable swapRouter;
45+
46+
/* ============ Constructor ============ */
47+
48+
/**
49+
* Set state variables
50+
*
51+
* @param _swapRouter Address of Uniswap V3 SwapRouter
52+
*/
53+
constructor(address _swapRouter) public {
54+
swapRouter = _swapRouter;
55+
}
56+
57+
/* ============ External Getter Functions ============ */
58+
59+
/**
60+
* Return calldata for Uniswap V3 SwapRouter
61+
*
62+
* @param _sourceToken Address of source token to be sold
63+
* @param _destinationToken Address of destination token to buy
64+
* @param _destinationAddress Address that assets should be transferred to
65+
* @param _sourceQuantity Amount of source token to sell
66+
* @param _minDestinationQuantity Min amount of destination token to buy
67+
* @param _data Uniswap V3 path. Equals the output of the generateDataParam function
68+
*
69+
* @return address Target contract address
70+
* @return uint256 Call value
71+
* @return bytes Trade calldata
72+
*/
73+
function getTradeCalldata(
74+
address _sourceToken,
75+
address _destinationToken,
76+
address _destinationAddress,
77+
uint256 _sourceQuantity,
78+
uint256 _minDestinationQuantity,
79+
bytes calldata _data
80+
)
81+
external
82+
view
83+
returns (address, uint256, bytes memory)
84+
{
85+
86+
address sourceFromPath = _data.toAddress(0);
87+
require(_sourceToken == sourceFromPath, "UniswapV3ExchangeAdapter: source token path mismatch");
88+
89+
address destinationFromPath = _data.toAddress(_data.length - 20);
90+
require(_destinationToken == destinationFromPath, "UniswapV3ExchangeAdapter: destination token path mismatch");
91+
92+
ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams(
93+
_data,
94+
_destinationAddress,
95+
block.timestamp,
96+
_sourceQuantity,
97+
_minDestinationQuantity
98+
);
99+
100+
bytes memory callData = abi.encodeWithSignature(EXACT_INPUT, params);
101+
return (swapRouter, 0, callData);
102+
}
103+
104+
/**
105+
* Returns the address to approve source tokens to for trading. This is the Uniswap SwapRouter address
106+
*
107+
* @return address Address of the contract to approve tokens to
108+
*/
109+
function getSpender() external view returns (address) {
110+
return swapRouter;
111+
}
112+
113+
/**
114+
* Returns the appropriate _data argument for getTradeCalldata. Equal to the encodePacked path with the
115+
* fee of each hop between it, e.g [token1, fee1, token2, fee2, token3]. Note: _fees.length == _path.length - 1
116+
*
117+
* @param _path array of addresses to use as the path for the trade
118+
* @param _fees array of uint24 representing the pool fee to use for each hop
119+
*/
120+
function generateDataParam(address[] calldata _path, uint24[] calldata _fees) external pure returns (bytes memory) {
121+
bytes memory data = "";
122+
for (uint256 i = 0; i < _path.length - 1; i++) {
123+
data = abi.encodePacked(data, _path[i], _fees[i]);
124+
}
125+
126+
// last encode has no fee associated with it since _fees.length == _path.length - 1
127+
return abi.encodePacked(data, _path[_path.length - 1]);
128+
}
129+
}

test/fixtures/uniswapV3.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe("UniswapV3Fixture", () => {
9595

9696
const slot0 = await pool.slot0();
9797

98-
const expectedSqrtPrice = uniswapV3Fixture._getSqrtPriceX96(1e-12);
98+
const expectedSqrtPrice = uniswapV3Fixture._getSqrtPriceX96(1e12);
9999

100100
expect(slot0.sqrtPriceX96).to.eq(expectedSqrtPrice);
101101
});
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import "module-alias/register";
2+
3+
import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
4+
import { solidityPack } from "ethers/lib/utils";
5+
6+
import { Address, Bytes } from "@utils/types";
7+
import { Account } from "@utils/test/types";
8+
import { ZERO } from "@utils/constants";
9+
import { UniswapV3ExchangeAdapter } from "@utils/contracts";
10+
import DeployHelper from "@utils/deploys";
11+
import { ether } from "@utils/index";
12+
import {
13+
addSnapshotBeforeRestoreAfterEach,
14+
getAccounts,
15+
getSystemFixture,
16+
getWaffleExpect,
17+
getLastBlockTimestamp,
18+
getUniswapV3Fixture,
19+
getRandomAddress
20+
} from "@utils/test/index";
21+
22+
import { SystemFixture, UniswapV3Fixture } from "@utils/fixtures";
23+
24+
const expect = getWaffleExpect();
25+
26+
describe("UniswapV3ExchangeAdapter", () => {
27+
let owner: Account;
28+
let mockSetToken: Account;
29+
let deployer: DeployHelper;
30+
let setup: SystemFixture;
31+
let uniswapV3Fixture: UniswapV3Fixture;
32+
33+
let uniswapV3ExchangeAdapter: UniswapV3ExchangeAdapter;
34+
35+
before(async () => {
36+
[
37+
owner,
38+
mockSetToken,
39+
] = await getAccounts();
40+
41+
deployer = new DeployHelper(owner.wallet);
42+
setup = getSystemFixture(owner.address);
43+
await setup.initialize();
44+
45+
uniswapV3Fixture = getUniswapV3Fixture(owner.address);
46+
await uniswapV3Fixture.initialize(
47+
owner,
48+
setup.weth,
49+
2500,
50+
setup.wbtc,
51+
35000,
52+
setup.dai
53+
);
54+
55+
uniswapV3ExchangeAdapter = await deployer.adapters.deployUniswapV3ExchangeAdapter(uniswapV3Fixture.swapRouter.address);
56+
});
57+
58+
addSnapshotBeforeRestoreAfterEach();
59+
60+
describe("#constructor", async () => {
61+
let subjectSwapRouter: Address;
62+
63+
beforeEach(async () => {
64+
subjectSwapRouter = uniswapV3Fixture.swapRouter.address;
65+
});
66+
67+
async function subject(): Promise<any> {
68+
return await deployer.adapters.deployUniswapV3ExchangeAdapter(subjectSwapRouter);
69+
}
70+
71+
it("should have the correct SwapRouter address", async () => {
72+
const deployedUniswapV3ExchangeAdapter = await subject();
73+
74+
const actualRouterAddress = await deployedUniswapV3ExchangeAdapter.swapRouter();
75+
expect(actualRouterAddress).to.eq(uniswapV3Fixture.swapRouter.address);
76+
});
77+
});
78+
79+
describe("#getSpender", async () => {
80+
async function subject(): Promise<any> {
81+
return await uniswapV3ExchangeAdapter.getSpender();
82+
}
83+
84+
it("should return the correct spender address", async () => {
85+
const spender = await subject();
86+
87+
expect(spender).to.eq(uniswapV3Fixture.swapRouter.address);
88+
});
89+
});
90+
91+
describe("#getTradeCalldata", async () => {
92+
93+
let subjectMockSetToken: Address;
94+
let subjectSourceToken: Address;
95+
let subjectDestinationToken: Address;
96+
let subjectSourceQuantity: BigNumber;
97+
let subjectMinDestinationQuantity: BigNumber;
98+
let subjectPath: Bytes;
99+
100+
beforeEach(async () => {
101+
subjectSourceToken = setup.wbtc.address;
102+
subjectSourceQuantity = BigNumber.from(100000000);
103+
subjectDestinationToken = setup.weth.address;
104+
subjectMinDestinationQuantity = ether(25);
105+
106+
subjectMockSetToken = mockSetToken.address;
107+
108+
subjectPath = solidityPack(["address", "uint24", "address"], [subjectSourceToken, BigNumber.from(3000), subjectDestinationToken]);
109+
});
110+
111+
async function subject(): Promise<any> {
112+
return await uniswapV3ExchangeAdapter.getTradeCalldata(
113+
subjectSourceToken,
114+
subjectDestinationToken,
115+
subjectMockSetToken,
116+
subjectSourceQuantity,
117+
subjectMinDestinationQuantity,
118+
subjectPath,
119+
);
120+
}
121+
122+
it("should return the correct trade calldata", async () => {
123+
const calldata = await subject();
124+
const callTimestamp = await getLastBlockTimestamp();
125+
126+
const expectedCallData = uniswapV3Fixture.swapRouter.interface.encodeFunctionData("exactInput", [{
127+
path: subjectPath,
128+
recipient: mockSetToken.address,
129+
deadline: callTimestamp,
130+
amountIn: subjectSourceQuantity,
131+
amountOutMinimum: subjectMinDestinationQuantity,
132+
}]);
133+
134+
expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapV3Fixture.swapRouter.address, ZERO, expectedCallData]));
135+
});
136+
137+
context("when source token does not match path", async () => {
138+
beforeEach(async () => {
139+
subjectSourceToken = await getRandomAddress();
140+
});
141+
142+
it("should revert", async () => {
143+
await expect(subject()).to.be.revertedWith("UniswapV3ExchangeAdapter: source token path mismatch");
144+
});
145+
});
146+
147+
context("when destination token does not match path", async () => {
148+
beforeEach(async () => {
149+
subjectDestinationToken = await getRandomAddress();
150+
});
151+
152+
it("should revert", async () => {
153+
await expect(subject()).to.be.revertedWith("UniswapV3ExchangeAdapter: destination token path mismatch");
154+
});
155+
});
156+
});
157+
158+
describe("#generateDataParam", async () => {
159+
160+
let subjectToken1: Address;
161+
let subjectFee1: BigNumberish;
162+
let subjectToken2: Address;
163+
let subjectFee2: BigNumberish;
164+
let subjectToken3: Address;
165+
166+
beforeEach(async () => {
167+
subjectToken1 = setup.wbtc.address;
168+
subjectFee1 = 3000;
169+
subjectToken2 = setup.weth.address;
170+
subjectFee2 = 500;
171+
subjectToken3 = setup.weth.address;
172+
});
173+
174+
async function subject(): Promise<string> {
175+
return await uniswapV3ExchangeAdapter.generateDataParam([subjectToken1, subjectToken2, subjectToken3], [subjectFee1, subjectFee2]);
176+
}
177+
178+
it("should create the correct path data", async () => {
179+
const data = await subject();
180+
181+
const expectedData = solidityPack(
182+
["address", "uint24", "address", "uint24", "address"],
183+
[subjectToken1, subjectFee1, subjectToken2, subjectFee2, subjectToken3]
184+
);
185+
186+
expect(data).to.eq(expectedData);
187+
});
188+
});
189+
});

0 commit comments

Comments
 (0)