Skip to content

Commit e83d38c

Browse files
ncitronrichardliang
authored andcommitted
Add AMMSplitter (#99)
* add _getUniSplit function * add swapping * only rever when total output < min output * use better math for split * add tests * rename contracts and functions to be inline with spec * add multi hop support * add tradeExactOutput * clean up imports * shorten name * add javadocs * add getQuote function * clean up ratio calculation * remove hardhat console * add TradeSplitterExchangeAdapter * fix typo causing tests to break * add TradeSplitterIndexExchangeAdapter * improve coverage * increase coverage * refactor * remove redundant if in getQuote * improve reverts * improve coverage * move TradeSplitter into products * add TradeSplitterExchangeAdapter integration test * split getQuote into two functions for exact input/output * add TradeSplitter GIM integration tests * fix tests * make TradeSplitter adhere to the UniswapV2Router interface * use uniswap v2 adapters for trade splitter * cleanup * adhere to uniswap interface for getting quotes * change name of contracts * improve AMMSplitter tests * refactoring * fix bug with intermediary tokens with less than 18 decimals * refactor * add extra tests * update revert messages * improve comments * add events * use input and output tokens instead of path for events
1 parent c33727c commit e83d38c

File tree

8 files changed

+3031
-3
lines changed

8 files changed

+3031
-3
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
pragma solidity 0.6.10;
2+
3+
interface IUniswapV2Factory {
4+
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
5+
6+
function getPair(address tokenA, address tokenB) external view returns (address pair);
7+
function allPairs(uint) external view returns (address pair);
8+
function allPairsLength() external view returns (uint);
9+
10+
function feeTo() external view returns (address);
11+
function feeToSetter() external view returns (address);
12+
13+
function createPair(address tokenA, address tokenB) external returns (address pair);
14+
}

contracts/product/AMMSplitter.sol

Lines changed: 440 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import "module-alias/register";
2+
import { BigNumber } from "@ethersproject/bignumber";
3+
4+
import { Address } from "@utils/types";
5+
import { Account } from "@utils/test/types";
6+
import { ADDRESS_ZERO, MAX_UINT_256, ZERO } from "@utils/constants";
7+
import {
8+
GeneralIndexModule,
9+
SetToken,
10+
AMMSplitter,
11+
UniswapV2IndexExchangeAdapter
12+
} from "@utils/contracts";
13+
import DeployHelper from "@utils/deploys";
14+
import {
15+
bitcoin,
16+
ether,
17+
preciseDiv,
18+
preciseMul,
19+
} from "@utils/index";
20+
import {
21+
cacheBeforeEach,
22+
getAccounts,
23+
getSystemFixture,
24+
getUniswapFixture,
25+
getWaffleExpect
26+
} from "@utils/test/index";
27+
import { SystemFixture, UniswapFixture } from "@utils/fixtures";
28+
import { ContractTransaction } from "ethers";
29+
30+
const expect = getWaffleExpect();
31+
32+
describe("AMMSplitterGeneralIndexModule", () => {
33+
let owner: Account;
34+
let trader: Account;
35+
let positionModule: Account;
36+
let deployer: DeployHelper;
37+
let setup: SystemFixture;
38+
39+
let uniswapSetup: UniswapFixture;
40+
let sushiswapSetup: UniswapFixture;
41+
let tradeSplitter: AMMSplitter;
42+
43+
let index: SetToken;
44+
let indexModule: GeneralIndexModule;
45+
46+
let tradeSplitterAdapter: UniswapV2IndexExchangeAdapter;
47+
let tradeSplitterAdapterName: string;
48+
49+
let indexComponents: Address[];
50+
let indexUnits: BigNumber[];
51+
52+
before(async () => {
53+
[
54+
owner,
55+
trader,
56+
positionModule,
57+
] = await getAccounts();
58+
59+
deployer = new DeployHelper(owner.wallet);
60+
setup = getSystemFixture(owner.address);
61+
uniswapSetup = getUniswapFixture(owner.address);
62+
sushiswapSetup = getUniswapFixture(owner.address);
63+
64+
await setup.initialize();
65+
await uniswapSetup.initialize(owner, setup.weth.address, setup.wbtc.address, setup.dai.address);
66+
await sushiswapSetup.initialize(owner, setup.weth.address, setup.wbtc.address, setup.dai.address);
67+
68+
indexModule = await deployer.modules.deployGeneralIndexModule(
69+
setup.controller.address,
70+
setup.weth.address
71+
);
72+
await setup.controller.addModule(indexModule.address);
73+
await setup.controller.addModule(positionModule.address);
74+
75+
76+
tradeSplitter = await deployer.product.deployAMMSplitter(uniswapSetup.router.address, sushiswapSetup.router.address);
77+
tradeSplitterAdapter = await deployer.adapters.deployUniswapV2IndexExchangeAdapter(tradeSplitter.address);
78+
tradeSplitterAdapterName = "TRADESPLITTER";
79+
80+
81+
await setup.integrationRegistry.batchAddIntegration(
82+
[ indexModule.address ],
83+
[ tradeSplitterAdapterName ],
84+
[ tradeSplitterAdapter.address ]
85+
);
86+
});
87+
88+
cacheBeforeEach(async () => {
89+
indexComponents = [setup.wbtc.address, setup.dai.address];
90+
indexUnits = [ bitcoin(0.01), ether(4000) ];
91+
92+
index = await setup.createSetToken(
93+
indexComponents,
94+
indexUnits,
95+
[setup.issuanceModule.address, indexModule.address, positionModule.address],
96+
);
97+
98+
await setup.issuanceModule.initialize(index.address, ADDRESS_ZERO);
99+
await index.connect(positionModule.wallet).initializeModule();
100+
101+
await setup.weth.connect(owner.wallet).approve(sushiswapSetup.router.address, ether(2000));
102+
await setup.dai.connect(owner.wallet).approve(sushiswapSetup.router.address, ether(400000));
103+
await sushiswapSetup.router.connect(owner.wallet).addLiquidity(
104+
setup.weth.address,
105+
setup.dai.address,
106+
ether(200),
107+
ether(400000),
108+
ether(148),
109+
ether(173000),
110+
owner.address,
111+
MAX_UINT_256
112+
);
113+
114+
await setup.weth.connect(owner.wallet).approve(uniswapSetup.router.address, ether(1000));
115+
await setup.wbtc.connect(owner.wallet).approve(uniswapSetup.router.address, ether(26));
116+
await uniswapSetup.router.addLiquidity(
117+
setup.weth.address,
118+
setup.wbtc.address,
119+
ether(1000),
120+
bitcoin(25.5555),
121+
ether(999),
122+
ether(25.3),
123+
owner.address,
124+
MAX_UINT_256
125+
);
126+
});
127+
128+
describe("when module is initalized", async () => {
129+
let subjectSetToken: SetToken;
130+
let subjectCaller: Account;
131+
132+
let newComponents: Address[];
133+
let newTargetUnits: BigNumber[];
134+
let oldTargetUnits: BigNumber[];
135+
let issueAmount: BigNumber;
136+
137+
async function initSetToken(
138+
setToken: SetToken, components: Address[], tradeMaximums: BigNumber[], exchanges: string[], coolOffPeriods: BigNumber[]
139+
) {
140+
await indexModule.initialize(setToken.address);
141+
await indexModule.setTradeMaximums(setToken.address, components, tradeMaximums);
142+
await indexModule.setExchanges(setToken.address, components, exchanges);
143+
await indexModule.setCoolOffPeriods(setToken.address, components, coolOffPeriods);
144+
await indexModule.setTraderStatus(setToken.address, [trader.address], [true]);
145+
}
146+
147+
const startRebalance = async () => {
148+
await setup.approveAndIssueSetToken(subjectSetToken, issueAmount);
149+
await indexModule.startRebalance(
150+
subjectSetToken.address,
151+
newComponents,
152+
newTargetUnits,
153+
oldTargetUnits,
154+
await index.positionMultiplier()
155+
);
156+
};
157+
158+
before(async () => {
159+
newComponents = [];
160+
oldTargetUnits = [bitcoin(0.1), ether(1)];
161+
newTargetUnits = [];
162+
issueAmount = ether("20.000000000000000001");
163+
});
164+
165+
cacheBeforeEach(async () => {
166+
await initSetToken(
167+
index,
168+
[setup.wbtc.address, setup.dai.address],
169+
[bitcoin(1000), ether(100000)],
170+
[tradeSplitterAdapterName, tradeSplitterAdapterName],
171+
[ZERO, ZERO]
172+
);
173+
});
174+
175+
describe("#trade", async () => {
176+
let subjectComponent: Address;
177+
let subjectEthQuantityLimit: BigNumber;
178+
179+
let expectedOut: BigNumber;
180+
181+
const initializeSubjectVariables = () => {
182+
subjectSetToken = index;
183+
subjectCaller = trader;
184+
subjectComponent = setup.dai.address;
185+
subjectEthQuantityLimit = ZERO;
186+
};
187+
188+
async function subject(): Promise<ContractTransaction> {
189+
return await indexModule.connect(subjectCaller.wallet).trade(
190+
subjectSetToken.address,
191+
subjectComponent,
192+
subjectEthQuantityLimit
193+
);
194+
}
195+
196+
describe("with default target units", async () => {
197+
beforeEach(async () => {
198+
initializeSubjectVariables();
199+
200+
expectedOut = (await tradeSplitter.getAmountsOut(
201+
preciseMul(ether(3999), issueAmount),
202+
[ setup.dai.address, setup.weth.address ]
203+
))[1];
204+
205+
subjectEthQuantityLimit = BigNumber.from(0);
206+
});
207+
cacheBeforeEach(startRebalance);
208+
209+
it("should sell using TradeSplitter", async () => {
210+
const currentWethAmount = await setup.weth.balanceOf(subjectSetToken.address);
211+
const totalSupply = await subjectSetToken.totalSupply();
212+
213+
await subject();
214+
215+
const expectedWethPositionUnits = preciseDiv(currentWethAmount.add(expectedOut), totalSupply);
216+
const expectedDaiPositionUnits = ether(1);
217+
218+
const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address);
219+
const daiPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.dai.address);
220+
221+
expect(wethPositionUnits).to.eq(expectedWethPositionUnits);
222+
expect(daiPositionUnits).to.eq(expectedDaiPositionUnits);
223+
});
224+
});
225+
});
226+
227+
describe("#tradeRemainingWETH", async () => {
228+
229+
let subjectComponent: Address;
230+
let subjectEthQuantityLimit: BigNumber;
231+
232+
let expectedOut: BigNumber;
233+
234+
const initializeSubjectVariables = () => {
235+
subjectSetToken = index;
236+
subjectCaller = trader;
237+
subjectComponent = setup.dai.address;
238+
subjectEthQuantityLimit = ZERO;
239+
};
240+
241+
async function subject(): Promise<ContractTransaction> {
242+
return await indexModule.connect(subjectCaller.wallet).tradeRemainingWETH(
243+
subjectSetToken.address,
244+
subjectComponent,
245+
subjectEthQuantityLimit
246+
);
247+
}
248+
249+
describe("with default target units", async () => {
250+
251+
beforeEach(async () => {
252+
initializeSubjectVariables();
253+
});
254+
cacheBeforeEach(startRebalance);
255+
256+
context("when it is the last trade", async () => {
257+
258+
beforeEach(async () => {
259+
await indexModule.connect(subjectCaller.wallet).trade(
260+
subjectSetToken.address,
261+
subjectComponent,
262+
subjectEthQuantityLimit
263+
);
264+
265+
expectedOut = (await tradeSplitter.getAmountsOut(
266+
await setup.weth.balanceOf(subjectSetToken.address),
267+
[ setup.weth.address, setup.wbtc.address ]
268+
))[1];
269+
270+
subjectComponent = setup.wbtc.address;
271+
subjectEthQuantityLimit = ZERO;
272+
});
273+
274+
it("should buy using TradeSplitter", async () => {
275+
const currentWbtcAmount = await setup.wbtc.balanceOf(subjectSetToken.address);
276+
const totalSupply = await subjectSetToken.totalSupply();
277+
278+
await subject();
279+
280+
const expectedWbtcPositionUnits = preciseDiv(currentWbtcAmount.add(expectedOut), totalSupply);
281+
const expectedWethPositionUnits = ether(0);
282+
283+
const wethPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.weth.address);
284+
const wbtcPositionUnits = await subjectSetToken.getDefaultPositionRealUnit(setup.wbtc.address);
285+
286+
expect(wbtcPositionUnits).to.eq(expectedWbtcPositionUnits);
287+
expect(wethPositionUnits).to.eq(expectedWethPositionUnits);
288+
});
289+
});
290+
});
291+
});
292+
});
293+
});

0 commit comments

Comments
 (0)