Skip to content

Commit

Permalink
chore: added test scaffolding (keep3r-network#2)
Browse files Browse the repository at this point in the history
* chore: added hardhat boilerplate

* chore: run linter

* fix: rm legacy code

* feat: added vyper compiler

* feat: added github workflows

* fix: linter error

* feat: adding basic test structure

* fix: typings bug

* feat: added tests scaffolding

* fix: revert linter

* fix: run prettier
  • Loading branch information
wei3erHase authored May 11, 2022
1 parent 5e79b10 commit 1f9cd61
Show file tree
Hide file tree
Showing 17 changed files with 440 additions and 103 deletions.
26 changes: 7 additions & 19 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,25 @@ import 'hardhat-gas-reporter';
import 'hardhat-deploy';
import 'solidity-coverage';
import { HardhatUserConfig, MultiSolcUserConfig, NetworksUserConfig } from 'hardhat/types';
import * as env from './utils/env';
import 'tsconfig-paths/register';
import { getNodeUrl, accounts } from './utils/network';

const networks: NetworksUserConfig = process.env.TEST
? {}
: {
hardhat: {
forking: {
enabled: process.env.FORK ? true : false,
url: getNodeUrl('mainnet'),
url: env.getNodeUrl('ethereum'),
},
},
localhost: {
url: getNodeUrl('localhost'),
accounts: accounts('localhost'),
},
kovan: {
url: getNodeUrl('kovan'),
accounts: accounts('kovan'),
},
rinkeby: {
url: getNodeUrl('rinkeby'),
accounts: accounts('rinkeby'),
},
ropsten: {
url: getNodeUrl('ropsten'),
accounts: accounts('ropsten'),
url: env.getNodeUrl('kovan'),
accounts: env.getAccounts('kovan'),
},
mainnet: {
url: getNodeUrl('mainnet'),
accounts: accounts('mainnet'),
ethereum: {
url: env.getNodeUrl('ethereum'),
accounts: env.getAccounts('ethereum'),
},
};

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"eth-gas-reporter/colors": "1.4.0"
},
"dependencies": {
"@nomiclabs/hardhat-vyper": "^3.0.0",
"@nomiclabs/hardhat-vyper": "3.0.0",
"@openzeppelin/contracts": "4.6.0",
"solhint-plugin-wonderland": "0.0.1"
},
Expand All @@ -81,7 +81,7 @@
"cross-env": "7.0.3",
"dotenv": "16.0.0",
"ethereum-waffle": "3.4.4",
"ethers": "5.6.4",
"ethers": "5.6.5",
"hardhat": "2.9.3",
"hardhat-deploy": "0.11.4",
"hardhat-gas-reporter": "1.0.8",
Expand Down
42 changes: 42 additions & 0 deletions test/e2e/fixed-forex.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getMainnetSdk } from '@dethcrypto/eth-sdk-client';
import { Keep3rV1 } from '@eth-sdk-types';
import { FixedForex, FixedForex__factory } from '@typechained';
import { ethers } from 'hardhat';
import { evm } from '@utils';
import { expect } from 'chai';
import { getNodeUrl } from 'utils/env';
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';

describe('FixedForex @skip-on-coverage', () => {
let deployer: SignerWithAddress;
let keep3rV1: Keep3rV1;
let snapshotId: string;
let fixedForex: FixedForex;

before(async () => {
[deployer] = await ethers.getSigners();

await evm.reset({
jsonRpcUrl: getNodeUrl('ethereum'),
blockNumber: 14750000,
});

const sdk = getMainnetSdk(deployer);
keep3rV1 = sdk.keep3rV1;

const fixedForexFactory = (await ethers.getContractFactory('FixedForex')) as FixedForex__factory;
fixedForex = await fixedForexFactory.connect(deployer).deploy(keep3rV1.address);

snapshotId = await evm.snapshot.take();
});

beforeEach(async () => {
await evm.snapshot.revert(snapshotId);
});

describe('fixed-forex', () => {
it('should be deployed', async () => {
expect(await fixedForex.deployed());
});
});
});
26 changes: 26 additions & 0 deletions test/unit/GaugeProxy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import chai, { expect } from 'chai';
import { MockContract, MockContractFactory, smock } from '@defi-wonderland/smock';
import { GaugeProxy, GaugeProxy__factory } from '@typechained';
import { evm } from '@utils';

chai.use(smock.matchers);

