init repository

This commit is contained in:
moebius
2024-12-16 16:36:48 -03:00
parent 4e6a7d2d3f
commit 689f8d1971
68 changed files with 8236 additions and 1 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# General
yarn-error.log
node_modules
.DS_STORE
.vscode
# Config files
.env
# Avoid ignoring gitkeep
!/**/.gitkeep

View File

@@ -1 +1 @@
# privacy-pool-core
# Privacy Pool Core

13
package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "privacy-pool-core",
"version": "1.0.0",
"description": "Core repository for the Privacy Pool protocol circuits and smart contracts",
"repository": "https://github.com/defi-wonderland/privacy-pool-core",
"author": "Defi Wonderland",
"license": "MIT",
"private": true,
"workspaces": [
"packages/circuits",
"packages/contracts"
]
}

View File

@@ -0,0 +1,38 @@
name: tests
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install --yes \
build-essential \
libgmp-dev \
libsodium-dev \
nasm \
nlohmann-json3-dev
- name: Download Circom Binary v2.1.5
run: |
wget -qO /home/runner/work/circom https://github.com/iden3/circom/releases/download/v2.1.5/circom-linux-amd64
chmod +x /home/runner/work/circom
sudo mv /home/runner/work/circom /bin/circom
- name: Print Circom version
run: circom --version
- name: Install dependencies
run: yarn
- name: Run tests
run: yarn test

122
packages/circuits/.gitignore vendored Normal file
View File

@@ -0,0 +1,122 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# 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
*.lcov
# 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
# builds
build
dist
# circuit-specific powers of tau are ignored
*.ptau
# universal ptaus not ignored
!ptau/*
# temporary ptaus are ignored
tmp.ptau
# is this still a thing lol
.DS_Store
# ignore auto generated test circuits
circuits/test
ptau

View File

@@ -0,0 +1,7 @@
{
"extension": ["ts"],
"require": "ts-node/register",
"spec": "tests/**/*.test.ts",
"timeout": 100000,
"exit": true
}

View File

@@ -0,0 +1,3 @@
module.exports = {
printWidth: 120,
};

138
packages/circuits/README.md Normal file
View File

