feat() add hardhat test and template

This commit is contained in:
Clément 'birdy' Danjou
2023-07-27 17:09:19 +02:00
parent 9ec128383b
commit d7ab5c3db2
26 changed files with 8227 additions and 2696 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
export INFURA_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
export MNEMONIC="adapt mosquito move limb mobile illegal tree voyage juice mosquito burger raise father hope layer"
# Block explorer API keys
export ARBISCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
export BSCSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
export ETHERSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
export OPTIMISM_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
export POLYGONSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
export SNOWTRACE_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"

21
.eslintignore Normal file
View File

@@ -0,0 +1,21 @@
# directories
.coverage_artifacts
.coverage_cache
.coverage_contracts
artifacts
build
cache
coverage
dist
node_modules
types
# files
*.env
*.log
.DS_Store
.pnp.*
coverage.json
package-lock.json
pnpm-lock.yaml
yarn.lock

22
.eslintrc.yml Normal file
View File

@@ -0,0 +1,22 @@
extends:
- "eslint:recommended"
- "plugin:@typescript-eslint/eslint-recommended"
- "plugin:@typescript-eslint/recommended"
- "prettier"
parser: "@typescript-eslint/parser"
parserOptions:
project: "tsconfig.json"
plugins:
- "@typescript-eslint"
root: true
rules:
"@typescript-eslint/no-floating-promises":
- error
- ignoreIIFE: true
ignoreVoid: true
"@typescript-eslint/no-inferrable-types": "off"
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-unused-vars":
- error
- argsIgnorePattern: "_"
varsIgnorePattern: "_"

142
.gitignore vendored
View File

@@ -1,18 +1,132 @@
decrypt/target
decrypt/ciphertexts
res/
node_modules/
.venv
.vscode
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
node_modules
.env
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
coverage.json
typechain
typechain-types
*.lcov
# Hardhat files
cache
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
public/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Python app
res/
.venv
.env
# Hardhat
artifacts
.coverage_artifacts
.coverage_cache
.coverage_contracts
coverage.json
build
cache
types
deployments
typechain
typechain-types

10
.gitpod.yml Normal file
View File

@@ -0,0 +1,10 @@
image: "gitpod/workspace-node:latest"
tasks:
- init: "pnpm install"
vscode:
extensions:
- "esbenp.prettier-vscode"
- "NomicFoundation.hardhat-solidity"
- "ritwickdey.LiveServer"

21
.prettierignore Normal file
View File

@@ -0,0 +1,21 @@
# directories
.coverage_artifacts
.coverage_cache
.coverage_contracts
artifacts
build
cache
coverage
dist
node_modules
types
# files
*.env
*.log
.DS_Store
.pnp.*
coverage.json
package-lock.json
pnpm-lock.yaml
yarn.lock

23
.prettierrc.yml Normal file
View File

@@ -0,0 +1,23 @@
bracketSpacing: true
plugins:
- "@trivago/prettier-plugin-sort-imports"
- "prettier-plugin-solidity"
printWidth: 120
proseWrap: "always"
singleQuote: false
tabWidth: 2
trailingComma: "all"
overrides:
- files: "*.sol"
options:
compiler: "0.8.17"
parser: "solidity-parse"
tabWidth: 4
- files: "*.ts"
options:
importOrder: ["<THIRD_PARTY_MODULES>", "^[./]"]
importOrderParserPlugins: ["typescript"]
importOrderSeparation: true
importOrderSortSpecifiers: true
parser: "typescript"

7
.solcover.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
istanbulReporter: ["html", "lcov"],
providerOptions: {
mnemonic: process.env.MNEMONIC,
},
skipFiles: ["test"],
};

19
.solhint.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "solhint:recommended",
"plugins": ["prettier"],
"rules": {
"code-complexity": ["error", 8],
"compiler-version": ["error", ">=0.8.4"],
"func-visibility": ["error", { "ignoreConstructors": true }],
"max-line-length": ["error", 120],
"named-parameters-mapping": "warn",
"no-console": "off",
"not-rely-on-time": "off",
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
}

3
.solhintignore Normal file
View File

@@ -0,0 +1,3 @@
# directories
**/artifacts
**/node_modules

View File

