UNPKG

@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
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 () => { [governance, slasher, disputer] = 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', { [job]: 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', { [job]: { [tokenA.address]: initialTokenA, [tokenB.address]: 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', { [job]: 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', { [job]: { [liquidityA.address]: initialLiquidityA, [liquidityB.address]: 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', { [job]: 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)); }); }); }); });