@@ -0,0 +1,138 @@
# Circomkit Examples
In this repository, we are using [Circomkit](https://github.com/erhant/circomkit) to test some example circuits using Mocha. The circuits and the statements that they prove are as follows:
- **Multiplier**: "I know `n` factors that make up some number".
- **Fibonacci**: "I know the `n`'th Fibonacci number".
- **SHA256**: "I know the `n`-byte preimage of some SHA256 digest".
- **Sudoku**: "I know the solution to some `(n^2)x(n^2)` Sudoku puzzle".
- **Floating-Point Addition**: "I know two floating-point numbers that make up some number with `e` exponent and `m` mantissa bits." (adapted from [Berkeley ZKP MOOC 2023 - Lab](https://github.com/rdi-berkeley/zkp-mooc-lab)).
## CLI Usage
To use Circomkit CLI with a circuit, let's say for Sudoku 9x9, we follow the steps below:
1. We write a circuit config in `circuits.json` with the desired parameters. In this case, we are working with the 9x9 Sudoku solution circuit, and the board size is calculated by the square of our template parameter so we should give 3. Furthermore, `puzzle` is a public input so we should specify that too.
```json
{
"sudoku_9x9": {
"file": "sudoku",
"template": "Sudoku",
"pubs": ["puzzle"],
"params": [3]
}
}
```
2. Compile the circuit with Circomkit, providing the same circuit name as in `circuits.json`:
```sh
npx circomkit compile sudoku_9x9
# print circuit info if you want to
npx circomkit info sudoku_9x9
```
3. Commence circuit-specific setup. Normally, this requires us to download a Phase-1 PTAU file and provide it's path; however, Circomkit can determine the required PTAU and download it automatically when using `bn128` curve, thanks to [Perpetual Powers of Tau](https://github.com/privacy-scaling-explorations/perpetualpowersoftau). In this case, `sudoku_9x9` circuit has 4617 constraints, so Circomkit will download `powersOfTau28_hez_final_13.ptau` (see [here](https://github.com/iden3/snarkjs#7-prepare-phase-2)).
```sh
npx circomkit setup sudoku_9x9
# alternative: provide the PTAU yourself
npx circomkit setup sudoku_9x9 <path-to-ptau>
```
4. Prepare your input file under `./inputs/sudoku_9x9/default.json`.
```json
{
"solution": [
[1, 9, 4, 8, 6, 5, 2, 3, 7],
[7, 3, 5, 4, 1, 2, 9, 6, 8],
[8, 6, 2, 3, 9, 7, 1, 4, 5],
[9, 2, 1, 7, 4, 8, 3, 5, 6],
[6, 7, 8, 5, 3, 1, 4, 2, 9],
[4, 5, 3, 9, 2, 6, 8, 7, 1],
[3, 8, 9, 6, 5, 4, 7, 1, 2],
[2, 4, 6, 1, 7, 9, 5, 8, 3],
[5, 1, 7, 2, 8, 3, 6, 9, 4]
],
"puzzle": [
[0, 0, 0, 8, 6, 0, 2, 3, 0],
[7, 0, 5, 0, 0, 0, 9, 0, 8],
[0, 6, 0, 3, 0, 7, 0, 4, 0],
[0, 2, 0, 7, 0, 8, 0, 5, 0],
[0, 7, 8, 5, 0, 0, 0, 0, 0],
[4, 0, 0, 9, 0, 6, 0, 7, 0],
[3, 0, 9, 0, 5, 0, 7, 0, 2],
[0, 4, 0, 1, 0, 9, 0, 8, 0],
[5, 0, 7, 0, 8, 0, 0, 9, 4]
]
}
```
5. We are ready to create a proof!
```sh
npx circomkit prove sudoku_9x9 default
```
6. We can then verify our proof. You can try and modify the public input at `./build/sudoku_9x9/default/public.json` and see if the proof verifies or not!
```sh
npx circomkit verify sudoku_9x9 default
```
## In-Code Usage
If you would like to use Circomkit within the code itself, rather than the CLI, you can see the example at `src/index.ts`. You can `yarn start` to see it in action.
```ts
// create circomkit
const circomkit = new Circomkit({
protocol: "groth16",
});
// artifacts output at `build/multiplier_3` directory
await circomkit.compile("multiplier_3", {
file: "multiplier",
template: "Multiplier",
params: [3],
});
// proof & public signals at `build/multiplier_3/my_input` directory
await circomkit.prove("multiplier_3", "my_input", { in: [3, 5, 7] });
// verify with proof & public signals at `build/multiplier_3/my_input`
const ok = await circomkit.verify("multiplier_3", "my_input");
if (ok) {
circomkit.log("Proof verified!", "success");
} else {
circomkit.log("Verification failed.", "error");
}
```
## Configuration
Circomkit checks for `circomkit.json` to override it's default configurations. We could for example change the target version, prime field and the proof system by setting `circomkit.json` to be:
```json
{
"version": "2.1.2",
"protocol": "plonk",
"prime": "bls12381"
}
```
## Testing
You can use the following commands to test the circuits:
```sh
# test everything
yarn test
# test a specific circuit
yarn test -g <circuit-name>
```

View File

@@ -0,0 +1,5 @@
{
"version": "2.1.2",
"proofSystem": "plonk",
"curve": "bn128"
}

View File

@@ -0,0 +1,39 @@
{
"multiplier_3": {
"file": "multiplier",
"template": "Multiplier",
"params": [3]
},
"sha256_32": {
"file": "sha256",
"template": "Sha256Bytes",
"params": [32]
},
"sudoku_9x9": {
"file": "sudoku",
"template": "Sudoku",
"pubs": ["puzzle"],
"params": [3]
},
"sudoku_4x4": {
"file": "sudoku",
"template": "Sudoku",
"pubs": ["puzzle"],
"params": [2]
},
"fp64": {
"file": "float_add",
"template": "FloatAdd",
"params": [11, 52]
},
"fp32": {
"file": "float_add",
"template": "FloatAdd",
"params": [8, 23]
},
"fibonacci_11": {
"file": "fibonacci",
"template": "Fibonacci",
"params": [11]
}
}

View File

@@ -0,0 +1,33 @@
pragma circom 2.0.0;
// Fibonacci with custom starting numbers
template Fibonacci(n) {
assert(n >= 2);
signal input in[2];
signal output out;
signal fib[n+1];
fib[0] <== in[0];
fib[1] <== in[1];
for (var i = 2; i <= n; i++) {
fib[i] <== fib[i-2] + fib[i-1];
}
out <== fib[n];
}
// Fibonacci with custom starting numbers, recursive & inefficient
template FibonacciRecursive(n) {
signal input in[2];
signal output out;
component f1, f2;
if (n <= 1) {
out <== in[n];
} else {
f1 = FibonacciRecursive(n-1);
f1.in <== in;
f2 = FibonacciRecursive(n-2);
f2.in <== in;
out <== f1.out + f2.out;
}
}

View File

@@ -0,0 +1,417 @@
pragma circom 2.0.0;
// circuits adapted from https://github.com/rdi-berkeley/zkp-mooc-lab
include "circomlib/circuits/comparators.circom";
include "circomlib/circuits/switcher.circom";
include "circomlib/circuits/gates.circom";
include "circomlib/circuits/bitify.circom";
/*
* Finds Math.floor(log2(n))
*/
function log2(n) {
var tmp = 1, ans = 1;
while (tmp < n) {
ans++;
tmp *= 2;
}
return ans;
}
/*
* Basically `out = cond ? ifTrue : ifFalse`
*/
template IfElse() {
signal input cond;
signal input ifTrue;
signal input ifFalse;
signal output out;
// cond * T - cond * F + F
// 0 * T - 0 * F + F = 0 - 0 + F = F
// 1 * T - 1 * F + F = T - F + F = T
out <== cond * (ifTrue - ifFalse) + ifFalse;
}
/*
* Outputs `out` = 1 if `in` is at most `b` bits long, and 0 otherwise.
*/
template CheckBitLength(b) {
assert(b < 254);
signal input in;
signal output out;
// compute b-bit representation of the number
signal bits[b];
var sum_of_bits = 0;
for (var i = 0; i < b; i++) {
bits[i] <-- (in >> i) & 1;
bits[i] * (1 - bits[i]) === 0;
sum_of_bits += (2 ** i) * bits[i];
}
// check if sum is equal to number itself
component eq = IsEqual();
eq.in[0] <== sum_of_bits;
eq.in[1] <== in;
out <== eq.out;
}
/*
* Enforces the well-formedness of an exponent-mantissa pair (e, m), which is defined as follows:
* if `e` is zero, then `m` must be zero
* else, `e` must be at most `k` bits long, and `m` must be in the range [2^p, 2^p+1)
*/
template CheckWellFormedness(k, p) {
signal input e;
signal input m;
// check if `e` is zero
component is_e_zero = IsZero();
is_e_zero.in <== e;
// Case I: `e` is zero
//// `m` must be zero
component is_m_zero = IsZero();
is_m_zero.in <== m;
// Case II: `e` is nonzero
//// `e` is `k` bits
component check_e_bits = CheckBitLength(k);
check_e_bits.in <== e;
//// `m` is `p`+1 bits with the MSB equal to 1
//// equivalent to check `m` - 2^`p` is in `p` bits
component check_m_bits = CheckBitLength(p);
check_m_bits.in <== m - (1 << p);
// choose the right checks based on `is_e_zero`
component if_else = IfElse();
if_else.cond <== is_e_zero.out;
if_else.ifTrue <== is_m_zero.out;
//// check_m_bits.out * check_e_bits.out is equivalent to check_m_bits.out AND check_e_bits.out
if_else.ifFalse <== check_m_bits.out * check_e_bits.out;
// assert that those checks passed
if_else.out === 1;
}
/*
* Right-shifts `x` by `shift` bits to output `y`, where `shift` is a public circuit parameter.
*/
template RightShift(b, shift) {
assert(shift < b);
signal input x;
signal output y;
// convert number to bits
component x_bits = Num2Bits(b);
x_bits.in <== x;
// do the shifting
signal y_bits[b-shift];
for (var i = 0; i < b-shift; i++) {
y_bits[i] <== x_bits.out[shift+i];
}
// convert shifted bits to number
component y_num = Bits2Num(b-shift);
y_num.in <== y_bits;
y <== y_num.out;
}
/*
* Rounds the input floating-point number and checks to ensure that rounding does not make the mantissa unnormalized.
* Rounding is necessary to prevent the bitlength of the mantissa from growing with each successive operation.
* The input is a normalized floating-point number (e, m) with precision `P`, where `e` is a `k`-bit exponent and `m` is a `P`+1-bit mantissa.
* The output is a normalized floating-point number (e_out, m_out) representing the same value with a lower precision `p`.
*/
template RoundAndCheck(k, p, P) {
signal input e;
signal input m;
signal output e_out;
signal output m_out;
assert(P > p);
// check if no overflow occurs
component if_no_overflow = LessThan(P+1);
if_no_overflow.in[0] <== m;
if_no_overflow.in[1] <== (1 << (P+1)) - (1 << (P-p-1));
signal no_overflow <== if_no_overflow.out;
var round_amt = P-p;
// Case I: no overflow
// compute (m + 2^{round_amt-1}) >> round_amt
var m_prime = m + (1 << (round_amt-1));
component right_shift = RightShift(P+2, round_amt);
right_shift.x <== m_prime;
var m_out_1 = right_shift.y;
var e_out_1 = e;
// Case II: overflow
var e_out_2 = e + 1;
var m_out_2 = (1 << p);
// select right output based on no_overflow
component if_else[2];
for (var i = 0; i < 2; i++) {
if_else[i] = IfElse();
if_else[i].cond <== no_overflow;
}
if_else[0].ifTrue <== e_out_1;
if_else[0].ifFalse <== e_out_2;
if_else[1].ifTrue <== m_out_1;
if_else[1].ifFalse <== m_out_2;
e_out <== if_else[0].out;
m_out <== if_else[1].out;
}
template Num2BitsWithSkipChecks(b) {
signal input in;
signal input skip_checks;
signal output out[b];
for (var i = 0; i < b; i++) {
out[i] <-- (in >> i) & 1;
out[i] * (1 - out[i]) === 0;
}
var sum_of_bits = 0;
for (var i = 0; i < b; i++) {
sum_of_bits += (2 ** i) * out[i];
}
// is always true if skip_checks is 1
(sum_of_bits - in) * (1 - skip_checks) === 0;
}
template LessThanWithSkipChecks(n) {
assert(n <= 252);
signal input in[2];
signal input skip_checks;
signal output out;
component n2b = Num2BitsWithSkipChecks(n+1);
n2b.in <== in[0] + (1<<n) - in[1];
n2b.skip_checks <== skip_checks;
out <== 1-n2b.out[n];
}
/*
* Left-shifts `x` by `shift` bits to output `y`.
* Enforces 0 <= `shift` < `shift_bound`.
* If `skip_checks` = 1, then we don't care about the output
* and the `shift_bound` constraint is not enforced.
*/
template LeftShift(shift_bound) {
signal input x;
signal input shift;
signal input skip_checks;
signal output y;
// find number of bits in shift_bound
var n = log2(shift_bound) + 1;
// convert "shift" to bits
component shift_bits = Num2BitsWithSkipChecks(n);
shift_bits.in <== shift;
shift_bits.skip_checks <== skip_checks;
// check "shift" < "shift_bound"
component less_than = LessThanWithSkipChecks(n);
less_than.in[0] <== shift;
less_than.in[1] <== shift_bound;
less_than.skip_checks <== skip_checks;
(less_than.out - 1) * (1 - skip_checks) === 0;
// compute pow2_shift from bits
// represents the shift amount
var pow2_shift = 1;
component muxes[n];
for (var i = 0; i < n; i++) {
muxes[i] = IfElse();
muxes[i].cond <== shift_bits.out[i];
muxes[i].ifTrue <== pow2_shift * (2 ** (2 ** i));
muxes[i].ifFalse <== pow2_shift;
pow2_shift = muxes[i].out;
}
// if skip checks, set pow2_shift to 0
component if_else = IfElse();
if_else.cond <== skip_checks;
if_else.ifTrue <== 0;
if_else.ifFalse <== pow2_shift;
pow2_shift = if_else.out; // not <== because it's a variable
// do the shift
y <== x * pow2_shift;
}
/*
* Find the Most-Significant Non-Zero Bit (MSNZB) of `in`, where `in` is assumed to be non-zero value of `b` bits.
* Outputs the MSNZB as a one-hot vector `one_hot` of `b` bits, where `one_hot`[i] = 1 if MSNZB(`in`) = i and 0 otherwise.
* The MSNZB is output as a one-hot vector to reduce the number of constraints in the subsequent `Normalize` template.
* Enforces that `in` is non-zero as MSNZB(0) is undefined.
* If `skip_checks` = 1, then we don't care about the output and the non-zero constraint is not enforced.
*/
template MSNZB(b) {
signal input in;
signal input skip_checks;
signal output one_hot[b];
// compute ell, ensuring that it is made of bits too
for (var i = 0; i < b; i++) {
var temp;
if (((1 << i) <= in) && (in < (1 << (i + 1)))) {
temp = 1;
} else {
temp = 0;
}
one_hot[i] <-- temp;
}
// verify that one_hot only has bits, and has only one set bit
var sum_of_bits = 0;
for (var i = 0; i < b; i++) {
sum_of_bits += one_hot[i];
one_hot[i] * (1 - one_hot[i]) === 0; // is bit
}
(1 - sum_of_bits) * (1 - skip_checks) === 0;
// verify that the set bit is at correct place
var pow2_ell = 0;
var pow2_ell_plus1 = 0;
for (var i = 0; i < b; i++) {
pow2_ell += one_hot[i] * (1 << i);
pow2_ell_plus1 += one_hot[i] * (1 << (i + 1));
}
component lt1 = LessThan(b+1);
lt1.in[0] <== in;
lt1.in[1] <== pow2_ell_plus1;
(lt1.out - 1) * (1 - skip_checks) === 0;
component lt2 = LessThan(b);
lt2.in[0] <== pow2_ell - 1;
lt2.in[1] <== in;
(lt2.out - 1) * (1 - skip_checks) === 0;
}
/*
* Normalizes the input floating-point number.
* The input is a floating-point number with a `k`-bit exponent `e` and a `P`+1-bit *unnormalized* mantissa `m` with precision `p`, where `m` is assumed to be non-zero.
* The output is a floating-point number representing the same value with exponent `e_out` and a *normalized* mantissa `m_out` of `P`+1-bits and precision `P`.
* Enforces that `m` is non-zero as a zero-value can not be normalized.
* If `skip_checks` = 1, then we don't care about the output and the non-zero constraint is not enforced.
*/
template Normalize(k, p, P) {
signal input e;
signal input m;
signal input skip_checks;
signal output e_out;
signal output m_out;
assert(P > p);
// compute ell = MSNZB
component msnzb = MSNZB(P+1);
msnzb.in <== m;
msnzb.skip_checks <== skip_checks;
// compute ell and L = 2 ** (P - ell)
var ell, L;
for (var i = 0; i < P+1; i++) {
ell += msnzb.one_hot[i] * i;
L += msnzb.one_hot[i] * (1 << (P - i));
}
// return
e_out <== e + ell - p;
m_out <== m * L;
}
/*
* Adds two floating-point numbers.
* The inputs are normalized floating-point numbers with `k`-bit exponents `e` and `p`+1-bit mantissas `m` with scale `p`.
* Does not assume that the inputs are well-formed and makes appropriate checks for the same.
* The output is a normalized floating-point number with exponent `e_out` and mantissa `m_out` of `p`+1-bits and scale `p`.
* Enforces that inputs are well-formed.
*/
template FloatAdd(k, p) {
signal input e[2];
signal input m[2];
signal output e_out;
signal output m_out;
// check well formedness
component well_form[2];
for (var i = 0; i < 2; i++) {
well_form[i] = CheckWellFormedness(k, p);
well_form[i].e <== e[i];
well_form[i].m <== m[i];
}
// find the larger magnitude
var magn[2];
component larger_magn = LessThan(k+p+1);
for (var i = 0; i < 2; i++) {
magn[i] = (e[i] * (1 << (p+1))) + m[i];
larger_magn.in[i] <== magn[i];
}
signal is_input2_larger <== larger_magn.out;
// arrange by magnitude
var input_1[2] = [e[0], m[0]];
var input_2[2] = [e[1], m[1]];
component switcher[2];
for (var i = 0; i < 2; i++) {
switcher[i] = Switcher();
switcher[i].sel <== is_input2_larger;
switcher[i].L <== input_1[i];
switcher[i].R <== input_2[i];
}
var alpha_e = switcher[0].outL;
var alpha_m = switcher[1].outL;
var beta_e = switcher[0].outR;
var beta_m = switcher[1].outR;
// if-else part
var diff_e = alpha_e - beta_e;
component compare_diff_e = LessThan(k);
compare_diff_e.in[0] <== p+1;
compare_diff_e.in[1] <== diff_e;
component is_alpha_e_zero = IsZero();
is_alpha_e_zero.in <== alpha_e;
//// case 1
component or = OR();
or.a <== compare_diff_e.out;
or.b <== is_alpha_e_zero.out;
signal is_case_1 <== or.out; // true branch
var case_1_output[2] = [alpha_e, alpha_m];
//// case 2
component shl = LeftShift(p+2);
shl.x <== alpha_m;
shl.shift <== diff_e;
shl.skip_checks <== is_case_1; // skip if this isnt the case
alpha_m = shl.y;
var mantissa = alpha_m + beta_m;
var exponent = beta_e;
component normalize = Normalize(k, p, 2*p + 1);
normalize.m <== mantissa;
normalize.e <== exponent;
normalize.skip_checks <== is_case_1;
component rnc = RoundAndCheck(k, p, 2*p + 1);
rnc.m <== normalize.m_out;
rnc.e <== normalize.e_out;
var case_2_output[2] = [rnc.e_out, rnc.m_out];
// return
component if_else[2];
for (var i = 0; i < 2; i++) {
if_else[i] = IfElse();
if_else[i].cond <== is_case_1;
if_else[i].ifTrue <== case_1_output[i];
if_else[i].ifFalse <== case_2_output[i];
}
e_out <== if_else[0].out;
m_out <== if_else[1].out;
}

View File

@@ -0,0 +1,53 @@
pragma circom 2.0.0;
template MultiplicationGate() {
signal input in[2];
signal output out <== in[0] * in[1];
}
template Multiplier(N) {
assert(N > 1);
signal input in[N];
signal output out;
component gate[N-1];
// instantiate gates
for (var i = 0; i < N-1; i++) {
gate[i] = MultiplicationGate();
}
// multiply
gate[0].in <== [in[0], in[1]];
for (var i = 0; i < N-2; i++) {
gate[i+1].in <== [gate[i].out, in[i+2]];
}
out <== gate[N-2].out;
}
// Alternative way using anonymous components
template MultiplierAnonymous(N) {
assert(N > 1);
signal input in[N];
signal output out;
signal inner[N-1];
inner[0] <== MultiplicationGate()([in[0], in[1]]);
for(var i = 0; i < N-2; i++) {
inner[i+1] <== MultiplicationGate()([inner[i], in[i+2]]);
}
out <== inner[N-2];
}
// Alternative way without the gate component
template MultiplierSimple(N) {
assert(N > 1);
signal input in[N];
signal output out;
signal inner[N-1];
inner[0] <== in[0] * in[1];
for(var i = 2; i < N; i++) {
inner[i-1] <== inner[i-2] * in[i];
}
out <== inner[N-2];
}

View File

@@ -0,0 +1,42 @@
pragma circom 2.0.0;
include "circomlib/circuits/sha256/sha256.circom";
include "circomlib/circuits/bitify.circom";
/**
* Wrapper around SHA256 to support bytes as input instead of bits
* @param N The number of input bytes
* @input in The input bytes
* @output out The SHA256 output of the n input bytes, in bytes
*
* SOURCE: https://github.com/celer-network/zk-benchmark/blob/main/circom/circuits/sha256/sha256_bytes.circom
*/
template Sha256Bytes(N) {
signal input in[N];
signal output out[32];
// convert input bytes to bits
component byte_to_bits[N];
for (var i = 0; i < N; i++) {
byte_to_bits[i] = Num2Bits(8);
byte_to_bits[i].in <== in[i];
}
// sha256 over bits
component sha256 = Sha256(N*8);
for (var i = 0; i < N; i++) {
for (var j = 0; j < 8; j++) {
sha256.in[i*8+j] <== byte_to_bits[i].out[7-j];
}
}
// convert output bytes to bits
component bits_to_bytes[32];
for (var i = 0; i < 32; i++) {
bits_to_bytes[i] = Bits2Num(8);
for (var j = 0; j < 8; j++) {
bits_to_bytes[i].in[7-j] <== sha256.out[i*8+j];
}
out[i] <== bits_to_bytes[i].out;
}
}

View File

@@ -0,0 +1,136 @@
pragma circom 2.0.0;
include "circomlib/circuits/bitify.circom";
// Finds Math.floor(log2(n))
function log2(n) {
var tmp = 1, ans = 1;
while (tmp < n) {
ans++;
tmp *= 2;
}
return ans;
}
// Assert that two elements are not equal
template NonEqual() {
signal input in[2];
signal inv;
// we check if (in[0] - in[1] != 0)
// because 1/0 results in 0, so the constraint won't hold
inv <-- 1 / (in[1] - in[0]);
inv * (in[1] - in[0]) === 1;
}
// Assert that number is representable by b-bits
template AssertBitLength(b) {
assert(b < 254);
signal input in;
// compute b-bit representation of the number
signal bits[b];
var sum_of_bits = 0;
for (var i = 0; i < b; i++) {
bits[i] <-- (in >> i) & 1;
bits[i] * (1 - bits[i]) === 0;
sum_of_bits += (2 ** i) * bits[i];
}
// check if sum is equal to number itself
in === sum_of_bits;
}
// Checks that `in` is in range [MIN, MAX]
template InRange(MIN, MAX) {
assert(MIN < MAX);
signal input in;
// number of bits to represent MAX
var b = log2(MAX) + 1;
component lowerBound = AssertBitLength(b);
component upperBound = AssertBitLength(b);
lowerBound.in <== in - MIN; // e.g. 1 - 1 = 0 (for 0 <= in)
upperBound.in <== in + (2 ** b) - MAX - 1; // e.g. 9 + (15 - 9) = 15 (for in <= 15)
}
// Assert that all given values are unique
template Distinct(n) {
signal input in[n];
component nonEqual[n][n]; // TODO; has extra comps here
for(var i = 0; i < n; i++){
for(var j = 0; j < i; j++){
nonEqual[i][j] = NonEqual();
nonEqual[i][j].in <== [in[i], in[j]];
}
}
}
template Sudoku(n_sqrt) {
var n = n_sqrt * n_sqrt;
signal input solution[n][n]; // solution is a 2D array of numbers
signal input puzzle[n][n]; // puzzle is the same, but a zero indicates a blank
// ensure that solution & puzzle agrees
for (var row_i = 0; row_i < n; row_i++) {
for (var col_i = 0; col_i < n; col_i++) {
// puzzle is either empty (0), or the same as solution
puzzle[row_i][col_i] * (puzzle[row_i][col_i] - solution[row_i][col_i]) === 0;
}
}
// ensure all values in the solution are in range
component inRange[n][n];
for (var row_i = 0; row_i < n; row_i++) {
for (var col_i = 0; col_i < n; col_i++) {
inRange[row_i][col_i] = InRange(1, n);
inRange[row_i][col_i].in <== solution[row_i][col_i];
}
}
// ensure all values in the solution are distinct
component distinctRows[n];
for (var row_i = 0; row_i < n; row_i++) {
for (var col_i = 0; col_i < n; col_i++) {
if (row_i == 0) {
distinctRows[col_i] = Distinct(n);
}
distinctRows[col_i].in[row_i] <== solution[row_i][col_i];
}
}
component distinctCols[n];
for (var col_i = 0; col_i < n; col_i++) {
for (var row_i = 0; row_i < n; row_i++) {
if (col_i == 0) {
distinctCols[row_i] = Distinct(n);
}
distinctCols[row_i].in[col_i] <== solution[row_i][col_i];
}
}
// ensure that all values in squares are distinct
component distinctSquares[n];
var s_i = 0;
for (var sr_i = 0; sr_i < n_sqrt; sr_i++) {
for (var sc_i = 0; sc_i < n_sqrt; sc_i++) {
// square index
distinctSquares[s_i] = Distinct(n);
// (r, c) now marks the start of this square
var r = sr_i * n_sqrt;
var c = sc_i * n_sqrt;
var i = 0;
for (var row_i = r; row_i < r + n_sqrt; row_i++) {
for (var col_i = c; col_i < c + n_sqrt; col_i++) {
distinctSquares[s_i].in[i] <== solution[row_i][col_i];
i++;
}
}
s_i++;
}
}
}

View File

@@ -0,0 +1,3 @@
{
"in": [1, 1]
}

View File

@@ -0,0 +1,4 @@
{
"e": ["1122", "1024"],
"m": ["7807742059002284", "7045130465601185"]
}

View File

@@ -0,0 +1,3 @@
{
"in": [2, 4, 10]
}

View File

@@ -0,0 +1,6 @@
{
"in": [
116, 111, 100, 97, 121, 32, 105, 115, 32, 97, 32, 103, 111, 111, 100, 32, 100, 97, 121, 44, 32, 110, 111, 116, 32,
101, 118, 101, 114, 121, 100, 97, 121, 32, 105, 115
]
}

View File

@@ -0,0 +1,24 @@
{
"solution": [
[1, 9, 4, 8, 6, 5, 2, 3, 7],
[7, 3, 5, 4, 1, 2, 9, 6, 8],
[8, 6, 2, 3, 9, 7, 1, 4, 5],
[9, 2, 1, 7, 4, 8, 3, 5, 6],
[6, 7, 8, 5, 3, 1, 4, 2, 9],
[4, 5, 3, 9, 2, 6, 8, 7, 1],
[3, 8, 9, 6, 5, 4, 7, 1, 2],
[2, 4, 6, 1, 7, 9, 5, 8, 3],
[5, 1, 7, 2, 8, 3, 6, 9, 4]
],
"puzzle": [
[0, 0, 0, 8, 6, 0, 2, 3, 0],
[7, 0, 5, 0, 0, 0, 9, 0, 8],
[0, 6, 0, 3, 0, 7, 0, 4, 0],
[0, 2, 0, 7, 0, 8, 0, 5, 0],
[0, 7, 8, 5, 0, 0, 0, 0, 0],
[4, 0, 0, 9, 0, 6, 0, 7, 0],
[3, 0, 9, 0, 5, 0, 7, 0, 2],
[0, 4, 0, 1, 0, 9, 0, 8, 0],
[5, 0, 7, 0, 8, 0, 0, 9, 4]
]
}

View File

@@ -0,0 +1,18 @@
{
"description": "Circomkit examples",
"scripts": {
"start": "npx ts-node ./src/index.ts",
"test": "npx mocha"
},
"dependencies": {
"circomkit": "^0.0.22",
"circomlib": "^2.0.5"
},
"devDependencies": {
"@types/mocha": "^10.0.1",
"@types/node": "^20.3.0",
"mocha": "^10.2.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
}
}

View File

@@ -0,0 +1,33 @@
import { Circomkit } from "circomkit";
async function main() {
// create circomkit
const circomkit = new Circomkit({
protocol: "groth16",
});
// artifacts output at `build/multiplier_3` directory
await circomkit.compile("multiplier_3", {
file: "multiplier",
template: "Multiplier",
params: [3],
});
// proof & public signals at `build/multiplier_3/my_input` directory
await circomkit.prove("multiplier_3", "my_input", { in: [3, 5, 7] });
// verify with proof & public signals at `build/multiplier_3/my_input`
const ok = await circomkit.verify("multiplier_3", "my_input");
if (ok) {
circomkit.log("Proof verified!", "success");
} else {
circomkit.log("Verification failed.", "error");
}
}
main()
.then(() => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,5 @@
import { Circomkit } from "circomkit";
export const circomkit = new Circomkit({
verbose: false,
});

View File

@@ -0,0 +1,51 @@
import { WitnessTester } from "circomkit";
import { circomkit } from "./common";
describe("fibonacci", () => {
const N = 7;
let circuit: WitnessTester<["in"], ["out"]>;
describe("vanilla", () => {
before(async () => {
circuit = await circomkit.WitnessTester(`fibonacci_${N}`, {
file: "fibonacci",
template: "Fibonacci",
params: [N],
});
console.log("#constraints:", await circuit.getConstraintCount());
});
it("should compute correctly", async () => {
await circuit.expectPass({ in: [1, 1] }, { out: fibonacci([1, 1], N) });
});
});
describe("recursive", () => {
before(async () => {
circuit = await circomkit.WitnessTester(`fibonacci_${N}_recursive`, {
file: "fibonacci",
template: "FibonacciRecursive",
params: [N],
});
console.log("#constraints:", await circuit.getConstraintCount());
});
it("should compute correctly", async () => {
await circuit.expectPass({ in: [1, 1] }, { out: fibonacci([1, 1], N) });
});
});
});
// simple fibonacci with 2 variables
function fibonacci(init: [number, number], n: number): number {
if (n < 0) {
throw new Error("N must be positive");
}
let [a, b] = init;
for (let i = 2; i <= n; i++) {
b = a + b;
a = b - a;
}
return n === 0 ? a : b;
}

View File

@@ -0,0 +1,427 @@
import { WitnessTester } from "circomkit";
import { circomkit } from "./common";
const expectedConstraints = {
fp32: 401,
fp64: 819,
checkBitLength: (bits: number) => bits + 2,
leftShift: (shiftBound: number) => shiftBound + 2,
right: (bits: number) => bits + 2,
normalize: (P: number) => 3 * P + 1 + 1, // MSNZB + 1
msnzb: (bits: number) => 3 * bits + 1,
};
// tests adapted from https://github.com/rdi-berkeley/zkp-mooc-lab
describe("float_add 32-bit", () => {
let circuit: WitnessTester<["e", "m"], ["e_out", "m_out"]>;
const k = 8;
const p = 23;
before(async () => {
circuit = await circomkit.WitnessTester("fp32", {
file: "float_add",
template: "FloatAdd",
params: [k, p],
});
});
it("should have correct number of constraints", async () => {
await circuit.expectConstraintCount(expectedConstraints.fp32);
});
it("case I test", async () => {
await circuit.expectPass(
{
e: ["43", "5"],
m: ["11672136", "10566265"],
},
{ e_out: "43", m_out: "11672136" }
);
});
it("case II test 1", async () => {
await circuit.expectPass(
{
e: ["104", "106"],
m: ["12444445", "14159003"],
},
{ e_out: "107", m_out: "8635057" }
);
});
it("case II test 2", async () => {
await circuit.expectPass(
{
e: ["176", "152"],
m: ["16777215", "16777215"],
},
{ e_out: "177", m_out: "8388608" }
);
});
it("case II test 3", async () => {
await circuit.expectPass(
{
e: ["142", "142"],
m: ["13291872", "13291872"],
},
{ e_out: "143", m_out: "13291872" }
);
});
it("one input zero test", async () => {
await circuit.expectPass(
{
e: ["0", "43"],
m: ["0", "10566265"],
},
{ e_out: "43", m_out: "10566265" }
);
});
it("both inputs zero test", async () => {
await circuit.expectPass(
{
e: ["0", "0"],
m: ["0", "0"],
},
{ e_out: "0", m_out: "0" }
);
});
it("should fail - exponent zero but mantissa non-zero", async () => {
await circuit.expectFail({
e: ["0", "0"],
m: ["0", "10566265"],
});
});
it("should fail - mantissa ≥ 2^(p+1)", async () => {
await circuit.expectFail({
e: ["0", "43"],
m: ["0", "16777216"],
});
});
it("should fail - mantissa < 2^p", async () => {
await circuit.expectFail({
e: ["0", "43"],
m: ["0", "6777216"],
});
});
it("should fail - exponent ≥ 2^k", async () => {
await circuit.expectFail({
e: ["0", "256"],
m: ["0", "10566265"],
});
});
});
describe("float_add 64-bit", () => {
const k = 11;
const p = 52;
let circuit: WitnessTester<["e", "m"], ["e_out", "m_out"]>;
before(async () => {
circuit = await circomkit.WitnessTester("fp64", {
file: "float_add",
template: "FloatAdd",
params: [k, p],
});
});
it("should have correct number of constraints", async () => {
await circuit.expectConstraintCount(expectedConstraints.fp64);
});
it("case I test", async () => {
await circuit.expectPass(
{
e: ["1122", "1024"],
m: ["7807742059002284", "7045130465601185"],
},
{ e_out: "1122", m_out: "7807742059002284" }
);
});
it("case II test 1", async () => {
await circuit.expectPass(
{
e: ["1056", "1053"],
m: ["8879495032259305", "5030141535601637"],
},
{ e_out: "1057", m_out: "4754131362104755" }
);
});
it("case II test 2", async () => {
await circuit.expectPass(
{
e: ["1035", "982"],
m: ["4804509148660890", "8505192799372177"],
},
{ e_out: "1035", m_out: "4804509148660891" }
);
});
it("case II test 3", async () => {
await circuit.expectPass(
{
e: ["982", "982"],
m: ["8505192799372177", "8505192799372177"],
},
{ e_out: "983", m_out: "8505192799372177" }
);
});
it("one input zero test", async () => {
await circuit.expectPass(
{
e: ["0", "982"],
m: ["0", "8505192799372177"],
},
{ e_out: "982", m_out: "8505192799372177" }
);
});
it("both inputs zero test", async () => {
await circuit.expectPass(
{
e: ["0", "0"],
m: ["0", "0"],
},
{ e_out: "0", m_out: "0" }
);
});
it("should fail - exponent zero but mantissa non-zero", async () => {
await circuit.expectFail({
e: ["0", "0"],
m: ["0", "8505192799372177"],
});
});
it("should fail - mantissa < 2^p", async () => {
await circuit.expectFail({
e: ["0", "43"],
m: ["0", "16777216"],
});
});
});
describe("float_add utilities", () => {
describe("check bit length", () => {
const b = 23; // bit count
let circuit: WitnessTester<["in"], ["out"]>;
before(async () => {
circuit = await circomkit.WitnessTester(`cbl_${b}`, {
file: "float_add",
template: "CheckBitLength",
params: [b],
dir: "test/float_add",
});
});
it("should have correct number of constraints", async () => {
await circuit.expectConstraintCount(expectedConstraints.checkBitLength(b));
});
it("should give 1 for in ≤ b", async () => {
await circuit.expectPass({ in: "4903265" }, { out: "1" });
});
it("should give 0 for in > b", async () => {
await circuit.expectPass({ in: "13291873" }, { out: "0" });
});
});
describe("left shift", () => {
const shift_bound = 25;
let circuit: WitnessTester<["x", "shift", "skip_checks"], ["y"]>;
before(async () => {
circuit = await circomkit.WitnessTester(`shl_${shift_bound}`, {
file: "float_add",
template: "LeftShift",
dir: "test/float_add",
params: [shift_bound],
});
});
it("should have correct number of constraints", async () => {
await circuit.expectConstraintCount(expectedConstraints.leftShift(shift_bound));
});
it("should pass test 1 - don't skip checks", async () => {
await circuit.expectPass(
{
x: "65",
shift: "24",
skip_checks: "0",
},
{ y: "1090519040" }
);
});
it("should pass test 2 - don't skip checks", async () => {
await circuit.expectPass(
{
x: "65",
shift: "0",
skip_checks: "0",
},
{ y: "65" }
);
});
it("should fail - don't skip checks", async () => {
await circuit.expectFail({
x: "65",
shift: "25",
skip_checks: "0",
});
});
it("should pass when skip_checks = 1 and shift is ≥ shift_bound", async () => {
await circuit.expectPass({
x: "65",
shift: "25",
skip_checks: "1",
});
});
});
describe("right shift", () => {
const b = 49;
const shift = 24;
let circuit: WitnessTester<["x"], ["y"]>;
before(async () => {
circuit = await circomkit.WitnessTester(`shr_${b}`, {
file: "float_add",
template: "RightShift",
dir: "test/float_add",
params: [b, shift],
});
});
it("should have correct number of constraints", async () => {
await circuit.expectConstraintCount(b);
});
it("should pass - small bitwidth", async () => {
await circuit.expectPass({ x: "82263136010365" }, { y: "4903265" });
});
it("should fail - large bitwidth", async () => {
await circuit.expectFail({ x: "15087340228765024367" });
});
});
describe("normalize", () => {
const k = 8;
const p = 23;
const P = 47;
let circuit: WitnessTester<["e", "m", "skip_checks"], ["e_out", "m_out"]>;
before(async () => {
circuit = await circomkit.WitnessTester(`normalize_${k}_${p}_${P}`, {
file: "float_add",
template: "Normalize",
params: [k, p, P],
dir: "test/float_add",
});
});
it("should have correct number of constraints", async () => {
await circuit.expectConstraintCount(expectedConstraints.normalize(P));
});
it("should pass - don't skip checks", async () => {
await circuit.expectPass(
{
e: "100",
m: "20565784002591",
skip_checks: "0",
},
{ e_out: "121", m_out: "164526272020728" }
);
});
it("should pass - already normalized and don't skip checks", async () => {
await circuit.expectPass(
{
e: "100",
m: "164526272020728",
skip_checks: "0",
},
{ e_out: "124", m_out: "164526272020728" }
);
});
it("should fail when m = 0 - don't skip checks", async () => {
await circuit.expectFail({
e: "100",
m: "0",
skip_checks: "0",
});
});
it("should pass when skip_checks = 1 and m is 0", async () => {
await circuit.expectPass({
e: "100",
m: "0",
skip_checks: "1",
});
});
});
describe("msnzb", () => {
const b = 48;
let circuit: WitnessTester<["in", "skip_checks"], ["one_hot"]>;
before(async () => {
circuit = await circomkit.WitnessTester(`msnzb_${b}`, {
file: "float_add",
template: "MSNZB",
dir: "test/float_add",
params: [b],
});
});
it("should have correct number of constraints", async () => {
await circuit.expectConstraintCount(expectedConstraints.msnzb(b));
});
it("should pass test 1 - don't skip checks", async () => {
await circuit.expectPass(
{ in: "1", skip_checks: "0" },
{
// prettier-ignore
one_hot: ['1', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'],
}
);
});
it("should pass test 2 - don't skip checks", async () => {
await circuit.expectPass(
{ in: "281474976710655", skip_checks: "0" },
{
// prettier-ignore
one_hot: ['0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1'],
}
);
});
it("should fail when in = 0 - don't skip checks", async () => {
await circuit.expectFail({ in: "0", skip_checks: "0" });
});
it("should pass when skip_checks = 1 and in is 0", async () => {
await circuit.expectPass({ in: "0", skip_checks: "1" });
});
});
});

View File

@@ -0,0 +1,42 @@
import { WitnessTester } from "circomkit";
import { circomkit } from "./common";
describe("multiplier", () => {
const N = 3;
let circuit: WitnessTester<["in"], ["out"]>;
before(async () => {
circuit = await circomkit.WitnessTester(`multiplier_${N}`, {
file: "multiplier",
template: "Multiplier",
params: [N],
});
});
it("should have correct number of constraints", async () => {
await circuit.expectConstraintCount(N - 1);
});
it("should multiply correctly", async () => {
const randomNumbers = Array.from({ length: N }, () => Math.floor(Math.random() * 100 * N));
await circuit.expectPass({ in: randomNumbers }, { out: randomNumbers.reduce((prev, acc) => acc * prev) });
});
});
describe("multiplier utilities", () => {
describe("multiplication gate", () => {
let circuit: WitnessTester<["in"], ["out"]>;
before(async () => {
circuit = await circomkit.WitnessTester("mulgate", {
file: "multiplier",
template: "MultiplicationGate",
dir: "test/multiplier",
});
});
it("should multiply correctly", async () => {
await circuit.expectPass({ in: [7, 5] }, { out: 7 * 5 });
});
});
});

View File

@@ -0,0 +1,38 @@
import { WitnessTester } from "circomkit";
import { createHash } from "crypto";
import { circomkit } from "./common";
describe("sha256", () => {
let circuit: WitnessTester<["in"], ["out"]>;
// number of bytes for the sha256 input
const NUM_BYTES = 36;
// preimage and its byte array
const PREIMAGE = Buffer.from("today is a good day, not everyday is");
const PREIMAGE_BYTES = PREIMAGE.toJSON().data;
// digest and its byte array
const DIGEST = createHash("sha256").update(PREIMAGE).digest("hex");
const DIGEST_BYTES = Buffer.from(DIGEST, "hex").toJSON().data;
// circuit signals
const INPUT = {
in: PREIMAGE_BYTES,
};
const OUTPUT = {
out: DIGEST_BYTES,
};
before(async () => {
circuit = await circomkit.WitnessTester(`sha256_${NUM_BYTES}`, {
file: "sha256",
template: "Sha256Bytes",
params: [NUM_BYTES],
});
});
it("should compute hash correctly", async () => {
await circuit.expectPass(INPUT, OUTPUT);
});
});

View File

@@ -0,0 +1,200 @@
import { WitnessTester, CircuitSignals } from "circomkit";
import { circomkit } from "./common";
const BOARD_SIZES = [4, 9] as const;
const INPUTS: { [N in (typeof BOARD_SIZES)[number]]: CircuitSignals<["solution", "puzzle"]> } = {
9: {
solution: [
[1, 9, 4, 8, 6, 5, 2, 3, 7],
[7, 3, 5, 4, 1, 2, 9, 6, 8],
[8, 6, 2, 3, 9, 7, 1, 4, 5],
[9, 2, 1, 7, 4, 8, 3, 5, 6],
[6, 7, 8, 5, 3, 1, 4, 2, 9],
[4, 5, 3, 9, 2, 6, 8, 7, 1],
[3, 8, 9, 6, 5, 4, 7, 1, 2],
[2, 4, 6, 1, 7, 9, 5, 8, 3],
[5, 1, 7, 2, 8, 3, 6, 9, 4],
],
puzzle: [
[0, 0, 0, 8, 6, 0, 2, 3, 0],
[7, 0, 5, 0, 0, 0, 9, 0, 8],
[0, 6, 0, 3, 0, 7, 0, 4, 0],
[0, 2, 0, 7, 0, 8, 0, 5, 0],
[0, 7, 8, 5, 0, 0, 0, 0, 0],
[4, 0, 0, 9, 0, 6, 0, 7, 0],
[3, 0, 9, 0, 5, 0, 7, 0, 2],
[0, 4, 0, 1, 0, 9, 0, 8, 0],
[5, 0, 7, 0, 8, 0, 0, 9, 4],
],
},
4: {
solution: [
[4, 1, 3, 2],
[3, 2, 4, 1],
[2, 4, 1, 3],
[1, 3, 2, 4],
],
puzzle: [
[0, 1, 0, 2],
[3, 2, 0, 0],
[0, 0, 1, 0],
[1, 0, 0, 0],
],
},
};
BOARD_SIZES.map((N) =>
describe(`sudoku (${N} by ${N})`, () => {
const INPUT = INPUTS[N];
let circuit: WitnessTester<["solution", "puzzle"]>;
before(async () => {
circuit = await circomkit.WitnessTester(`sudoku_${N}x${N}`, {
file: "sudoku",
template: "Sudoku",
pubs: ["puzzle"],
params: [Math.sqrt(N)],
});
});
it("should compute correctly", async () => {
await circuit.expectPass(INPUT);
});
it("should NOT accept non-distinct rows", async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = badInput.solution[0][1];
await circuit.expectFail(badInput);
});
it("should NOT accept non-distinct columns", async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = badInput.solution[1][0];
await circuit.expectFail(badInput);
});
it("should NOT accept non-distinct square", async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = badInput.solution[1][1];
await circuit.expectFail(badInput);
});
it("should NOT accept empty value in solution", async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = 0;
await circuit.expectFail(badInput);
});
it("should NOT accept out-of-range values", async () => {
const badInput = JSON.parse(JSON.stringify(INPUT));
badInput.solution[0][0] = 99999;
await circuit.expectFail(badInput);
});
})
);
describe("sudoku utilities", () => {
describe("assert bit length", () => {
const b = 3; // bit count
let circuit: WitnessTester<["in"], []>;
before(async () => {
circuit = await circomkit.WitnessTester(`bitlen_${b}`, {
file: "sudoku",
template: "AssertBitLength",
dir: "test/sudoku",
params: [b],
});
});
it("should pass for input < 2^b", async () => {
await circuit.expectPass({
in: 2 ** b - 1,
});
});
it("should fail for input ≥ 2^b ", async () => {
await circuit.expectFail({
in: 2 ** b,
});
await circuit.expectFail({
in: 2 ** b + 1,
});
});
});
describe("distinct", () => {
const n = 3;
let circuit: WitnessTester<["in"], []>;
before(async () => {
circuit = await circomkit.WitnessTester(`distinct_${n}`, {
file: "sudoku",
template: "Distinct",
dir: "test/sudoku",
params: [n],
});
});
it("should pass if all inputs are unique", async () => {
await circuit.expectPass({
in: Array(n)
.fill(0)
.map((v, i) => v + i),
});
});
it("should fail if there is a duplicate", async () => {
const arr = Array(n)
.fill(0)
.map((v, i) => v + i);
// make a duplicate
arr[0] = arr[arr.length - 1];
await circuit.expectFail({
in: arr,
});
});
});
describe("in range", () => {
const MIN = 1;
const MAX = 9;
let circuit: WitnessTester<["in"]>;
before(async () => {
circuit = await circomkit.WitnessTester(`inRange_${MIN}_${MAX}`, {
file: "sudoku",
template: "InRange",
dir: "test/sudoku",
params: [MIN, MAX],
});
});
it("should pass for in range", async () => {
await circuit.expectPass({
in: MAX,
});
await circuit.expectPass({
in: MIN,
});
await circuit.expectPass({
in: Math.floor((MIN + MAX) / 2),
});
});
it("should FAIL for out of range (upper bound)", async () => {
await circuit.expectFail({
in: MAX + 1,
});
});
it("should FAIL for out of range (lower bound)", async () => {
if (MIN > 0) {
await circuit.expectFail({
in: MIN - 1,
});
}
});
});
});

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"types": ["mocha", "node"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true
}
}

