@defi-wonderland/keep3r-v2
Version:
The Keep3r Network is a decentralized network for projects that need external devops, and for external teams to find keeper jobs
278 lines (230 loc) • 11.9 kB
text/typescript
import { smock } from '@defi-wonderland/smock';
import { BigNumber } from '@ethersproject/bignumber';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import {
IERC20,
ISwapRouter,
IUniswapV3Pool,
LiquidityAmountsForTest,
LiquidityAmountsForTest__factory,
UniV3PairManager,
UniV3PairManagerFactory,
UniV3PairManagerFactory__factory,
} from '@types';
import { evm, wallet } from '@utils';
import { toUnit } from '@utils/bn';
import { snapshot } from '@utils/evm';
import chai, { expect } from 'chai';
import { JsonRpcSigner } from 'ethers/node_modules/@ethersproject/providers';
import { ethers } from 'hardhat';
import * as common from './common';
chai.use(smock.matchers);
const UNIV3_ROUTER_ADDRESS = '0xE592427A0AEce92De3Edee1F18E0157C05861564';
// pools with different fees => name convention = TOKEN0_TOKEN1_FEE
const KP3R_WETH_1 = common.KP3R_WETH_V3_POOL_ADDRESS;
const WBTC_WETH_0_3 = '0xCBCdF9626bC03E24f779434178A73a0B4bad62eD';
const DAI_WETH_0_0_5 = '0x60594a405d53811d3BC4766596EFD80fd545A270';
const DAI_USDC_0_0_1 = '0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168';
// whales
const WHALE = '0x28c6c06298d514db089934071355e5743bf21d60';
describe('UniV3PairManager', () => {
//factories
let uniV3PairManagerFactory: UniV3PairManagerFactory__factory;
//contracts
let daiWethPool: IUniswapV3Pool;
let kp3rWethPoolOne: IUniswapV3Pool;
let kp3rWethPoolTwo: IUniswapV3Pool;
let daiUsdcPool: IUniswapV3Pool;
let liquidityAmounts: LiquidityAmountsForTest;
let liquidityAmountsFactory: LiquidityAmountsForTest__factory;
let uniRouter: ISwapRouter;
let uniPairFactory: UniV3PairManagerFactory;
//tokens
let dai: IERC20;
let weth: IERC20;
let kp3r: IERC20;
let usdc: IERC20;
let wbtc: IERC20;
//signers
let governance: SignerWithAddress;
let whale: JsonRpcSigner;
//misc
let liquidity: BigNumber;
let tenTokens: BigNumber = toUnit(10);
let twentyTokens: BigNumber = toUnit(20);
let twentyUSDC: BigNumber = toUnit(20).div(10 ** 12);
let tenUSDC: BigNumber = toUnit(10).div(10 ** 12);
let amount0MinIsZero: number = 0;
let amount1MinIsZero: number = 0;
let snapshotId: string;
let pair: UniV3PairManager;
let pool: IUniswapV3Pool;
// pair managers
let firstPair: UniV3PairManager;
let secondPair: UniV3PairManager;
let thirdPair: UniV3PairManager;
let fourthPair: UniV3PairManager;
let firstPairAddress: string;
let secondPairAddress: string;
let thirdPairAddress: string;
let fourthPairAddress: string;
before(async () => {
await evm.reset({
jsonRpcUrl: process.env.MAINNET_HTTPS_URL,
blockNumber: common.FORK_BLOCK_NUMBER,
});
[, governance] = await ethers.getSigners();
dai = (await ethers.getContractAt('@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', common.DAI_ADDRESS)) as IERC20;
weth = (await ethers.getContractAt('@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', common.WETH_ADDRESS)) as IERC20;
kp3r = (await ethers.getContractAt('@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', common.KP3R_V1_ADDRESS)) as IERC20;
usdc = (await ethers.getContractAt('@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', common.USDC_ADDRESS)) as IERC20;
wbtc = (await ethers.getContractAt('@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', common.WBTC_ADDRESS)) as IERC20;
uniRouter = (await ethers.getContractAt('ISwapRouter', UNIV3_ROUTER_ADDRESS)) as ISwapRouter;
kp3rWethPoolOne = (await ethers.getContractAt('IUniswapV3Pool', KP3R_WETH_1)) as IUniswapV3Pool;
kp3rWethPoolTwo = (await ethers.getContractAt('IUniswapV3Pool', WBTC_WETH_0_3)) as IUniswapV3Pool;
daiWethPool = (await ethers.getContractAt('IUniswapV3Pool', DAI_WETH_0_0_5)) as IUniswapV3Pool;
daiUsdcPool = (await ethers.getContractAt('IUniswapV3Pool', DAI_USDC_0_0_1)) as IUniswapV3Pool;
uniV3PairManagerFactory = (await ethers.getContractFactory('UniV3PairManagerFactory')) as UniV3PairManagerFactory__factory;
liquidityAmountsFactory = (await ethers.getContractFactory('LiquidityAmountsForTest')) as LiquidityAmountsForTest__factory;
uniPairFactory = await uniV3PairManagerFactory.deploy(governance.address);
// deploying different pairs
await uniPairFactory.createPairManager(KP3R_WETH_1);
await uniPairFactory.createPairManager(WBTC_WETH_0_3);
await uniPairFactory.createPairManager(DAI_WETH_0_0_5);
await uniPairFactory.createPairManager(DAI_USDC_0_0_1);
// getting the different created pairs
firstPairAddress = await uniPairFactory.callStatic.pairManagers(KP3R_WETH_1);
secondPairAddress = await uniPairFactory.callStatic.pairManagers(WBTC_WETH_0_3);
thirdPairAddress = await uniPairFactory.callStatic.pairManagers(DAI_WETH_0_0_5);
fourthPairAddress = await uniPairFactory.callStatic.pairManagers(DAI_USDC_0_0_1);
firstPair = (await ethers.getContractAt('IUniV3PairManager', firstPairAddress)) as UniV3PairManager; // 1% fee
secondPair = (await ethers.getContractAt('IUniV3PairManager', secondPairAddress)) as UniV3PairManager; // 0.3% fee
thirdPair = (await ethers.getContractAt('IUniV3PairManager', thirdPairAddress)) as UniV3PairManager; // 0.05% fee
fourthPair = (await ethers.getContractAt('IUniV3PairManager', fourthPairAddress)) as UniV3PairManager; // 0.01% fee
liquidityAmounts = await liquidityAmountsFactory.deploy();
whale = await wallet.impersonate(WHALE);
//mint approvals
await kp3r.connect(whale).approve(firstPair.address, twentyTokens);
await weth.connect(whale).approve(firstPair.address, twentyTokens);
await wbtc.connect(whale).approve(secondPair.address, twentyTokens);
await weth.connect(whale).approve(secondPair.address, twentyTokens);
await dai.connect(whale).approve(thirdPair.address, twentyTokens);
await weth.connect(whale).approve(thirdPair.address, twentyTokens);
await dai.connect(whale).approve(fourthPair.address, twentyTokens);
await usdc.connect(whale).approve(fourthPair.address, twentyUSDC);
//swap approvals
await dai.connect(whale).approve(uniRouter.address, tenTokens);
await weth.connect(whale).approve(uniRouter.address, tenTokens);
await kp3r.connect(whale).approve(uniRouter.address, tenTokens);
await usdc.connect(whale).approve(uniRouter.address, tenUSDC);
snapshotId = await snapshot.take();
});
beforeEach(async () => {
await snapshot.revert(snapshotId);
});
async function calculateLiquidity(token0: BigNumber, token1: BigNumber, pool: IUniswapV3Pool, pair: UniV3PairManager): Promise<BigNumber> {
const sqrtPriceX96 = (await pool.slot0()).sqrtPriceX96;
const sqrtRatioAX96 = await pair.sqrtRatioAX96();
const sqrtRatioBX96 = await pair.sqrtRatioBX96();
const liquidity = await liquidityAmounts.getLiquidityForAmounts(sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, token0, token1);
return liquidity;
}
async function calculateLiquidityAndMint(
token0: BigNumber,
token1: BigNumber,
pool: IUniswapV3Pool,
pair: UniV3PairManager
): Promise<BigNumber> {
let liquidity;
if ((await pair.token0()) && (await pair.token1()) !== usdc.address) {
liquidity = await calculateLiquidity(token0, token1, pool, pair);
await pair.connect(whale).mint(token0, token1, amount0MinIsZero, amount1MinIsZero, whale._address);
} else if ((await pair.token0()) == usdc.address) {
liquidity = await calculateLiquidity(token0.div(10 ** 12), token1, pool, pair);
await pair.connect(whale).mint(token0.div(10 ** 12), token1, amount0MinIsZero, amount1MinIsZero, whale._address);
} else {
liquidity = await calculateLiquidity(token0, token1.div(10 ** 12), pool, pair);
await pair.connect(whale).mint(token0, token1.div(10 ** 12), amount0MinIsZero, amount1MinIsZero, whale._address);
}
return liquidity;
}
describe('mint', () => {
[
{ getPair: () => firstPair, getPool: () => kp3rWethPoolOne, fee: '1%' },
{ getPair: () => secondPair, getPool: () => kp3rWethPoolTwo, fee: '0.3%' },
{ getPair: () => thirdPair, getPool: () => daiWethPool, fee: '0.05%' },
{ getPair: () => fourthPair, getPool: () => daiUsdcPool, fee: '0.01%' },
].forEach(({ getPair, getPool, fee }) => {
it(`should increase the position of the contract if the user provides liquidity in a pair with ${fee} fee`, async () => {
pair = getPair();
pool = getPool();
liquidity = await calculateLiquidityAndMint(tenTokens, tenTokens, pool, pair);
expect((await pair.position()).liquidity).to.eq(liquidity);
});
it(`should mint credit to the user if the user deposited liquidity in a ${fee} pair`, async () => {
pair = getPair();
pool = getPool();
liquidity = await calculateLiquidityAndMint(tenTokens, tenTokens, pool, pair);
expect(await pair.balanceOf(whale._address)).to.eq(liquidity);
});
});
});
//helper function to reduce shared setup by collect() and burn()
async function provideLiquidityAndSwap() {
await firstPair.connect(whale).mint(tenTokens, tenTokens, amount0MinIsZero, amount1MinIsZero, whale._address);
//simulates swap in uniswap pool
await uniRouter.connect(whale).exactInputSingle({
tokenIn: await firstPair.token0(),
tokenOut: await firstPair.token1(),
fee: await firstPair.fee(),
recipient: whale._address,
deadline: 1000000000000,
amountIn: tenTokens,
amountOutMinimum: toUnit(0.00001),
sqrtPriceLimitX96: 0,
});
await firstPair.connect(whale).mint(tenTokens, tenTokens, amount0MinIsZero, amount1MinIsZero, whale._address);
}
describe('collect', () => {
context('when the contract has liquidity and accrued fees', () => {
beforeEach(async () => {
await provideLiquidityAndSwap();
});
it('should send the collected fees to governance', async () => {
const tokensOwed0 = (await firstPair.position()).tokensOwed0;
const tokensOwed1 = (await firstPair.position()).tokensOwed1;
await firstPair.connect(governance).collect();
expect(await kp3r.balanceOf(governance.address)).to.equal(tokensOwed0);
expect(await weth.balanceOf(governance.address)).to.equal(tokensOwed1);
});
});
});
describe('burn', () => {
context('when the contract has liquidity and accrued fees', () => {
beforeEach(async () => {
await provideLiquidityAndSwap();
liquidity = (await firstPair.position()).liquidity;
});
it('should burn the provided liquidity', async () => {
await firstPair.connect(whale).burn(liquidity, amount0MinIsZero, amount1MinIsZero, whale._address);
expect((await firstPair.position()).liquidity).to.equal(0);
});
it('should send the gathered fees to recipient', async () => {
//check the initial balance is 0
expect(await kp3r.balanceOf(governance.address)).to.equal(0);
expect(await weth.balanceOf(governance.address)).to.equal(0);
//expect the balance to grow after liquidity is burned and tokens are sent to him
await firstPair.connect(whale).burn(liquidity, amount0MinIsZero, amount1MinIsZero, governance.address);
expect(await kp3r.balanceOf(governance.address)).to.be.gt(0);
expect(await weth.balanceOf(governance.address)).to.be.gt(0);
});
it('should burn credits from the user who burns liquidity', async () => {
//check the caller has credits
expect(await firstPair.balanceOf(whale._address)).to.equal(liquidity);
//check credits they're burned after calling burn
await firstPair.connect(whale).burn(liquidity, amount0MinIsZero, amount1MinIsZero, whale._address);
expect(await firstPair.balanceOf(whale._address)).to.equal(0);
});
});
});
});