describe('GaugeProxy', () => {
let gauge: MockContract<GaugeProxy>;
let gaugeFactory: MockContractFactory<GaugeProxy__factory>;
let snapshotId: string;

before(async () => {
gaugeFactory = await smock.mock<GaugeProxy__factory>('GaugeProxy');
gauge = await gaugeFactory.deploy();
snapshotId = await evm.snapshot.take();
});

beforeEach(async () => {
await evm.snapshot.revert(snapshotId);
});

it('should be deployed', async () => {
expect(await gauge.deployed());
});
});
9 changes: 9 additions & 0 deletions test/utils/bdd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Suite, SuiteFunction } from 'mocha';

export const then = it;
export const given = beforeEach;
export const when: SuiteFunction = <SuiteFunction>function (title: string, fn: (this: Suite) => void) {
context('when ' + title, fn);
};
when.only = (title: string, fn?: (this: Suite) => void) => context.only('when ' + title, fn!);
when.skip = (title: string, fn: (this: Suite) => void) => context.skip('when ' + title, fn);
49 changes: 49 additions & 0 deletions test/utils/behaviours.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { smock } from '@defi-wonderland/smock';
import { Provider } from '@ethersproject/providers';
import chai, { expect } from 'chai';
import { Signer } from 'ethers';
import { contracts, wallet } from '.';
import { toUnit } from './bn';

chai.use(smock.matchers);

export type Impersonator = Signer | Provider | string;

export const onlyMaker = createOnlyCallableCheck(['maker'], 'OnlyMaker()');
export const onlyKeeper = createOnlyCallableCheck(['keeper'], 'OnlyKeeper()');
export const onlyGovernor = createOnlyCallableCheck(['governance'], 'OnlyGovernor()');
export const onlyPendingGovernor = createOnlyCallableCheck(['pending governance'], 'OnlyPendingGovernor()');

export function createOnlyCallableCheck(allowedLabels: string[], error: string) {
return (
delayedContract: () => any,
fnName: string,
allowedWallet: Impersonator | Impersonator[] | (() => Impersonator | Impersonator[]),
args: unknown[] | (() => unknown[])
) => {
allowedLabels.forEach((allowedLabel, index) => {
it(`should be callable by ${allowedLabel}`, async () => {
let impersonator = allowedWallet;
if (typeof allowedWallet === 'function') impersonator = allowedWallet();
if (Array.isArray(impersonator)) impersonator = impersonator[index];

return expect(callFunction(impersonator as Impersonator)).not.to.be.revertedWith(error);
});
});

it('should not be callable by any address', async () => {
const any = await wallet.generateRandom();
await wallet.setBalance({ account: any.address, balance: toUnit(1) });
return expect(callFunction(any)).to.be.revertedWith(error);
});

function callFunction(impersonator: Impersonator) {
const argsArray: unknown[] = typeof args === 'function' ? args() : args;
const fn = delayedContract().connect(impersonator)[fnName] as (...args: unknown[]) => unknown;
return fn(...argsArray, {
gasLimit: 1e6,
gasPrice: 500e9,
});
}
};
}
28 changes: 28 additions & 0 deletions test/utils/bn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BigNumber, utils } from 'ethers';
import { expect } from 'chai';

export const expectToEqualWithThreshold = ({
value,
to,
threshold,
}: {
value: BigNumber | number | string;
to: BigNumber | number | string;
threshold: BigNumber | number | string;
}): void => {
value = toBN(value);
to = toBN(to);
threshold = toBN(threshold);
expect(
to.sub(threshold).lte(value) && to.add(threshold).gte(value),
`Expected ${value.toString()} to be between ${to.sub(threshold).toString()} and ${to.add(threshold).toString()}`
).to.be.true;
};

export const toBN = (value: string | number | BigNumber): BigNumber => {
return BigNumber.isBigNumber(value) ? value : BigNumber.from(value);
};

export const toUnit = (value: number): BigNumber => {
return utils.parseUnits(value.toString());
};
2 changes: 2 additions & 0 deletions test/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export const ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
18 changes: 18 additions & 0 deletions test/utils/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Contract, ContractFactory } from '@ethersproject/contracts';
import { TransactionResponse } from '@ethersproject/abstract-provider';
import { ContractInterface, Signer } from 'ethers';
import { getStatic } from 'ethers/lib/utils';