1123
packages/circuits/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
MAINNET_RPC=
MAINNET_DEPLOYER_NAME=
SEPOLIA_RPC=
SEPOLIA_DEPLOYER_NAME=
ETHERSCAN_API_KEY=

1
packages/contracts/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.sol linguist-language=Solidity

View File

@@ -0,0 +1,59 @@
name: Canary Release
on: workflow_dispatch
jobs:
export:
name: Generate Interfaces And Contracts
# 1) Remove the following line if you wish to export your Solidity contracts and interfaces and publish them to NPM
if: false
runs-on: ubuntu-latest
strategy:
matrix:
export_type: ['interfaces', 'all']
env:
# 2) Fill the project name to be used in NPM
NPM_PACKAGE_NAME: 'my-cool-project'
EXPORT_NAME: ${{ matrix.export_type == 'interfaces' && '-interfaces' || '' }}
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Install Node
uses: actions/setup-node@v4
with:
registry-url: 'https://registry.npmjs.org'
node-version: 20.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Build project and generate out directory
run: yarn build
- name: Update version
run: yarn version --new-version "0.0.0-${GITHUB_SHA::8}" --no-git-tag-version
- name: Export Solidity - Export Type ${{ matrix.export_type }}
uses: defi-wonderland/solidity-exporter-action@v2.1.0
with:
package_name: ${{ env.NPM_PACKAGE_NAME }}
out: 'out'
interfaces: 'solidity/interfaces'
contracts: 'solidity/contracts'
libraries: "solidity/libraries"
export_type: '${{ matrix.export_type }}'
- name: Publish to NPM - Export Type ${{ matrix.export_type }}
run: cd export/${{ env.NPM_PACKAGE_NAME }}${{ env.EXPORT_NAME }} && npm publish --access public --tag canary
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -0,0 +1,73 @@
name: Coverage Check
on: [push]
env:
COVERAGE_SENSITIVITY_PERCENT: 1
jobs:
upload-coverage:
name: Upload Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile --network-concurrency 1
- name: Run coverage
shell: bash
run: yarn coverage
- name: Setup LCOV
uses: hrishikesh-kadam/setup-lcov@v1
- name: Filter directories
run: lcov --remove lcov.info 'test/*' 'script/*' --output-file lcovNew.info --rc lcov_branch_coverage=1 --rc derive_function_end_line=0 --ignore-errors unused
- name: Capture coverage output
id: new-coverage
uses: zgosalvez/github-actions-report-lcov@v4
with:
coverage-files: lcovNew.info
- name: Retrieve previous coverage
uses: actions/download-artifact@v4
with:
name: coverage.info
continue-on-error: true
- name: Check if a previous coverage exists
run: |
if [ ! -f coverage.info ]; then
echo "Artifact not found. Initializing at 0"
echo "0" >> coverage.info
fi
- name: Compare previous coverage
run: |
old=$(cat coverage.info)
new=$(( ${{ steps.new-coverage.outputs.total-coverage }} + ${{ env.COVERAGE_SENSITIVITY_PERCENT }} ))
if [ "$new" -lt "$old" ]; then
echo "Coverage decreased from $old to $new"
exit 1
fi
mv lcovNew.info coverage.info
- name: Upload the new coverage
uses: actions/upload-artifact@v4
with:
name: coverage.info
path: ./coverage.info

