Skip to content

Commit 685ca7f

Browse files
snreynoldslint-actionNoahZinsmeister
authored
Flash example checkpoint (Uniswap#132)
* final checkpoint tests pass * fix: minor edits * Fix code style issues with Prettier * fix flash contract fix migrator tests * update snaps Co-authored-by: Lint Action <[email protected]> Co-authored-by: Noah Zinsmeister <[email protected]>
1 parent c56f98a commit 685ca7f

6 files changed

+350
-27
lines changed

contracts/examples/PairFlash.sol

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity =0.7.6;
3+
pragma abicoder v2;
4+
5+
import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3FlashCallback.sol';
6+
import '@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol';
7+
8+
import '../base/PeripheryPayments.sol';
9+
import '../base/PeripheryImmutableState.sol';
10+
import '../libraries/PoolAddress.sol';
11+
import '../libraries/CallbackValidation.sol';
12+
import '../libraries/TransferHelper.sol';
13+
import '../interfaces/ISwapRouter.sol';
14+
15+
/// @title Flash contract implementation
16+
/// @notice An example contract using the Uniswap V3 flash function
17+
contract PairFlash is IUniswapV3FlashCallback, PeripheryPayments {
18+
using LowGasSafeMath for uint256;
19+
using LowGasSafeMath for int256;
20+
21+
ISwapRouter public immutable swapRouter;
22+
23+
constructor(
24+
ISwapRouter _swapRouter,
25+
address _factory,
26+
address _WETH9
27+
) PeripheryImmutableState(_factory, _WETH9) {
28+
swapRouter = _swapRouter;
29+
}
30+
31+
// fee2 and fee3 are the two other fees associated with the two other pools of token0 and token1
32+
struct FlashCallbackData {
33+
uint256 amount0;
34+
uint256 amount1;
35+
address payer;
36+
PoolAddress.PoolKey poolKey;
37+
uint24 poolFee2;
38+
uint24 poolFee3;
39+
}
40+
41+
/// @param fee0 The fee from calling flash for token0
42+
/// @param fee1 The fee from calling flash for token1
43+
/// @param data The data needed in the callback passed as FlashCallbackData from `initFlash`
44+
/// @notice implements the callback called from flash
45+
/// @dev fails if the flash is not profitable, meaning the amountOut from the flash is less than the amount borrowed
46+
function uniswapV3FlashCallback(
47+
uint256 fee0,
48+
uint256 fee1,
49+
bytes calldata data
50+
) external override {
51+
FlashCallbackData memory decoded = abi.decode(data, (FlashCallbackData));
52+
CallbackValidation.verifyCallback(factory, decoded.poolKey);
53+
54+
address token0 = decoded.poolKey.token0;
55+
address token1 = decoded.poolKey.token1;
56+
57+
// profitability parameters - we must receive at least the required payment from the arbitrage swaps
58+
// exactInputSingle will fail if this amount not met
59+
uint256 amount0Min = LowGasSafeMath.add(decoded.amount0, fee0);
60+
uint256 amount1Min = LowGasSafeMath.add(decoded.amount1, fee1);
61+
62+
// call exactInputSingle for swapping token1 for token0 in pool with fee2
63+
TransferHelper.safeApprove(token1, address(swapRouter), decoded.amount1);
64+
uint256 amountOut0 =
65+
swapRouter.exactInputSingle(
66+
ISwapRouter.ExactInputSingleParams({
67+
tokenIn: token1,
68+
tokenOut: token0,
69+
fee: decoded.poolFee2,
70+
recipient: address(this),
71+
deadline: block.timestamp,
72+
amountIn: decoded.amount1,
73+
amountOutMinimum: amount0Min,
74+
sqrtPriceLimitX96: 0
75+
})
76+
);
77+
78+
// call exactInputSingle for swapping token0 for token 1 in pool with fee3
79+
TransferHelper.safeApprove(token0, address(swapRouter), decoded.amount0);
80+
uint256 amountOut1 =
81+
swapRouter.exactInputSingle(
82+
ISwapRouter.ExactInputSingleParams({
83+
tokenIn: token0,
84+
tokenOut: token1,
85+
fee: decoded.poolFee3,
86+
recipient: address(this),
87+
deadline: block.timestamp,
88+
amountIn: decoded.amount0,
89+
amountOutMinimum: amount1Min,
90+
sqrtPriceLimitX96: 0
91+
})
92+
);
93+
94+
// end up with amountOut0 of token0 from first swap and amountOut1 of token1 from second swap
95+
uint256 amount0Owed = LowGasSafeMath.add(decoded.amount0, fee0);
96+
uint256 amount1Owed = LowGasSafeMath.add(decoded.amount1, fee1);
97+
98+
// pay the required amounts back to the pair
99+
if (amount0Min > 0) pay(token0, address(this), msg.sender, amount0Min);
100+
if (amount1Min > 0) pay(token1, address(this), msg.sender, amount1Min);
101+
102+
// if profitable pay profits to payer
103+
if (amountOut0 > amount0Owed) {
104+
uint256 profit0 = amountOut0 - amount0Owed;
105+
pay(token0, address(this), decoded.payer, profit0);
106+
}
107+
if (amountOut1 > amount1Owed) {
108+
uint256 profit1 = amountOut1 - amount1Owed;
109+
pay(token1, address(this), decoded.payer, profit1);
110+
}
111+
}
112+
113+
//fee1 is the fee of the pool from the initial borrow
114+
//fee2 is the fee of the first pool to arb from
115+
//fee3 is the fee of the second pool to arb from
116+
struct FlashParams {
117+
address token0;
118+
address token1;
119+
uint24 fee1;
120+
uint256 amount0;
121+
uint256 amount1;
122+
uint24 fee2;
123+
uint24 fee3;
124+
}
125+
126+
/// @param params The parameters necessary for flash and the callback, passed in as FlashParams
127+
/// @notice Calls the pools flash function with data needed in `uniswapV3FlashCallback`
128+
function initFlash(FlashParams memory params) external {
129+
PoolAddress.PoolKey memory poolKey =
130+
PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee1});
131+
IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
132+
// recipient of borrowed amounts
133+
// amount of token0 requested to borrow
134+
// amount of token1 requested to borrow
135+
// need amount 0 and amount1 in callback to pay back pool
136+
// recipient of flash should be THIS contract
137+
pool.flash(
138+
address(this),
139+
params.amount0,
140+
params.amount1,
141+
abi.encode(
142+
FlashCallbackData({
143+
amount0: params.amount0,
144+
amount1: params.amount1,
145+
payer: msg.sender,
146+
poolKey: poolKey,
147+
poolFee2: params.fee2,
148+
poolFee3: params.fee3
149+
})
150+
)
151+
);
152+
}
153+
}

