@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
323 lines (261 loc) • 13.7 kB
text/typescript
import { FakeContract, MockContract, MockContractFactory, smock } from '@defi-wonderland/smock';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import ERC20Artifact from '@openzeppelin/contracts/build/contracts/ERC20.json';
import IUniswapV3PoolArtifact from '@solidity/for-test/IUniswapV3PoolForTest.sol/IUniswapV3PoolForTest.json';
import IKeep3rV1Artifact from '@solidity/interfaces/external/IKeep3rV1.sol/IKeep3rV1.json';
import IKeep3rV1ProxyArtifact from '@solidity/interfaces/external/IKeep3rV1Proxy.sol/IKeep3rV1Proxy.json';
import IKeep3rHelperArtifact from '@solidity/interfaces/IKeep3rHelper.sol/IKeep3rHelper.json';
import {
ERC20,
IKeep3rV1,
IKeep3rV1Proxy,
IUniswapV3PoolForTest,
Keep3rHelper,
Keep3rJobDisputableForTest,
Keep3rJobDisputableForTest__factory,
UniV3PairManager,
} from '@types';
import { wallet } from '@utils';
import { toUnit } from '@utils/bn';
import { MathUtils, mathUtilsFactory } from '@utils/math';
import chai, { expect } from 'chai';
import { BigNumber, ContractTransaction } from 'ethers';
import { ethers } from 'hardhat';
chai.use(smock.matchers);
describe('Keep3rJobDisputable', () => {
const job = wallet.generateRandomAddress();
let governance: SignerWithAddress;
let slasher: SignerWithAddress;
let disputer: SignerWithAddress;
let jobDisputable: MockContract<Keep3rJobDisputableForTest>;
let keep3rV1: FakeContract<IKeep3rV1>;
let keep3rV1Proxy: FakeContract<IKeep3rV1Proxy>;
let helper: FakeContract<Keep3rHelper>;
let jobDisputableFactory: MockContractFactory<Keep3rJobDisputableForTest__factory>;
let liquidityA: FakeContract<UniV3PairManager>;
let liquidityB: FakeContract<UniV3PairManager>;
let oraclePool: FakeContract<IUniswapV3PoolForTest>;
// Parameter and function equivalent to contract's
let rewardPeriodTime: number;
let inflationPeriodTime: number;
let mathUtils: MathUtils;
before(async () => {
[] = await ethers.getSigners();
jobDisputableFactory = await smock.mock('Keep3rJobDisputableForTest');
});
beforeEach(async () => {
helper = await smock.fake(IKeep3rHelperArtifact);
keep3rV1 = await smock.fake(IKeep3rV1Artifact);
keep3rV1Proxy = await smock.fake(IKeep3rV1ProxyArtifact);
oraclePool = await smock.fake(IUniswapV3PoolArtifact);
helper.isKP3RToken0.returns(true);
jobDisputable = await jobDisputableFactory.deploy(helper.address, keep3rV1.address, keep3rV1Proxy.address, oraclePool.address);
await jobDisputable.setVariable('slashers', { [slasher.address]: true });
await jobDisputable.setVariable('disputers', { [disputer.address]: true });
rewardPeriodTime = (await jobDisputable.rewardPeriodTime()).toNumber();
inflationPeriodTime = (await jobDisputable.inflationPeriod()).toNumber();
mathUtils = mathUtilsFactory(rewardPeriodTime, inflationPeriodTime);
await jobDisputable.setVariable('disputes', {
[]: true,
});
helper.observe.returns([0, 0, true]);
helper.getKP3RsAtTick.returns(([amount]: [BigNumber]) => amount);
});
describe('slashTokenFromJob', () => {
let tokenA: FakeContract<ERC20>;
let tokenB: FakeContract<ERC20>;
let tx: ContractTransaction;
let initialTokenA = toUnit(1);
let initialTokenB = toUnit(2);
let tokenAToRemove = toUnit(0.9);
beforeEach(async () => {
// setup tokens
tokenA = await smock.fake<ERC20>(ERC20Artifact);
tokenB = await smock.fake<ERC20>(ERC20Artifact);
tokenA.transfer.returns(true);
tokenB.transfer.returns(true);
await jobDisputable.setJobToken(job, tokenA.address);
await jobDisputable.setJobToken(job, tokenB.address);
await jobDisputable.setVariable('jobTokenCredits', {
[]: {
[]: initialTokenA,
[]: initialTokenB,
},
});
});
it('should fail to slash unexistent token', async () => {
await expect(jobDisputable.connect(slasher).slashTokenFromJob(job, wallet.generateRandomAddress(), 1)).to.be.revertedWith(
'JobTokenUnexistent()'
);
});
it('should fail to slash more than balance', async () => {
await expect(jobDisputable.connect(slasher).slashTokenFromJob(job, tokenA.address, initialTokenA.add(1))).to.be.revertedWith(
'JobTokenInsufficient()'
);
});
it('should revert if job is not disputed', async () => {
await jobDisputable.setVariable('disputes', {
[]: false,
});
await expect(jobDisputable.connect(slasher).slashTokenFromJob(job, tokenA.address, initialTokenA)).to.be.revertedWith('NotDisputed()');
});
it('should remove token from list if there is no remaining', async () => {
await jobDisputable.connect(slasher).slashTokenFromJob(job, tokenA.address, initialTokenA);
expect(await jobDisputable.internalJobTokens(job)).to.deep.equal([tokenB.address]);
});
context('when partially slashing a token', () => {
beforeEach(async () => {
tokenA.transfer.returns(true);
tx = await jobDisputable.connect(slasher).slashTokenFromJob(job, tokenA.address, tokenAToRemove);
});
it('should transfer the tokens to governance', async () => {
expect(tokenA.transfer).to.be.calledOnceWith(governance.address, tokenAToRemove);
});
it('should reduce the specified amount from token credits', async () => {
expect(await jobDisputable.jobTokenCredits(job, tokenA.address)).to.equal(initialTokenA.sub(tokenAToRemove));
});
it('should not remove liquidity from list if there is some remaining', async () => {
expect(await jobDisputable.internalJobTokens(job)).to.deep.equal([tokenA.address, tokenB.address]);
});
it('should not affect other liquidity balance', async () => {
expect(await jobDisputable.jobTokenCredits(job, tokenB.address)).to.equal(initialTokenB);
});
it('should emit event', async () => {
await expect(tx).to.emit(jobDisputable, 'JobSlashToken').withArgs(job, tokenA.address, slasher.address, tokenAToRemove);
});
});
context('when some transfer fails', () => {
beforeEach(async () => {
tokenA.transfer.reverts();
});
it('should not revert', async () => {
await expect(jobDisputable.connect(slasher).slashTokenFromJob(job, tokenA.address, initialTokenA)).not.to.be.reverted;
});
it('should call the transfer function', async () => {
await jobDisputable.connect(slasher).slashTokenFromJob(job, tokenA.address, initialTokenA);
expect(tokenA.transfer).to.be.calledOnceWith(governance.address, initialTokenA);
});
it('should slash the token', async () => {
await jobDisputable.connect(slasher).slashTokenFromJob(job, tokenA.address, initialTokenA);
expect(await jobDisputable.jobTokenCredits(job, tokenA.address)).to.equal(initialTokenA.sub(initialTokenA));
});
});
});
describe('slashLiquidityFromJob', () => {
let tx: ContractTransaction;
let initialLiquidityA: BigNumber;
let initialLiquidityB: BigNumber;
let initialLiquidityCredits = toUnit(1);
let liquidityAToRemove = toUnit(0.3);
beforeEach(async () => {
initialLiquidityA = mathUtils.calcLiquidityToAdd(toUnit(1));
initialLiquidityB = mathUtils.calcLiquidityToAdd(toUnit(2));
// setup liquidity
liquidityA = await smock.fake<UniV3PairManager>('UniV3PairManager');
liquidityB = await smock.fake<UniV3PairManager>('UniV3PairManager');
await jobDisputable.setApprovedLiquidity(liquidityA.address);
await jobDisputable.setApprovedLiquidity(liquidityB.address);
await jobDisputable.setJobLiquidity(job, liquidityA.address);
await jobDisputable.setJobLiquidity(job, liquidityB.address);
await jobDisputable.setVariable('liquidityAmount', {
[]: {
[]: initialLiquidityA,
[]: initialLiquidityB,
},
});
// setup credits
await jobDisputable.setVariable('_jobLiquidityCredits', { [job]: initialLiquidityCredits });
await jobDisputable.setVariable('_jobPeriodCredits', { [job]: initialLiquidityA.add(initialLiquidityB) });
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobDisputable.setVariable('rewardedAt', { [job]: blockTimestamp });
await jobDisputable.setVariable('_tick', { [liquidityA.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
await jobDisputable.setVariable('_tick', { [liquidityB.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
});
it('should fail to slash unexistent liquidity', async () => {
await expect(jobDisputable.connect(slasher).slashLiquidityFromJob(job, wallet.generateRandomAddress(), 1)).to.be.revertedWith(
'JobLiquidityUnexistent()'
);
});
it('should fail to slash more than balance', async () => {
await expect(jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, initialLiquidityA.add(1))).to.be.revertedWith(
'JobLiquidityInsufficient()'
);
});
it('should revert if job is not disputed', async () => {
await jobDisputable.setVariable('disputes', {
[]: false,
});
await expect(jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, liquidityAToRemove)).to.be.revertedWith(
'NotDisputed()'
);
});
it('should remove liquidity from list if there is no remaining', async () => {
liquidityA.transfer.returns(true);
await jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, initialLiquidityA);
expect(await jobDisputable.internalJobLiquidities(job)).to.deep.equal([liquidityB.address]);
});
context('when liquidity is revoked', () => {
beforeEach(async () => {
liquidityA.transfer.returns(true);
await jobDisputable.setRevokedLiquidity(liquidityA.address);
});
it('should transfer the tokens to governance', async () => {
await jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, liquidityAToRemove);
expect(liquidityA.transfer).to.be.calledOnceWith(governance.address, liquidityAToRemove);
});
it('should emit an event', async () => {
await expect(jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, liquidityAToRemove))
.to.emit(jobDisputable, 'JobSlashLiquidity')
.withArgs(job, liquidityA.address, slasher.address, liquidityAToRemove);
});
});
context('when partially slashing a liquidity', () => {
beforeEach(async () => {
liquidityA.transfer.returns(true);
tx = await jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, liquidityAToRemove);
});
it('should transfer the tokens to governance', async () => {
expect(liquidityA.transfer).to.be.calledOnceWith(governance.address, liquidityAToRemove);
});
it('should reduce the specified amount from liquidity accountance', async () => {
expect(await jobDisputable.liquidityAmount(job, liquidityA.address)).to.equal(initialLiquidityA.sub(liquidityAToRemove));
});
it('should not remove liquidity from list if there is some remaining', async () => {
expect(await jobDisputable.internalJobLiquidities(job)).to.deep.equal([liquidityA.address, liquidityB.address]);
});
it('should not affect other liquidity balance', async () => {
expect(await jobDisputable.liquidityAmount(job, liquidityB.address)).to.equal(initialLiquidityB);
});
it('should reduce liquidity credits proportionally', async () => {
const totalLiquidity = initialLiquidityA.add(initialLiquidityB);
const expected = initialLiquidityCredits.mul(totalLiquidity.sub(liquidityAToRemove)).div(totalLiquidity);
expect(await jobDisputable.jobLiquidityCredits(job)).to.equal(expected);
});
it('should recalculate period credits', async () => {
let quotedCredits: BigNumber;
quotedCredits = mathUtils.calcPeriodCredits(initialLiquidityA.sub(liquidityAToRemove));
quotedCredits = quotedCredits.add(mathUtils.calcPeriodCredits(initialLiquidityB));
expect(await jobDisputable.jobPeriodCredits(job)).to.equal(quotedCredits);
});
it('should emit event', async () => {
await expect(tx).to.emit(jobDisputable, 'JobSlashLiquidity').withArgs(job, liquidityA.address, slasher.address, liquidityAToRemove);
});
});
context('when some transfer fails', () => {
beforeEach(async () => {
liquidityA.transfer.reverts();
});
it('should not revert', async () => {
await expect(jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, liquidityAToRemove)).not.to.be.reverted;
});
it('should call the transfer function', async () => {
await jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, liquidityAToRemove);
expect(liquidityA.transfer).to.be.calledOnceWith(governance.address, liquidityAToRemove);
});
it('should slash the liquidity', async () => {
await jobDisputable.connect(slasher).slashLiquidityFromJob(job, liquidityA.address, liquidityAToRemove);
expect(await jobDisputable.liquidityAmount(job, liquidityA.address)).to.equal(initialLiquidityA.sub(liquidityAToRemove));
});
});
});
});