View File

@@ -0,0 +1,58 @@
name: Production Release
on:
release:
types: [published]
jobs:
release:
name: Release
# 1) Remove the following line if you wish to export your Solidity contracts and interfaces and publish them to NPM
if: false
runs-on: ubuntu-latest
strategy:
matrix:
export_type: ['interfaces', 'all']
env:
# 2) Fill the project name to be used in NPM
NPM_PACKAGE_NAME: 'my-cool-project'
EXPORT_NAME: ${{ matrix.export_type == 'interfaces' && '-interfaces' || '' }}
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Install Node
uses: actions/setup-node@v4
with:
registry-url: 'https://registry.npmjs.org'
node-version: 20.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Build project and generate out directory
run: yarn build
- name: Export Solidity - Export Type ${{ matrix.export_type }}
uses: defi-wonderland/solidity-exporter-action@v2.1.0
with:
package_name: ${{ env.NPM_PACKAGE_NAME }}
out: 'out'
interfaces: 'solidity/interfaces'
contracts: 'solidity/contracts'
libraries: "solidity/libraries"
export_type: '${{ matrix.export_type }}'
- name: Publish to NPM - Export Type ${{ matrix.export_type }}
run: cd export/${{ env.NPM_PACKAGE_NAME }}${{ env.EXPORT_NAME }} && npm publish --access public --tag latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -0,0 +1,118 @@
name: CI
on: [push]
concurrency:
group: ${{github.workflow}}-${{github.ref}}
cancel-in-progress: true
env:
MAINNET_RPC: ${{ secrets.MAINNET_RPC }}
SEPOLIA_RPC: ${{ secrets.SEPOLIA_RPC }}
jobs:
unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile --network-concurrency 1
- name: Precompile
run: yarn build
- name: Run tests
shell: bash
run: yarn test:unit
integration-tests:
name: Run Integration Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile --network-concurrency 1
- name: Precompile
run: yarn build
- name: Run tests
run: yarn test:integration
halmos-tests:
name: Run symbolic execution tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile --network-concurrency 1
- name: Precompile
run: yarn build
- name: Run tests
run: yarn test:integration
lint:
name: Lint Commit Messages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v5
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile --network-concurrency 1
- run: yarn lint:check

