@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
1,165 lines • 52.1 kB
text/typescript
import { FakeContract, MockContract, MockContractFactory, smock } from '@defi-wonderland/smock';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import IUniswapV3PoolForTestArtifact 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 {
IKeep3rV1,
IKeep3rV1Proxy,
IUniswapV3Pool,
Keep3rHelper,
Keep3rJobFundableLiquidityForTest,
Keep3rJobFundableLiquidityForTest__factory,
UniV3PairManager,
} from '@types';
import { behaviours, evm, wallet } from '@utils';
import { onlyJobOwner } from '@utils/behaviours';
import { toUnit } from '@utils/bn';
import { ZERO_ADDRESS } from '@utils/constants';
import { MathUtils, mathUtilsFactory } from '@utils/math';
import chai, { expect } from 'chai';
import { BigNumber } from 'ethers';
import { ethers } from 'hardhat';
import moment from 'moment';
chai.use(smock.matchers);
describe('Keep3rJobFundableLiquidity', () => {
const randomJob = wallet.generateRandomAddress();
let governance: SignerWithAddress;
let provider: SignerWithAddress;
let jobOwner: SignerWithAddress;
let jobFundable: MockContract<Keep3rJobFundableLiquidityForTest>;
let keep3rV1: FakeContract<IKeep3rV1>;
let keep3rV1Proxy: FakeContract<IKeep3rV1Proxy>;
let helper: FakeContract<Keep3rHelper>;
let randomLiquidity: FakeContract<UniV3PairManager>;
let approvedLiquidity: FakeContract<UniV3PairManager>;
let jobFundableFactory: MockContractFactory<Keep3rJobFundableLiquidityForTest__factory>;
let oraclePool: FakeContract<IUniswapV3Pool>;
// Parameter and function equivalent to contract's
let rewardPeriodTime: number;
let inflationPeriodTime: number;
let mathUtils: MathUtils;
let oneTick: number;
before(async () => {
[governance, jobOwner, provider] = await ethers.getSigners();
jobFundableFactory = await smock.mock<Keep3rJobFundableLiquidityForTest__factory>('Keep3rJobFundableLiquidityForTest');
});
beforeEach(async () => {
helper = await smock.fake(IKeep3rHelperArtifact);
keep3rV1 = await smock.fake(IKeep3rV1Artifact);
keep3rV1Proxy = await smock.fake(IKeep3rV1ProxyArtifact);
randomLiquidity = await smock.fake('UniV3PairManager');
approvedLiquidity = await smock.fake('UniV3PairManager');
oraclePool = await smock.fake(IUniswapV3PoolForTestArtifact);
helper.isKP3RToken0.returns(true);
approvedLiquidity.transfer.returns(true);
approvedLiquidity.transferFrom.returns(true);
jobFundable = await jobFundableFactory.deploy(helper.address, keep3rV1.address, keep3rV1Proxy.address, oraclePool.address);
await jobFundable.setVariable('jobOwner', {
[randomJob]: jobOwner.address,
});
rewardPeriodTime = (await jobFundable.rewardPeriodTime()).toNumber();
inflationPeriodTime = (await jobFundable.inflationPeriod()).toNumber();
mathUtils = mathUtilsFactory(rewardPeriodTime, inflationPeriodTime);
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
const testPeriodTime = mathUtils.calcPeriod(blockTimestamp + rewardPeriodTime) + rewardPeriodTime / 2;
// set the test to start mid-period
evm.advanceToTime(testPeriodTime);
evm.advanceBlock();
oneTick = rewardPeriodTime;
helper.observe.returns([0, 0, true]);
helper.getKP3RsAtTick.returns(([amount]: [BigNumber]) => amount);
// set oraclePool to be updated
await jobFundable.setVariable('_tick', { [oraclePool.address]: { period: mathUtils.calcPeriod(testPeriodTime) } });
// set and initialize approvedLiquidity
await jobFundable.setApprovedLiquidity(approvedLiquidity.address);
await jobFundable.setVariable('_liquidityPool', { [approvedLiquidity.address]: oraclePool.address });
await jobFundable.setVariable('_isKP3RToken0', { [approvedLiquidity.address]: true });
});
describe('jobPeriodCredits', () => {
const liquidityAmount = toUnit(1);
beforeEach(async () => {
await jobFundable.setJobLiquidity(randomJob, approvedLiquidity.address);
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: liquidityAmount } });
});
context('when liquidity is updated', () => {
beforeEach(async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
});
it('should not call the oracle', async () => {
await jobFundable.jobPeriodCredits(randomJob);
expect(helper.observe).not.to.have.been.called;
});
it('should return a full period of credits', async () => {
const expectedCredits = mathUtils.calcPeriodCredits(toUnit(1));
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.eq(expectedCredits);
});
});
context('when liquidity is outdated', () => {
beforeEach(async () => {
helper.observe.reset();
helper.observe.returns([0, 0, true]);
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('_tick', {
[approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime) },
});
});
it('should call the oracle', async () => {
await jobFundable.jobPeriodCredits(randomJob);
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(helper.observe).to.have.been.calledOnceWith(oraclePool.address, [blockTimestamp - mathUtils.calcPeriod(blockTimestamp)]);
});
it('should return a full period of credits', async () => {
const expectedCredits = mathUtils.calcPeriodCredits(toUnit(1));
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.eq(expectedCredits);
});
});
context('when liquidity is expired', () => {
beforeEach(async () => {
helper.observe.reset();
helper.observe.returns([0, 0, true]);
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('_tick', {
[approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp - 2 * rewardPeriodTime) },
});
});
it('should call the oracle', async () => {
await jobFundable.jobPeriodCredits(randomJob);
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(helper.observe).to.have.been.calledOnceWith(oraclePool.address, [
blockTimestamp - mathUtils.calcPeriod(blockTimestamp),
blockTimestamp - mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime),
]);
});
it('should return a full period of credits', async () => {
const expectedCredits = mathUtils.calcPeriodCredits(toUnit(1));
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.eq(expectedCredits);
});
});
context('when liquidity twap has changed', () => {
let oldCreditsForComparison: BigNumber;
beforeEach(async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
const liquidityParams = {
current: 0,
difference: 0,
period: mathUtils.calcPeriod(blockTimestamp),
};
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: liquidityParams });
oldCreditsForComparison = mathUtils.calcPeriodCredits(toUnit(1));
await jobFundable.setVariable('_tick', {
[approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime) },
});
});
// If KP3R price went up, previous credits are worth less KP3Rs
it('should return an decreased amount if increased', async () => {
helper.observe.returns([rewardPeriodTime, 0, true]);
await jobFundable.setVariable('_isKP3RToken0', { [approvedLiquidity.address]: true });
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.closeTo(
mathUtils.decrease1Tick(oldCreditsForComparison),
mathUtils.blockShiftPrecision
);
});
it('should return an increased amount if decreased', async () => {
helper.observe.returns([-rewardPeriodTime, 0, true]);
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.closeTo(
mathUtils.increase1Tick(oldCreditsForComparison),
mathUtils.blockShiftPrecision
);
});
});
context('when there are more than 1 liquidities', () => {
const liquidityAmount = toUnit(1);
beforeEach(async () => {
randomLiquidity.token0.returns(keep3rV1.address);
await jobFundable.connect(governance).approveLiquidity(randomLiquidity.address);
await jobFundable.setJobLiquidity(randomJob, randomLiquidity.address);
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [randomLiquidity.address]: liquidityAmount } });
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
await jobFundable.setVariable('_tick', { [randomLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
});
it('should return a full period for each liquidity', async () => {
const expectedCredits1 = mathUtils.calcPeriodCredits(liquidityAmount);
const expectedCredits2 = mathUtils.calcPeriodCredits(liquidityAmount);
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.eq(expectedCredits1.add(expectedCredits2));
});
});
});
describe('jobLiquidityCredits', () => {
let blockTimestamp: number;
beforeEach(async () => {
await jobFundable.setJobLiquidity(randomJob, approvedLiquidity.address);
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: toUnit(1) } });
await jobFundable.setVariable('_jobLiquidityCredits', { [randomJob]: toUnit(1) });
await jobFundable.setVariable('_jobPeriodCredits', { [randomJob]: toUnit(1) });
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
let tickSetting = {
current: rewardPeriodTime,
difference: rewardPeriodTime,
period: mathUtils.calcPeriod(blockTimestamp),
};
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: tickSetting });
helper.observe.returns([rewardPeriodTime, 0, true]);
});
context('when job accountance is updated', () => {
beforeEach(async () => {
await jobFundable.setVariable('workedAt', { [randomJob]: blockTimestamp });
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
});
it('should return current job credits', async () => {
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.closeTo(
mathUtils.calcPeriodCredits(toUnit(1)),
mathUtils.blockShiftPrecision
);
});
});
context('when job accountance is outdated', () => {
beforeEach(async () => {
await jobFundable.setVariable('workedAt', { [randomJob]: blockTimestamp - rewardPeriodTime });
let tickSetting = {
current: rewardPeriodTime,
difference: rewardPeriodTime,
period: mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime),
};
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: tickSetting });
helper.observe.returns([2 * rewardPeriodTime, rewardPeriodTime, true]);
});
it('should return old job credits updated to current price', async () => {
const previousPeriodCredits = mathUtils.calcPeriodCredits(toUnit(1));
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.closeTo(
mathUtils.increase1Tick(previousPeriodCredits),
mathUtils.blockShiftPrecision
);
});
});
context('when job accountance is expired', () => {
beforeEach(async () => {
await jobFundable.setVariable('rewardedAt', { [randomJob]: blockTimestamp - 2 * rewardPeriodTime });
});
it('should return a full period of credits', async () => {
const expectedCredits = mathUtils.calcPeriodCredits(toUnit(1));
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.closeTo(expectedCredits, mathUtils.blockShiftPrecision);
});
});
});
describe('totalJobCredits', () => {
let blockTimestamp: number;
const liquidityAdded: BigNumber = toUnit(1);
let jobPeriodCredits: BigNumber;
it('should return 0 with an empty job', async () => {
expect(await jobFundable.totalJobCredits(randomJob)).to.be.eq(0);
});
context('when job has only forced credits', () => {
beforeEach(async () => {
await jobFundable.setVariable('_jobLiquidityCredits', { [randomJob]: toUnit(1) });
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
});
it('should return forced credits if are updated', async () => {
await jobFundable.setVariable('rewardedAt', { [randomJob]: blockTimestamp });
expect(await jobFundable.totalJobCredits(randomJob)).to.be.eq(toUnit(1));
});
it('should return 0 if forced credits are outdated', async () => {
await jobFundable.setVariable('rewardedAt', { [randomJob]: blockTimestamp - rewardPeriodTime });
expect(await jobFundable.totalJobCredits(randomJob)).to.be.eq(0);
});
});
context('when job has added liquidity', () => {
beforeEach(async () => {
jobPeriodCredits = mathUtils.calcPeriodCredits(liquidityAdded);
await jobFundable.setJobLiquidity(randomJob, approvedLiquidity.address);
await jobFundable.setVariable('_jobPeriodCredits', { [randomJob]: jobPeriodCredits });
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
});
context('when job was rewarded this period', () => {
beforeEach(async () => {
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: liquidityAdded } });
await jobFundable.setVariable('rewardedAt', { [randomJob]: mathUtils.calcPeriod(blockTimestamp) });
// if job accountance is updated, then it's liquidity must updated be as well
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
});
it('should not call the oracle', async () => {
await jobFundable.totalJobCredits(randomJob);
expect(helper.observe).not.to.have.been.called;
});
it('should return current credits + minted since period start', async () => {
const jobLiquidityCredits = await jobFundable.jobLiquidityCredits(randomJob);
expect(await jobFundable.totalJobCredits(randomJob)).to.be.eq(
jobLiquidityCredits.add(mathUtils.calcMintedCredits(jobPeriodCredits, blockTimestamp - mathUtils.calcPeriod(blockTimestamp)))
);
});
context('when job was rewarded after period started', () => {
let rewardTimestamp: number;
beforeEach(async () => {
rewardTimestamp = Math.floor((mathUtils.calcPeriod(blockTimestamp) + blockTimestamp) / 2);
await jobFundable.setVariable('rewardedAt', { [randomJob]: rewardTimestamp });
});
it('should return current credits + minted since reward reference', async () => {
const jobLiquidityCredits = await jobFundable.jobLiquidityCredits(randomJob);
expect(await jobFundable.totalJobCredits(randomJob)).to.be.eq(
jobLiquidityCredits.add(mathUtils.calcMintedCredits(jobPeriodCredits, blockTimestamp - rewardTimestamp))
);
});
});
});
context('when job was rewarded last period', () => {
let oldLiquidityCredits: BigNumber;
beforeEach(async () => {
oldLiquidityCredits = mathUtils.calcPeriodCredits(toUnit(1));
await jobFundable.setVariable('_jobLiquidityCredits', { [randomJob]: oldLiquidityCredits });
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: liquidityAdded } });
await jobFundable.setVariable('rewardedAt', { [randomJob]: mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime) });
});
it('should call the oracle', async () => {
await jobFundable.totalJobCredits(randomJob);
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(helper.observe).to.have.been.calledWith(oraclePool.address, [
blockTimestamp - mathUtils.calcPeriod(blockTimestamp),
blockTimestamp - mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime),
]);
});
it('should return updated credits + minted since period start', async () => {
const totalJobCredits = await jobFundable.totalJobCredits(randomJob);
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(totalJobCredits).to.be.closeTo(
mathUtils
.decrease1Tick(oldLiquidityCredits)
.add(mathUtils.calcMintedCredits(jobPeriodCredits, blockTimestamp - mathUtils.calcPeriod(blockTimestamp))),
mathUtils.blockShiftPrecision
);
});
context('when job was rewarded after period started', () => {
let rewardTimestamp: number;
beforeEach(async () => {
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
rewardTimestamp = Math.floor(mathUtils.calcPeriod(blockTimestamp) - rewardPeriodTime / 10);
await jobFundable.setVariable('rewardedAt', { [randomJob]: rewardTimestamp });
let tickSetting = {
current: oneTick,
difference: oneTick,
period: mathUtils.calcPeriod(blockTimestamp),
};
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: tickSetting });
});
it('should return updated credits + minted since reward reference', async () => {
const totalJobCredits = await jobFundable.totalJobCredits(randomJob);
expect(totalJobCredits).to.be.closeTo(
mathUtils
.decrease1Tick(oldLiquidityCredits)
.add(mathUtils.calcMintedCredits(mathUtils.decrease1Tick(oldLiquidityCredits), blockTimestamp - rewardTimestamp)),
mathUtils.blockShiftPrecision
);
});
});
});
context('when job was rewarded exactly 1 period ago', () => {
beforeEach(async () => {
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: liquidityAdded } });
await jobFundable.setVariable('rewardedAt', { [randomJob]: blockTimestamp - rewardPeriodTime });
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
});
it('should return a full period of credits', async () => {
const expectedCredits = mathUtils.calcPeriodCredits(liquidityAdded);
expect(await jobFundable.totalJobCredits(randomJob)).to.be.eq(expectedCredits);
});
});
context('when job was rewarded more than 1 period ago', () => {
let rewardTimestamp: number;
beforeEach(async () => {
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
rewardTimestamp = blockTimestamp - 1.1 * rewardPeriodTime;
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: liquidityAdded } });
await jobFundable.setVariable('rewardedAt', { [randomJob]: rewardTimestamp });
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
});
it('should return a full period of credits + minted sice reward reference', async () => {
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(await jobFundable.totalJobCredits(randomJob)).to.be.closeTo(
jobPeriodCredits.add(mathUtils.calcMintedCredits(jobPeriodCredits, blockTimestamp - (rewardTimestamp + rewardPeriodTime))),
mathUtils.blockShiftPrecision
);
});
});
context('when job was rewarded more than 2 periods ago', () => {
beforeEach(async () => {
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: toUnit(1) } });
await jobFundable.setVariable('workedAt', { [randomJob]: mathUtils.calcPeriod(blockTimestamp - 2 * rewardPeriodTime) });
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: { period: blockTimestamp - 2 * rewardPeriodTime } });
});
it('should return a full period of credits + minted since period start', async () => {
expect(await jobFundable.totalJobCredits(randomJob)).to.be.eq(
(await jobFundable.jobPeriodCredits(randomJob)).add(
(await jobFundable.jobPeriodCredits(randomJob)).mul(blockTimestamp - mathUtils.calcPeriod(blockTimestamp)).div(rewardPeriodTime)
)
);
});
});
});
});
describe('quoteLiquidity', () => {
it('should return 0 if liquidity is not approved', async () => {
expect(await jobFundable.quoteLiquidity(randomLiquidity.address, toUnit(1))).to.be.eq(0);
});
it('should not call the oracle when liquidity is updated', async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp) } });
await jobFundable.quoteLiquidity(approvedLiquidity.address, toUnit(1));
expect(helper.observe).not.to.have.been.called;
});
it('should call the oracle when liquidity is outdated', async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('_tick', {
[approvedLiquidity.address]: { period: mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime) },
});
await jobFundable.quoteLiquidity(approvedLiquidity.address, toUnit(1));
expect(helper.observe).have.been.calledWith(oraclePool.address, [blockTimestamp - mathUtils.calcPeriod(blockTimestamp)]);
});
it('should call the oracle when liquidity is expired', async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.quoteLiquidity(approvedLiquidity.address, toUnit(1));
expect(helper.observe).have.been.calledWith(oraclePool.address, [
blockTimestamp - mathUtils.calcPeriod(blockTimestamp),
blockTimestamp - mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime),
]);
});
it('should quote the liquidity with reward calculation', async () => {
/*
// REWARD CALCULATION
// twapCalculation: amountIn * 1.0001**(-difference/timeElapsed)
// difference: rewardPeriodTime
// timeElapsed: rewardPeriodTime
// twapCalculation: amountIn / 1.0001
//
// rewardCalculation: twapCalculation * rewardPeriod / inflationPeriod
*/
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
const liquidityParams = {
period: mathUtils.calcPeriod(blockTimestamp), // liquidity is updated
difference: rewardPeriodTime, // = +1 tick per second since previous observation
// as KP3R increases value, a liquidity unit should be rewarded with less KP3R
};
const amountIn = toUnit(1);
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: liquidityParams });
expect(await jobFundable.quoteLiquidity(approvedLiquidity.address, amountIn)).to.be.closeTo(
mathUtils.calcPeriodCredits(mathUtils.decrease1Tick(amountIn)),
mathUtils.blockShiftPrecision
);
});
});
describe('observeLiquidity', () => {
let blockTimestamp: number;
beforeEach(async () => {
helper.observe.reset();
const liquidityParams = {
current: 0,
difference: 0,
};
await jobFundable.setVariable('_tick', { [randomLiquidity.address]: liquidityParams });
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
});
context('when liquidity is updated', () => {
let period: number;
beforeEach(async () => {
period = mathUtils.calcPeriod(blockTimestamp);
await jobFundable.setVariable('_tick', { [randomLiquidity.address]: { period: period } });
});
it('should return current tick', async () => {
expect(await jobFundable.observeLiquidity(randomLiquidity.address)).to.deep.equal([
BigNumber.from(0),
BigNumber.from(0),
BigNumber.from(period),
]);
});
it('should not call the oracle', async () => {
await jobFundable.observeLiquidity(randomLiquidity.address);
expect(helper.observe).not.to.be.called;
});
});
context('when liquidity is outdated', () => {
let period: number;
beforeEach(async () => {
period = mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime);
await jobFundable.setVariable('_tick', { [randomLiquidity.address]: { period: period } });
await jobFundable.setVariable('_liquidityPool', { [randomLiquidity.address]: oraclePool.address });
helper.observe.returns([1, 0, true]);
});
it('should return oracle tick and calculate difference', async () => {
expect(await jobFundable.observeLiquidity(randomLiquidity.address)).to.deep.equal([
BigNumber.from(1),
BigNumber.from(1),
BigNumber.from(mathUtils.calcPeriod(blockTimestamp)),
]);
});
it('should call the oracle', async () => {
await jobFundable.observeLiquidity(randomLiquidity.address);
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(helper.observe).to.have.be.calledWith(oraclePool.address, [blockTimestamp - mathUtils.calcPeriod(blockTimestamp)]);
});
});
context('when liquidity is expired', () => {
beforeEach(async () => {
helper.observe.returns([2, 1, true]);
});
it('should return oracle tick and difference', async () => {
expect(await jobFundable.observeLiquidity(approvedLiquidity.address)).to.deep.equal([
BigNumber.from(2),
BigNumber.from(1),
BigNumber.from(mathUtils.calcPeriod(blockTimestamp)),
]);
});
it('should call the oracle', async () => {
await jobFundable.observeLiquidity(approvedLiquidity.address);
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(helper.observe).to.have.be.calledWith(oraclePool.address, [
blockTimestamp - mathUtils.calcPeriod(blockTimestamp),
blockTimestamp - mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime),
]);
});
});
});
describe('forceLiquidityCreditsToJob', () => {
behaviours.onlyGovernance(() => jobFundable, 'forceLiquidityCreditsToJob', governance, [randomJob, 1]);
it('should revert when called with unallowed job', async () => {
await expect(jobFundable.forceLiquidityCreditsToJob(randomJob, toUnit(1))).to.be.revertedWith('JobUnavailable()');
});
context('when job was approved', () => {
beforeEach(async () => {
await jobFundable.setJob(randomJob);
});
it('should reward job previously minted credits', async () => {
const block = await ethers.provider.getBlock('latest');
await jobFundable.setVariable('_jobPeriodCredits', { [randomJob]: toUnit(1) });
await jobFundable.setVariable('rewardedAt', { [randomJob]: mathUtils.calcPeriod(block.timestamp) });
// The job has 0 credits but should be rewarded some
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.eq(0);
await jobFundable.forceLiquidityCreditsToJob(randomJob, 0);
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.gt(0);
});
it('should update last reward timestamp', async () => {
await jobFundable.forceLiquidityCreditsToJob(randomJob, toUnit(1));
const block = await ethers.provider.getBlock('latest');
expect(await jobFundable.rewardedAt(randomJob)).to.be.eq(block.timestamp);
});
it('should increase job liquidity credits', async () => {
await jobFundable.forceLiquidityCreditsToJob(randomJob, toUnit(1));
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.equal(toUnit(1));
});
it('should add liquidity credits that dont change value', async () => {
await jobFundable.forceLiquidityCreditsToJob(randomJob, toUnit(1));
helper.observe.returns([rewardPeriodTime, 0, true]);
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('_tick', {
[approvedLiquidity.address]: {
current: 0,
difference: 0,
period: mathUtils.calcPeriod(blockTimestamp),
},
});
await evm.advanceTimeAndBlock(rewardPeriodTime - 10);
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.equal(toUnit(1));
});
it('should add liquidity credits that expire', async () => {
await jobFundable.forceLiquidityCreditsToJob(randomJob, toUnit(1));
await evm.advanceTimeAndBlock(rewardPeriodTime);
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.equal(0);
});
it('should emit event', async () => {
const forcedLiquidityAmount = toUnit(1);
const tx = await jobFundable.connect(governance).forceLiquidityCreditsToJob(randomJob, forcedLiquidityAmount);
const rewardedAt = (await ethers.provider.getBlock('latest')).timestamp;
await expect(tx).to.emit(jobFundable, 'LiquidityCreditsForced').withArgs(randomJob, rewardedAt, forcedLiquidityAmount);
});
});
});
describe('approveLiquidity', () => {
behaviours.onlyGovernance(
() => jobFundable,
'approveLiquidity',
governance,
() => [approvedLiquidity.address]
);
it('should revert when liquidity already approved', async () => {
await expect(jobFundable.connect(governance).approveLiquidity(approvedLiquidity.address)).to.be.revertedWith('LiquidityPairApproved()');
});
it('should add the liquidity to approved liquidities list', async () => {
await jobFundable.connect(governance).approveLiquidity(randomLiquidity.address);
expect(await jobFundable.approvedLiquidities()).to.contain(randomLiquidity.address);
});
it('should sort the tokens in the liquidity pair', async () => {
await jobFundable.connect(governance).approveLiquidity(randomLiquidity.address);
expect(await jobFundable.viewTickOrder(randomLiquidity.address)).to.be.true;
});
it('should initialize twap for liquidity', async () => {
await jobFundable.connect(governance).approveLiquidity(randomLiquidity.address);
expect(helper.observe).to.have.been.called;
});
it('should emit event', async () => {
await expect(jobFundable.connect(governance).approveLiquidity(randomLiquidity.address))
.to.emit(jobFundable, 'LiquidityApproval')
.withArgs(randomLiquidity.address);
});
});
describe('revokeLiquidity', () => {
behaviours.onlyGovernance(
() => jobFundable,
'revokeLiquidity',
governance,
() => [approvedLiquidity.address]
);
it('should not be able to remove unapproved liquidity', async () => {
await expect(jobFundable.connect(governance).revokeLiquidity(randomLiquidity.address)).to.be.revertedWith('LiquidityPairUnexistent()');
});
it('should not be able to remove the same liquidity twice', async () => {
await jobFundable.connect(governance).revokeLiquidity(approvedLiquidity.address);
await expect(jobFundable.connect(governance).revokeLiquidity(approvedLiquidity.address)).to.be.revertedWith('LiquidityPairUnexistent()');
});
it('should remove liquidity', async () => {
await jobFundable.connect(governance).revokeLiquidity(approvedLiquidity.address);
expect(await jobFundable.approvedLiquidities()).not.to.contain(approvedLiquidity.address);
});
it('should not remove other liquidities', async () => {
await jobFundable.connect(governance).approveLiquidity(randomLiquidity.address);
await jobFundable.connect(governance).revokeLiquidity(approvedLiquidity.address);
expect(await jobFundable.approvedLiquidities()).to.contain(randomLiquidity.address);
});
it('should avoid a revoked liquidity from minting new credits', async () => {
await jobFundable.setJob(randomJob);
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, toUnit(10));
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.gt(0);
await jobFundable.connect(governance).revokeLiquidity(approvedLiquidity.address);
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.eq(toUnit(0));
});
it('should emit event', async () => {
await expect(jobFundable.connect(governance).revokeLiquidity(approvedLiquidity.address))
.to.emit(jobFundable, 'LiquidityRevocation')
.withArgs(approvedLiquidity.address);
});
});
describe('addLiquidityToJob', () => {
it('should revert when liquidity pair is not accepted', async () => {
await expect(jobFundable.connect(provider).addLiquidityToJob(randomJob, randomLiquidity.address, toUnit(1))).to.be.revertedWith(
'LiquidityPairUnapproved()'
);
});
it('should revert when job is not accepted', async () => {
await expect(jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, toUnit(1))).to.be.revertedWith(
'JobUnavailable()'
);
});
context('when liquidity pair and job are accepted', async () => {
beforeEach(async () => {
await jobFundable.setJob(randomJob);
});
it('should revert when transfer reverts', async () => {
approvedLiquidity.transferFrom.returns(false);
const liquidityToAdd = mathUtils.calcLiquidityToAdd(toUnit(1));
await expect(jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, liquidityToAdd)).to.be.revertedWith(
'ERC20 operation did not succeed'
);
});
it('should revert if the amount is less than the minimum', async () => {
await expect(jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, toUnit(0.49))).to.be.revertedWith(
'JobLiquidityLessThanMin()'
);
});
it('should not revert when adding a tiny amount of liquidity if the minimum is already satisfied', async () => {
const liquidityToAdd = mathUtils.calcLiquidityToAdd(toUnit(1));
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, liquidityToAdd);
await expect(jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, toUnit(0.01))).not.to.be.revertedWith(
'JobLiquidityLessThanMin()'
);
});
it('should transfer the liquidity tokens to contract', async () => {
const liquidityToAdd = mathUtils.calcLiquidityToAdd(toUnit(1));
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, liquidityToAdd);
expect(approvedLiquidity.transferFrom).to.be.calledOnceWith(provider.address, jobFundable.address, liquidityToAdd);
});
it('should add liquidity amount to balance', async () => {
const liquidityToAdd = mathUtils.calcLiquidityToAdd(toUnit(1));
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, liquidityToAdd);
expect(await jobFundable.liquidityAmount(randomJob, approvedLiquidity.address)).to.equal(liquidityToAdd);
});
it('should update last reward timestamp', async () => {
const liquidityToAdd = mathUtils.calcLiquidityToAdd(toUnit(1));
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, liquidityToAdd);
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(await jobFundable.rewardedAt(randomJob)).to.be.eq(blockTimestamp);
});
it('should update job period credits for job', async () => {
const jobLiquidityAmount = toUnit(10);
const calculatedJobPeriodCredits = mathUtils.calcPeriodCredits(jobLiquidityAmount);
await jobFundable.setVariable('_jobPeriodCredits', { [randomJob]: 0 });
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: jobLiquidityAmount } });
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, 0);
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.eq(calculatedJobPeriodCredits);
});
it('should emit event', async () => {
const liquidityToAdd = mathUtils.calcLiquidityToAdd(toUnit(1));
const tx = await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, liquidityToAdd);
await expect(tx)
.to.emit(jobFundable, 'LiquidityAddition')
.withArgs(randomJob, approvedLiquidity.address, provider.address, liquidityToAdd);
});
context('when there was previous liquidity', () => {
beforeEach(async () => {
const previousJobLiquidityAmount = toUnit(10);
await jobFundable.setJobLiquidity(randomJob, randomLiquidity.address);
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: previousJobLiquidityAmount } });
await jobFundable.setVariable('_jobPeriodCredits', {
[randomJob]: mathUtils.calcPeriodCredits(previousJobLiquidityAmount),
});
});
it('should settle current credits debt of previous liquidity', async () => {
await evm.advanceTimeAndBlock(moment.duration(1, 'days').as('seconds'));
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, toUnit(1));
let totalCredits = await jobFundable.totalJobCredits(randomJob);
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.eq(totalCredits);
});
});
context('when liquidity twaps are outdated', () => {
let previousJobLiquidityAmount: BigNumber;
beforeEach(async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
previousJobLiquidityAmount = toUnit(10);
await jobFundable.setJobLiquidity(randomJob, randomLiquidity.address);
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: previousJobLiquidityAmount } });
await jobFundable.setVariable('rewardedAt', { [randomJob]: 0 });
await jobFundable.setVariable('_jobPeriodCredits', {
[randomJob]: mathUtils.calcPeriodCredits(previousJobLiquidityAmount),
});
let tickSetting = {
period: mathUtils.calcPeriod(blockTimestamp - rewardPeriodTime),
};
await jobFundable.setVariable('_tick', { [approvedLiquidity.address]: tickSetting });
});
it('should update twaps for liquidity', async () => {
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, toUnit(1));
expect(helper.observe).to.have.been.called;
});
it('should recalculate previous credits to current prices', async () => {
helper.getKP3RsAtTick.returns(([amount]: [BigNumber]) => {
return mathUtils.decrease1Tick(amount);
});
let previousJobCredits = mathUtils.calcPeriodCredits(previousJobLiquidityAmount);
await jobFundable.connect(provider).addLiquidityToJob(randomJob, approvedLiquidity.address, toUnit(1));
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.closeTo(
mathUtils.decrease1Tick(previousJobCredits),
mathUtils.blockShiftPrecision
);
});
});
});
});
describe('unbondLiquidityFromJob', () => {
beforeEach(async () => {
helper.observe.reset();
helper.observe.returns([rewardPeriodTime, 0, true]);
});
onlyJobOwner(
() => jobFundable,
'unbondLiquidityFromJob',
jobOwner,
() => [randomJob, approvedLiquidity.address, toUnit(1)]
);
it('should revert if job doesnt have the requested liquidity', async () => {
await expect(jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, approvedLiquidity.address, toUnit(1))).to.be.revertedWith(
'JobLiquidityUnexistent()'
);
});
context('when job has requested liquidity', () => {
const jobLiquidityAmount = toUnit(1);
beforeEach(async () => {
await jobFundable.setJobLiquidity(randomJob, approvedLiquidity.address);
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: jobLiquidityAmount } });
await jobFundable.setVariable('_jobLiquidityCredits', { [randomJob]: jobLiquidityAmount });
await jobFundable.setVariable('_jobPeriodCredits', { [randomJob]: jobLiquidityAmount });
});
it('should revert if trying to withdraw more liquidity than the job has', async () => {
await expect(
jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, approvedLiquidity.address, jobLiquidityAmount.add(1))
).to.be.revertedWith('JobLiquidityInsufficient()');
});
it('should not reset last reward timestamp', async () => {
const previousRewardedAt = await jobFundable.rewardedAt(randomJob);
await jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, approvedLiquidity.address, jobLiquidityAmount);
expect(await jobFundable.rewardedAt(randomJob)).to.be.eq(previousRewardedAt);
});
it('should remove liquidity from job if all is unbonded', async () => {
await jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, approvedLiquidity.address, jobLiquidityAmount);
expect(await jobFundable.internalJobLiquidities(randomJob)).to.deep.equal([]);
});
it('should update the period job accountance', async () => {
await jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, approvedLiquidity.address, jobLiquidityAmount);
expect(await jobFundable.jobPeriodCredits(randomJob)).to.equal(0);
});
it('should unbond the liquidity', async () => {
await jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, approvedLiquidity.address, jobLiquidityAmount);
expect(await jobFundable.callStatic.pendingUnbonds(randomJob, approvedLiquidity.address)).to.equal(jobLiquidityAmount);
});
it('should lock the unbonded liquidity', async () => {
await jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, approvedLiquidity.address, jobLiquidityAmount);
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
const expectedLockTime = blockTimestamp + moment.duration(14, 'days').as('seconds');
expect(await jobFundable.callStatic.canWithdrawAfter(randomJob, approvedLiquidity.address)).to.equal(expectedLockTime);
});
it('should emit event', async () => {
const tx = await jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, approvedLiquidity.address, jobLiquidityAmount);
await expect(tx).to.emit(jobFundable, 'Unbonding').withArgs(randomJob, approvedLiquidity.address, jobLiquidityAmount);
});
context('when liquidity is revoked', () => {
let revokedLiquidity: FakeContract<UniV3PairManager>;
beforeEach(async () => {
await jobFundable.setRevokedLiquidity(approvedLiquidity.address);
revokedLiquidity = approvedLiquidity;
});
it('should be able to unbond', async () => {
await jobFundable.connect(jobOwner).unbondLiquidityFromJob(randomJob, revokedLiquidity.address, jobLiquidityAmount);
expect(await jobFundable.callStatic.pendingUnbonds(randomJob, approvedLiquidity.address)).to.be.gt(0);
});
});
});
});
describe('withdrawLiquidityFromJob', () => {
let initialLiquidityAmount: BigNumber = toUnit(1);
beforeEach(async () => {
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: initialLiquidityAmount } });
await jobFundable.setVariable('_jobLiquidityCredits', { [randomJob]: toUnit(1) });
await jobFundable.setVariable('rewardedAt', { [randomJob]: 0 });
helper.observe.reset();
helper.observe.returns([rewardPeriodTime, 0, true]);
});
onlyJobOwner(
() => jobFundable,
'withdrawLiquidityFromJob',
jobOwner,
() => [randomJob, approvedLiquidity.address, jobOwner.address]
);
it('should revert if never unbonded', async () => {
await expect(
jobFundable.connect(jobOwner).withdrawLiquidityFromJob(randomJob, approvedLiquidity.address, jobOwner.address)
).to.be.revertedWith('UnbondsUnexistent()');
});
it('should revert if unbonded tokens are still locked', async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('pendingUnbonds', {
[randomJob]: { [approvedLiquidity.address]: toUnit(1) },
});
await jobFundable.setVariable('canWithdrawAfter', {
[randomJob]: { [approvedLiquidity.address]: blockTimestamp + moment.duration('1', 'hour').asSeconds() },
});
await expect(
jobFundable.connect(jobOwner).withdrawLiquidityFromJob(randomJob, approvedLiquidity.address, jobOwner.address)
).to.be.revertedWith('UnbondsLocked()');
});
it('should revert when receiver is zero address', async () => {
await expect(
jobFundable.connect(jobOwner).withdrawLiquidityFromJob(randomJob, approvedLiquidity.address, ZERO_ADDRESS)
).to.be.revertedWith('ZeroAddress()');
});
context('when unbonded tokens and waited', () => {
const unbondedAmount = toUnit(1);
beforeEach(async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('canWithdrawAfter', {
[randomJob]: { [approvedLiquidity.address]: blockTimestamp },
});
await jobFundable.setVariable('pendingUnbonds', {
[randomJob]: { [approvedLiquidity.address]: unbondedAmount },
});
});
it('should revert if job is disputed', async () => {
await jobFundable.setVariable('disputes', {
[randomJob]: true,
});
await expect(
jobFundable.connect(jobOwner).withdrawLiquidityFromJob(randomJob, approvedLiquidity.address, jobOwner.address)
).to.be.revertedWith('Disputed()');
});
it('should transfer unbonded liquidity to the receiver', async () => {
await jobFundable.connect(jobOwner).withdrawLiquidityFromJob(randomJob, approvedLiquidity.address, governance.address);
expect(approvedLiquidity.transfer).to.have.been.calledOnceWith(governance.address, unbondedAmount);
});
it('should emit event', async () => {
const tx = await jobFundable.connect(jobOwner).withdrawLiquidityFromJob(randomJob, approvedLiquidity.address, jobOwner.address);
await expect(tx)
.to.emit(jobFundable, 'LiquidityWithdrawal')
.withArgs(randomJob, approvedLiquidity.address, jobOwner.address, unbondedAmount);
});
it('should reset the pending unbond amount', async () => {
await jobFundable.connect(jobOwner).withdrawLiquidityFromJob(randomJob, approvedLiquidity.address, jobOwner.address);
expect(await jobFundable.pendingUnbonds(randomJob, approvedLiquidity.address)).to.equal(0);
});
});
});
describe('_settleJobAccountance', () => {
let calculatedJobPeriodCredits: BigNumber;
let blockTimestamp: number;
beforeEach(async () => {
const jobLiquidityAmount = toUnit(10);
calculatedJobPeriodCredits = mathUtils.calcPeriodCredits(jobLiquidityAmount);
await jobFundable.setJobLiquidity(randomJob, approvedLiquidity.address);
await jobFundable.setVariable('_jobPeriodCredits', { [randomJob]: calculatedJobPeriodCredits });
await jobFundable.setVariable('_liquidityPool', { [approvedLiquidity.address]: oraclePool.address });
await jobFundable.setVariable('liquidityAmount', { [randomJob]: { [approvedLiquidity.address]: jobLiquidityAmount } });
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
});
it('should update job credits to current quote', async () => {
helper.getKP3RsAtTick.returns(([amount]: [BigNumber]) => {
return mathUtils.decrease1Tick(amount);
});
await jobFundable.connect(provider).internalJobLiquidities(randomJob);
expect(await jobFundable.jobPeriodCredits(randomJob)).to.be.closeTo(
mathUtils.decrease1Tick(calculatedJobPeriodCredits),
mathUtils.blockShiftPrecision
);
});
it('should reward all job pending credits', async () => {
await jobFundable.setVariable('_jobPeriodCredits', { [randomJob]: calculatedJobPeriodCredits });
await jobFundable.setVariable('rewardedAt', { [randomJob]: mathUtils.calcPeriod(blockTimestamp) });
// The job has 0 credits but should be rewarded some
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.eq(0);
await jobFundable.internalSettleJobAccountance(randomJob);
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.gt(0);
});
it('should max the possible credits to 1 period', async () => {
await jobFundable.setVariable('_jobLiquidityCredits', { [randomJob]: calculatedJobPeriodCredits });
await jobFundable.setVariable('_jobPeriodCredits', { [randomJob]: calculatedJobPeriodCredits });
await jobFundable.setVariable('rewardedAt', { [randomJob]: mathUtils.calcPeriod(blockTimestamp) });
await jobFundable.internalSettleJobAccountance(randomJob);
expect(await jobFundable.jobLiquidityCredits(randomJob)).to.be.eq(calculatedJobPeriodCredits);
});
it('should set job reward timestamp to current timestamp', async () => {
await jobFundable.internalSettleJobAccountance(randomJob);
blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(await jobFundable.rewardedAt(randomJob)).to.be.eq(blockTimestamp);
});
});
});