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

247 lines (211 loc) 10.4 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, 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 () => { [governance, provider, jobOwner] = 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', { [approvedJob]: { [token.address]: 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', { [approvedJob]: { [token.address]: 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', { [approvedJob]: 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', { [approvedJob]: { [token.address]: 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', { [approvedJob]: { [token.address]: 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', { [approvedJob]: { [token.address]: 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', { [approvedJob]: { [token.address]: 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', { [approvedJob]: { [token.address]: 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', { [approvedJob]: { [token.address]: 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; }); }); });