test/PairFlash.spec.ts

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { ethers, waffle } from 'hardhat'
2+
import { BigNumber, constants, Contract, ContractTransaction } from 'ethers'
3+
import {
4+
IWETH9,
5+
MockTimeNonfungiblePositionManager,
6+
MockTimeSwapRouter,
7+
PairFlash,
8+
IUniswapV3Pool,
9+
TestERC20,
10+
TestERC20Metadata,
11+
IUniswapV3Factory,
12+
NFTDescriptor,
13+
Quoter,
14+
SwapRouter,
15+
} from '../typechain'
16+
import completeFixture from './shared/completeFixture'
17+
import { FeeAmount, MaxUint128, TICK_SPACINGS } from './shared/constants'
18+
import { encodePriceSqrt } from './shared/encodePriceSqrt'
19+
import snapshotGasCost from './shared/snapshotGasCost'
20+
21+
import { expect } from './shared/expect'
22+
import { getMaxTick, getMinTick } from './shared/ticks'
23+
import { computePoolAddress } from './shared/computePoolAddress'
24+
25+
describe('PairFlash test', () => {
26+
const provider = waffle.provider
27+
const wallets = waffle.provider.getWallets()
28+
const wallet = wallets[0]
29+
30+
let flash: PairFlash
31+
let nft: MockTimeNonfungiblePositionManager
32+
let token0: TestERC20
33+
let token1: TestERC20
34+
let factory: IUniswapV3Factory
35+
let quoter: Quoter
36+
37+
async function createPool(tokenAddressA: string, tokenAddressB: string, fee: FeeAmount, price: BigNumber) {
38+
if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase())
39+
[tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA]
40+
41+
await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, fee, price)
42+
43+
const liquidityParams = {
44+
token0: tokenAddressA,
45+
token1: tokenAddressB,
46+
fee: fee,
47+
tickLower: getMinTick(TICK_SPACINGS[fee]),
48+
tickUpper: getMaxTick(TICK_SPACINGS[fee]),
49+
recipient: wallet.address,
50+
amount0Desired: 1000000,
51+
amount1Desired: 1000000,
52+
amount0Min: 0,
53+
amount1Min: 0,
54+
deadline: 1,
55+
}
56+
57+
return nft.mint(liquidityParams)
58+
}
59+
60+
const flashFixture = async () => {
61+
const { router, tokens, factory, weth9, nft } = await completeFixture(wallets, provider)
62+
const token0 = tokens[0]
63+
const token1 = tokens[1]
64+
65+
const flashContractFactory = await ethers.getContractFactory('PairFlash')
66+
const flash = (await flashContractFactory.deploy(router.address, factory.address, weth9.address)) as PairFlash
67+
68+
const quoterFactory = await ethers.getContractFactory('Quoter')
69+
const quoter = (await quoterFactory.deploy(factory.address, weth9.address)) as Quoter
70+
71+
return {
72+
token0,
73+
token1,
74+
flash,
75+
factory,
76+
weth9,
77+
nft,
78+
quoter,
79+
router,
80+
}
81+
}
82+
83+
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
84+
85+
before('create fixture loader', async () => {
86+
loadFixture = waffle.createFixtureLoader(wallets)
87+
})
88+
89+
beforeEach('load fixture', async () => {
90+
;({ factory, token0, token1, flash, nft, quoter } = await loadFixture(flashFixture))
91+
92+
await token0.approve(nft.address, MaxUint128)
93+
await token1.approve(nft.address, MaxUint128)
94+
await createPool(token0.address, token1.address, FeeAmount.LOW, encodePriceSqrt(5, 10))
95+
await createPool(token0.address, token1.address, FeeAmount.MEDIUM, encodePriceSqrt(1, 1))
96+
await createPool(token0.address, token1.address, FeeAmount.HIGH, encodePriceSqrt(20, 10))
97+
})
98+
99+
describe('flash', () => {
100+
it('test correct transfer events', async () => {
101+
//choose amountIn to test
102+
const amount0In = 1000
103+
const amount1In = 1000
104+
105+
const fee0 = Math.ceil((amount0In * FeeAmount.MEDIUM) / 1000000)
106+
const fee1 = Math.ceil((amount1In * FeeAmount.MEDIUM) / 1000000)
107+
108+
const flashParams = {
109+
token0: token0.address,
110+
token1: token1.address,
111+
fee1: FeeAmount.MEDIUM,
112+
amount0: amount0In,
113+
amount1: amount1In,
114+
fee2: FeeAmount.LOW,
115+
fee3: FeeAmount.HIGH,
116+
}
117+
// pool1 is the borrow pool
118+
const pool1 = computePoolAddress(factory.address, [token0.address, token1.address], FeeAmount.MEDIUM)
119+
const pool2 = computePoolAddress(factory.address, [token0.address, token1.address], FeeAmount.LOW)
120+
const pool3 = computePoolAddress(factory.address, [token0.address, token1.address], FeeAmount.HIGH)
121+
122+
const expectedAmountOut0 = await quoter.callStatic.quoteExactInputSingle(
123+
token1.address,
124+
token0.address,
125+
FeeAmount.LOW,
126+
amount1In,
127+
encodePriceSqrt(20, 10)
128+
)
129+
const expectedAmountOut1 = await quoter.callStatic.quoteExactInputSingle(
130+
token0.address,
131+
token1.address,
132+
FeeAmount.HIGH,
133+
amount0In,
134+
encodePriceSqrt(5, 10)
135+
)
136+
137+
await expect(flash.initFlash(flashParams))
138+
.to.emit(token0, 'Transfer')
139+
.withArgs(pool1, flash.address, amount0In)
140+
.to.emit(token1, 'Transfer')
141+
.withArgs(pool1, flash.address, amount1In)
142+
.to.emit(token0, 'Transfer')
143+
.withArgs(pool2, flash.address, expectedAmountOut0)
144+
.to.emit(token1, 'Transfer')
145+
.withArgs(pool3, flash.address, expectedAmountOut1)
146+
.to.emit(token0, 'Transfer')
147+
.withArgs(flash.address, wallet.address, expectedAmountOut0.toNumber() - amount0In - fee0)
148+
.to.emit(token1, 'Transfer')
149+
.withArgs(flash.address, wallet.address, expectedAmountOut1.toNumber() - amount1In - fee1)
150+
})
151+
152+
it('gas', async () => {
153+
const amount0In = 1000
154+
const amount1In = 1000
155+
156+
const flashParams = {
157+
token0: token0.address,
158+
token1: token1.address,
159+
fee1: FeeAmount.MEDIUM,
160+
amount0: amount0In,
161+
amount1: amount1In,
162+
fee2: FeeAmount.LOW,
163+
fee3: FeeAmount.HIGH,
164+
}
165+
await snapshotGasCost(flash.initFlash(flashParams))
166+
})
167+
})
168+
})

