@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
247 lines (211 loc) • 10.4 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,
ERC20ForTest,
ERC20ForTest__factory,
IKeep3rV1,
IKeep3rV1Proxy,
IUniswapV3Pool,
Keep3rHelper,
Keep3rJobFundableCreditsForTest,
Keep3rJobFundableCreditsForTest__factory,
} from '@types';
import { evm, wallet } from '@utils';
import { onlyJobOwner } from '@utils/behaviours';
import { toUnit } from '@utils/bn';
import chai, { expect } from 'chai';
import { ethers } from 'hardhat';
chai.use(smock.matchers);
describe('Keep3rJobFundableCredits', () => {
const approvedJob = wallet.generateRandomAddress();
const randomJob = wallet.generateRandomAddress();
let governance: SignerWithAddress;
let provider: SignerWithAddress;
let jobOwner: SignerWithAddress;
let jobFundable: MockContract<Keep3rJobFundableCreditsForTest>;
let keep3rV1: FakeContract<IKeep3rV1>;
let keep3rV1Proxy: FakeContract<IKeep3rV1Proxy>;
let helper: FakeContract<Keep3rHelper>;
let oraclePool: FakeContract<IUniswapV3Pool>;
let jobFundableFactory: MockContractFactory<Keep3rJobFundableCreditsForTest__factory>;
before(async () => {
[] = await ethers.getSigners();
jobFundableFactory = await smock.mock<Keep3rJobFundableCreditsForTest__factory>('Keep3rJobFundableCreditsForTest');
});
beforeEach(async () => {
helper = await smock.fake(IKeep3rHelperArtifact);
keep3rV1 = await smock.fake(IKeep3rV1Artifact);
keep3rV1Proxy = await smock.fake(IKeep3rV1ProxyArtifact);
oraclePool = await smock.fake(IUniswapV3PoolArtifact);
oraclePool.token0.returns(keep3rV1.address);
jobFundable = await jobFundableFactory.deploy(helper.address, keep3rV1.address, keep3rV1Proxy.address, oraclePool.address);
await jobFundable.setJob(approvedJob, jobOwner.address);
});
describe('addTokenCreditsToJob', () => {
let token: ERC20ForTest;
let erc20Factory: ERC20ForTest__factory;
before(async () => {
erc20Factory = (await ethers.getContractFactory('ERC20ForTest')) as ERC20ForTest__factory;
});
beforeEach(async () => {
token = await erc20Factory.deploy('Sample', 'SMP', provider.address, toUnit(10));
await token.connect(provider).approve(jobFundable.address, toUnit(10));
});
it('should revert when called with unallowed job', async () => {
await expect(jobFundable.connect(provider).addTokenCreditsToJob(randomJob, token.address, toUnit(1))).to.be.revertedWith(
'JobUnavailable()'
);
});
it('should revert when when token is KP3R', async () => {
await expect(jobFundable.connect(provider).addTokenCreditsToJob(approvedJob, keep3rV1.address, toUnit(1))).to.be.revertedWith(
'TokenUnallowed()'
);
});
it('should revert if transfer fails', async () => {
await expect(jobFundable.connect(provider).addTokenCreditsToJob(approvedJob, token.address, toUnit(11))).to.be.revertedWith(
'ERC20: transfer amount exceeds balance'
);
});
it('should increase job token credits, after fees', async () => {
await jobFundable.connect(provider).addTokenCreditsToJob(approvedJob, token.address, toUnit(1));
expect(await jobFundable.jobTokenCredits(approvedJob, token.address)).to.equal(toUnit(0.997));
});
it('should transfer tokens to contract', async () => {
await jobFundable.connect(provider).addTokenCreditsToJob(approvedJob, token.address, toUnit(1));
expect(await token.balanceOf(jobFundable.address)).to.equal(toUnit(0.997));
});
it('should save the block timestamp of when the credits were added', async () => {
await jobFundable.connect(provider).addTokenCreditsToJob(approvedJob, token.address, toUnit(1));
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
expect(await jobFundable.jobTokenCreditsAddedAt(approvedJob, token.address)).to.equal(blockTimestamp);
});
it('should transfer fee in tokens to governance', async () => {
await jobFundable.connect(provider).addTokenCreditsToJob(approvedJob, token.address, toUnit(1));
expect(await token.balanceOf(governance.address)).to.equal(toUnit(0.003));
});
it('should emit event', async () => {
await expect(jobFundable.connect(provider).addTokenCreditsToJob(approvedJob, token.address, toUnit(1)))
.to.emit(jobFundable, 'TokenCreditAddition')
.withArgs(approvedJob, token.address, provider.address, toUnit(1));
});
it('should add token address to the job token list', async () => {
await jobFundable.connect(provider).addTokenCreditsToJob(approvedJob, token.address, toUnit(1));
expect(await jobFundable.isJobToken(approvedJob, token.address)).to.be.true;
});
});
describe('withdrawTokenCreditsFromJob', () => {
let token: FakeContract<ERC20>;
beforeEach(async () => {
token = await smock.fake(ERC20Artifact);
token.transfer.returns(true);
await jobFundable.setVariable('jobTokenCredits', {
[]: {
[]: toUnit(1),
},
});
});
onlyJobOwner(
() => jobFundable,
'withdrawTokenCreditsFromJob',
jobOwner,
() => [approvedJob, token.address, toUnit(1), provider.address]
);
it('should revert if credits were deposited in the less than 60 seconds ago', async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('jobTokenCreditsAddedAt', {
[]: {
[]: blockTimestamp,
},
});
await evm.advanceToTime(blockTimestamp + 60);
await expect(
jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(1), provider.address)
).to.be.revertedWith('JobTokenCreditsLocked()');
});
it('should revert if the job is disputed', async () => {
await jobFundable.setVariable('disputes', {
[]: true,
});
await expect(
jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(1), provider.address)
).to.be.revertedWith('JobDisputed()');
});
it('should not revert if credits were deposited 60 seconds ago', async () => {
const blockTimestamp = (await ethers.provider.getBlock('latest')).timestamp;
await jobFundable.setVariable('jobTokenCreditsAddedAt', {
[]: {
[]: blockTimestamp,
},
});
await evm.advanceToTime(blockTimestamp + 61);
await expect(
jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(1), provider.address)
).not.to.be.revertedWith('JobTokenCreditsLocked()');
});
it('should revert if transfer fails', async () => {
token.transfer.returns(false);
await expect(
jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(1), provider.address)
).to.be.revertedWith('SafeERC20: ERC20 operation did not succeed');
});
it('should revert if job does not have enough credits', async () => {
await expect(
jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(2), provider.address)
).to.be.revertedWith('InsufficientJobTokenCredits()');
});
it('should reduce the amount withdrawn from job balance', async () => {
await jobFundable.setVariable('jobTokenCreditsAddedAt', {
[]: {
[]: 0,
},
});
await jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(0.4), provider.address);
expect(await jobFundable.jobTokenCredits(approvedJob, token.address)).to.equal(toUnit(0.6));
});
it('should transfer tokens to specified receiver', async () => {
await jobFundable.setVariable('jobTokenCreditsAddedAt', {
[]: {
[]: 0,
},
});
await jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(1), provider.address);
expect(token.transfer).to.be.calledOnceWith(provider.address, toUnit(1));
});
it('should emit event', async () => {
await jobFundable.setVariable('jobTokenCreditsAddedAt', {
[]: {
[]: 0,
},
});
await expect(jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(0.4), provider.address))
.to.emit(jobFundable, 'TokenCreditWithdrawal')
.withArgs(approvedJob, token.address, provider.address, toUnit(0.4));
});
it('should not remove token from the job token list when partially withdrawn', async () => {
await jobFundable.setVariable('jobTokenCreditsAddedAt', {
[]: {
[]: 0,
},
});
await jobFundable.setJobToken(approvedJob, token.address);
await jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(0.4), provider.address);
expect(await jobFundable.isJobToken(approvedJob, token.address)).to.be.true;
});
it('should remove token from the job token list when fully withdrawn', async () => {
await jobFundable.setVariable('jobTokenCreditsAddedAt', {
[]: {
[]: 0,
},
});
await jobFundable.setJobToken(approvedJob, token.address);
await jobFundable.connect(jobOwner).withdrawTokenCreditsFromJob(approvedJob, token.address, toUnit(1), provider.address);
expect(await jobFundable.isJobToken(approvedJob, token.address)).to.be.false;
});
});
});