@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
166 lines (136 loc) • 7.65 kB
text/typescript
import { JsonRpcSigner } from '@ethersproject/providers';
import { IAggregatorV3, IKeep3rV1, IKeep3rV1Proxy, IUniswapV3Pool, JobForTest, Keep3r, Keep3rHelperForTest, UniV3PairManager } from '@types';
import { evm, wallet } from '@utils';
import { toUnit } from '@utils/bn';
import { expect } from 'chai';
import { BigNumber, ContractTransaction } from 'ethers';
import { ethers } from 'hardhat';
import moment from 'moment';
import * as common from './common';
describe('@skip-on-coverage Keeper Job Interaction', () => {
let jobOwner: JsonRpcSigner;
let keep3r: Keep3r;
let keep3rV1: IKeep3rV1;
let helper: Keep3rHelperForTest;
let job: JobForTest;
let governance: JsonRpcSigner;
let keep3rV1Proxy: IKeep3rV1Proxy;
let keep3rV1ProxyGovernance: JsonRpcSigner;
let keeper: JsonRpcSigner;
let pair: UniV3PairManager;
let pool: IUniswapV3Pool;
beforeEach(async () => {
await evm.reset({
jsonRpcUrl: process.env.MAINNET_HTTPS_URL,
blockNumber: common.FORK_BLOCK_NUMBER,
});
jobOwner = await wallet.impersonate(common.RICH_KP3R_ADDRESS);
keeper = await wallet.impersonate(common.RICH_ETH_ADDRESS);
({ keep3r, governance, keep3rV1, keep3rV1Proxy, keep3rV1ProxyGovernance, helper } = await common.setupKeep3r());
// create job
job = await common.createJobForTest(keep3r.address, jobOwner);
await keep3r.connect(governance).addJob(job.address);
// create keeper
await keep3r.connect(keeper).bond(keep3rV1.address, 0);
await evm.advanceTimeAndBlock(moment.duration(3, 'days').as('seconds'));
await keep3r.connect(keeper).activate(keep3rV1.address);
pair = await common.createLiquidityPair(governance);
await keep3r.connect(governance).approveLiquidity(pair.address);
pool = (await ethers.getContractAt('IUniswapV3Pool', common.KP3R_WETH_V3_POOL_ADDRESS)) as IUniswapV3Pool;
});
it('should not be able to work if there are no funds in job', async () => {
await expect(job.connect(keeper).work()).to.be.revertedWith('InsufficientFunds()');
});
it('should pay the keeper with bonds from job credits', async () => {
// add liquidity to pair
const { liquidity } = await common.addLiquidityToPair(jobOwner, pair, toUnit(10), jobOwner);
// add credit to job
await pair.connect(jobOwner).approve(keep3r.address, liquidity);
await keep3r.connect(jobOwner).addLiquidityToJob(job.address, pair.address, liquidity);
// wait some time to mint credits
await evm.advanceTimeAndBlock(moment.duration(5, 'days').as('seconds'));
const keeperBondsBeforeWork: BigNumber = await keep3r.bonds(keeper._address, keep3rV1.address);
const jobLiquidityCreditsBeforeWork: BigNumber = await keep3r.jobLiquidityCredits(job.address);
// work as keeper
await job.connect(keeper).work();
const jobLiquidityCreditsAfterWork: BigNumber = await keep3r.jobLiquidityCredits(job.address);
const keeperBondsAfterWork: BigNumber = await keep3r.bonds(keeper._address, keep3rV1.address);
const liquidityCreditsSpent: BigNumber = jobLiquidityCreditsBeforeWork.sub(jobLiquidityCreditsAfterWork);
const bondsEarned: BigNumber = keeperBondsAfterWork.sub(keeperBondsBeforeWork);
expect(liquidityCreditsSpent).to.be.gt(0);
expect(bondsEarned).to.be.gt(0);
expect(liquidityCreditsSpent).to.be.eq(bondsEarned);
});
[
{ fnName: 'work', workFn: async () => await job.connect(keeper).work() },
{ fnName: 'workHard', workFn: async () => await job.connect(keeper).workHard(10) },
].forEach(({ fnName, workFn }) => {
context(fnName, () => {
it('should pay the keeper for the accounted gas plus extra with min boost', async () => {
const minBoost = await helper.minBoost();
await testKeeperPayment(minBoost, workFn);
});
it('should pay the keeper for the accounted gas plus extra with max boost', async () => {
// mint, bond and activate a ton of KP3R
const toBond = toUnit(500);
await keep3rV1Proxy.connect(keep3rV1ProxyGovernance)['mint(address,uint256)'](keeper._address, toBond);
await keep3rV1.connect(keeper).approve(keep3r.address, toBond);
await keep3r.connect(keeper).bond(keep3rV1.address, toBond);
await evm.advanceTimeAndBlock(moment.duration(3, 'days').as('seconds'));
await keep3r.connect(keeper).activate(keep3rV1.address);
const maxBoost = await helper.maxBoost();
await testKeeperPayment(maxBoost, workFn);
});
});
});
async function testKeeperPayment(expectedBoost: BigNumber, workFn: () => Promise<ContractTransaction>) {
// add liquidity to pair
const { liquidity } = await common.addLiquidityToPair(jobOwner, pair, toUnit(100), jobOwner);
// add credit to job
await pair.connect(jobOwner).approve(keep3r.address, liquidity);
await keep3r.connect(jobOwner).addLiquidityToJob(job.address, pair.address, liquidity);
// wait some time to mint credits
await evm.advanceTimeAndBlock(moment.duration(5, 'days').as('seconds'));
await job.connect(keeper).work(); // avoid first work tx outlier
let initialGas: BigNumber;
let finalGas: BigNumber;
// work as keeper
const blockNumberBeforeWork = await ethers.provider.getBlockNumber();
const keeperBondsBeforeWork: BigNumber = await keep3r.bonds(keeper._address, keep3rV1.address);
const workTx = await workFn();
const keeperBondsAfterWork: BigNumber = await keep3r.bonds(keeper._address, keep3rV1.address);
// events
const validationEvent = (await keep3r.queryFilter(keep3r.filters.KeeperValidation(), workTx.blockNumber, workTx.blockNumber))[0];
const workEvent = (await keep3r.queryFilter(keep3r.filters.KeeperWork(), workTx.blockNumber, workTx.blockNumber))[0];
initialGas = BigNumber.from(validationEvent.args._gasLeft);
finalGas = BigNumber.from(workEvent.args._gasLeft);
// gas calculation
const accountedGas = initialGas.sub(finalGas);
const extraGas = await helper.workExtraGas();
const gasRewarded = accountedGas.add(extraGas);
const quote = await helper.connect(keeper).getRewardAmount(gasRewarded, { blockTag: blockNumberBeforeWork });
const bondsEarned: BigNumber = keeperBondsAfterWork.sub(keeperBondsBeforeWork);
// twap calculation
const BASE = 1_000_000;
const baseFee = await helper.basefee();
const boostBase = await helper.BOOST_BASE();
const ethToQuote = gasRewarded.mul(baseFee).mul(expectedBoost).div(boostBase);
const expectedReward = await helper.quote(ethToQuote);
// uniswap calculation
const period = await helper.quoteTwapTime();
const { tickCumulatives } = await pool.observe([period, 0]);
const tick0 = tickCumulatives[0];
const tick1 = tickCumulatives[1];
const uniswapQuote = 1.0001 ** tick1.sub(tick0).div(period).toNumber();
const calculatedReward = ethToQuote.mul(BASE).div(Math.floor(uniswapQuote * BASE));
// chainlink price feed calculation
const aggregatorV3 = (await ethers.getContractAt('IAggregatorV3', common.CHAINLINK_KP3R_ETH_PRICE_FEED)) as IAggregatorV3;
const { answer: priceFeedAnswer } = await aggregatorV3.latestRoundData();
const chainlinkQuotedReward = ethToQuote.mul(toUnit(1)).div(priceFeedAnswer);
expect((await workTx.wait()).gasUsed).to.be.closeTo(gasRewarded, 3_000);
expect(bondsEarned).to.be.closeTo(quote, 1);
expect(bondsEarned).to.be.closeTo(expectedReward, toUnit(0.001).toNumber());
expect(bondsEarned).to.be.closeTo(calculatedReward, toUnit(0.001).toNumber());
expect(bondsEarned).to.be.closeTo(chainlinkQuotedReward, toUnit(0.1) as any);
}
});