@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
417 lines (349 loc) • 17.3 kB
text/typescript
import { FakeContract, MockContract, MockContractFactory, smock } from '@defi-wonderland/smock';
import { BigNumber } from '@ethersproject/bignumber';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import ERC20Artifact from '@openzeppelin/contracts/build/contracts/ERC20.json';
import IUniswapV3PoolForTestArtifact from '@solidity/for-test/IUniswapV3PoolForTest.sol/IUniswapV3PoolForTest.json';
import {
ERC20ForTest,
ERC20ForTest__factory,
IERC20Metadata,
IUniswapV3PoolForTest,
UniV3PairManagerForTest,
UniV3PairManagerForTest__factory,
} from '@types';
import { toUnit } from '@utils/bn';
import { ZERO_ADDRESS } from '@utils/constants';
import chai, { expect } from 'chai';
import { solidity } from 'ethereum-waffle';
import { solidityKeccak256 } from 'ethers/lib/utils';
import { ethers } from 'hardhat';
chai.use(solidity);
chai.use(smock.matchers);
describe('UniV3PairManager', () => {
//factories
let uniV3PairManagerFactory: MockContractFactory<UniV3PairManagerForTest__factory>;
let fakeERC20Factory: ERC20ForTest__factory;
//contracts
let fakeERC20: ERC20ForTest;
//fake and mocks
let uniV3PairManager: MockContract<UniV3PairManagerForTest>;
let uniswapPool: FakeContract<IUniswapV3PoolForTest>;
let token0: FakeContract<IERC20Metadata>;
let token1: FakeContract<IERC20Metadata>;
//signers
let deployer: SignerWithAddress;
let newGovernance: SignerWithAddress;
let randomJobProvider: SignerWithAddress;
//misc
let returnValues: BigNumber[];
let tenTokens: BigNumber = toUnit(10);
let actualTickLower: number;
let actualTickUpper: number;
let liquidity: number;
let amount0Min: number;
let amount1Min: number;
let tokensOwed0: number;
let tokensOwed1: number;
let amount0Desired: number;
let amount1Desired: number;
const TICK_SPACING: number = 10;
const MAX_TICK: number = 887272;
before(async () => {
[deployer, newGovernance, randomJobProvider] = await ethers.getSigners();
uniV3PairManagerFactory = await smock.mock<UniV3PairManagerForTest__factory>('UniV3PairManagerForTest');
fakeERC20Factory = (await ethers.getContractFactory('ERC20ForTest')) as ERC20ForTest__factory;
});
beforeEach(async () => {
uniswapPool = await smock.fake(IUniswapV3PoolForTestArtifact);
token0 = await smock.fake(ERC20Artifact);
token1 = await smock.fake(ERC20Artifact);
uniswapPool.token0.returns(token0.address);
uniswapPool.token1.returns(token1.address);
uniswapPool.tickSpacing.returns(10);
token0.symbol.returns('DAI');
token1.symbol.returns('WETH');
uniV3PairManager = await uniV3PairManagerFactory.deploy(uniswapPool.address, deployer.address);
fakeERC20 = await fakeERC20Factory.deploy('FAKE', 'FAKE', deployer.address, toUnit(100));
actualTickLower = await uniV3PairManager.tickLower();
actualTickUpper = await uniV3PairManager.tickUpper();
await fakeERC20.mint(uniV3PairManager.address, toUnit(100));
});
describe('constructor', () => {
it('should assign factory to the msg sender', async () => {
expect(await uniV3PairManager.factory()).to.equal(deployer.address);
});
it('should assign pool to the DAI-WETH pool', async () => {
expect(await uniV3PairManager.pool()).to.equal(uniswapPool.address);
});
it('should assign fee to the DAI-WETH fee', async () => {
expect(await uniV3PairManager.fee()).to.equal(await uniswapPool.fee());
});
it('should assign token0 to the DAI-WETH pool token0', async () => {
expect(await uniV3PairManager.token0()).to.equal(await uniswapPool.token0());
});
it('should assign token0 to the DAI-WETH pool token1', async () => {
expect(await uniV3PairManager.token1()).to.equal(await uniswapPool.token1());
});
it('should assign name to Keep3rLP - DAI/WETH', async () => {
expect(await uniV3PairManager.name()).to.equal('Keep3rLP - DAI/WETH');
});
it('should assign symbol to kLP-DAI/WETH', async () => {
expect(await uniV3PairManager.symbol()).to.equal('kLP-DAI/WETH');
});
it('should assign the right lower tick', async () => {
const lowerTick = -(MAX_TICK - (MAX_TICK % TICK_SPACING));
expect(actualTickLower).to.equal(lowerTick);
});
it('should assign the right upper tick', async () => {
const upperTick = MAX_TICK - (MAX_TICK % TICK_SPACING);
expect(actualTickUpper).to.equal(upperTick);
});
it('should assign governance to deployer', async () => {
expect(await uniV3PairManager.governance()).to.equal(deployer.address);
});
});
describe('uniswapV3MintCallback', () => {
it('should revert if the caller is not the pool', async () => {
const encodedStruct = ethers.utils.defaultAbiCoder.encode(
['address', 'address', 'uint24', 'address'],
[await uniV3PairManager.token0(), await uniV3PairManager.token1(), await uniV3PairManager.fee(), deployer.address]
);
await expect(uniV3PairManager.connect(deployer).uniswapV3MintCallback(10, 10, encodedStruct)).to.be.revertedWith('OnlyPool()');
});
});
describe('position', () => {
it('should call uniswap pool positions function with the correct arguments', async () => {
await uniV3PairManager.position();
expect(uniswapPool.positions).to.be.calledOnceWith(
solidityKeccak256(['address', 'int24', 'int24'], [uniV3PairManager.address, actualTickLower, actualTickUpper])
);
});
it('should return the returning values of calling uniswap pool position function', async () => {
returnValues = [1, 2, 3, 4, 5].map(BigNumber.from);
uniswapPool.positions.returns(returnValues);
const values = await uniV3PairManager.position();
expect(values).to.deep.equal(returnValues);
});
});
describe('collect', () => {
it('should revert if the caller is not governance', async () => {
await expect(uniV3PairManager.connect(randomJobProvider).collect()).to.be.revertedWith('OnlyGovernance()');
});
it('should call collect with the correct arguments', async () => {
tokensOwed0 = 4;
tokensOwed1 = 5;
uniswapPool.positions.returns([1, 2, 3, 4, 5].map(BigNumber.from));
await uniV3PairManager.collect();
expect(uniswapPool.collect).to.be.calledOnceWith(deployer.address, actualTickLower, actualTickUpper, tokensOwed0, tokensOwed1);
});
it('should return the correct return values of the pool collect function', async () => {
returnValues = [1, 2].map(BigNumber.from);
uniswapPool.collect.returns(returnValues);
const values = await uniV3PairManager.callStatic.collect();
expect(values).to.deep.equal(returnValues);
});
});
describe('burn', () => {
it('should revert if caller does not have credits', async () => {
amount0Min = 5;
amount1Min = 5;
uniswapPool.burn.returns([10, 20]);
uniV3PairManager.burn.returns([10, 20]);
await expect(uniV3PairManager.connect(randomJobProvider).burn(liquidity, amount0Min, amount1Min, newGovernance.address)).to.be.reverted;
});
context('when the caller has credits', () => {
beforeEach(async () => {
liquidity = 10000;
amount0Min = 5;
amount1Min = 5;
tokensOwed0 = 10;
tokensOwed1 = 20;
await uniV3PairManager.setVariable('balanceOf', {
[deployer.address]: tenTokens,
});
await uniV3PairManager.setVariable('totalSupply', tenTokens);
uniswapPool.burn.returns([10, 20]);
uniV3PairManager.burn.returns([10, 20]);
});
it('should call the pools burn function with the correct arguments', async () => {
await uniV3PairManager.connect(deployer).burn(liquidity, amount0Min, amount1Min, newGovernance.address);
expect(uniswapPool.burn).to.be.calledOnceWith(actualTickLower, actualTickUpper, liquidity);
});
it('should call the pools collect function with the correct arguments', async () => {
await uniV3PairManager.connect(deployer).burn(liquidity, amount0Min, amount1Min, newGovernance.address);
expect(uniswapPool.collect).to.be.calledOnceWith(newGovernance.address, actualTickLower, actualTickUpper, tokensOwed0, tokensOwed1);
});
it('should revert if burn returns less than amountMin', async () => {
liquidity = 1;
amount0Min = 20;
amount1Min = 30;
expect(uniV3PairManager.burn(liquidity, amount0Min, amount1Min, deployer.address)).to.be.revertedWith('ExcessiveSlippage()');
});
});
});
describe('approve', () => {
it('should increase the balance of the spender', async () => {
await uniV3PairManager.connect(deployer).approve(newGovernance.address, tenTokens);
expect(await uniV3PairManager.allowance(deployer.address, newGovernance.address)).to.equal(tenTokens);
});
it('should emit an event if approve is successful', async () => {
await expect(await uniV3PairManager.connect(deployer).approve(newGovernance.address, tenTokens))
.to.emit(uniV3PairManager, 'Approval')
.withArgs(deployer.address, newGovernance.address, tenTokens);
});
});
describe('transfer', () => {
context('when user does not have credits and tries to transfer', () => {
it('should revert', async () => {
await expect(uniV3PairManager.connect(deployer).transfer(newGovernance.address, tenTokens)).to.be.reverted;
});
});
context('when user has credits', () => {
beforeEach(async () => {
await uniV3PairManager.setVariable('balanceOf', {
[deployer.address]: tenTokens,
});
});
it('should transfer tokens from one account to another', async () => {
await uniV3PairManager.connect(deployer).transfer(newGovernance.address, tenTokens);
expect(await uniV3PairManager.balanceOf(newGovernance.address)).to.deep.equal(tenTokens);
});
it('should emit an event when a transfer is successful', async () => {
await expect(uniV3PairManager.connect(deployer).transfer(newGovernance.address, tenTokens))
.to.emit(uniV3PairManager, 'Transfer')
.withArgs(deployer.address, newGovernance.address, tenTokens);
});
});
});
describe('transferFrom', () => {
it('it should revert when the user does not have funds and has approved an spender', async () => {
expect(await uniV3PairManager.connect(deployer).approve(newGovernance.address, tenTokens));
await expect(uniV3PairManager.connect(newGovernance).transferFrom(deployer.address, newGovernance.address, tenTokens)).to.be.reverted;
});
context('when user has funds and has approved an spender', () => {
beforeEach(async () => {
await uniV3PairManager.setVariable('balanceOf', {
[deployer.address]: tenTokens,
});
await uniV3PairManager.connect(deployer).approve(newGovernance.address, tenTokens);
});
it('should transfer tokens from one account to another', async () => {
await uniV3PairManager.connect(newGovernance).transferFrom(deployer.address, newGovernance.address, tenTokens);
expect(await uniV3PairManager.balanceOf(newGovernance.address)).to.deep.equal(tenTokens);
});
it('should emit an event when a transfer is successful', async () => {
await expect(await uniV3PairManager.connect(newGovernance).transferFrom(deployer.address, newGovernance.address, tenTokens))
.to.emit(uniV3PairManager, 'Transfer')
.withArgs(deployer.address, newGovernance.address, tenTokens);
});
it('should reduce the spenders allowance after a transferFrom', async () => {
await uniV3PairManager.connect(newGovernance).transferFrom(deployer.address, newGovernance.address, tenTokens);
expect(await uniV3PairManager.allowance(deployer.address, newGovernance.address)).to.deep.equal(0);
});
it('should emit an event when the allowance is changed', async () => {
await expect(await uniV3PairManager.connect(newGovernance).transferFrom(deployer.address, newGovernance.address, tenTokens))
.to.emit(uniV3PairManager, 'Approval')
.withArgs(deployer.address, newGovernance.address, 0);
});
});
});
describe('_addLiquidity', () => {
///@notice for the purpose of testing internal functions, they've been made external in the for-test contract
/// and given the name: internal + [original internal function name] for clarity.
// Example: internalAddLiquidity
amount0Desired = 10;
amount1Desired = 20;
amount0Min = 30;
amount1Min = 40;
it('should revert if the pools mint function return values are not set', async () => {
await expect(
uniV3PairManager.connect(deployer).internalAddLiquidity(amount0Desired, amount1Desired, amount0Min, amount1Min)
).to.be.revertedWith('ExcessiveSlippage()');
});
context('when the pools mint function return values are set', () => {
beforeEach(async () => {
returnValues = [100, 200].map(BigNumber.from);
uniswapPool.mint.returns(returnValues);
});
it('should return the right return values of the pools mint function', async () => {
const encodedStruct = ethers.utils.defaultAbiCoder.encode(
['address', 'address', 'uint24', 'address'],
[await uniV3PairManager.token0(), await uniV3PairManager.token1(), await uniV3PairManager.fee(), deployer.address]
);
await uniV3PairManager.connect(deployer).internalAddLiquidity(amount0Desired, amount1Desired, amount0Min, amount1Min);
expect(uniswapPool.mint).to.have.been.calledOnceWith(
uniV3PairManager.address,
actualTickLower,
actualTickUpper,
BigNumber.from(0),
encodedStruct
);
});
it('should call pool slot0', async () => {
await uniV3PairManager.internalAddLiquidity(amount0Desired, amount1Desired, amount0Min, amount1Min);
expect(uniswapPool.slot0).to.have.been.calledOnce;
});
it('should revert if amountOut is lower than amountMin', async () => {
amount0Min = 300;
amount1Min = 400;
await expect(uniV3PairManager.internalAddLiquidity(amount0Desired, amount1Desired, amount0Min, amount1Min)).to.be.revertedWith(
'ExcessiveSlippage()'
);
});
});
});
describe('_mint', () => {
it('should mint credits to the recipient', async () => {
await uniV3PairManager.internalMint(newGovernance.address, tenTokens);
expect(await uniV3PairManager.balanceOf(newGovernance.address)).to.equal(tenTokens);
});
it('should increase the contracts totalSupply', async () => {
await uniV3PairManager.internalMint(newGovernance.address, tenTokens);
expect(await uniV3PairManager.totalSupply()).to.equal(tenTokens);
});
it('should emit an event if the credits have been minted successfuly', async () => {
await expect(await uniV3PairManager.internalMint(newGovernance.address, tenTokens))
.to.emit(uniV3PairManager, 'Transfer')
.withArgs(ZERO_ADDRESS, newGovernance.address, tenTokens);
});
});
describe('_burn', () => {
it('should revert if the user does not have credits in his balance and tries to burn', async () => {
const smallTokenAmount = 1;
await expect(uniV3PairManager.internalBurn(deployer.address, smallTokenAmount)).to.be.reverted;
});
context('when user has credits in his balance', () => {
beforeEach(async () => {
await uniV3PairManager.setVariable('totalSupply', tenTokens);
await uniV3PairManager.setVariable('balanceOf', {
[deployer.address]: tenTokens,
});
});
it('should burn credits to the recipient', async () => {
await uniV3PairManager.internalBurn(deployer.address, tenTokens);
expect(await uniV3PairManager.balanceOf(deployer.address)).to.equal(0);
});
it('should reduce the total supply after burning credits', async () => {
await uniV3PairManager.internalBurn(deployer.address, tenTokens);
expect(await uniV3PairManager.totalSupply()).to.equal(0);
});
it('should emit an event if the credits have been burned successfuly', async () => {
await expect(await uniV3PairManager.internalBurn(deployer.address, tenTokens))
.to.emit(uniV3PairManager, 'Transfer')
.withArgs(deployer.address, ZERO_ADDRESS, tenTokens);
});
});
});
describe('_pay', () => {
it('should transfer tokens to the recipient', async () => {
await fakeERC20.connect(deployer).approve(uniV3PairManager.address, tenTokens);
await uniV3PairManager.internalPay(fakeERC20.address, deployer.address, newGovernance.address, tenTokens);
expect(await fakeERC20.balanceOf(newGovernance.address)).to.equal(tenTokens);
});
it('should fail if payer did not approve the contract to spend his tokens', async () => {
await expect(uniV3PairManager.internalPay(fakeERC20.address, deployer.address, newGovernance.address, tenTokens)).to.be.revertedWith(
'UnsuccessfulTransfer()'
);
});
});
});