@@ -1,10 +1,83 @@
import { HardhatUserConfig } from "hardhat/config";
import "./tasks/accounts";
import "./tasks/mint";
import "./tasks/taskDeploy";
import "@nomicfoundation/hardhat-toolbox";
import { config as dotenvConfig } from "dotenv";
import "hardhat-deploy";
import type { HardhatUserConfig } from "hardhat/config";
import type { NetworkUserConfig } from "hardhat/types";
import { resolve } from "path";
const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || "./.env";
dotenvConfig({ path: resolve(__dirname, dotenvConfigPath) });
// Ensure that we have all the environment variables we need.
const mnemonic: string | undefined = process.env.MNEMONIC;
if (!mnemonic) {
throw new Error("Please set your MNEMONIC in a .env file");
}
const chainIds = {
zama: 8009,
};
function getChainConfig(chain: keyof typeof chainIds): NetworkUserConfig {
let jsonRpcUrl: string;
switch (chain) {
case "zama":
jsonRpcUrl = "https://devnet.zama.ai";
break;
}
return {
accounts: {
count: 10,
mnemonic,
path: "m/44'/60'/0'/0",
},
chainId: chainIds[chain],
url: jsonRpcUrl,
};
}
const config: HardhatUserConfig = {
solidity: "0.8.18",
defaultNetwork: "zama",
namedAccounts: {
deployer: 0,
},
gasReporter: {
currency: "USD",
enabled: process.env.REPORT_GAS ? true : false,
excludeContracts: [],
src: "./contracts",
},
networks: {
zama: getChainConfig("zama"),
},
paths: {
sources: "./examples",
artifacts: "./artifacts",
cache: "./cache",
sources: "./contracts",
tests: "./test",
},
solidity: {
version: "0.8.17",
settings: {
metadata: {
// Not including the metadata hash
// https://github.com/paulrberg/hardhat-template/issues/31
bytecodeHash: "none",
},
// Disable the optimizer when debugging
// https://hardhat.org/hardhat-network/#solidity-optimizer-support
optimizer: {
enabled: true,
runs: 800,
},
},
},
typechain: {
outDir: "types",
target: "ethers-v6",
},
};