export const deploy = async (contract: ContractFactory, args: any[]): Promise<{ tx: TransactionResponse; contract: Contract }> => {
const deploymentTransactionRequest = await contract.getDeployTransaction(...args);
const deploymentTx = await contract.signer.sendTransaction(deploymentTransactionRequest);
const contractAddress = getStatic<(deploymentTx: TransactionResponse) => string>(contract.constructor, 'getContractAddress')(deploymentTx);
const deployedContract = getStatic<(contractAddress: string, contractInterface: ContractInterface, signer?: Signer) => Contract>(
contract.constructor,
'getContract'
)(contractAddress, contract.interface, contract.signer);
return {
tx: deploymentTx,
contract: deployedContract,
};
};
20 changes: 20 additions & 0 deletions test/utils/erc20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { BigNumber, Contract } from 'ethers';
import { ethers } from 'hardhat';

export const deploy = async ({
name,
symbol,
decimals,
initialAccount,
initialAmount,
}: {
name: string;
symbol: string;
decimals?: BigNumber | number;
initialAccount: string;
initialAmount: BigNumber;
}): Promise<Contract> => {
const erc20MockContract = await ethers.getContractFactory('contracts/mocks/ERC20Mock.sol:ERC20Mock');
const deployedContract = await erc20MockContract.deploy(name, symbol, decimals || 18, initialAccount, initialAmount);
return deployedContract;
};
36 changes: 36 additions & 0 deletions test/utils/event-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TransactionResponse, TransactionReceipt } from '@ethersproject/abstract-provider';
import { expect } from 'chai';

export async function expectNoEventWithName(response: TransactionResponse, eventName: string) {
const receipt = await response.wait();
for (const event of getEvents(receipt)) {
expect(event.event).not.to.equal(eventName);
}
}

export async function readArgFromEvent<T>(response: TransactionResponse, eventName: string, paramName: string): Promise<T | undefined> {
const receipt = await response.wait();
for (const event of getEvents(receipt)) {
if (event.event === eventName) {
return event.args[paramName];
}
}
}

export async function readArgFromEventOrFail<T>(response: TransactionResponse, eventName: string, paramName: string): Promise<T> {
const result = await readArgFromEvent<T>(response, eventName, paramName);
if (result) {
return result;
}
throw new Error(`Failed to find event with name ${eventName}`);
}

function getEvents(receipt: TransactionReceipt): Event[] {
// @ts-ignore
return receipt.events;
}

type Event = {
event: string; // Event name
args: any;
};
73 changes: 73 additions & 0 deletions test/utils/evm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { BigNumber, BigNumberish } from 'ethers';
import { network } from 'hardhat';

export const advanceTimeAndBlock = async (time: number): Promise<void> => {
await advanceTime(time);
await advanceBlocks(1);
};

export const advanceToTimeAndBlock = async (time: number): Promise<void> => {
await advanceToTime(time);
await advanceBlocks(1);
};

export const advanceTime = async (time: number): Promise<void> => {
await network.provider.request({
method: 'evm_increaseTime',
params: [time],
});
};

export const advanceToTime = async (time: number): Promise<void> => {
await network.provider.request({
method: 'evm_setNextBlockTimestamp',
params: [time],
});
};

export const advanceBlocks = async (blocks: BigNumberish) => {
blocks = !BigNumber.isBigNumber(blocks) ? BigNumber.from(`${blocks}`) : blocks;
await network.provider.request({
method: 'hardhat_mine',
params: [blocks.toHexString().replace('0x0', '0x')],
});
};

export const reset = async (forking?: { [key: string]: any }) => {
const params = forking ? [{ forking }] : [];
await network.provider.request({
method: 'hardhat_reset',
params,
});
};

class SnapshotManager {
snapshots: { [id: string]: string } = {};

async take(): Promise<string> {
const id = await this.takeSnapshot();
this.snapshots[id] = id;
return id;
}

async revert(id: string): Promise<void> {
await this.revertSnapshot(this.snapshots[id]);
this.snapshots[id] = await this.takeSnapshot();
}

private async takeSnapshot(): Promise<string> {
return (await network.provider.request({
method: 'evm_snapshot',
params: [],
})) as string;
}

private async revertSnapshot(id: string) {
await network.provider.request({
method: 'evm_revert',
params: [id],
});
}
}

export const snapshot = new SnapshotManager();
8 changes: 8 additions & 0 deletions test/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as behaviours from './behaviours';
import * as contracts from './contracts';
import * as erc20 from './erc20';
import * as evm from './evm';
import * as bn from './bn';
import * as wallet from './wallet';

export { contracts, behaviours, bn, erc20, evm, wallet };
Loading

0 comments on commit 1f9cd61

Please sign in to comment.