@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
231 lines (171 loc) • 9.04 kB
text/typescript
import { JsonRpcSigner } from '@ethersproject/providers';
import { BasicJob, BasicJob__factory, IKasparov, IKeep3rV1, Keep3r, Keep3rHelperForTest, UniV3PairManager } from '@types';
import { evm, wallet } from '@utils';
import { toUnit } from '@utils/bn';
import { snapshot } from '@utils/evm';
import { expect } from 'chai';
import { BigNumber, ContractTransaction } from 'ethers';
import { ethers } from 'hardhat';
import moment from 'moment';
import * as common from './common';
import { HELPER_FOR_TEST_BASE_FEE } from './common';
interface GasReading {
gasUsed: BigNumber;
gasAccounted: BigNumber;
gasUnaccounted: BigNumber;
}
const DAY = moment.duration(1, 'day').as('seconds');
const GAS_DELTA = 2_000;
describe('@skip-on-coverage Basic Keeper Job Interaction', () => {
let jobOwner: JsonRpcSigner;
let keep3r: Keep3r;
let keep3rV1: IKeep3rV1;
let helper: Keep3rHelperForTest;
let job: BasicJob;
let governance: JsonRpcSigner;
let keeper: JsonRpcSigner;
let snapshotId: string;
let pair: UniV3PairManager;
before(async () => {
await evm.reset({
jsonRpcUrl: process.env.MAINNET_HTTPS_URL,
blockNumber: 14271214, // block when Kasparov job is workable
});
jobOwner = await wallet.impersonate(common.RICH_KP3R_ADDRESS);
keeper = await wallet.impersonate(common.RICH_ETH_ADDRESS);
({ keep3r, governance, keep3rV1, helper } = await common.setupKeep3r());
// create job
const jobFactory = (await ethers.getContractFactory('BasicJob')) as BasicJob__factory;
job = await jobFactory.connect(jobOwner).deploy(keep3r.address);
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);
const { liquidity } = await common.addLiquidityToPair(jobOwner, pair, toUnit(1000), jobOwner);
await pair.connect(jobOwner).approve(keep3r.address, liquidity);
snapshotId = await snapshot.take();
});
beforeEach(async () => {
await snapshot.revert(snapshotId);
});
describe('gas recordings', () => {
let extraGas: BigNumber;
let keeper2: JsonRpcSigner;
before(async () => {
extraGas = await helper.workExtraGas();
});
beforeEach(async () => {
await keep3r.connect(jobOwner).addLiquidityToJob(job.address, pair.address, toUnit(100));
await evm.advanceTimeAndBlock(DAY * 100);
});
// lots of variables are getting initialized in storage, that gas is actually unaccounted, thus not rewarded
it('should underpay first ever work transaction', async () => {
const tx = await job.connect(keeper).work();
const { gasUnaccounted } = await getGasReading(tx);
expect(gasUnaccounted).to.be.closeTo(BigNumber.from(43500), GAS_DELTA);
});
// some variables for the keeper are getting initialized in storage, that gas is actually unaccounted, thus not rewarded
it('should underpay first keeper work transaction', async () => {
keeper2 = await wallet.impersonate(common.RICH_ETH_2_ADDRESS);
await keep3r.connect(keeper2).bond(keep3rV1.address, 0);
await evm.advanceTimeAndBlock(moment.duration(3, 'days').as('seconds'));
await keep3r.connect(keeper2).activate(keep3rV1.address);
await job.connect(keeper).work();
await evm.advanceTimeAndBlock(DAY * 10);
const tx = await job.connect(keeper2).work();
const { gasUnaccounted } = await getGasReading(tx);
expect(gasUnaccounted).to.be.closeTo(BigNumber.from(26500), GAS_DELTA);
});
it('should account all the gas used after the first work tx', async () => {
await job.connect(keeper).work();
const tx = await job.connect(keeper).workHard(1000);
const { gasUnaccounted } = await getGasReading(tx);
expect(gasUnaccounted).to.be.closeTo(BigNumber.from(0), GAS_DELTA);
});
it('should account the same amount of gas for more difficult works', async () => {
await job.connect(keeper).work(); // avoid first work tx outlier
const tx1 = await job.connect(keeper).workHard(10);
const { gasUnaccounted: gasUnaccounted1 } = await getGasReading(tx1);
const tx2 = await job.connect(keeper).workHard(20);
const { gasUnaccounted: gasUnaccounted2 } = await getGasReading(tx2);
const difference = gasUnaccounted1.sub(gasUnaccounted2);
expect(difference).to.be.closeTo(BigNumber.from(0), 1);
});
it('should account all the gas used after the different work txs', async () => {
await job.connect(keeper).work();
const tx = await job.connect(keeper).workHard(10);
const { gasUnaccounted } = await getGasReading(tx);
expect(gasUnaccounted).to.be.closeTo(BigNumber.from(0), GAS_DELTA);
});
// some deletions happen, giving unaccounted refunds to the keeper, rewarding more than needed
it('should overpay a bit for work after period change', async () => {
await job.connect(keeper).work();
await evm.advanceTimeAndBlock(DAY * 10);
const tx = await job.connect(keeper).work();
const { gasUnaccounted } = await getGasReading(tx);
expect(gasUnaccounted).to.be.closeTo(BigNumber.from(-7500), GAS_DELTA);
});
// deletions are unaccounted refunds of gas, that's why the network is overpaying
it('should overpay for works with lots of refunds', async () => {
await job.connect(keeper).work(); // avoid first work tx outlier
const tx = await job.connect(keeper).workRefund(100);
const { gasUnaccounted } = await getGasReading(tx);
expect(gasUnaccounted).to.be.lt(-450_000);
});
// this job has several deletions, generating a lot of unaccounted refunds
it('should overpay for Kasparov job work', async () => {
const chessJob = (await ethers.getContractAt('IKasparov', common.KASPAROV_JOB)) as IKasparov;
const chessGovernance = await wallet.impersonate(await chessJob.governor());
await keep3r.connect(jobOwner).addJob(chessJob.address);
await keep3r.connect(jobOwner).addLiquidityToJob(chessJob.address, pair.address, toUnit(100));
await evm.advanceTimeAndBlock(DAY * 10);
await chessJob.connect(chessGovernance).setKeep3r(keep3r.address);
await job.connect(keeper).work();
const tx = await chessJob.connect(keeper).work();
const { gasUnaccounted } = await getGasReading(tx);
expect(gasUnaccounted).to.be.closeTo(BigNumber.from(-12_000), GAS_DELTA);
});
async function getGasReading(workTx: ContractTransaction): Promise<GasReading> {
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];
const initialGas = BigNumber.from(validationEvent.args._gasLeft);
const finalGas = BigNumber.from(workEvent.args._gasLeft);
const gasUsed = (await workTx.wait()).gasUsed;
const gasAccounted = initialGas.sub(finalGas).add(extraGas);
const gasUnaccounted = gasUsed.sub(gasAccounted);
return { gasUsed, gasAccounted, gasUnaccounted };
}
});
describe('reward', () => {
let initialBondedKP3R: BigNumber;
beforeEach(async () => {
initialBondedKP3R = await keep3r.callStatic.bonds(keeper._address, keep3rV1.address);
await keep3r.connect(jobOwner).addLiquidityToJob(job.address, pair.address, toUnit(100));
await evm.advanceTimeAndBlock(DAY);
});
it('should work the job and pay the keeper in bonded KP3R', async () => {
await job.connect(keeper).work();
const afterWorkBondedKP3R = await keep3r.callStatic.bonds(keeper._address, keep3rV1.address);
expect(afterWorkBondedKP3R).to.be.gt(initialBondedKP3R);
});
it('should pay the keeper gas costs with helper boost and extra', async () => {
await job.connect(keeper).work(); // avoid first work tx outlier
initialBondedKP3R = await keep3r.callStatic.bonds(keeper._address, keep3rV1.address);
// get parameters
const base = await helper.callStatic.BOOST_BASE();
const helperBoost = await helper.minBoost();
// work
const workTx = await job.connect(keeper).workHard(10_000);
// calculate reward from work
const afterWorkBondedKP3R = await keep3r.callStatic.bonds(keeper._address, keep3rV1.address);
const reward = afterWorkBondedKP3R.sub(initialBondedKP3R);
// calculate expected reward
const txCost = (await workTx.wait()).gasUsed.mul(HELPER_FOR_TEST_BASE_FEE);
const expectedReward = await helper.quote(txCost.mul(helperBoost).div(base));
expect(reward).to.be.closeTo(expectedReward, toUnit(0.001).toNumber());
});
});
});