test/V3Migrator.spec.ts

+17-18
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ describe('V3Migrator', () => {
6767

6868
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
6969

70+
const expectedLiquidity = 10000 - 1000
71+
7072
before('create fixture loader', async () => {
7173
const wallets = await (ethers as any).getSigners()
7274
wallet = wallets[0]
@@ -78,6 +80,21 @@ describe('V3Migrator', () => {
7880
;({ factoryV2, factoryV3, token, weth9, nft, migrator } = await loadFixture(migratorFixture))
7981
})
8082

83+
beforeEach('add V2 liquidity', async () => {
84+
await factoryV2.createPair(token.address, weth9.address)
85+
86+
const pairAddress = await factoryV2.getPair(token.address, weth9.address)
87+
88+
pair = new ethers.Contract(pairAddress, PAIR_V2_ABI, wallet) as IUniswapV2Pair
89+
90+
await token.transfer(pair.address, 10000)
91+
await weth9.transfer(pair.address, 10000)
92+
93+
await pair.mint(wallet.address)
94+
95+
expect(await pair.balanceOf(wallet.address)).to.be.eq(expectedLiquidity)
96+
})
97+
8198
afterEach('ensure allowances are cleared', async () => {
8299
const allowanceToken = await token.allowance(migrator.address, nft.address)
83100
const allowanceWETH9 = await weth9.allowance(migrator.address, nft.address)
@@ -99,28 +116,10 @@ describe('V3Migrator', () => {
99116

100117
describe('#migrate', () => {
101118
let tokenLower: boolean
102-
103-
const expectedLiquidity = 10000 - 1000
104-
105119
beforeEach(() => {
106120
tokenLower = token.address.toLowerCase() < weth9.address.toLowerCase()
107121
})
108122

109-
beforeEach('add V2 liquidity', async () => {
110-
await factoryV2.createPair(token.address, weth9.address)
111-
112-
const pairAddress = await factoryV2.getPair(token.address, weth9.address)
113-
114-
pair = new ethers.Contract(pairAddress, PAIR_V2_ABI, wallet) as IUniswapV2Pair
115-
116-
await token.transfer(pair.address, 10000)
117-
await weth9.transfer(pair.address, 10000)
118-
119-
await pair.mint(wallet.address)
120-
121-
expect(await pair.balanceOf(wallet.address)).to.be.eq(expectedLiquidity)
122-
})
123-
124123
it('fails if v3 pool is not initialized', async () => {
125124
await pair.approve(migrator.address, expectedLiquidity)
126125
await expect(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`PairFlash test flash gas 1`] = `334941`;

0 commit comments

Comments
 (0)