10239
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,18 @@
"lib": "lib"
},
"scripts": {
"codegen": "cd ./lib && python ../codegen/codegen.py && npm run prettier && cd ..",
"lint": "prettier --list-different '**/*.sol'",
"prettier": "prettier --write '**/*.sol'",
"clean": "rimraf ./artifacts ./cache ./coverage ./types ./coverage.json && npm run typechain",
"compile": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat compile",
"coverage": "hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"test/**/*.ts\" && pnpm typechain",
"deploy:contracts": "hardhat deploy",
"lint": "npm run lint:sol && npm run lint:ts && npm run prettier:check",
"lint:sol": "solhint --max-warnings 0 \"contracts/**/*.sol\"",
"lint:ts": "eslint --ignore-path ./.eslintignore --ext .js,.ts .",
"prettier:check": "prettier --check \"**/*.{js,json,md,sol,ts,yml}\"",
"prettier": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"",
"test": "hardhat test",
"build": "hardhat compile"
"typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain",
"codegen": "cd ./lib && python ../codegen/codegen.py && npm run prettier && cd .."
},
"repository": {
"type": "git",
@@ -25,12 +32,44 @@
},
"homepage": "https://github.com/zama-ai/fhevm-solidity#readme",
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.6",
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
"hardhat": "^2.16.1",
"@nomicfoundation/hardhat-verify": "^1.0.0",
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
"@typechain/ethers-v6": "^0.4.0",
"@typechain/hardhat": "^8.0.0",
"@types/chai": "^4.3.4",
"@types/fs-extra": "^9.0.13",
"@types/mocha": "^10.0.0",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"chai": "^4.3.7",
"cross-env": "^7.0.3",
"dotenv": "^16.0.3",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"ethers": "^6.4.0",
"fhevmjs": "^0.1.3",
"fs-extra": "^10.1.0",
"ganache-cli": "^6.12.2",
"hardhat": "^2.12.2",
"hardhat-deploy": "^0.11.29",
"hardhat-gas-reporter": "^1.0.2",
"lodash": "^4.17.21",
"mocha": "^10.1.0",
"prettier": "^2.8.8",
"prettier-plugin-solidity": "^1.1.3",
"rimraf": "^4.1.2",
"solhint": "^3.4.0",
"solhint-plugin-prettier": "^0.0.5",
"solidity-coverage": "^0.8.1",
"ts-generator": "^0.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.5"
"typechain": "^8.2.0",
"typescript": "^5.1.6"
},
"dependencies": {
"@openzeppelin/contracts": "^4.9.2"

9
tasks/accounts.ts Normal file
View File

@@ -0,0 +1,9 @@
import { task } from "hardhat/config";
task("accounts", "Prints the list of accounts", async (_taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});

22
tasks/mint.ts Normal file
View File

@@ -0,0 +1,22 @@
import { task } from "hardhat/config";
import type { TaskArguments } from "hardhat/types";
import { getInstance } from "../test/instance";
task("task:mint")
.addParam("mint", "Tokens to mint")
.addParam("account", "Specify which account [0, 9]")
.setAction(async function (taskArguments: TaskArguments, hre) {
const { ethers, deployments } = hre;
const EncryptedERC20 = await deployments.get("EncryptedERC20");
const instance = await getInstance(EncryptedERC20.address, ethers);
const signers = await ethers.getSigners();
const encryptedERC20 = await ethers.getContractAt("EncryptedERC20", EncryptedERC20.address);
await encryptedERC20.connect(signers[taskArguments.account]).mint(instance.encrypt32(taskArguments.mint));
console.log("Mint done: ", taskArguments.mint);
});

10
tasks/taskDeploy.ts Normal file
View File

@@ -0,0 +1,10 @@
import { task } from "hardhat/config";
import type { TaskArguments } from "hardhat/types";
task("task:deployERC20").setAction(async function (taskArguments: TaskArguments, { ethers }) {
const signers = await ethers.getSigners();
const erc20Factory = await ethers.getContractFactory("EncryptedERC20");
const encryptedERC20 = await erc20Factory.connect(signers[0]).deploy();
await encryptedERC20.waitForDeployment();
console.log("EncryptedERC20 deployed to: ", await encryptedERC20.getAddress());
});

View File

@@ -0,0 +1,14 @@
import { ethers } from "hardhat";
import type { EncryptedERC20 } from "../../types/contracts/EncryptedERC20";
export async function deployEncryptedERC20Fixture(): Promise<EncryptedERC20> {
const signers = await ethers.getSigners();
const admin = signers[0];
const contractFactory = await ethers.getContractFactory("EncryptedERC20");
const contract = await contractFactory.connect(admin).deploy();
await contract.waitForDeployment();
return contract;
}

View File

@@ -0,0 +1,113 @@
import { createInstances } from "../instance";
import type { Signers } from "../types";
import { deployEncryptedERC20Fixture } from "./EncryptedERC20.fixture";
import { expect } from "chai";
import { ethers } from "hardhat";
describe("Unit tests", function () {
before(async function () {
this.signers = {} as Signers;
const signers = await ethers.getSigners();
this.signers.alice = signers[0];
this.signers.bob = signers[1];
this.signers.carol = signers[2];
this.signers.dave = signers[3];
});
describe("EncryptedERC20", function () {
beforeEach(async function () {
const contract = await deployEncryptedERC20Fixture();
this.contractAddress = await contract.getAddress();
this.erc20 = contract;
const instances = await createInstances(
this.contractAddress,
ethers,
this.signers
);
this.instances = instances;
});
it("should mint the contract", async function () {
const encryptedAmount = this.instances.alice.encrypt32(1000);
const transaction = await this.erc20.mint(encryptedAmount);
await transaction.wait();
// Call the method
const token = this.instances.alice.getTokenSignature(
this.contractAddress
) || {
signature: "",
publicKey: "",
};
const encryptedBalance = await this.erc20.balanceOf(
token.publicKey,
token.signature
);
// Decrypt the balance
const balance = this.instances.alice.decrypt(
this.contractAddress,
encryptedBalance
);
expect(balance).to.equal(1000);
const encryptedTotalSupply = await this.erc20.getTotalSupply(
token.publicKey,
token.signature
);
// Decrypt the total supply
const totalSupply = this.instances.alice.decrypt(
this.contractAddress,
encryptedTotalSupply
);
expect(totalSupply).to.equal(1000);
});
it("should transfer tokens between two users", async function () {
const encryptedAmount = this.instances.alice.encrypt32(10000);
const transaction = await this.erc20.mint(encryptedAmount);
await transaction.wait();
const encryptedTransferAmount = this.instances.alice.encrypt32(1337);
const tx = await this.erc20["transfer(address,bytes)"](
this.signers.bob.address,
encryptedTransferAmount
);
await tx.wait();
const tokenAlice = this.instances.alice.getTokenSignature(
this.contractAddress
)!;
const encryptedBalanceAlice = await this.erc20.balanceOf(
tokenAlice.publicKey,
tokenAlice.signature
);
// Decrypt the balance
const balanceAlice = this.instances.alice.decrypt(
this.contractAddress,
encryptedBalanceAlice
);
expect(balanceAlice).to.equal(10000 - 1337);
const bobErc20 = this.erc20.connect(this.signers.bob);
const tokenBob = this.instances.bob.getTokenSignature(
this.contractAddress
)!;
const encryptedBalanceBob = await bobErc20.balanceOf(
tokenBob.publicKey,
tokenBob.signature
);
// Decrypt the balance
const balanceBob = this.instances.bob.decrypt(
this.contractAddress,
encryptedBalanceBob
);
expect(balanceBob).to.equal(1337);
});
});
});

61
test/instance.ts Normal file
View File

@@ -0,0 +1,61 @@
import { FhevmInstances, Signers } from "./types";
import { Signer } from "ethers";
import fhevmjs, { FhevmInstance } from "fhevmjs";
import { ethers as hethers } from "hardhat";
let publicKey: string;
let chainId: number;
export const createInstances = async (
contractAddress: string,
ethers: typeof hethers,
accounts: Signers
): Promise<FhevmInstances> => {
if (!publicKey || !chainId) {
// 1. Get chain id
const provider = ethers.provider;
const network = await provider.getNetwork();
chainId = +network.chainId.toString(); // Need to be a number
// Get blockchain public key
publicKey = await provider.call({
to: "0x0000000000000000000000000000000000000044",
});
}
// Create instance
const instances: FhevmInstances = {} as FhevmInstances;
await Promise.all(
Object.keys(accounts).map(async (k) => {
const instance = await fhevmjs.createInstance({ chainId, publicKey });
await generateToken(
contractAddress,
accounts[k as keyof Signers],
instance
);
instances[k as keyof FhevmInstances] = instance;
})
);
return instances;
};
const generateToken = async (
contractAddress: string,
signer: Signer,
instance: FhevmInstance
) => {
// Generate token to decrypt
const generatedToken = instance.generateToken({
verifyingContract: contractAddress,
});
// Sign the public key
const signature = await signer.signTypedData(
generatedToken.token.domain,
{ Reencrypt: generatedToken.token.types.Reencrypt }, // Need to remove EIP712Domain from types
generatedToken.token.message
);
instance.setTokenSignature(contractAddress, signature);
};

26
test/types.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { EncryptedERC20 } from "../types/contracts/EncryptedERC20";
import type { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/dist/src/signer-with-address";
import type { FhevmInstance } from "fhevmjs";
declare module "mocha" {
export interface Context {
signers: Signers;
contractAddress: string;
instances: FhevmInstances;
erc20: EncryptedERC20;
}
}
export interface Signers {
alice: SignerWithAddress;
bob: SignerWithAddress;
carol: SignerWithAddress;
dave: SignerWithAddress;
}
export interface FhevmInstances {
alice: FhevmInstance;
bob: FhevmInstance;
carol: FhevmInstance;
dave: FhevmInstance;
}

View File

@@ -1,11 +1,22 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"declaration": true,
"declarationMap": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"lib": ["es2020"],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"removeComments": true,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
}
"target": "es2020"
},
"exclude": ["node_modules"],
"files": ["./hardhat.config.ts"],
"include": ["src/**/*", "tasks/**/*", "test/**/*", "deploy/**/*", "types/"]
}