25
packages/contracts/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# General
yarn-error.log
node_modules
.DS_STORE
.vscode
# Foundry files
cache
out-via-ir
# Config files
.env
# Avoid ignoring gitkeep
!/**/.gitkeep
# Keep the latest deployment only
broadcast/*/*/*
# Out dir
out
crytic-export
# Echidna corpus
test/invariants/fuzz/echidna_coverage

1
packages/contracts/.husky/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_

View File

@@ -0,0 +1 @@
npx --no-install commitlint --edit $1

View File

@@ -0,0 +1,4 @@
# 1. Build the contracts
# 2. Stage build output
# 3. Lint and stage style improvements
yarn build && npx lint-staged

View File

@@ -0,0 +1,14 @@
{
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["warn"],
"quotes": "off",
"func-visibility": ["warn", { "ignoreConstructors": true }],
"no-inline-assembly": "off",
"no-empty-blocks": "off",
"private-vars-leading-underscore": ["warn", { "strict": false }],
"ordering": "warn",
"avoid-low-level-calls": "off",
"named-parameters-mapping": "warn"
}
}

View File

@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright © 2023 Wonderland
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,174 @@
<img src="https://raw.githubusercontent.com/defi-wonderland/brand/v1.0.0/external/solidity-foundry-boilerplate-banner.png" alt="wonderland banner" align="center" />
<br />
<div align="center"><strong>Start your next Solidity project with Foundry in seconds</strong></div>
<div align="center">A highly scalable foundation focused on DX and best practices</div>
<br />
## Features
<dl>
<dt>Sample contracts</dt>
<dd>Basic Greeter contract with an external interface.</dd>
<dt>Foundry setup</dt>
<dd>Foundry configuration with multiple custom profiles and remappings.</dd>
<dt>Deployment scripts</dt>
<dd>Sample scripts to deploy contracts on both mainnet and testnet.</dd>
<dt>Sample Integration, Unit, Property-based fuzzed and symbolic tests</dt>
<dd>Example tests showcasing mocking, assertions and configuration for mainnet forking. As well it includes everything needed in order to check code coverage.</dd>
<dd>Unit tests are built based on the <a href="https://twitter.com/PaulRBerg/status/1682346315806539776">Branched-Tree Technique</a>, using <a href="https://github.com/alexfertel/bulloak">Bulloak</a>.
<dd>Formal verification and property-based fuzzing are achieved with <a href="https://github.com/a16z/halmos">Halmos</a> and <a href="https://github.com/crytic/medusa">Medusa</a> (resp.).
<dt>Linter</dt>
<dd>Simple and fast solidity linting thanks to forge fmt.</dd>
<dd>Find missing natspec automatically.</dd>
<dt>Github workflows CI</dt>
<dd>Run all tests and see the coverage as you push your changes.</dd>
<dd>Export your Solidity interfaces and contracts as packages, and publish them to NPM.</dd>
</dl>
## Setup
1. Install Foundry by following the instructions from [their repository](https://github.com/foundry-rs/foundry#installation).
2. Copy the `.env.example` file to `.env` and fill in the variables.
3. Install the dependencies by running: `yarn install`. In case there is an error with the commands, run `foundryup` and try them again.
## Build
The default way to build the code is suboptimal but fast, you can run it via:
```bash
yarn build
```
In order to build a more optimized code ([via IR](https://docs.soliditylang.org/en/v0.8.15/ir-breaking-changes.html#solidity-ir-based-codegen-changes)), run:
```bash
yarn build:optimized
```
## Running tests
Unit tests should be isolated from any externalities, while Integration usually run in a fork of the blockchain. In this boilerplate you will find example of both.
In order to run both unit and integration tests, run:
```bash
yarn test
```
In order to just run unit tests, run:
```bash
yarn test:unit
```
In order to run unit tests and run way more fuzzing than usual (5x), run:
```bash
yarn test:unit:deep
```
In order to just run integration tests, run:
```bash
yarn test:integration
```
In order to start the Medusa fuzzing campaign (requires [Medusa](https://github.com/crytic/medusa/blob/master/docs/src/getting_started/installation.md) installed), run:
```bash
yarn test:fuzz
```
In order to just run the symbolic execution tests (requires [Halmos](https://github.com/a16z/halmos/blob/main/README.md#installation) installed), run:
```bash
yarn test:symbolic
```
In order to check your current code coverage, run:
```bash
yarn coverage
```
<br>
## Deploy & verify
### Setup
Configure the `.env` variables and source them:
```bash
source .env
```
Import your private keys into Foundry's encrypted keystore:
```bash
cast wallet import $MAINNET_DEPLOYER_NAME --interactive
```
```bash
cast wallet import $SEPOLIA_DEPLOYER_NAME --interactive
```
### Sepolia
```bash
yarn deploy:sepolia
```
### Mainnet
```bash
yarn deploy:mainnet
```
The deployments are stored in ./broadcast
See the [Foundry Book for available options](https://book.getfoundry.sh/reference/forge/forge-create.html).
## Export And Publish
Export TypeScript interfaces from Solidity contracts and interfaces providing compatibility with TypeChain. Publish the exported packages to NPM.
To enable this feature, make sure you've set the `NPM_TOKEN` on your org's secrets. Then set the job's conditional to `true`:
```yaml
jobs:
export:
name: Generate Interfaces And Contracts
# Remove the following line if you wish to export your Solidity contracts and interfaces and publish them to NPM
if: true
...
```
Also, remember to update the `package_name` param to your package name:
```yaml
- name: Export Solidity - ${{ matrix.export_type }}
uses: defi-wonderland/solidity-exporter-action@1dbf5371c260add4a354e7a8d3467e5d3b9580b8
with:
# Update package_name with your package name
package_name: "my-cool-project"
...
- name: Publish to NPM - ${{ matrix.export_type }}
# Update `my-cool-project` with your package name
run: cd export/my-cool-project-${{ matrix.export_type }} && npm publish --access public
...
```
You can take a look at our [solidity-exporter-action](https://github.com/defi-wonderland/solidity-exporter-action) repository for more information and usage examples.
## Licensing
The primary license for the boilerplate is MIT, see [`LICENSE`](https://github.com/defi-wonderland/solidity-foundry-boilerplate/blob/main/LICENSE)

View File

@@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

View File

@@ -0,0 +1,36 @@
[fmt]
line_length = 120
tab_width = 2
bracket_spacing = false
int_types = 'long'
quote_style = 'single'
number_underscore = 'thousands'
multiline_func_header = 'params_first_multi'
sort_imports = true
[profile.default]
solc_version = '0.8.23'
libs = ['../../node_modules', 'lib']
optimizer_runs = 10_000
[profile.optimized]
via_ir = true
out = 'out-via-ir'
[profile.test]
via_ir = true
out = 'out-via-ir'
[profile.docs]
src = 'src/interfaces/'
[fuzz]
runs = 1000
[rpc_endpoints]
mainnet = "${MAINNET_RPC}"
sepolia = "${SEPOLIA_RPC}"
[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
sepolia = { key = "${ETHERSCAN_API_KEY}" }

View File

@@ -0,0 +1,89 @@
{
"fuzzing": {
"workers": 10,
"workerResetLimit": 50,
"timeout": 0,
"testLimit": 0,
"shrinkLimit": 5000,
"callSequenceLength": 100,
"corpusDirectory": "",
"coverageEnabled": true,
"coverageFormats": [
"html",
"lcov"
],
"targetContracts": ["FuzzTest"],
"predeployedContracts": {},
"targetContractsBalances": [],
"constructorArgs": {},
"deployerAddress": "0x30000",
"senderAddresses": [
"0x10000",
"0x20000",
"0x30000"
],
"blockNumberDelayMax": 60480,
"blockTimestampDelayMax": 604800,
"blockGasLimit": 125000000,
"transactionGasLimit": 12500000,
"testing": {
"stopOnFailedTest": true,
"stopOnFailedContractMatching": false,
"stopOnNoTests": true,
"testAllContracts": false,
"traceAll": false,
"assertionTesting": {
"enabled": true,
"testViewMethods": true,
"panicCodeConfig": {
"failOnCompilerInsertedPanic": false,
"failOnAssertion": true,
"failOnArithmeticUnderflow": false,
"failOnDivideByZero": false,
"failOnEnumTypeConversionOutOfBounds": false,
"failOnIncorrectStorageAccess": false,
"failOnPopEmptyArray": false,
"failOnOutOfBoundsArrayAccess": false,
"failOnAllocateTooMuchMemory": false,
"failOnCallUninitializedVariable": false
}
},
"propertyTesting": {
"enabled": false,
"testPrefixes": [
"property_"
]
},
"optimizationTesting": {
"enabled": false,
"testPrefixes": [
"optimize_"
]
},
"targetFunctionSignatures": [],
"excludeFunctionSignatures": []
},
"chainConfig": {
"codeSizeCheckDisabled": true,
"cheatCodes": {
"cheatCodesEnabled": true,
"enableFFI": false
},
"skipAccountChecks": true
}
},
"compilation": {
"platform": "crytic-compile",
"platformConfig": {
"target": "test/invariants/fuzz/FuzzTest.t.sol",
"solcVersion": "",
"exportDirectory": "",
"args": []
}
},
"logging": {
"level": "info",
"logDirectory": "",
"noColor": false
}
}

View File

@@ -0,0 +1,9 @@
/**
* List of supported options: https://github.com/defi-wonderland/natspec-smells?tab=readme-ov-file#options
*/
/** @type {import('@defi-wonderland/natspec-smells').Config} */
module.exports = {
include: 'src/**/*.sol',
exclude: '(test|scripts)/**/*.sol',
};

View File

@@ -0,0 +1,46 @@
{
"name": "solidity-foundry-boilerplate",
"version": "1.0.0",
"description": "Production ready Solidity boilerplate with Foundry",
"homepage": "https://github.com/defi-wonderland/solidity-foundry-boilerplate#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/defi-wonderland/solidity-foundry-boilerplate.git"
},
"license": "MIT",
"author": "Wonderland",
"scripts": {
"build": "forge build",
"build:optimized": "FOUNDRY_PROFILE=optimized forge build",
"coverage": "forge coverage --report summary --report lcov --match-path 'test/unit/*'",
"deploy:mainnet": "bash -c 'source .env && forge script Deploy --rpc-url $MAINNET_RPC --account $MAINNET_DEPLOYER_NAME --broadcast --verify --chain mainnet -vvvvv'",
"deploy:sepolia": "bash -c 'source .env && forge script Deploy --rpc-url $SEPOLIA_RPC --account $SEPOLIA_DEPLOYER_NAME --broadcast --verify --chain sepolia -vvvvv'",
"lint:check": "yarn lint:sol && forge fmt --check",
"lint:fix": "sort-package-json && forge fmt && yarn lint:sol --fix",
"lint:natspec": "npx @defi-wonderland/natspec-smells --config natspec-smells.config.js",
"lint:sol": "solhint 'src/**/*.sol' 'script/**/*.sol' 'test/**/*.sol'",
"prepare": "husky",
"test": "forge test -vvv",
"test:fuzz": "medusa fuzz",
"test:integration": "forge test --match-contract Integration -vvv",
"test:symbolic": "halmos",
"test:unit": "forge test --match-contract Unit -vvv",
"test:unit:deep": "FOUNDRY_FUZZ_RUNS=5000 yarn test:unit"
},
"lint-staged": {
"*.{js,css,md,ts,sol}": "forge fmt",
"(src|test|script)/**/*.sol": "yarn lint:sol",
"package.json": "sort-package-json"
},
"devDependencies": {
"@commitlint/cli": "19.3.0",
"@commitlint/config-conventional": "19.2.2",
"@defi-wonderland/natspec-smells": "1.1.3",
"forge-std": "github:foundry-rs/forge-std#1.9.2",
"halmos-cheatcodes": "github:a16z/halmos-cheatcodes#c0d8655",
"husky": ">=9",
"lint-staged": ">=10",
"solhint-community": "4.0.0",
"sort-package-json": "2.10.0"
}
}

View File

@@ -0,0 +1,5 @@
forge-std/=../../node_modules/forge-std/src
halmos-cheatcodes=../../node_modules/halmos-cheatcodes
contracts/=src/contracts
interfaces/=src/interfaces

View File

@@ -0,0 +1,7 @@
{
"rules": {
"ordering": "off",
"one-contract-per-file": "off",
"no-console": "off"
}
}

View File

@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {Greeter} from 'contracts/Greeter.sol';
import {Script} from 'forge-std/Script.sol';
import {IERC20} from 'forge-std/interfaces/IERC20.sol';
contract Deploy is Script {
struct DeploymentParams {
string greeting;
IERC20 token;
}
/// @notice Deployment parameters for each chain
mapping(uint256 _chainId => DeploymentParams _params) internal _deploymentParams;
function setUp() public {
// Mainnet
_deploymentParams[1] = DeploymentParams('Hello, Mainnet!', IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));
// Sepolia
_deploymentParams[11_155_111] =
DeploymentParams('Hello, Sepolia!', IERC20(0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6));
}
function run() public {
DeploymentParams memory _params = _deploymentParams[block.chainid];
vm.startBroadcast();
new Greeter(_params.greeting, _params.token);
vm.stopBroadcast();
}
}

View File

@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {IERC20} from 'forge-std/interfaces/IERC20.sol';
import {IGreeter} from 'interfaces/IGreeter.sol';
contract Greeter is IGreeter {
/**
* @notice Empty string for revert checks
* @dev result of doing keccak256(bytes(''))
*/
bytes32 internal constant _EMPTY_STRING = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
/// @inheritdoc IGreeter
address public immutable OWNER;
/// @inheritdoc IGreeter
string public greeting;
/// @inheritdoc IGreeter
IERC20 public token;
/**
* @notice Reverts in case the function was not called by the owner of the contract
*/
modifier onlyOwner() {
if (msg.sender != OWNER) {
revert Greeter_OnlyOwner();
}
_;
}
/**
* @notice Defines the owner to the msg.sender and sets the initial greeting
* @param _greeting Initial greeting
* @param _token Initial token
*/
constructor(string memory _greeting, IERC20 _token) {
OWNER = msg.sender;
token = _token;
setGreeting(_greeting);
}
/// @inheritdoc IGreeter
function greet() external view returns (string memory _greeting, uint256 _balance) {
_greeting = greeting;
_balance = token.balanceOf(msg.sender);
}
/// @inheritdoc IGreeter
function setGreeting(string memory _greeting) public onlyOwner {
if (keccak256(bytes(_greeting)) == _EMPTY_STRING) {
revert Greeter_InvalidGreeting();
}
greeting = _greeting;
emit GreetingSet(_greeting);
}
}

View File

@@ -0,0 +1,11 @@
{
"rules": {
"ordering": "warn",
"style-guide-casing": [
"warn",
{
"ignoreExternalFunctions": true
}
]
}
}

View File

@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {IERC20} from 'forge-std/interfaces/IERC20.sol';
/**
* @title Greeter Contract
* @author Wonderland
* @notice This is a basic contract created in order to portray some
* best practices and foundry functionality.
*/
interface IGreeter {
/*///////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
/**
* @notice Greeting has changed
* @param _greeting The new greeting
*/
event GreetingSet(string _greeting);
/*///////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
/**
* @notice Throws if the function was called by someone else than the owner
*/
error Greeter_OnlyOwner();
/**
* @notice Throws if the greeting set is invalid
* @dev Empty string is an invalid greeting
*/
error Greeter_InvalidGreeting();
/*///////////////////////////////////////////////////////////////
VARIABLES
//////////////////////////////////////////////////////////////*/
/**
* @notice Returns the owner of the contract
* @dev The owner will always be the deployer of the contract
* @return _owner The owner of the contract
*/
function OWNER() external view returns (address _owner);
/**
* @notice Returns the previously set greeting
* @return _greet The greeting
*/
function greeting() external view returns (string memory _greet);
/**
* @notice Returns the token used to greet callers
* @return _token The address of the token
*/
function token() external view returns (IERC20 _token);
/*///////////////////////////////////////////////////////////////
LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Sets a new greeting
* @dev Only callable by the owner
* @param _newGreeting The new greeting to be set
*/
function setGreeting(string memory _newGreeting) external;
/**
* @notice Greets the caller
* @return _greeting The greeting
* @return _balance Current token balance of the caller
*/
function greet() external view returns (string memory _greeting, uint256 _balance);
}

View File

@@ -0,0 +1,16 @@
{
"rules": {
"style-guide-casing": [
"warn",
{
"ignorePublicFunctions":true,
"ignoreExternalFunctions":true,
"ignoreContracts":true
}
],
"no-global-import": "off",
"max-states-count": "off",
"ordering": "off",
"one-contract-per-file": "off"
}
}

View File

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {IntegrationBase} from 'test/integration/IntegrationBase.sol';
contract IntegrationGreeter is IntegrationBase {
function test_Greet() public {
uint256 _whaleBalance = _dai.balanceOf(_daiWhale);
vm.prank(_daiWhale);
(string memory _greeting, uint256 _balance) = _greeter.greet();
assertEq(_whaleBalance, _balance);
assertEq(_initialGreeting, _greeting);
}
}

View File

@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {Greeter, IGreeter} from 'contracts/Greeter.sol';
import {Test} from 'forge-std/Test.sol';
import {IERC20} from 'forge-std/interfaces/IERC20.sol';
contract IntegrationBase is Test {
uint256 internal constant _FORK_BLOCK = 18_920_905;
string internal _initialGreeting = 'hola';
address internal _user = makeAddr('user');
address internal _owner = makeAddr('owner');
address internal _daiWhale = 0x42f8CA49E88A8fd8F0bfA2C739e648468b8f9dec;
IERC20 internal _dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
IGreeter internal _greeter;
function setUp() public {
vm.createSelectFork(vm.rpcUrl('mainnet'), _FORK_BLOCK);
vm.prank(_owner);
_greeter = new Greeter(_initialGreeting, _dai);
}
}

View File

@@ -0,0 +1,4 @@
| Id | Properties | Type |
| --- | --------------------------------------------------- | ------------ |
| 1 | Greeting should never be empty | Valid state |
| 2 | Only the owner can set the greeting | State transition |

View File

@@ -0,0 +1,8 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;
import {GreeterGuidedHandlers} from './handlers/guided/Greeter.t.sol';
import {GreeterUnguidedHandlers} from './handlers/unguided/Greeter.t.sol';
import {GreeterProperties} from './properties/Greeter.t.sol';
contract FuzzTest is GreeterGuidedHandlers, GreeterUnguidedHandlers, GreeterProperties {}

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;
import {GreeterSetup} from '../../setup/Greeter.t.sol';
contract GreeterGuidedHandlers is GreeterSetup {
function handler_setGreeting(string memory _newGreeting) external {
// no need to prank since this contract deployed the greeter and is therefore its owner
try _targetContract.setGreeting(_newGreeting) {
assert(keccak256(bytes(_targetContract.greeting())) == keccak256(bytes(_newGreeting)));
} catch {
assert(keccak256(bytes(_newGreeting)) == keccak256(''));
}
}
}

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;
import {GreeterSetup} from '../../setup/Greeter.t.sol';
contract GreeterUnguidedHandlers is GreeterSetup {
/// @custom:property-id 2
/// @custom:property Only the owner can set the greeting
function handler_setGreeting(address _caller, string memory _newGreeting) external {
vm.prank(_caller);
try _targetContract.setGreeting(_newGreeting) {
assert(keccak256(bytes(_targetContract.greeting())) == keccak256(bytes(_newGreeting)));
assert(_caller == _targetContract.OWNER());
} catch {
assert(_caller != _targetContract.OWNER() || keccak256(bytes(_newGreeting)) == keccak256(''));
}
}
}

View File

@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;
import {GreeterSetup} from '../setup/Greeter.t.sol';
contract GreeterProperties is GreeterSetup {
/// @custom:property-id 1
/// @custom:property Greeting should never be empty
function property_greetingIsNeverEmpty() external view {
assert(keccak256(bytes(_targetContract.greeting())) != keccak256(''));
}
}

View File

@@ -0,0 +1,13 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;
import {Greeter, IERC20} from 'contracts/Greeter.sol';
import {CommonBase} from 'forge-std/Base.sol';
contract GreeterSetup is CommonBase {
Greeter internal _targetContract;
constructor() {
_targetContract = new Greeter('a', IERC20(address(1)));
}
}

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;
import {Greeter, IERC20} from 'contracts/Greeter.sol';
import {Test} from 'forge-std/Test.sol';
import {SymTest} from 'halmos-cheatcodes/src/SymTest.sol'; // See https://github.com/a16z/halmos-cheatcodes?tab=readme-ov-file
contract SymbolicGreeter is SymTest, Test {
Greeter public targetContract;
function setUp() public {
string memory _initialGreeting = svm.createString(64, 'initial greeting');
address _token = svm.createAddress('token');
targetContract = new Greeter(_initialGreeting, IERC20(_token));
}
function check_validState_greeterNeverEmpty(address _caller) public {
// Input conditions: any caller
vm.prank(_caller);
// Execution: Halmos cannot use a dynamic-sized array, iterate over multiple string lengths
bool _success;
for (uint256 i = 1; i < 3; i++) {
string memory greeting = svm.createString(i, 'greeting');
(_success,) = address(targetContract).call(abi.encodeCall(Greeter.setGreeting, (greeting)));
// Output condition check
vm.assume(_success); // discard failing calls
assert(keccak256(bytes(targetContract.greeting())) != keccak256(bytes('')));
}
// Add the empty string (bypass the non-empty check of svm.createString)
(_success,) = address(targetContract).call(abi.encodeCall(Greeter.setGreeting, ('')));
// Output condition check
vm.assume(_success); // discard failing calls
assert(keccak256(bytes(targetContract.greeting())) != keccak256(bytes('')));
}
function check_setGreeting_onlyOwnerSetsGreeting(address _caller) public {
// Input conditions
string memory _newGreeting = svm.createString(64, 'new greeting');
// Execution
vm.prank(_caller);
(bool _success,) = address(targetContract).call(abi.encodeCall(Greeter.setGreeting, (_newGreeting)));
// Output condition check
if (_success) {
assert(_caller == targetContract.OWNER());
assert(keccak256(bytes(targetContract.greeting())) == keccak256(bytes(_newGreeting)));
} else {
assert(_caller != targetContract.OWNER() || keccak256(bytes(_newGreeting)) == keccak256(bytes('')));
}
}
}

View File

@@ -0,0 +1,99 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.23;
import {Greeter, IGreeter} from 'contracts/Greeter.sol';
import {Test} from 'forge-std/Test.sol';
import {IERC20} from 'forge-std/interfaces/IERC20.sol';
contract UnitGreeter is Test {
address internal _owner = makeAddr('owner');
IERC20 internal _token = IERC20(makeAddr('token'));
uint256 internal _initialBalance = 100;
string internal _initialGreeting = 'hola';
Greeter internal _greeter;
event GreetingSet(string _greeting);
function setUp() external {
vm.prank(_owner);
_greeter = new Greeter(_initialGreeting, _token);
vm.etch(address(_token), new bytes(0x1));
}
function test_EmptyTestExample() external {
// it does nothing
vm.skip(true);
}
function test_ConstructorWhenPassingValidGreetingString() external {
vm.prank(_owner);
// it deploys
_greeter = new Greeter(_initialGreeting, _token);
// it sets the greeting string
assertEq(_greeter.greeting(), _initialGreeting);
// it sets the owner as sender
assertEq(_greeter.OWNER(), _owner);
// it sets the token used
assertEq(address(_greeter.token()), address(_token));
}
function test_ConstructorWhenPassingAnEmptyGreetingString() external {
vm.prank(_owner);
// it reverts
vm.expectRevert(IGreeter.Greeter_InvalidGreeting.selector);
_greeter = new Greeter('', _token);
}
function test_GreetWhenCalled() external {
vm.mockCall(address(_token), abi.encodeWithSelector(IERC20.balanceOf.selector), abi.encode(_initialBalance));
vm.expectCall(address(_token), abi.encodeWithSelector(IERC20.balanceOf.selector));
(string memory _greet, uint256 _balance) = _greeter.greet();
// it returns the greeting string
assertEq(_greet, _initialGreeting);
// it returns the token balance of the contract
assertEq(_balance, _initialBalance);
}
modifier whenCalledByTheOwner() {
vm.startPrank(_owner);
_;
vm.stopPrank();
}
function test_SetGreetingWhenPassingAValidGreetingString() external whenCalledByTheOwner {
string memory _newGreeting = 'hello';
// it emit GreetingSet
vm.expectEmit(true, true, true, true, address(_greeter));
emit GreetingSet(_newGreeting);
_greeter.setGreeting(_newGreeting);
// it sets the greeting string
assertEq(_greeter.greeting(), _newGreeting);
}
function test_SetGreetingWhenPassingAnEmptyGreetingString() external whenCalledByTheOwner {
// it reverts
vm.expectRevert(IGreeter.Greeter_InvalidGreeting.selector);
_greeter.setGreeting('');
}
function test_SetGreetingWhenCalledByANon_owner(address _caller) external {
vm.assume(_caller != _owner);
vm.prank(_caller);
// it reverts
vm.expectRevert(IGreeter.Greeter_OnlyOwner.selector);
_greeter.setGreeting('new greeting');
}
}

View File

@@ -0,0 +1,25 @@
Greeter::constructor
├── when passing valid greeting string
│ ├── it deploys
│ ├── it sets the greeting string
│ ├── it sets the owner as sender
│ └── it sets the token used
└── when passing an empty greeting string
└── it reverts
Greeter::greet
└── when called
├── it returns the greeting string
└── it returns the token balance of the contract
Greeter::setGreeting
├── when called by the owner
│ ├── when passing a valid greeting string
│ │ ├── it sets the greeting string
│ │ └── it emit GreetingSet
│ └── when passing an empty greeting string
│ └── it reverts
└── when called by a non-owner
└── it reverts

1991
packages/contracts/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

1980
yarn.lock Normal file

File diff suppressed because it is too large Load Diff