Skip to content

Commit 2dc46cd

Browse files
richardliangasoong
authored andcommitted
Add new Uniswap transfer fee adapter
1 parent 8ae005e commit 2dc46cd

File tree

5 files changed

+451
-2
lines changed

5 files changed

+451
-2
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
/**
23+
* @title UniswapV2TransferFeeExchangeAdapter
24+
* @author Set Protocol
25+
*
26+
* Exchange adapter for Uniswap V2 Router02 that supports trading tokens with transfer fees
27+
*/
28+
contract UniswapV2TransferFeeExchangeAdapter {
29+
30+
/* ============ State Variables ============ */
31+
32+
// Address of Uniswap V2 Router02 contract
33+
address public immutable router;
34+
35+
/* ============ Constructor ============ */
36+
37+
/**
38+
* Set state variables
39+
*
40+
* @param _router Address of Uniswap V2 Router02 contract
41+
*/
42+
constructor(address _router) public {
43+
router = _router;
44+
}
45+
46+
/* ============ External Getter Functions ============ */
47+
48+
/**
49+
* Return calldata for Uniswap V2 Router02 when trading a token with a transfer fee
50+
*
51+
* @param _sourceToken Address of source token to be sold
52+
* @param _destinationToken Address of destination token to buy
53+
* @param _destinationAddress Address that assets should be transferred to
54+
* @param _sourceQuantity Amount of source token to sell
55+
* @param _minDestinationQuantity Min amount of destination token to buy
56+
* @param _data Arbitrary bytes containing trade call data
57+
*
58+
* @return address Target contract address
59+
* @return uint256 Call value
60+
* @return bytes Trade calldata
61+
*/
62+
function getTradeCalldata(
63+
address _sourceToken,
64+
address _destinationToken,
65+
address _destinationAddress,
66+
uint256 _sourceQuantity,
67+
uint256 _minDestinationQuantity,
68+
bytes memory _data
69+
)
70+
external
71+
view
72+
returns (address, uint256, bytes memory)
73+
{
74+
address[] memory path;
75+
76+
if(_data.length == 0){
77+
path = new address[](2);
78+
path[0] = _sourceToken;
79+
path[1] = _destinationToken;
80+
} else {
81+
path = abi.decode(_data, (address[]));
82+
}
83+
84+
bytes memory callData = abi.encodeWithSignature(
85+
"swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256,uint256,address[],address,uint256)",
86+
_sourceQuantity,
87+
_minDestinationQuantity,
88+
path,
89+
_destinationAddress,
90+
block.timestamp
91+
);
92+
return (router, 0, callData);
93+
}
94+
95+
/**
96+
* Returns the address to approve source tokens to for trading. This is the Uniswap router address
97+
*
98+
* @return address Address of the contract to approve tokens to
99+
*/
100+
function getSpender()
101+
external
102+
view
103+
returns (address)
104+
{
105+
return router;
106+
}
107+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import "module-alias/register";
2+
3+
import { BigNumber } from "@ethersproject/bignumber";
4+
import { defaultAbiCoder } from "ethers/lib/utils";
5+
6+
import { Address, Bytes } from "@utils/types";
7+
import { Account } from "@utils/test/types";
8+
import {
9+
EMPTY_BYTES,
10+
ZERO,
11+
} from "@utils/constants";
12+
import { UniswapV2TransferFeeExchangeAdapter } from "@utils/contracts";
13+
import DeployHelper from "@utils/deploys";
14+
import {
15+
ether,
16+
} from "@utils/index";
17+
import {
18+
addSnapshotBeforeRestoreAfterEach,
19+
getAccounts,
20+
getSystemFixture,
21+
getUniswapFixture,
22+
getWaffleExpect,
23+
getLastBlockTimestamp
24+
} from "@utils/test/index";
25+
26+
import { SystemFixture, UniswapFixture } from "@utils/fixtures";
27+
28+
const expect = getWaffleExpect();
29+
30+
describe("UniswapV2TransferFeeExchangeAdapter", () => {
31+
let owner: Account;
32+
let mockSetToken: Account;
33+
let deployer: DeployHelper;
34+
let setup: SystemFixture;
35+
let uniswapSetup: UniswapFixture;
36+
37+
let uniswapV2TransferFeeExchangeAdapter: UniswapV2TransferFeeExchangeAdapter;
38+
39+
before(async () => {
40+
[
41+
owner,
42+
mockSetToken,
43+
] = await getAccounts();
44+
45+
deployer = new DeployHelper(owner.wallet);
46+
setup = getSystemFixture(owner.address);
47+
await setup.initialize();
48+
49+
uniswapSetup = getUniswapFixture(owner.address);
50+
await uniswapSetup.initialize(
51+
owner,
52+
setup.weth.address,
53+
setup.wbtc.address,
54+
setup.dai.address
55+
);
56+
57+
uniswapV2TransferFeeExchangeAdapter = await deployer.adapters.deployUniswapV2TransferFeeExchangeAdapter(uniswapSetup.router.address);
58+
});
59+
60+
addSnapshotBeforeRestoreAfterEach();
61+
62+
describe("constructor", async () => {
63+
let subjectUniswapRouter: Address;
64+
65+
beforeEach(async () => {
66+
subjectUniswapRouter = uniswapSetup.router.address;
67+
});
68+
69+
async function subject(): Promise<any> {
70+
return await deployer.adapters.deployUniswapV2TransferFeeExchangeAdapter(subjectUniswapRouter);
71+
}
72+
73+
it("should have the correct router address", async () => {
74+
const deployedUniswapV2TransferFeeExchangeAdapter = await subject();
75+
76+
const actualRouterAddress = await deployedUniswapV2TransferFeeExchangeAdapter.router();
77+
expect(actualRouterAddress).to.eq(uniswapSetup.router.address);
78+
});
79+
});
80+
81+
describe("getSpender", async () => {
82+
async function subject(): Promise<any> {
83+
return await uniswapV2TransferFeeExchangeAdapter.getSpender();
84+
}
85+
86+
it("should return the correct spender address", async () => {
87+
const spender = await subject();
88+
89+
expect(spender).to.eq(uniswapSetup.router.address);
90+
});
91+
});
92+
93+
describe("getTradeCalldata", async () => {
94+
let sourceAddress: Address;
95+
let destinationAddress: Address;
96+
let sourceQuantity: BigNumber;
97+
let destinationQuantity: BigNumber;
98+
99+
let subjectMockSetToken: Address;
100+
let subjectSourceToken: Address;
101+
let subjectDestinationToken: Address;
102+
let subjectSourceQuantity: BigNumber;
103+
let subjectMinDestinationQuantity: BigNumber;
104+
let subjectData: Bytes;
105+
106+
beforeEach(async () => {
107+
sourceAddress = setup.wbtc.address; // WBTC Address
108+
sourceQuantity = BigNumber.from(100000000); // Trade 1 WBTC
109+
destinationAddress = setup.dai.address; // DAI Address
110+
destinationQuantity = ether(30000); // Receive at least 30k DAI
111+
112+
subjectSourceToken = sourceAddress;
113+
subjectDestinationToken = destinationAddress;
114+
subjectMockSetToken = mockSetToken.address;
115+
subjectSourceQuantity = sourceQuantity;
116+
subjectMinDestinationQuantity = destinationQuantity;
117+
subjectData = EMPTY_BYTES;
118+
});
119+
120+
async function subject(): Promise<any> {
121+
return await uniswapV2TransferFeeExchangeAdapter.getTradeCalldata(
122+
subjectSourceToken,
123+
subjectDestinationToken,
124+
subjectMockSetToken,
125+
subjectSourceQuantity,
126+
subjectMinDestinationQuantity,
127+
subjectData,
128+
);
129+
}
130+
131+
it("should return the correct trade calldata", async () => {
132+
const calldata = await subject();
133+
const callTimestamp = await getLastBlockTimestamp();
134+
const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("swapExactTokensForTokensSupportingFeeOnTransferTokens", [
135+
sourceQuantity,
136+
destinationQuantity,
137+
[sourceAddress, destinationAddress],
138+
subjectMockSetToken,
139+
callTimestamp,
140+
]);
141+
expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData]));
142+
});
143+
144+
describe("when passed in custom path to trade data", async () => {
145+
beforeEach(async () => {
146+
const path = [sourceAddress, setup.weth.address, destinationAddress];
147+
subjectData = defaultAbiCoder.encode(
148+
["address[]"],
149+
[path]
150+
);
151+
});
152+
153+
it("should return the correct trade calldata", async () => {
154+
const calldata = await subject();
155+
const callTimestamp = await getLastBlockTimestamp();
156+
const expectedCallData = uniswapSetup.router.interface.encodeFunctionData("swapExactTokensForTokensSupportingFeeOnTransferTokens", [
157+
sourceQuantity,
158+
destinationQuantity,
159+
[sourceAddress, setup.weth.address, destinationAddress],
160+
subjectMockSetToken,
161+
callTimestamp,
162+
]);
163+
expect(JSON.stringify(calldata)).to.eq(JSON.stringify([uniswapSetup.router.address, ZERO, expectedCallData]));
164+
});
165+
});
166+
});
167+
});

0 commit comments

Comments
 (0)