├── .commitlintrc.js
├── .editorconfig
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── ---bug.md
│ ├── ---feature.md
│ ├── ---package.md
│ └── ---refactoring.md
├── pull_request_template.md
├── sripts
│ └── slither-comment.js
└── workflows
│ ├── main.yml
│ └── release.yml
├── .gitignore
├── .husky
├── commit-msg
├── pre-commit
└── prepare-commit-msg
├── .lintstagedrc.json
├── .prettierignore
├── .prettierrc.json
├── .yarn
├── patches
│ └── changelogithub-npm-0.13.3-1783949906.patch
└── releases
│ └── yarn-4.2.1.cjs
├── .yarnrc.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── changelogithub.config.json
├── package.json
├── packages
├── excubiae
│ ├── .env.example
│ ├── .prettierrc.json
│ ├── .solcover.js
│ ├── .solhint.json
│ ├── LICENSE
│ ├── README.md
│ ├── contracts
│ │ ├── Excubia.sol
│ │ ├── IExcubia.sol
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── extensions
│ │ │ ├── EASExcubia.sol
│ │ │ ├── ERC721Excubia.sol
│ │ │ ├── FreeForAllExcubia.sol
│ │ │ ├── GitcoinPassportExcubia.sol
│ │ │ ├── HatsExcubia.sol
│ │ │ ├── SemaphoreExcubia.sol
│ │ │ ├── ZKEdDSAEventTicketPCDExcubia.sol
│ │ │ ├── interfaces
│ │ │ │ ├── IGitcoinPassportDecoder.sol
│ │ │ │ └── IHatsMinimal.sol
│ │ │ └── verifiers
│ │ │ │ └── ZKEdDSAEventTicketPCDVerifier.sol
│ │ ├── package.json
│ │ └── test
│ │ │ ├── MockEAS.sol
│ │ │ ├── MockERC721.sol
│ │ │ ├── MockGitcoinPassportDecoder.sol
│ │ │ ├── MockHats.sol
│ │ │ └── MockSemaphore.sol
│ ├── hardhat.config.ts
│ ├── package.json
│ ├── tasks
│ │ └── .gitkeep
│ ├── test
│ │ ├── EASExcubia.test.ts
│ │ ├── ERC721Excubia.test.ts
│ │ ├── FreeForAllExcubia.test.ts
│ │ ├── GitcoinPassportExcubia.test.ts
│ │ ├── HatsExcubia.test.ts
│ │ ├── SemaphoreExcubia.test.ts
│ │ └── ZKEdDSAEventTicketPCD.test.ts
│ └── tsconfig.json
├── imt
│ ├── .env.example
│ ├── .prettierrc.json
│ ├── .solcover.js
│ ├── .solhint.json
│ ├── LICENSE
│ ├── README.md
│ ├── contracts
│ │ ├── BinaryIMT.sol
│ │ ├── Constants.sol
│ │ ├── InternalBinaryIMT.sol
│ │ ├── InternalQuinaryIMT.sol
│ │ ├── LICENSE
│ │ ├── QuinaryIMT.sol
│ │ ├── README.md
│ │ ├── package.json
│ │ └── test
│ │ │ ├── BinaryIMTTest.sol
│ │ │ └── QuinaryIMTTest.sol
│ ├── hardhat.config.ts
│ ├── package.json
│ ├── tasks
│ │ └── deploy-imt-test.ts
│ ├── test
│ │ ├── BinaryIMT.ts
│ │ └── QuinaryIMT.ts
│ └── tsconfig.json
├── lazy-imt
│ ├── .env.example
│ ├── .prettierrc.json
│ ├── .solcover.js
│ ├── .solhint.json
│ ├── LICENSE
│ ├── README.md
│ ├── contracts
│ │ ├── Constants.sol
│ │ ├── InternalLazyIMT.sol
│ │ ├── LICENSE
│ │ ├── LazyIMT.sol
│ │ ├── README.md
│ │ ├── package.json
│ │ └── test
│ │ │ └── LazyIMTTest.sol
│ ├── hardhat.config.ts
│ ├── package.json
│ ├── scripts
│ │ └── defaultZeroes.mjs
│ ├── tasks
│ │ └── deploy-imt-test.ts
│ ├── test
│ │ └── LazyIMT.ts
│ └── tsconfig.json
├── lazytower
│ ├── .env.example
│ ├── .prettierrc.json
│ ├── .solcover.js
│ ├── .solhint.json
│ ├── .solhintignore
│ ├── LICENSE
│ ├── README.md
│ ├── contracts
│ │ ├── LICENSE
│ │ ├── LazyTowerHashChain.sol
│ │ ├── README.md
│ │ ├── package.json
│ │ └── test
│ │ │ └── LazyTowerHashChainTest.sol
│ ├── hardhat.config.ts
│ ├── package.json
│ ├── tasks
│ │ └── deploy-lazytower-test.ts
│ ├── test
│ │ ├── LazyTowerHashChainTest.ts
│ │ └── utils.ts
│ └── tsconfig.json
└── lean-imt
│ ├── .env.example
│ ├── .prettierrc.json
│ ├── .solcover.js
│ ├── .solhint.json
│ ├── LICENSE
│ ├── README.md
│ ├── contracts
│ ├── Constants.sol
│ ├── InternalLeanIMT.sol
│ ├── LICENSE
│ ├── LeanIMT.sol
│ ├── README.md
│ ├── package.json
│ └── test
│ │ └── LeanIMTTest.sol
│ ├── hardhat.config.ts
│ ├── package.json
│ ├── tasks
│ └── deploy-imt-test.ts
│ ├── test
│ └── LeanIMT.ts
│ └── tsconfig.json
├── scripts
├── check-slither.sh
└── remove-stable-version-field.ts
└── yarn.lock
/.commitlintrc.js:
--------------------------------------------------------------------------------
1 | const fs = require("node:fs")
2 | const path = require("node:path")
3 |
4 | const packages = fs.readdirSync(path.resolve(__dirname, "packages"))
5 |
6 | module.exports = {
7 | extends: ["@commitlint/config-conventional"],
8 | prompt: {
9 | scopes: [...packages],
10 | markBreakingChangeMode: true,
11 | allowCustomIssuePrefix: false,
12 | allowEmptyIssuePrefix: false,
13 | issuePrefixes: [
14 | {
15 | value: "re",
16 | name: "re: ISSUES related"
17 | }
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | #root = true
2 |
3 | [*]
4 | indent_style = space
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | max_line_length = 120
10 | indent_size = 4
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These owners will be the default owners for everything in the repo.
2 | # They will be added as reviewers for PR related to configuration files.
3 | * @vplasencia
4 |
5 | # When someone opens a pull request that only modifies specific packages,
6 | # only the following users and not the default owners defined above will be
7 | # requested for a review.
8 | /packages/imt/ @vplasencia
9 | /packages/lazy-imt/ @chancehudson
10 | /packages/lazytower/ @LCamel
11 | /packages/lean-imt/ @vplasencia
12 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41E Bug"
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: "bug \U0001F41B"
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | 1. Go to '...'
17 | 2. Click on '....'
18 | 3. Scroll down to '....'
19 | 4. See error
20 |
21 | **Expected behavior**
22 | A clear and concise description of what you expected to happen.
23 |
24 | **Screenshots**
25 | If applicable, add screenshots to help explain your problem.
26 |
27 | **Technologies (please complete the following information):**
28 |
29 | - Node.js version
30 | - NPM version
31 | - Solidity version
32 |
33 | **Additional context**
34 | Add any other context about the problem here.
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F680 Feature"
3 | about: Suggest an idea for ZK-Kit
4 | title: ''
5 | labels: 'feature :rocket:'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---package.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F4E6 Package"
3 | about: Propose a new ZK-Kit Solidity package
4 | title: ''
5 | labels: 'feature :rocket:'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the package you'd like**
11 | A clear and concise description of the type of package you have in mind.
12 |
13 | **Additional context**
14 | Add any other context about the package here.
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/---refactoring.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\u267B Refactoring"
3 | about: Suggest any improvements for this project
4 | title: ''
5 | labels: 'refactoring :recycle:'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the improvement you're thinking about**
11 | A clear and concise description of what you think could improve the code.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the improvement request here.
18 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ## Description
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## Related Issue(s)
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ## Other information
24 |
25 |
26 |
27 |
28 | ## Checklist
29 |
30 |
31 |
32 | - [ ] I have read and understand the [contributor guidelines](https://github.com/privacy-scaling-explorations/zk-kit.solidity/blob/main/CONTRIBUTING.md) and [code of conduct](https://github.com/privacy-scaling-explorations/zk-kit.solidity/blob/main/CODE_OF_CONDUCT.md).
33 | - [ ] I have performed a self-review of my code
34 | - [ ] I have commented my code, particularly in hard-to-understand areas
35 | - [ ] My changes generate no new warnings
36 | - [ ] I have run `yarn style` without getting any errors
37 | - [ ] I have added tests that prove my fix is effective or that my feature works
38 | - [ ] New and existing unit tests pass locally with my changes
39 |
40 | > [!IMPORTANT]
41 | > We do not accept minor grammatical fixes (e.g., correcting typos, rewording sentences) unless they significantly improve clarity in technical documentation. These contributions, while appreciated, are not a priority for merging. If there is a grammatical mistake, please feel free to message the team.
42 |
--------------------------------------------------------------------------------
/.github/sripts/slither-comment.js:
--------------------------------------------------------------------------------
1 | module.exports = async ({ github, context, header, body }) => {
2 | const comment = [header, body].join("\n")
3 |
4 | const { data: comments } = await github.rest.issues.listComments({
5 | owner: context.repo.owner,
6 | repo: context.repo.repo,
7 | issue_number: context.payload.number
8 | })
9 |
10 | const botComment = comments.find(
11 | (comment) =>
12 | // github-actions bot user
13 | comment.user.id === 41898282 && comment.body.startsWith(header)
14 | )
15 |
16 | const commentFn = botComment ? "updateComment" : "createComment"
17 |
18 | await github.rest.issues[commentFn]({
19 | owner: context.repo.owner,
20 | repo: context.repo.repo,
21 | body: comment,
22 | ...(botComment ? { comment_id: botComment.id } : { issue_number: context.payload.number })
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: main
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 |
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | deps:
14 | runs-on: ubuntu-latest
15 | outputs:
16 | cache-key: ${{ steps.cache-env.outputs.cache-key }}
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: 20
22 |
23 | - name: Output cache key
24 | id: cache-env
25 | run: echo "cache-key=${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}" >> $GITHUB_OUTPUT
26 |
27 | - uses: actions/cache@v4
28 | id: cache
29 | with:
30 | path: node_modules
31 | key: ${{ steps.cache-env.outputs.cache-key }}
32 | restore-keys: ${{ runner.os }}-node_modules-
33 |
34 | - if: steps.cache.outputs.cache-hit != 'true'
35 | run: yarn
36 |
37 | changed-files:
38 | runs-on: ubuntu-latest
39 | outputs:
40 | any_changed: ${{ steps.changed-files.outputs.any_changed }}
41 | modified_files: ${{ steps.changed-files.outputs.modified_files }}
42 | steps:
43 | - uses: actions/checkout@v4
44 | - name: Get changed files
45 | id: changed-files
46 | uses: tj-actions/changed-files@v44
47 | with:
48 | files: packages/**/*.{json,sol,ts}
49 |
50 | compile:
51 | if: needs.changed-files.outputs.any_changed == 'true'
52 | needs: [changed-files, deps]
53 | runs-on: ubuntu-latest
54 | steps:
55 | - uses: actions/checkout@v4
56 | - uses: actions/setup-node@v4
57 | with:
58 | node-version: 20
59 | - uses: actions/cache/restore@v4
60 | with:
61 | path: node_modules
62 | key: ${{ needs.deps.outputs.cache-key }}
63 |
64 | - run: yarn compile
65 |
66 | - name: Upload compilation results
67 | uses: actions/upload-artifact@v4
68 | with:
69 | name: all-artifacts
70 | path: packages/**/artifacts/**
71 |
72 | style:
73 | needs: deps
74 | runs-on: ubuntu-latest
75 | steps:
76 | - uses: actions/checkout@v4
77 | - uses: actions/setup-node@v4
78 | with:
79 | node-version: 20
80 | - uses: actions/cache/restore@v4
81 | with:
82 | path: node_modules
83 | key: ${{ needs.deps.outputs.cache-key }}
84 |
85 | - run: yarn format
86 |
87 | _tests:
88 | if: needs.changed-files.outputs.any_changed == 'true'
89 | needs: compile
90 | runs-on: ubuntu-latest
91 | strategy:
92 | matrix:
93 | dir: ${{ fromJson(needs.set-matrix.outputs.matrix) }}
94 |
95 | steps:
96 | - uses: actions/checkout@v4
97 | - uses: actions/setup-node@v4
98 | with:
99 | node-version: 20
100 | - uses: actions/cache/restore@v4
101 | with:
102 | path: node_modules
103 | key: ${{ needs.deps.outputs.cache-key }}
104 | - uses: actions/download-artifact@v4
105 | with:
106 | name: all-artifacts
107 | path: packages/
108 |
109 | - if: contains(needs.changed-files.outputs.modified_files, matrix.dir)
110 | name: Test
111 | run: |
112 | workspace=$(jq -r '.name' packages/${{ matrix.dir }}/package.json)
113 | yarn workspace "$workspace" run test:coverage
114 |
115 | - if: contains(needs.changed-files.outputs.modified_files, matrix.dir) && github.event_name == 'push' && github.ref == 'refs/heads/main'
116 | name: Coveralls
117 | uses: coverallsapp/github-action@v2
118 | with:
119 | github-token: ${{ secrets.GITHUB_TOKEN }}
120 | parallel: true
121 | flag-name: run ${{ join(matrix.*, '-') }}
122 |
123 | tests:
124 | needs: _tests
125 | # workaround for https://github.com/orgs/community/discussions/13690
126 | # https://stackoverflow.com/a/77066140/9771158
127 | if: ${{ !(failure() || cancelled()) }}
128 | runs-on: ubuntu-latest
129 | steps:
130 | - name: Tests OK (passed or skipped)
131 | run: true
132 |
133 | set-matrix:
134 | if: needs.changed-files.outputs.any_changed == 'true'
135 | needs: changed-files
136 | runs-on: ubuntu-latest
137 | outputs:
138 | matrix: ${{ steps.set-matrix.outputs.matrix }}
139 | steps:
140 | - uses: actions/checkout@v4
141 | - name: Set matrix
142 | id: set-matrix
143 | run: |
144 | matrix=$(ls -1 packages | jq -Rsc 'split("\n") | map(select(length > 0))')
145 | echo "matrix=$matrix" >> $GITHUB_OUTPUT
146 |
147 | _slither:
148 | if: needs.changed-files.outputs.any_changed == 'true'
149 | needs: [changed-files, set-matrix, deps]
150 | runs-on: ubuntu-latest
151 | permissions:
152 | contents: read
153 | security-events: write
154 | strategy:
155 | matrix:
156 | dir: ${{ fromJson(needs.set-matrix.outputs.matrix) }}
157 | steps:
158 | - uses: actions/checkout@v4
159 |
160 | # FIXME this does not work as a way to restore compilation results for slither job but it does for the compile job ??
161 | #- uses: actions/download-artifact@v4
162 | # with:
163 | # name: all-artifacts
164 | # path: packages/
165 |
166 | - uses: actions/setup-node@v4
167 | with:
168 | node-version: 20
169 | - uses: actions/cache/restore@v4
170 | with:
171 | path: node_modules
172 | key: ${{ needs.deps.outputs.cache-key }}
173 | - if: contains(needs.changed-files.outputs.modified_files, matrix.dir)
174 | name: Compile contracts
175 | run: |
176 | workspace=$(jq -r '.name' packages/${{ matrix.dir }}/package.json)
177 | yarn workspace "$workspace" run compile
178 |
179 | - if: contains(needs.changed-files.outputs.modified_files, matrix.dir)
180 | name: Run slither
181 | uses: crytic/slither-action@v0.4.0
182 | id: slither
183 | with:
184 | ignore-compile: true
185 | node-version: 20
186 | fail-on: none
187 | sarif: results.sarif
188 | slither-args: --filter-paths "test" --exclude-dependencies --markdown-root ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/
189 | target: packages/${{ matrix.dir }}
190 |
191 | - if: contains(needs.changed-files.outputs.modified_files, matrix.dir)
192 | name: Upload SARIF files
193 | uses: github/codeql-action/upload-sarif@v3
194 | with:
195 | sarif_file: ${{ steps.slither.outputs.sarif }}
196 |
197 | - name: Create/update checklist as PR comment
198 | uses: actions/github-script@v7
199 | if: github.even_name == 'pull_request'
200 | env:
201 | REPORT: ${{ steps.slither.stdout }}
202 | with:
203 | script: |
204 | const script = require('.github/scripts/slither-comment')
205 | const header = '# Slither report'
206 | const body = process.env.REPORT
207 | await script({ github, context, header, body })
208 |
209 | slither:
210 | needs: _slither
211 | if: ${{ !(failure() || cancelled()) }}
212 | runs-on: ubuntu-latest
213 | steps:
214 | - name: Slither analysis OK (passed or skipped)
215 | run: true
216 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | permissions:
4 | contents: write
5 |
6 | on:
7 | push:
8 | tags:
9 | - "*"
10 |
11 | jobs:
12 | release:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Install Node.js
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: 20
24 | cache: yarn
25 | registry-url: "https://registry.npmjs.org"
26 |
27 | - name: Authentication
28 | run: |
29 | echo npmAuthToken: "$NODE_AUTH_TOKEN" >> ./.yarnrc.yml
30 | env:
31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
32 |
33 | - name: Install dependencies
34 | run: yarn
35 |
36 | - name: Publish packages
37 | run: yarn version:publish
38 | env:
39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
40 |
41 | - run: yarn version:release
42 | env:
43 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # IDE
20 | .vscode
21 | .idea
22 |
23 | # Cargo
24 | target
25 |
26 | # Testing
27 | coverage
28 | coverage.json
29 | *.lcov
30 |
31 | # Dependency directories
32 | node_modules/
33 |
34 | # Parcel cache
35 | .parcel-cache
36 |
37 | # TypeScript cache
38 | *.tsbuildinfo
39 |
40 | # Output of 'npm pack'
41 | *.tgz
42 |
43 | # Microbundle cache
44 | .rpt2_cache/
45 | .rts2_cache_cjs/
46 | .rts2_cache_es/
47 | .rts2_cache_umd/
48 |
49 | # Yarn Integrity file
50 | .yarn-integrity
51 |
52 | # Generate output
53 | dist
54 | build
55 |
56 | # Hardhat
57 | artifacts
58 | cache
59 | typechain-types
60 |
61 | # dotenv environment variable files
62 | .env
63 | .env.development.local
64 | .env.test.local
65 | .env.production.local
66 | .env.local
67 |
68 | # Optional npm cache directory
69 | .npm
70 | .DS_Store
71 |
72 | # yarn v3
73 | .pnp.*
74 | .yarn/*
75 | !.yarn/patches
76 | !.yarn/plugins
77 | !.yarn/releases
78 | !.yarn/sdks
79 | !.yarn/versions
80 |
81 | .envrc
82 | .tool-versions
83 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.husky/prepare-commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | if [ "$NO_HOOK" != "1" ]; then
5 | exec < /dev/tty && npx czg --hook || true
6 | fi
7 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "**/*.{js,ts,md,json,sol,yml,yaml}": "yarn prettier --write"
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 | package-lock.json
4 | yarn.lock
5 | .yarn
6 |
7 | # testing
8 | coverage
9 | coverage.json
10 |
11 | # hardhat
12 | cache
13 | typechain-types
14 |
15 | # production
16 | dist
17 | build
18 |
19 | # github
20 | .github/ISSUE_TEMPLATE
21 |
22 | # misc
23 | .DS_Store
24 | *.pem
25 |
26 | # debug
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # others
32 | target
33 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "arrowParens": "always",
4 | "trailingComma": "none"
5 | }
6 |
--------------------------------------------------------------------------------
/.yarn/patches/changelogithub-npm-0.13.3-1783949906.patch:
--------------------------------------------------------------------------------
1 | diff --git a/dist/index.mjs b/dist/index.mjs
2 | index 8d005a0e1896ec10e6d09755b5859587882e3f93..580cb17bb6be135b45e8060155d0eb5c2614b02c 100644
3 | --- a/dist/index.mjs
4 | +++ b/dist/index.mjs
5 | @@ -192,7 +192,7 @@ function formatLine(commit, options) {
6 | function formatTitle(name, options) {
7 | if (!options.emoji)
8 | name = name.replace(emojisRE, "");
9 | - return `### ${name.trim()}`;
10 | + return `## ${name.trim()}`;
11 | }
12 | function formatSection(commits, sectionName, options) {
13 | if (!commits.length)
14 | @@ -209,7 +209,8 @@ function formatSection(commits, sectionName, options) {
15 | Object.keys(scopes).sort().forEach((scope) => {
16 | let padding = "";
17 | let prefix = "";
18 | - const scopeText = `**${options.scopeMap[scope] || scope}**`;
19 | + const url = `https://github.com/${options.repo}/tree/main/packages/${scope}`;
20 | + const scopeText = `[**@${options.repo.split("/")[1]}/${options.scopeMap[scope] || scope}**](${url})`;
21 | if (scope && (useScopeGroup === true || useScopeGroup === "multiple" && scopes[scope].length > 1)) {
22 | lines.push(`- ${scopeText}:`);
23 | padding = " ";
24 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | checksumBehavior: update
2 |
3 | compressionLevel: mixed
4 |
5 | enableGlobalCache: false
6 |
7 | nodeLinker: node-modules
8 |
9 | yarnPath: .yarn/releases/yarn-4.2.1.cjs
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement.
63 | All complaints will be reviewed and investigated promptly and fairly.
64 |
65 | All community leaders are obligated to respect the privacy and security of the
66 | reporter of any incident.
67 |
68 | ## Enforcement Guidelines
69 |
70 | Community leaders will follow these Community Impact Guidelines in determining
71 | the consequences for any action they deem in violation of this Code of Conduct:
72 |
73 | ### 1. Correction
74 |
75 | **Community Impact**: Use of inappropriate language or other behavior deemed
76 | unprofessional or unwelcome in the community.
77 |
78 | **Consequence**: A private, written warning from community leaders, providing
79 | clarity around the nature of the violation and an explanation of why the
80 | behavior was inappropriate. A public apology may be requested.
81 |
82 | ### 2. Warning
83 |
84 | **Community Impact**: A violation through a single incident or series
85 | of actions.
86 |
87 | **Consequence**: A warning with consequences for continued behavior. No
88 | interaction with the people involved, including unsolicited interaction with
89 | those enforcing the Code of Conduct, for a specified period of time. This
90 | includes avoiding interactions in community spaces as well as external channels
91 | like social media. Violating these terms may lead to a temporary or
92 | permanent ban.
93 |
94 | ### 3. Temporary Ban
95 |
96 | **Community Impact**: A serious violation of community standards, including
97 | sustained inappropriate behavior.
98 |
99 | **Consequence**: A temporary ban from any sort of interaction or public
100 | communication with the community for a specified period of time. No public or
101 | private interaction with the people involved, including unsolicited interaction
102 | with those enforcing the Code of Conduct, is allowed during this period.
103 | Violating these terms may lead to a permanent ban.
104 |
105 | ### 4. Permanent Ban
106 |
107 | **Community Impact**: Demonstrating a pattern of violation of community
108 | standards, including sustained inappropriate behavior, harassment of an
109 | individual, or aggression toward or disparagement of classes of individuals.
110 |
111 | **Consequence**: A permanent ban from any sort of public interaction within
112 | the community.
113 |
114 | ## Attribution
115 |
116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
117 | version 2.0, available at
118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
119 |
120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
121 | enforcement ladder](https://github.com/mozilla/diversity).
122 |
123 | [homepage]: https://www.contributor-covenant.org
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | https://www.contributor-covenant.org/faq. Translations are available at
127 | https://www.contributor-covenant.org/translations.
128 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | :tada: Thank you for being interested in contributing to the ZK-kit project! :tada:
4 |
5 | Feel welcome and read the following sections in order to know how to ask questions and how to work on something.
6 |
7 | All members of our community are expected to follow our [Code of Conduct](/CODE_OF_CONDUCT.md). Please make sure you are welcoming and friendly in all of our spaces.
8 |
9 | We're really glad you're reading this, because we need volunteer developers to help this project come to fruition. 👏
10 |
11 | ## Issues
12 |
13 | The best way to contribute to our projects is by opening a [new issue](https://github.com/privacy-scaling-explorations/zk-kit.solidity/issues/new/choose) or tackling one of the issues listed [here](https://github.com/privacy-scaling-explorations/zk-kit.solidity/contribute).
14 |
15 | ## Pull Requests
16 |
17 | Pull requests are great if you want to add a feature or fix a bug. Here's a quick guide:
18 |
19 | 1. Fork the repo.
20 |
21 | 2. Run the tests. We only take pull requests with passing tests.
22 |
23 | 3. Add a test for your change. Only refactoring and documentation changes require no new tests.
24 |
25 | 4. Make sure to check out the [Style Guide](/CONTRIBUTING.md#style-guide) and ensure that your code complies with the rules.
26 |
27 | 5. Make the test pass.
28 |
29 | 6. Commit your changes.
30 |
31 | 7. Push to your fork and submit a pull request on our `main` branch. Please provide us with some explanation of why you made the changes you made. For new features make sure to explain a standard use case to us.
32 |
33 | > [!NOTE]
34 | > When a new package is created or a new feature is added to the repository, the contributor will be added to the `.github/CODEOWNERS` file to review and approve any future changes to their code.
35 |
36 | > [!IMPORTANT]
37 | > We do not accept minor grammatical fixes (e.g. correcting typos, rewording sentences) unless they significantly improve clarity in technical documentation. These contributions, while appreciated, are not a priority for merging. If there is a grammatical mistake, please feel free to message the team.
38 |
39 | ## CI (Github Actions) Tests
40 |
41 | We use GitHub Actions to test each PR before it is merged.
42 |
43 | When you submit your PR (or later change that code), a CI build will automatically be kicked off. A note will be added to the PR, and will indicate the current status of the build.
44 |
45 | ## Style Guide
46 |
47 | ### Code rules
48 |
49 | We always use ESLint and Prettier. To check that your code follows the rules, simply run the npm script `yarn lint`.
50 |
51 | ### Commits rules
52 |
53 | For commits it is recommended to use [Conventional Commits](https://www.conventionalcommits.org).
54 |
55 | Don't worry if it looks complicated. In our repositories, `git commit` opens an interactive app to create your conventional commit.
56 |
57 | Each commit message consists of a **header**, a **body** and a **footer**. The **header** has a special format that includes a **type**, a **scope** and a **subject**:
58 |
59 | ():
60 |
61 |
62 |
63 |
7 |
8 |
9 |
10 |
11 |
12 | > [!NOTE]
13 | > This package has been DEPRECATED. Please, refer to [@excubiae/contracts](https://www.npmjs.com/package/@excubiae/contracts) on [excubiae](https://github.com/privacy-scaling-explorations/excubiae) monorepo.
14 |
15 | ---
16 |
17 | ---
18 |
19 | Excubiae is a generalized framework for on-chain gatekeepers that allows developers to define custom access control mechanisms using different on-chain credentials. By abstracting the gatekeeper logic, excubiae provides a reusable and composable solution for securing decentralised applications. This package provides a pre-defined set of specific excubia (_extensions_) for credentials based on different protocols.
20 |
21 | ## 🛠 Install
22 |
23 | ### npm or yarn
24 |
25 | Install the ` @zk-kit/excubiae` package with npm:
26 |
27 | ```bash
28 | npm i @zk-kit/excubiae --save
29 | ```
30 |
31 | or yarn:
32 |
33 | ```bash
34 | yarn add @zk-kit/excubiae
35 | ```
36 |
37 | ## 📜 Usage
38 |
39 | To build your own Excubia:
40 |
41 | 1. Inherit from the [Excubia](./Excubia.sol) abstract contract that conforms to the [IExcubia](./IExcubia.sol) interface.
42 | 2. Implement the `_check()` and `_pass()` methods logic defining your own checks to prevent unwanted access as sybils or avoid to pass the gate twice with the same data / identity.
43 |
44 | ```solidity
45 | // SPDX-License-Identifier: MIT
46 | pragma solidity >=0.8.0;
47 |
48 | import { Excubia } from "excubiae/contracts/Excubia.sol";
49 |
50 | contract MyExcubia is Excubia {
51 | // ...
52 |
53 | function _pass(address passerby, bytes calldata data) internal override {
54 | // Implement your logic to prevent unwanted access here.
55 | }
56 |
57 | function _check(address passerby, bytes calldata data) internal view override {
58 | // Implement custom access control logic here.
59 | }
60 |
61 | // ...
62 | }
63 | ```
64 |
65 | Please see the [extensions](./extensions/) folder for more complex reference implementations and the [test contracts](./test) folder for guidance on using the libraries.
66 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/EASExcubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Excubia} from "../Excubia.sol";
5 | import {IEAS} from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
6 | import {Attestation} from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol";
7 |
8 | /// @title EAS Excubia Contract.
9 | /// @notice This contract extends the Excubia contract to integrate with the Ethereum Attestation Service (EAS).
10 | /// This contract checks an EAS attestation to permit access through the gate.
11 | /// @dev The contract uses a specific attestation schema & attester to admit the recipient of the attestation.
12 | contract EASExcubia is Excubia {
13 | /// @notice The Ethereum Attestation Service contract interface.
14 | IEAS public immutable EAS;
15 | /// @notice The specific schema ID that attestations must match to pass the gate.
16 | bytes32 public immutable SCHEMA;
17 | /// @notice The trusted attester address whose attestations are considered
18 | /// the only ones valid to pass the gate.
19 | address public immutable ATTESTER;
20 |
21 | /// @notice Mapping to track which attestations have passed the gate to
22 | /// avoid passing it twice using the same attestation.
23 | mapping(bytes32 => bool) public passedAttestations;
24 |
25 | /// @notice Error thrown when the attestation does not match the designed schema.
26 | error UnexpectedSchema();
27 |
28 | /// @notice Error thrown when the attestation does not match the designed trusted attester.
29 | error UnexpectedAttester();
30 |
31 | /// @notice Error thrown when the attestation does not match the passerby as recipient.
32 | error UnexpectedRecipient();
33 |
34 | /// @notice Error thrown when the attestation has been revoked.
35 | error RevokedAttestation();
36 |
37 | /// @notice Constructor to initialize with target EAS contract with specific attester and schema.
38 | /// @param _eas The address of the EAS contract.
39 | /// @param _attester The address of the trusted attester.
40 | /// @param _schema The schema ID that attestations must match.
41 | constructor(address _eas, address _attester, bytes32 _schema) {
42 | if (_eas == address(0) || _attester == address(0)) revert ZeroAddress();
43 |
44 | EAS = IEAS(_eas);
45 | ATTESTER = _attester;
46 | SCHEMA = _schema;
47 | }
48 |
49 | /// @notice The trait of the Excubia contract.
50 | function trait() external pure override returns (string memory) {
51 | return "EAS";
52 | }
53 |
54 | /// @notice Internal function to handle the passing logic with check.
55 | /// @dev Calls the parent `_pass` function and stores the attestation to avoid pass the gate twice.
56 | /// @param passerby The address of the entity attempting to pass the gate.
57 | /// @param data Additional data required for the check (e.g., encoded attestation ID).
58 | function _pass(address passerby, bytes calldata data) internal override {
59 | bytes32 attestationId = abi.decode(data, (bytes32));
60 |
61 | // Avoiding passing the gate twice using the same attestation.
62 | if (passedAttestations[attestationId]) revert AlreadyPassed();
63 |
64 | passedAttestations[attestationId] = true;
65 |
66 | super._pass(passerby, data);
67 | }
68 |
69 | /// @notice Internal function to handle the gate protection (attestation check) logic.
70 | /// @dev Checks if the attestation matches the schema, attester, recipient, and is not revoked.
71 | /// @param passerby The address of the entity attempting to pass the gate.
72 | /// @param data Additional data required for the check (e.g., encoded attestation ID).
73 | function _check(address passerby, bytes calldata data) internal view override {
74 | super._check(passerby, data);
75 |
76 | bytes32 attestationId = abi.decode(data, (bytes32));
77 |
78 | Attestation memory attestation = EAS.getAttestation(attestationId);
79 |
80 | if (attestation.schema != SCHEMA) revert UnexpectedSchema();
81 | if (attestation.attester != ATTESTER) revert UnexpectedAttester();
82 | if (attestation.recipient != passerby) revert UnexpectedRecipient();
83 | if (attestation.revocationTime != 0) revert RevokedAttestation();
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/ERC721Excubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Excubia} from "../Excubia.sol";
5 | import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol";
6 |
7 | /// @title ERC721 Excubia Contract.
8 | /// @notice This contract extends the Excubia contract to integrate with an ERC721 token.
9 | /// This contract checks the ownership of an ERC721 token to permit access through the gate.
10 | /// @dev The contract refers to a contract implementing the ERC721 standard to admit the owner of the token.
11 | contract ERC721Excubia is Excubia {
12 | /// @notice The ERC721 token contract interface.
13 | IERC721 public immutable NFT;
14 |
15 | /// @notice Mapping to track which token IDs have passed by the gate to
16 | /// avoid passing the gate twice with the same token ID.
17 | mapping(uint256 => bool) public passedTokenIds;
18 |
19 | /// @notice Error thrown when the passerby is not the owner of the token.
20 | error UnexpectedTokenOwner();
21 |
22 | /// @notice Constructor to initialize with target ERC721 contract.
23 | /// @param _erc721 The address of the ERC721 contract.
24 | constructor(address _erc721) {
25 | if (_erc721 == address(0)) revert ZeroAddress();
26 |
27 | NFT = IERC721(_erc721);
28 | }
29 |
30 | /// @notice The trait of the Excubia contract.
31 | function trait() external pure override returns (string memory) {
32 | return "ERC721";
33 | }
34 |
35 | /// @notice Internal function to handle the passing logic with check.
36 | /// @dev Calls the parent `_pass` function and stores the NFT ID to avoid passing the gate twice.
37 | /// @param passerby The address of the entity attempting to pass the gate.
38 | /// @param data Additional data required for the check (e.g., encoded token ID).
39 | function _pass(address passerby, bytes calldata data) internal override {
40 | uint256 tokenId = abi.decode(data, (uint256));
41 |
42 | // Avoiding passing the gate twice with the same token ID.
43 | if (passedTokenIds[tokenId]) revert AlreadyPassed();
44 |
45 | passedTokenIds[tokenId] = true;
46 |
47 | super._pass(passerby, data);
48 | }
49 |
50 | /// @notice Internal function to handle the gate protection (token ownership check) logic.
51 | /// @dev Checks if the passerby is the owner of the token.
52 | /// @param passerby The address of the entity attempting to pass the gate.
53 | /// @param data Additional data required for the check (e.g., encoded token ID).
54 | function _check(address passerby, bytes calldata data) internal view override {
55 | super._check(passerby, data);
56 |
57 | uint256 tokenId = abi.decode(data, (uint256));
58 |
59 | // Check if the user owns the token.
60 | if (!(NFT.ownerOf(tokenId) == passerby)) revert UnexpectedTokenOwner();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/FreeForAllExcubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Excubia} from "../Excubia.sol";
5 |
6 | /// @title FreeForAll Excubia Contract.
7 | /// @notice This contract extends the Excubia contract to allow free access through the gate.
8 | /// This contract does not perform any checks and allows any passerby to pass the gate.
9 | /// @dev The contract overrides the `_check` function to always return true.
10 | contract FreeForAllExcubia is Excubia {
11 | /// @notice Constructor for the FreeForAllExcubia contract.
12 | constructor() {}
13 |
14 | /// @notice Mapping to track already passed passersby.
15 | mapping(address => bool) public passedPassersby;
16 |
17 | /// @notice The trait of the Excubia contract.
18 | function trait() external pure override returns (string memory) {
19 | return "FreeForAll";
20 | }
21 |
22 | /// @notice Internal function to handle the gate passing logic.
23 | /// @dev This function calls the parent `_pass` function and then tracks the passerby.
24 | /// @param passerby The address of the entity passing the gate.
25 | /// @param data Additional data required for the pass (not used in this implementation).
26 | function _pass(address passerby, bytes calldata data) internal override {
27 | // Avoiding passing the gate twice with the same address.
28 | if (passedPassersby[passerby]) revert AlreadyPassed();
29 |
30 | passedPassersby[passerby] = true;
31 |
32 | super._pass(passerby, data);
33 | }
34 |
35 | /// @notice Internal function to handle the gate protection logic.
36 | /// @dev This function always returns true, signaling that any passerby is able to pass the gate.
37 | /// @param passerby The address of the entity attempting to pass the gate.
38 | /// @param data Additional data required for the check (e.g., encoded attestation ID).
39 | function _check(address passerby, bytes calldata data) internal view override {
40 | super._check(passerby, data);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/GitcoinPassportExcubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Excubia} from "../Excubia.sol";
5 | import {IGitcoinPassportDecoder} from "./interfaces/IGitcoinPassportDecoder.sol";
6 |
7 | /// @title Gitcoin Passport Excubia Contract.
8 | /// @notice This contract extends the Excubia contract to integrate with the Gitcoin Passport Decoder.
9 | /// This contract checks the Gitcoin Passport user score to permit access through the gate.
10 | /// The Gitcoin Passport smart contract stack is built on top of Ethereum Attestation Service (EAS) contracts.
11 | /// @dev The contract uses a fixed threshold score to admit only passersby with a passport score
12 | /// equal to or greater than the fixed threshold based on their score (see _check() for more).
13 | contract GitcoinPassportExcubia is Excubia {
14 | /// @notice The factor used to scale the score.
15 | /// @dev https://docs.passport.xyz/building-with-passport/smart-contracts/contract-reference#available-methods
16 | uint256 public constant FACTOR = 100;
17 |
18 | /// @notice The Gitcoin Passport Decoder contract interface.
19 | IGitcoinPassportDecoder public immutable DECODER;
20 |
21 | /// @notice The minimum threshold score required to pass the gate.
22 | uint256 public immutable THRESHOLD_SCORE;
23 |
24 | /// @notice Mapping to track which users have already passed through the gate.
25 | mapping(address => bool) public passedUsers;
26 |
27 | /// @notice Error thrown when the user's score is insufficient to pass the gate.
28 | error InsufficientScore();
29 |
30 | /// @notice Error thrown when the threshold score is negative or zero.
31 | error NegativeOrZeroThresholdScore();
32 |
33 | /// @notice Constructor to initialize the contract with the target decoder and threshold score.
34 | /// @param _decoder The address of the Gitcoin Passport Decoder contract.
35 | /// @param _thresholdScore The minimum threshold score required to pass the gate.
36 | constructor(address _decoder, uint256 _thresholdScore) {
37 | if (_decoder == address(0)) revert ZeroAddress();
38 | if (_thresholdScore <= 0) revert NegativeOrZeroThresholdScore();
39 |
40 | DECODER = IGitcoinPassportDecoder(_decoder);
41 | THRESHOLD_SCORE = _thresholdScore;
42 | }
43 |
44 | /// @notice The trait of the Excubia contract.
45 | function trait() external pure override returns (string memory) {
46 | return "GitcoinPassport";
47 | }
48 |
49 | /// @notice Internal function to handle the passing logic with check.
50 | /// @dev Calls the parent `_pass` function and stores the user to avoid passing the gate twice.
51 | /// @param passerby The address of the entity attempting to pass the gate.
52 | /// @param data Additional data required for the check.
53 | function _pass(address passerby, bytes calldata data) internal override {
54 | if (passedUsers[passerby]) revert AlreadyPassed();
55 |
56 | passedUsers[passerby] = true;
57 |
58 | super._pass(passerby, data);
59 | }
60 |
61 | /// @notice Internal function to handle the gate protection (score check) logic.
62 | /// @dev Checks if the user's Gitcoin Passport score meets the threshold.
63 | /// @param passerby The address of the entity attempting to pass the gate.
64 | /// @param data Additional data required for the check.
65 | function _check(address passerby, bytes calldata data) internal view override {
66 | super._check(passerby, data);
67 |
68 | if ((DECODER.getScore(passerby) / FACTOR) < THRESHOLD_SCORE) revert InsufficientScore();
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/HatsExcubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Excubia} from "../Excubia.sol";
5 | import {IHatsMinimal} from "./interfaces/IHatsMinimal.sol";
6 |
7 | /// @title Hats Excubia Contract.
8 | /// @notice This contract extends the Excubia contract to integrate with the Hats protocol.
9 | /// This contract checks if a user is wearing a specific hat to permit access through the gate.
10 | /// @dev The contract uses a specific set of hats to admit the passerby wearing any of those hats.
11 | contract HatsExcubia is Excubia {
12 | /// @notice The Hats contract interface.
13 | IHatsMinimal public immutable HATS;
14 |
15 | /// @notice Mapping to track which hats are considered valid for passing the gate.
16 | mapping(uint256 => bool) public criterionHat;
17 | /// @notice Mapping to track which users have already passed through the gate.
18 | mapping(address => bool) public passedUsers;
19 |
20 | /// @notice Error thrown when the user is not wearing the required hat.
21 | error NotWearingCriterionHat();
22 | /// @notice Error thrown when the specified hat is not a criterion hat.
23 | error NotCriterionHat();
24 | /// @notice Error thrown when the array of criterion hats is empty.
25 | error ZeroCriterionHats();
26 |
27 | /// @notice Constructor to initialize the contract with the target Hats contract and criterion hats.
28 | /// @param _hats The address of the Hats contract.
29 | /// @param _criterionHats An array of hat IDs that are considered as criteria for passing the gate.
30 | constructor(address _hats, uint256[] memory _criterionHats) {
31 | if (_hats == address(0)) revert ZeroAddress();
32 | if (_criterionHats.length == 0) revert ZeroCriterionHats();
33 |
34 | HATS = IHatsMinimal(_hats);
35 |
36 | uint256 numberOfCriterionHats = _criterionHats.length;
37 |
38 | for (uint256 i = 0; i < numberOfCriterionHats; ++i) {
39 | criterionHat[_criterionHats[i]] = true;
40 | }
41 | }
42 |
43 | /// @notice The trait of the Excubia contract.
44 | function trait() external pure override returns (string memory) {
45 | return "Hats";
46 | }
47 |
48 | /// @notice Internal function to handle the passing logic with check.
49 | /// @dev Calls the parent `_pass` function and stores the user to avoid passing the gate twice.
50 | /// @param passerby The address of the entity attempting to pass the gate.
51 | /// @param data Additional data required for the check.
52 | function _pass(address passerby, bytes calldata data) internal override {
53 | // Avoiding passing the gate twice for the same user.
54 | if (passedUsers[passerby]) revert AlreadyPassed();
55 |
56 | passedUsers[passerby] = true;
57 |
58 | super._pass(passerby, data);
59 | }
60 |
61 | /// @notice Internal function to handle the gate protection (hat check) logic.
62 | /// @dev Checks if the user is wearing one of the criterion hats.
63 | /// @param passerby The address of the entity attempting to pass the gate.
64 | /// @param data Additional data required for the check.
65 | function _check(address passerby, bytes calldata data) internal view override {
66 | super._check(passerby, data);
67 |
68 | uint256 hat = abi.decode(data, (uint256));
69 |
70 | // Check if the hat is a criterion hat.
71 | if (!criterionHat[hat]) revert NotCriterionHat();
72 |
73 | // Check if the user is wearing the criterion hat.
74 | if (!HATS.isWearerOfHat(passerby, hat)) revert NotWearingCriterionHat();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/SemaphoreExcubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Excubia} from "../Excubia.sol";
5 | import {ISemaphore} from "@semaphore-protocol/contracts/interfaces/ISemaphore.sol";
6 |
7 | /// @title Semaphore Excubia Contract
8 | /// @notice This contract extends the Excubia contract to integrate with the Semaphore protocol.
9 | /// It verifies the passerby Semaphore group membership proofs to grant access through the gate.
10 | /// @dev To allow only specific Semaphore identities from a group, the contract stores the specific group identifier.
11 | /// To avoid identities from passing twice, nullifiers are stored upon successful verification of the proofs.
12 | contract SemaphoreExcubia is Excubia {
13 | /// @notice The Semaphore contract interface.
14 | ISemaphore public immutable SEMAPHORE;
15 | /// @notice The specific group identifier that proofs must match to pass the gate.
16 | /// @dev Used as a `scope` to ensure consistency during proof membership verification.
17 | uint256 public immutable GROUP_ID;
18 |
19 | /// @notice Mapping to track which nullifiers have been used to avoid passing the
20 | /// gate twice using the same Semaphore identity.
21 | /// @dev The nullifier is derived from the hash of the secret and group identifier,
22 | /// ensuring that the same identity cannot pass twice using the same group.
23 | mapping(uint256 => bool) public passedNullifiers;
24 |
25 | /// @notice Error thrown when the group identifier does not match the expected one.
26 | error InvalidGroup();
27 |
28 | /// @notice Error thrown when the proof is invalid.
29 | error InvalidProof();
30 |
31 | /// @notice Error thrown when the proof scope does not match the expected group identifier.
32 | error UnexpectedScope();
33 |
34 | /// @notice Constructor to initialize with target Semaphore contract and specific group identifier.
35 | /// @param _semaphore The address of the Semaphore contract.
36 | /// @param _groupId The group identifier that proofs must match.
37 | constructor(address _semaphore, uint256 _groupId) {
38 | if (_semaphore == address(0)) revert ZeroAddress();
39 |
40 | SEMAPHORE = ISemaphore(_semaphore);
41 |
42 | if (ISemaphore(_semaphore).groupCounter() <= _groupId) revert InvalidGroup();
43 |
44 | GROUP_ID = _groupId;
45 | }
46 |
47 | /// @notice The trait of the Excubia contract.
48 | function trait() external pure override returns (string memory) {
49 | return "Semaphore";
50 | }
51 |
52 | /// @notice Internal function to handle the passing logic with check.
53 | /// @dev Calls the parent `_pass` function and stores the nullifier to avoid passing the gate twice.
54 | /// @param passerby The address of the entity attempting to pass the gate.
55 | /// @param data Additional data required for the check (ie., encoded Semaphore proof).
56 | function _pass(address passerby, bytes calldata data) internal override {
57 | ISemaphore.SemaphoreProof memory proof = abi.decode(data, (ISemaphore.SemaphoreProof));
58 |
59 | // Avoiding passing the gate twice using the same nullifier.
60 | if (passedNullifiers[proof.nullifier]) revert AlreadyPassed();
61 |
62 | passedNullifiers[proof.nullifier] = true;
63 |
64 | super._pass(passerby, data);
65 | }
66 |
67 | /// @notice Internal function to handle the gate protection (proof check) logic.
68 | /// @dev Checks if the proof matches the group ID, scope, and is valid.
69 | /// @param passerby The address of the entity attempting to pass the gate.
70 | /// @param data Additional data required for the check (i.e., encoded Semaphore proof).
71 | function _check(address passerby, bytes calldata data) internal view override {
72 | super._check(passerby, data);
73 |
74 | ISemaphore.SemaphoreProof memory proof = abi.decode(data, (ISemaphore.SemaphoreProof));
75 |
76 | if (GROUP_ID != proof.scope) revert UnexpectedScope();
77 |
78 | if (!SEMAPHORE.verifyProof(GROUP_ID, proof)) revert InvalidProof();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/ZKEdDSAEventTicketPCDExcubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Excubia} from "../Excubia.sol";
5 | import {ZKEdDSAEventTicketPCDVerifier} from "./verifiers/ZKEdDSAEventTicketPCDVerifier.sol";
6 |
7 | /// @title ZKEdDSA Event Ticket PCD Excubia Contract.
8 | /// @notice This contract extends the Excubia contract to integrate with ZK EdDSA Event Ticket PCD.
9 | /// This contract verifies a ZK EdDSA Event Ticket PCD proof to permit access through the gate.
10 | /// You can find more about on the Zupass repository https://github.com/proofcarryingdata/zupass.
11 | /// @dev The contract uses specific event ID and signers to check against the verifier
12 | /// in order to admit the recipient (passerby) of the proof.
13 | contract ZKEdDSAEventTicketPCDExcubia is Excubia {
14 | /// @notice The valid event ID that proofs must match to pass the gate.
15 | uint256 public immutable VALID_EVENT_ID;
16 | /// @notice The first valid signer whose signatures are considered valid to pass the gate.
17 | uint256 public immutable VALID_SIGNER_1;
18 | /// @notice The second valid signer whose signatures are considered valid to pass the gate.
19 | uint256 public immutable VALID_SIGNER_2;
20 |
21 | /// @notice The ZKEdDSA Event Ticket PCD Verifier contract.
22 | ZKEdDSAEventTicketPCDVerifier public immutable VERIFIER;
23 |
24 | /// @notice Mapping to track which tickets have already passed the checks
25 | /// to avoid passing the gate twice with the same ticket.
26 | mapping(uint256 => bool) public passedZKEdDSAEventTicketPCDs;
27 |
28 | /// @notice Error thrown when the proof is invalid.
29 | error InvalidProof();
30 |
31 | /// @notice Error thrown when the event ID in the proof does not match the valid event ID.
32 | error InvalidEventId();
33 |
34 | /// @notice Error thrown when the signers in the proof do not match the valid signers.
35 | error InvalidSigners();
36 |
37 | /// @notice Error thrown when the watermark in the proof does not match the passerby address.
38 | error InvalidWatermark();
39 |
40 | /// @notice Constructor to initialize with target verifier, valid event ID, and valid signers.
41 | /// @param _verifier The address of the ZKEdDSA Event Ticket PCD Verifier contract.
42 | /// @param _validEventId The valid event ID that proofs must match.
43 | /// @param _validSigner1 The first valid signer whose signatures are considered valid.
44 | /// @param _validSigner2 The second valid signer whose signatures are considered valid.
45 | constructor(address _verifier, uint256 _validEventId, uint256 _validSigner1, uint256 _validSigner2) {
46 | if (_verifier == address(0)) revert ZeroAddress();
47 |
48 | VERIFIER = ZKEdDSAEventTicketPCDVerifier(_verifier);
49 | VALID_EVENT_ID = _validEventId;
50 | VALID_SIGNER_1 = _validSigner1;
51 | VALID_SIGNER_2 = _validSigner2;
52 | }
53 |
54 | /// @notice The trait of the Excubia contract.
55 | function trait() external pure override returns (string memory) {
56 | return "ZKEdDSAEventTicketPCD";
57 | }
58 |
59 | /// @notice Internal function to handle the passing logic with check.
60 | /// @dev Calls the parent `_pass` function and stores the ticket ID to avoid passing the gate twice.
61 | /// @param passerby The address of the entity attempting to pass the gate.
62 | /// @param data Additional data required for the check (i.e., encoded proof).
63 | function _pass(address passerby, bytes calldata data) internal override {
64 | // Decode the given data bytes.
65 | (, , , uint256[38] memory _pubSignals) = abi.decode(data, (uint256[2], uint256[2][2], uint256[2], uint256[38]));
66 |
67 | // Avoiding passing the gate twice using the same nullifier.
68 | /// @dev Ticket ID is stored at _pubSignals index 0.
69 | if (passedZKEdDSAEventTicketPCDs[_pubSignals[0]]) revert AlreadyPassed();
70 |
71 | passedZKEdDSAEventTicketPCDs[_pubSignals[0]] = true;
72 |
73 | super._pass(passerby, data);
74 | }
75 |
76 | /// @notice Internal function to handle the gate protection (proof check) logic.
77 | /// @dev Checks if the proof matches the event ID, signers, watermark, and is valid.
78 | /// @param passerby The address of the entity attempting to pass the gate.
79 | /// @param data Additional data required for the check (i.e., encoded proof).
80 | function _check(address passerby, bytes calldata data) internal view override {
81 | super._check(passerby, data);
82 |
83 | // Decode the given data bytes.
84 | (uint256[2] memory _pA, uint256[2][2] memory _pB, uint256[2] memory _pC, uint256[38] memory _pubSignals) = abi
85 | .decode(data, (uint256[2], uint256[2][2], uint256[2], uint256[38]));
86 |
87 | // Signers are stored at index 13 and 14.
88 | if (_pubSignals[13] != VALID_SIGNER_1 || _pubSignals[14] != VALID_SIGNER_2) revert InvalidSigners();
89 |
90 | // Event ID is stored at index 15.
91 | if (_pubSignals[15] != VALID_EVENT_ID) revert InvalidEventId();
92 |
93 | // Watermark is stored at index 37.
94 | if (_pubSignals[37] != uint256(uint160(passerby))) revert InvalidWatermark();
95 |
96 | // Proof verification.
97 | if (!VERIFIER.verifyProof(_pA, _pB, _pC, _pubSignals)) revert InvalidProof();
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/interfaces/IGitcoinPassportDecoder.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: GPL
2 | pragma solidity >=0.8.0;
3 |
4 | /// This interface has been copied & pasted from
5 | /// https://github.com/gitcoinco/eas-proxy/blob/main/contracts/IGitcoinPassportDecoder.sol
6 | /// with commit hash d82a73337216effdba719a625f92cb941b537850.
7 |
8 | /**
9 | * @dev A struct storing a passpor credential
10 | */
11 |
12 | struct Credential {
13 | string provider;
14 | bytes32 hash;
15 | uint64 time;
16 | uint64 expirationTime;
17 | }
18 |
19 | /**
20 | * @title IGitcoinPassportDecoder
21 | * @notice Minimal interface for consuming GitcoinPassportDecoder data
22 | */
23 | interface IGitcoinPassportDecoder {
24 | function getPassport(address user) external returns (Credential[] memory);
25 |
26 | function getScore(address user) external view returns (uint256);
27 |
28 | function isHuman(address user) external view returns (bool);
29 | }
30 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/extensions/interfaces/IHatsMinimal.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | /// This interface has been copied & pasted from MACI.
5 | /// https://github.com/privacy-scaling-explorations/maci/blob/dev/contracts/contracts/interfaces/IHats.sol
6 | /// with commit hash bb429aece0eed2eed5d526e2a23522722c42ba5c.
7 | /// credits to Spencer Graham (https://github.com/spengrah) for writing this.
8 |
9 | /// @title IHatsMinimal
10 | /// @notice Minimal interface for the Hats Protocol contract.
11 | /// @dev Includes only the functions required for the HatsExcubia and associated tests.
12 | interface IHatsMinimal {
13 | /// @notice Creates and mints a Hat that is its own admin, i.e. a "topHat"
14 | /// @dev A topHat has no eligibility and no toggle
15 | /// @param _target The address to which the newly created topHat is minted
16 | /// @param _details A description of the Hat [optional]. Should not be larger than 7000 bytes
17 | /// (enforced in changeHatDetails)
18 | /// @param _imageURI The image uri for this top hat and the fallback for its
19 | /// downstream hats [optional]. Should not be larger than 7000 bytes
20 | /// (enforced in changeHatImageURI)
21 | /// @return topHatId The id of the newly created topHat
22 | function mintTopHat(
23 | address _target,
24 | string calldata _details,
25 | string calldata _imageURI
26 | ) external returns (uint256);
27 |
28 | /// @notice Creates a new hat. The msg.sender must wear the `_admin` hat.
29 | /// @dev Initializes a new Hat struct, but does not mint any tokens.
30 | /// @param _details A description of the Hat. Should not be larger than 7000 bytes (enforced in changeHatDetails)
31 | /// @param _maxSupply The total instances of the Hat that can be worn at once
32 | /// @param _admin The id of the Hat that will control who wears the newly created hat
33 | /// @param _eligibility The address that can report on the Hat wearer's status
34 | /// @param _toggle The address that can deactivate the Hat
35 | /// @param _mutable Whether the hat's properties are changeable after creation
36 | /// @param _imageURI The image uri for this hat and the fallback for its
37 | /// downstream hats [optional]. Should not be larger than 7000 bytes (enforced in changeHatImageURI)
38 | /// @return newHatId The id of the newly created Hat
39 | function createHat(
40 | uint256 _admin,
41 | string calldata _details,
42 | uint32 _maxSupply,
43 | address _eligibility,
44 | address _toggle,
45 | bool _mutable,
46 | string calldata _imageURI
47 | ) external returns (uint256);
48 |
49 | /// @notice Mints an ERC1155-similar token of the Hat to an eligible recipient, who then "wears" the hat
50 | /// @dev The msg.sender must wear an admin Hat of `_hatId`, and the recipient must be eligible to wear `_hatId`
51 | /// @param _hatId The id of the Hat to mint
52 | /// @param _wearer The address to which the Hat is minted
53 | /// @return success Whether the mint succeeded
54 | function mintHat(uint256 _hatId, address _wearer) external returns (bool success);
55 |
56 | /// @notice Checks whether a given address wears a given Hat
57 | /// @dev Convenience function that wraps `balanceOf`
58 | /// @param account The address in question
59 | /// @param hat The id of the Hat that the `_user` might wear
60 | /// @return isWearer Whether the `_user` wears the Hat.
61 | function isWearerOfHat(address account, uint256 hat) external view returns (bool);
62 | }
63 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@zk-kit/excubiae",
3 | "version": "0.1.1",
4 | "description": "[DEPRECATED] A general purpose on-chain gatekeeping smart contract framework.",
5 | "license": "MIT",
6 | "files": [
7 | "*.sol",
8 | "!test/*",
9 | "README.md",
10 | "LICENSE"
11 | ],
12 | "keywords": [
13 | "deprecated",
14 | "blockchain",
15 | "ethereum",
16 | "hardhat",
17 | "smart-contracts",
18 | "solidity",
19 | "libraries",
20 | "gatekeepers",
21 | "credentials"
22 | ],
23 | "repository": "git@github.com:privacy-scaling-explorations/zk-kit.solidity.git",
24 | "homepage": "https://github.com/privacy-scaling-explorations/zk-kit.solidity/tree/main/packages/gatekeepers",
25 | "publishConfig": {
26 | "access": "public"
27 | },
28 | "dependencies": {
29 | "@ethereum-attestation-service/eas-contracts": "1.7.1",
30 | "@openzeppelin/contracts": "5.0.2",
31 | "@semaphore-protocol/contracts": "4.0.0-beta.16"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/test/MockEAS.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | /* solhint-disable max-line-length */
5 | import {IEAS, ISchemaRegistry, AttestationRequest, MultiAttestationRequest, DelegatedAttestationRequest, MultiDelegatedAttestationRequest, DelegatedRevocationRequest, RevocationRequest, MultiRevocationRequest, MultiDelegatedRevocationRequest} from "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
6 | import {Attestation} from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol";
7 |
8 | /// @title Mock Ethereum Attestation Service (EAS) Contract.
9 | /// @notice This contract is a mock implementation of the IEAS interface for testing purposes.
10 | /// @dev It simulates the behavior of a real EAS contract by providing predefined mocked attestations.
11 | contract MockEAS is IEAS {
12 | /// @notice A mock schema registry, represented simply as an address.
13 | ISchemaRegistry public override getSchemaRegistry;
14 |
15 | /// @notice A mapping to store mocked attestations by their unique identifiers.
16 | mapping(bytes32 => Attestation) private mockedAttestations;
17 |
18 | /// MOCKS ///
19 | /// @notice Constructor to initialize the mock contract with predefined attestations.
20 | /// @param _recipient The recipient address used in mocked attestations.
21 | /// @param _attester The attester address used in mocked attestations.
22 | /// @param _schema The schema identifier used in mocked attestations.
23 | constructor(address _recipient, address _attester, bytes32 _schema) {
24 | getSchemaRegistry = ISchemaRegistry(address(1));
25 |
26 | Attestation memory valid = Attestation({
27 | uid: bytes32(hex"0100000000000000000000000000000000000000000000000000000000000000"),
28 | schema: _schema,
29 | time: 0,
30 | expirationTime: 0,
31 | revocationTime: 0,
32 | refUID: bytes32(hex"0100000000000000000000000000000000000000000000000000000000000000"),
33 | recipient: _recipient,
34 | attester: _attester,
35 | revocable: true,
36 | data: bytes("")
37 | });
38 |
39 | Attestation memory revoked = Attestation({
40 | uid: bytes32(hex"0200000000000000000000000000000000000000000000000000000000000000"),
41 | schema: _schema,
42 | time: 0,
43 | expirationTime: 0,
44 | revocationTime: 1,
45 | refUID: bytes32(hex"0100000000000000000000000000000000000000000000000000000000000000"),
46 | recipient: _recipient,
47 | attester: _attester,
48 | revocable: true,
49 | data: bytes("")
50 | });
51 |
52 | Attestation memory invalidSchema = Attestation({
53 | uid: bytes32(hex"0300000000000000000000000000000000000000000000000000000000000000"),
54 | schema: bytes32(hex"0100000000000000000000000000000000000000000000000000000000000000"),
55 | time: 0,
56 | expirationTime: 0,
57 | revocationTime: 0,
58 | refUID: bytes32(hex"0100000000000000000000000000000000000000000000000000000000000000"),
59 | recipient: _recipient,
60 | attester: _attester,
61 | revocable: true,
62 | data: bytes("")
63 | });
64 |
65 | Attestation memory invalidRecipient = Attestation({
66 | uid: bytes32(hex"0400000000000000000000000000000000000000000000000000000000000000"),
67 | schema: _schema,
68 | time: 0,
69 | expirationTime: 0,
70 | revocationTime: 0,
71 | refUID: bytes32(hex"0100000000000000000000000000000000000000000000000000000000000000"),
72 | recipient: address(1),
73 | attester: _attester,
74 | revocable: true,
75 | data: bytes("")
76 | });
77 |
78 | Attestation memory invalidAttester = Attestation({
79 | uid: bytes32(hex"0500000000000000000000000000000000000000000000000000000000000000"),
80 | schema: _schema,
81 | time: 0,
82 | expirationTime: 0,
83 | revocationTime: 0,
84 | refUID: bytes32(hex"0100000000000000000000000000000000000000000000000000000000000000"),
85 | recipient: _recipient,
86 | attester: address(1),
87 | revocable: true,
88 | data: bytes("")
89 | });
90 |
91 | mockedAttestations[bytes32(hex"0100000000000000000000000000000000000000000000000000000000000000")] = valid;
92 | mockedAttestations[bytes32(hex"0200000000000000000000000000000000000000000000000000000000000000")] = revoked;
93 | mockedAttestations[
94 | bytes32(hex"0300000000000000000000000000000000000000000000000000000000000000")
95 | ] = invalidSchema;
96 | mockedAttestations[
97 | bytes32(hex"0400000000000000000000000000000000000000000000000000000000000000")
98 | ] = invalidRecipient;
99 | mockedAttestations[
100 | bytes32(hex"0500000000000000000000000000000000000000000000000000000000000000")
101 | ] = invalidAttester;
102 | }
103 |
104 | /// @notice Retrieves a mocked attestation by its unique identifier.
105 | /// @param uid The unique identifier of the attestation.
106 | /// @return The mocked attestation associated with the given identifier.
107 | function getAttestation(bytes32 uid) external view override returns (Attestation memory) {
108 | return mockedAttestations[uid];
109 | }
110 |
111 | /// STUBS ///
112 | // The following functions are stubs and do not perform any meaningful operations.
113 | // They are placeholders to comply with the IEAS interface.
114 | function attest(AttestationRequest calldata /*request*/) external payable override returns (bytes32) {
115 | return bytes32(0);
116 | }
117 |
118 | function attestByDelegation(
119 | DelegatedAttestationRequest calldata /*delegatedRequest*/
120 | ) external payable override returns (bytes32) {
121 | return bytes32(0);
122 | }
123 |
124 | function multiAttest(
125 | MultiAttestationRequest[] calldata multiRequests
126 | ) external payable override returns (bytes32[] memory) {
127 | return new bytes32[](multiRequests.length);
128 | }
129 |
130 | function multiAttestByDelegation(
131 | MultiDelegatedAttestationRequest[] calldata multiDelegatedRequests
132 | ) external payable override returns (bytes32[] memory) {
133 | return new bytes32[](multiDelegatedRequests.length);
134 | }
135 |
136 | function revoke(RevocationRequest calldata request) external payable override {}
137 |
138 | function revokeByDelegation(DelegatedRevocationRequest calldata delegatedRequest) external payable override {}
139 |
140 | function multiRevoke(MultiRevocationRequest[] calldata multiRequests) external payable override {}
141 |
142 | function multiRevokeByDelegation(
143 | MultiDelegatedRevocationRequest[] calldata multiDelegatedRequests
144 | ) external payable override {}
145 |
146 | function timestamp(bytes32 /*data*/) external view override returns (uint64) {
147 | return uint64(block.timestamp);
148 | }
149 |
150 | function multiTimestamp(bytes32[] calldata /*data*/) external view override returns (uint64) {
151 | return uint64(block.timestamp);
152 | }
153 |
154 | function revokeOffchain(bytes32 /*data*/) external view override returns (uint64) {
155 | return uint64(block.timestamp);
156 | }
157 |
158 | function multiRevokeOffchain(bytes32[] calldata /*data*/) external view override returns (uint64) {
159 | return uint64(block.timestamp);
160 | }
161 |
162 | function isAttestationValid(bytes32 uid) external view override returns (bool) {
163 | return mockedAttestations[uid].uid != bytes32(0);
164 | }
165 |
166 | function getTimestamp(bytes32 /*data*/) external view override returns (uint64) {
167 | return uint64(block.timestamp);
168 | }
169 |
170 | function getRevokeOffchain(address /*revoker*/, bytes32 /*data*/) external view override returns (uint64) {
171 | return uint64(block.timestamp);
172 | }
173 |
174 | function version() external pure returns (string memory) {
175 | return string("");
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/test/MockERC721.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
5 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6 |
7 | /// @title Mock ERC721 Token Contract.
8 | /// @notice This contract is a mock implementation of the ERC721 standard for testing purposes.
9 | /// @dev It simulates the behavior of a real ERC721 contract by providing basic minting functionality.
10 | contract MockERC721 is ERC721, Ownable(msg.sender) {
11 | /// @notice A counter to keep track of the token IDs.
12 | uint256 private _tokenIdCounter;
13 |
14 | /// @notice Constructor to initialize the mock contract with a name and symbol.
15 | constructor() payable ERC721("MockERC721Token", "MockERC721Token") {}
16 |
17 | /// @notice Mints a new token and assigns it to the specified recipient.
18 | /// @param recipient The address that will receive the minted token.
19 | function mintAndGiveToken(address recipient) public onlyOwner {
20 | _safeMint(recipient, _tokenIdCounter++);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/test/MockGitcoinPassportDecoder.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {IGitcoinPassportDecoder, Credential} from "../extensions/interfaces/IGitcoinPassportDecoder.sol";
5 |
6 | /// @title Mock Gitcoin Passport Decoder Contract.
7 | /// @notice This contract is a mock implementation of the IGitcoinPassportDecoder interface for testing purposes.
8 | /// @dev It simulates a Gitcoin Passport Decoder contract providing predefined scores and credentials.
9 | contract MockGitcoinPassportDecoder is IGitcoinPassportDecoder {
10 | /// @notice A mapping to store mocked scores for each user address.
11 | mapping(address => uint256) private mockedScores;
12 |
13 | /// MOCKS ///
14 | /// @notice Constructor to initialize the mock contract with predefined user scores.
15 | /// @param _users An array of user addresses.
16 | /// @param _scores An array of scores corresponding to the user addresses.
17 | constructor(address[] memory _users, uint256[] memory _scores) {
18 | for (uint256 i = 0; i < _users.length; i++) {
19 | mockedScores[_users[i]] = _scores[i];
20 | }
21 | }
22 |
23 | /// @notice Mock function to get the score of a user.
24 | /// @param user The address of the user.
25 | /// @return The mocked score of the user.
26 | function getScore(address user) external view returns (uint256) {
27 | return mockedScores[user];
28 | }
29 |
30 | /// @notice Mock function to check if a user is considered human based on their score.
31 | /// @dev check the documentation for more information about (20 is default threshold).
32 | /// @dev https://docs.passport.xyz/building-with-passport/smart-contracts/contract-reference#available-methods
33 | /// @param user The address of the user.
34 | /// @return True if the user's score is greater than 20, false otherwise.
35 | function isHuman(address user) external view returns (bool) {
36 | return mockedScores[user] > 20;
37 | }
38 |
39 | /// STUBS ///
40 | function getPassport(address /*user*/) external pure returns (Credential[] memory) {
41 | Credential[] memory credentials = new Credential[](1);
42 | credentials[0] = Credential({
43 | provider: "MockProvider",
44 | hash: keccak256("MockHash"),
45 | time: 1234567890123456789,
46 | expirationTime: 1234567890123456789 + 1 days
47 | });
48 | return credentials;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/test/MockHats.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.20;
3 |
4 | import {IHatsMinimal} from "../extensions/interfaces/IHatsMinimal.sol";
5 |
6 | /// @title Mock Hats Protocol Contract
7 | /// @notice This contract is a mock implementation of the IHatsMinimal interface for testing purposes.
8 | /// @dev It simulates the behavior of a real Hats protocol contract by providing predefined functionality
9 | /// for minting and checking hats.
10 | contract MockHats is IHatsMinimal {
11 | /// @notice A mapping to store the hats worn by each wearer address.
12 | mapping(address => uint256[]) private mockedWearers;
13 |
14 | /// @notice Constructor to initialize the mock contract with predefined hats and wearers.
15 | /// @param _hatsIds An array of hat IDs.
16 | /// @param _wearers An array of wearer addresses corresponding to the hat IDs.
17 | constructor(uint256[] memory _hatsIds, address[] memory _wearers) {
18 | for (uint256 i = 0; i < _hatsIds.length; i++) {
19 | mintHat(_hatsIds[i], _wearers[i]);
20 | }
21 | }
22 |
23 | /// @notice Mock function to mint a hat for a wearer.
24 | /// @dev This function simulates the minting of a hat by adding the hat ID to the wearer's list of hats.
25 | /// @param _hatId The ID of the hat to mint.
26 | /// @param _wearer The address of the wearer to mint the hat for.
27 | /// @return success A boolean indicating the success of the operation (always returns true).
28 | function mintHat(uint256 _hatId, address _wearer) public returns (bool success) {
29 | mockedWearers[_wearer].push(_hatId);
30 | return true;
31 | }
32 |
33 | /// @notice Mock function to check if an account is wearing a specific hat.
34 | /// @dev This function checks if the hat ID is present in the wearer's list of hats.
35 | /// @param account The address of the account to check.
36 | /// @param hat The ID of the hat to check.
37 | /// @return True if the account is wearing the hat, false otherwise.
38 | function isWearerOfHat(address account, uint256 hat) external view returns (bool) {
39 | uint256[] memory hats = mockedWearers[account];
40 | for (uint256 i = 0; i < hats.length; i++) {
41 | if (hats[i] == hat) {
42 | return true;
43 | }
44 | }
45 | return false;
46 | }
47 |
48 | /// STUBS ///
49 | function mintTopHat(
50 | address /*_target*/,
51 | string calldata /*_details*/,
52 | string calldata /*_imageURI*/
53 | ) external pure returns (uint256) {
54 | return 0;
55 | }
56 |
57 | function createHat(
58 | uint256 /*_admin*/,
59 | string calldata /*_details*/,
60 | uint32 /*_maxSupply*/,
61 | address /*_eligibility*/,
62 | address /*_toggle*/,
63 | bool /*_mutable*/,
64 | string calldata /*_imageURI*/
65 | ) external pure returns (uint256) {
66 | return 0;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/excubiae/contracts/test/MockSemaphore.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {ISemaphore} from "@semaphore-protocol/contracts/interfaces/ISemaphore.sol";
5 |
6 | /// @title Mock Semaphore Contract
7 | /// @notice This contract is a mock implementation of the ISemaphore interface for testing purposes.
8 | /// @dev It simulates the behavior of a real Semaphore contract by simulating the storage and verification
9 | /// of a set of predefined mocked proofs.
10 | contract MockSemaphore is ISemaphore {
11 | /// @dev Gets a group id and returns the relative group.
12 | mapping(uint256 => bool) public mockedGroups;
13 |
14 | /// @notice A mapping to store mocked proofs by their unique nullifiers.
15 | mapping(uint256 => bool) private mockedProofs;
16 |
17 | /// @dev Counter to assign an incremental id to the groups.
18 | /// This counter is used to keep track of the number of groups created.
19 | uint256 public groupCounter;
20 |
21 | /// MOCKS ///
22 | /// @notice Constructor to initialize the mock contract with predefined proofs.
23 | /// @param _groupIds An array of identifiers of groups to be intended as the contract managed groups.
24 | /// @param _nullifiers An array of nullifiers to be mocked as proofs.
25 | /// @param _validities An array of booleans to mock the validity of proofs associated with the nullifiers.
26 | constructor(uint256[] memory _groupIds, uint256[] memory _nullifiers, bool[] memory _validities) {
27 | for (uint256 i = 0; i < _groupIds.length; i++) {
28 | mockedGroups[_groupIds[i]] = true;
29 | groupCounter++;
30 | }
31 |
32 | for (uint256 i = 0; i < _nullifiers.length; i++) {
33 | mockedProofs[_nullifiers[i]] = _validities[i];
34 | }
35 | }
36 |
37 | function verifyProof(uint256 groupId, SemaphoreProof calldata proof) external view returns (bool) {
38 | return mockedGroups[groupId] && mockedProofs[proof.nullifier];
39 | }
40 |
41 | /// STUBS ///
42 | // The following functions are stubs and do not perform any meaningful operations.
43 | // They are placeholders to comply with the IEAS interface.
44 | function createGroup() external pure override returns (uint256) {
45 | return 0;
46 | }
47 |
48 | function createGroup(address /*admin*/) external pure override returns (uint256) {
49 | return 0;
50 | }
51 |
52 | function createGroup(address /*admin*/, uint256 /*merkleTreeDuration*/) external pure override returns (uint256) {
53 | return 0;
54 | }
55 |
56 | function updateGroupAdmin(uint256 /*groupId*/, address /*newAdmin*/) external override {}
57 |
58 | function acceptGroupAdmin(uint256 /*groupId*/) external override {}
59 |
60 | function updateGroupMerkleTreeDuration(uint256 /*groupId*/, uint256 /*newMerkleTreeDuration*/) external override {}
61 |
62 | function addMember(uint256 groupId, uint256 identityCommitment) external override {}
63 |
64 | function addMembers(uint256 groupId, uint256[] calldata identityCommitments) external override {}
65 |
66 | function updateMember(
67 | uint256 /*groupId*/,
68 | uint256 /*oldIdentityCommitment*/,
69 | uint256 /*newIdentityCommitment*/,
70 | uint256[] calldata /*merkleProofSiblings*/
71 | ) external override {}
72 |
73 | function removeMember(
74 | uint256 /*groupId*/,
75 | uint256 /*identityCommitment*/,
76 | uint256[] calldata /*merkleProofSiblings*/
77 | ) external override {}
78 |
79 | function validateProof(uint256 /*groupId*/, SemaphoreProof calldata /*proof*/) external override {}
80 | }
81 |
--------------------------------------------------------------------------------
/packages/excubiae/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import "@nomicfoundation/hardhat-toolbox"
2 | import { HardhatUserConfig } from "hardhat/config"
3 | import "dotenv/config"
4 |
5 | const TEST_MNEMONIC = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"
6 |
7 | const hardhatConfig: HardhatUserConfig = {
8 | networks: {
9 | hardhat: {
10 | accounts: {
11 | mnemonic: TEST_MNEMONIC,
12 | path: "m/44'/60'/0'/0",
13 | initialIndex: 0,
14 | count: 20
15 | }
16 | }
17 | },
18 | solidity: {
19 | version: "0.8.23",
20 | settings: {
21 | optimizer: {
22 | enabled: true
23 | }
24 | }
25 | },
26 | gasReporter: {
27 | currency: "USD",
28 | enabled: process.env.REPORT_GAS === "true",
29 | outputJSONFile: "gas-report-excubiae.json",
30 | outputJSON: process.env.REPORT_GAS_OUTPUT_JSON === "true"
31 | },
32 | typechain: {
33 | target: "ethers-v6"
34 | }
35 | }
36 |
37 | export default hardhatConfig
38 |
--------------------------------------------------------------------------------
/packages/excubiae/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "excubiae",
3 | "description": "a flexible & modular framework for general purpose on-chain gatekeepers.",
4 | "private": true,
5 | "scripts": {
6 | "start": "hardhat node",
7 | "compile": "hardhat compile",
8 | "test": "hardhat test",
9 | "test:report-gas": "REPORT_GAS=true hardhat test",
10 | "test:coverage": "hardhat coverage",
11 | "typechain": "hardhat typechain",
12 | "lint": "solhint 'contracts/**/*.sol'",
13 | "slither": "slither . --include-paths contracts --exclude-dependencies --ignore-compile"
14 | },
15 | "devDependencies": {
16 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.3",
17 | "@nomicfoundation/hardhat-ethers": "^3.0.0",
18 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
19 | "@nomicfoundation/hardhat-toolbox": "^4.0.0",
20 | "@nomicfoundation/hardhat-verify": "^2.0.0",
21 | "@typechain/ethers-v6": "^0.5.0",
22 | "@typechain/hardhat": "^9.0.0",
23 | "@types/chai": "^4.2.0",
24 | "@types/mocha": "^10.0.6",
25 | "@types/node": "^20.10.7",
26 | "chai": "^4.2.0",
27 | "ethers": "^6.4.0",
28 | "hardhat": "^2.19.4",
29 | "hardhat-gas-reporter": "^2.2.0",
30 | "prettier-plugin-solidity": "^1.3.1",
31 | "solhint": "^3.3.6",
32 | "solhint-plugin-prettier": "^0.1.0",
33 | "solidity-coverage": "^0.8.0",
34 | "ts-node": "^10.9.2",
35 | "typechain": "^8.3.0",
36 | "typescript": "^5.3.3"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/excubiae/tasks/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/privacy-scaling-explorations/zk-kit.solidity/309b8d8d7d4f553ef44e3aa672040a3698e5179c/packages/excubiae/tasks/.gitkeep
--------------------------------------------------------------------------------
/packages/excubiae/test/EASExcubia.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai"
2 | import { ethers } from "hardhat"
3 | import { AbiCoder, Signer, ZeroAddress, toBeHex, zeroPadBytes } from "ethers"
4 | import { EASExcubia, EASExcubia__factory, MockEAS, MockEAS__factory } from "../typechain-types"
5 |
6 | describe("EASExcubia", function () {
7 | let MockEASContract: MockEAS__factory
8 | let EASExcubiaContract: EASExcubia__factory
9 | let easExcubia: EASExcubia
10 |
11 | let signer: Signer
12 | let signerAddress: string
13 |
14 | let gate: Signer
15 | let gateAddress: string
16 |
17 | let mockEAS: MockEAS
18 | let mockEASAddress: string
19 |
20 | const schemaId = "0xfdcfdad2dbe7489e0ce56b260348b7f14e8365a8a325aef9834818c00d46b31b"
21 | const validAttestationId = AbiCoder.defaultAbiCoder().encode(["bytes32"], [zeroPadBytes(toBeHex(1), 32)])
22 | const revokedAttestationId = AbiCoder.defaultAbiCoder().encode(["bytes32"], [zeroPadBytes(toBeHex(2), 32)])
23 | const invalidSchemaAttestationId = AbiCoder.defaultAbiCoder().encode(["bytes32"], [zeroPadBytes(toBeHex(3), 32)])
24 | const invalidRecipientAttestationId = AbiCoder.defaultAbiCoder().encode(["bytes32"], [zeroPadBytes(toBeHex(4), 32)])
25 | const invalidAttesterAttestationId = AbiCoder.defaultAbiCoder().encode(["bytes32"], [zeroPadBytes(toBeHex(5), 32)])
26 |
27 | before(async function () {
28 | ;[signer, gate] = await ethers.getSigners()
29 | signerAddress = await signer.getAddress()
30 | gateAddress = await gate.getAddress()
31 |
32 | MockEASContract = await ethers.getContractFactory("MockEAS")
33 | mockEAS = await MockEASContract.deploy(signerAddress, signerAddress, schemaId)
34 | mockEASAddress = await mockEAS.getAddress()
35 |
36 | EASExcubiaContract = await ethers.getContractFactory("EASExcubia")
37 | easExcubia = await EASExcubiaContract.deploy(mockEASAddress, signerAddress, schemaId)
38 | })
39 |
40 | describe("constructor()", function () {
41 | it("Should deploy the EASExcubia contract correctly", async function () {
42 | expect(easExcubia).to.not.eq(undefined)
43 | })
44 |
45 | it("Should deploy the MockEAS contract correctly", async function () {
46 | expect(mockEAS).to.not.eq(undefined)
47 | })
48 |
49 | it("Should fail to deploy EASExcubia when eas parameter is not valid", async () => {
50 | await expect(EASExcubiaContract.deploy(ZeroAddress, ZeroAddress, schemaId)).to.be.revertedWithCustomError(
51 | easExcubia,
52 | "ZeroAddress"
53 | )
54 | })
55 |
56 | it("Should fail to deploy EASExcubia when attester parameter is not valid", async () => {
57 | await expect(
58 | EASExcubiaContract.deploy(await easExcubia.getAddress(), ZeroAddress, schemaId)
59 | ).to.be.revertedWithCustomError(easExcubia, "ZeroAddress")
60 | })
61 | })
62 |
63 | describe("trait()", function () {
64 | it("should return the trait of the Excubia contract", async () => {
65 | expect(await easExcubia.trait()).to.be.equal("EAS")
66 | })
67 | })
68 |
69 | describe("setGate()", function () {
70 | it("should fail to set the gate when the caller is not the owner", async () => {
71 | const [, notOwnerSigner] = await ethers.getSigners()
72 |
73 | await expect(easExcubia.connect(notOwnerSigner).setGate(gateAddress)).to.be.revertedWithCustomError(
74 | easExcubia,
75 | "OwnableUnauthorizedAccount"
76 | )
77 | })
78 |
79 | it("should fail to set the gate when the gate address is zero", async () => {
80 | await expect(easExcubia.setGate(ZeroAddress)).to.be.revertedWithCustomError(easExcubia, "ZeroAddress")
81 | })
82 |
83 | it("Should set the gate contract address correctly", async function () {
84 | const tx = await easExcubia.setGate(gateAddress)
85 | const receipt = await tx.wait()
86 | const event = EASExcubiaContract.interface.parseLog(
87 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
88 | ) as unknown as {
89 | args: {
90 | gate: string
91 | }
92 | }
93 |
94 | expect(receipt?.status).to.eq(1)
95 | expect(event.args.gate).to.eq(gateAddress)
96 | expect(await easExcubia.gate()).to.eq(gateAddress)
97 | })
98 |
99 | it("Should fail to set the gate if already set", async function () {
100 | await expect(easExcubia.setGate(gateAddress)).to.be.revertedWithCustomError(easExcubia, "GateAlreadySet")
101 | })
102 | })
103 |
104 | describe("check()", function () {
105 | it("should throw when the attestation is not owned by the correct recipient", async () => {
106 | await expect(easExcubia.check(signerAddress, invalidRecipientAttestationId)).to.be.revertedWithCustomError(
107 | easExcubia,
108 | "UnexpectedRecipient"
109 | )
110 | })
111 |
112 | it("should throw when the attestation has been revoked", async () => {
113 | await expect(easExcubia.check(signerAddress, revokedAttestationId)).to.be.revertedWithCustomError(
114 | easExcubia,
115 | "RevokedAttestation"
116 | )
117 | })
118 |
119 | it("should throw when the attestation schema is not the one expected", async () => {
120 | await expect(easExcubia.check(signerAddress, invalidSchemaAttestationId)).to.be.revertedWithCustomError(
121 | easExcubia,
122 | "UnexpectedSchema"
123 | )
124 | })
125 |
126 | it("should throw when the attestation is not signed by the attestation owner", async () => {
127 | await expect(easExcubia.check(signerAddress, invalidAttesterAttestationId)).to.be.revertedWithCustomError(
128 | easExcubia,
129 | "UnexpectedAttester"
130 | )
131 | })
132 |
133 | it("should check", async () => {
134 | await expect(easExcubia.check(signerAddress, validAttestationId)).to.not.be.reverted
135 |
136 | // check does NOT change the state of the contract (see pass()).
137 | expect(await easExcubia.passedAttestations(validAttestationId)).to.be.false
138 | })
139 | })
140 |
141 | describe("pass()", function () {
142 | it("should throw when the callee is not the gate", async () => {
143 | await expect(
144 | easExcubia.connect(signer).pass(signerAddress, invalidRecipientAttestationId)
145 | ).to.be.revertedWithCustomError(easExcubia, "GateOnly")
146 | })
147 |
148 | it("should throw when the attestation is not owned by the correct recipient", async () => {
149 | await expect(
150 | easExcubia.connect(gate).pass(signerAddress, invalidRecipientAttestationId)
151 | ).to.be.revertedWithCustomError(easExcubia, "UnexpectedRecipient")
152 | })
153 |
154 | it("should throw when the attestation has been revoked", async () => {
155 | await expect(
156 | easExcubia.connect(gate).pass(signerAddress, revokedAttestationId)
157 | ).to.be.revertedWithCustomError(easExcubia, "RevokedAttestation")
158 | })
159 |
160 | it("should throw when the attestation schema is not the one expected", async () => {
161 | await expect(
162 | easExcubia.connect(gate).pass(signerAddress, invalidSchemaAttestationId)
163 | ).to.be.revertedWithCustomError(easExcubia, "UnexpectedSchema")
164 | })
165 |
166 | it("should throw when the attestation is not signed by the attestation owner", async () => {
167 | await expect(
168 | easExcubia.connect(gate).pass(signerAddress, invalidAttesterAttestationId)
169 | ).to.be.revertedWithCustomError(easExcubia, "UnexpectedAttester")
170 | })
171 |
172 | it("should pass", async () => {
173 | const tx = await easExcubia.connect(gate).pass(signerAddress, validAttestationId)
174 | const receipt = await tx.wait()
175 | const event = EASExcubiaContract.interface.parseLog(
176 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
177 | ) as unknown as {
178 | args: {
179 | passerby: string
180 | gate: string
181 | }
182 | }
183 |
184 | expect(receipt?.status).to.eq(1)
185 | expect(event.args.passerby).to.eq(signerAddress)
186 | expect(event.args.gate).to.eq(gateAddress)
187 | expect(await easExcubia.passedAttestations(validAttestationId)).to.be.true
188 | })
189 |
190 | it("should prevent to pass twice", async () => {
191 | await expect(
192 | easExcubia.connect(gate).pass(signerAddress, validAttestationId)
193 | ).to.be.revertedWithCustomError(easExcubia, "AlreadyPassed")
194 | })
195 | })
196 | })
197 |
--------------------------------------------------------------------------------
/packages/excubiae/test/ERC721Excubia.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai"
2 | import { ethers } from "hardhat"
3 | import { AbiCoder, Signer, ZeroAddress } from "ethers"
4 | import { ERC721Excubia, ERC721Excubia__factory, MockERC721, MockERC721__factory } from "../typechain-types"
5 |
6 | describe("ERC721Excubia", function () {
7 | let MockERC721Contract: MockERC721__factory
8 | let ERC721ExcubiaContract: ERC721Excubia__factory
9 | let erc721Excubia: ERC721Excubia
10 |
11 | let signer: Signer
12 | let signerAddress: string
13 |
14 | let gate: Signer
15 | let gateAddress: string
16 |
17 | let anotherTokenOwner: Signer
18 | let anotherTokenOwnerAddress: string
19 |
20 | let mockERC721: MockERC721
21 | let mockERC721Address: string
22 |
23 | const rawValidTokenId = 0
24 | const rawInvalidOwnerTokenId = 1
25 |
26 | const encodedValidTokenId = AbiCoder.defaultAbiCoder().encode(["uint256"], [rawValidTokenId])
27 | const encodedInvalidOwnerTokenId = AbiCoder.defaultAbiCoder().encode(["uint256"], [rawInvalidOwnerTokenId])
28 |
29 | before(async function () {
30 | ;[signer, gate, anotherTokenOwner] = await ethers.getSigners()
31 | signerAddress = await signer.getAddress()
32 | gateAddress = await gate.getAddress()
33 | anotherTokenOwnerAddress = await anotherTokenOwner.getAddress()
34 |
35 | MockERC721Contract = await ethers.getContractFactory("MockERC721")
36 | mockERC721 = await MockERC721Contract.deploy()
37 | mockERC721Address = await mockERC721.getAddress()
38 |
39 | // assign to `signerAddress` token with id equal to `1`.
40 | await mockERC721.mintAndGiveToken(signerAddress)
41 | await mockERC721.mintAndGiveToken(anotherTokenOwnerAddress)
42 |
43 | ERC721ExcubiaContract = await ethers.getContractFactory("ERC721Excubia")
44 | erc721Excubia = await ERC721ExcubiaContract.deploy(mockERC721Address)
45 | })
46 |
47 | describe("constructor()", function () {
48 | it("Should deploy the ERC721Excubia contract correctly", async function () {
49 | expect(erc721Excubia).to.not.eq(undefined)
50 | })
51 |
52 | it("Should deploy the MockERC721 contract correctly", async function () {
53 | expect(mockERC721).to.not.eq(undefined)
54 | })
55 |
56 | it("Should fail to deploy ERC721Excubia when erc721 parameter is not valid", async () => {
57 | await expect(ERC721ExcubiaContract.deploy(ZeroAddress)).to.be.revertedWithCustomError(
58 | erc721Excubia,
59 | "ZeroAddress"
60 | )
61 | })
62 | })
63 |
64 | describe("trait()", function () {
65 | it("should return the trait of the Excubia contract", async () => {
66 | expect(await erc721Excubia.trait()).to.be.equal("ERC721")
67 | })
68 | })
69 |
70 | describe("setGate()", function () {
71 | it("should fail to set the gate when the caller is not the owner", async () => {
72 | const [, notOwnerSigner] = await ethers.getSigners()
73 |
74 | await expect(erc721Excubia.connect(notOwnerSigner).setGate(gateAddress)).to.be.revertedWithCustomError(
75 | erc721Excubia,
76 | "OwnableUnauthorizedAccount"
77 | )
78 | })
79 |
80 | it("should fail to set the gate when the gate address is zero", async () => {
81 | await expect(erc721Excubia.setGate(ZeroAddress)).to.be.revertedWithCustomError(erc721Excubia, "ZeroAddress")
82 | })
83 |
84 | it("Should set the gate contract address correctly", async function () {
85 | const tx = await erc721Excubia.setGate(gateAddress)
86 | const receipt = await tx.wait()
87 | const event = ERC721ExcubiaContract.interface.parseLog(
88 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
89 | ) as unknown as {
90 | args: {
91 | gate: string
92 | }
93 | }
94 |
95 | expect(receipt?.status).to.eq(1)
96 | expect(event.args.gate).to.eq(gateAddress)
97 | expect(await erc721Excubia.gate()).to.eq(gateAddress)
98 | })
99 |
100 | it("Should fail to set the gate if already set", async function () {
101 | await expect(erc721Excubia.setGate(gateAddress)).to.be.revertedWithCustomError(
102 | erc721Excubia,
103 | "GateAlreadySet"
104 | )
105 | })
106 | })
107 |
108 | describe("check()", function () {
109 | it("should throw when the token id is not owned by the correct recipient", async () => {
110 | expect(await mockERC721.ownerOf(encodedInvalidOwnerTokenId)).to.be.equal(anotherTokenOwnerAddress)
111 |
112 | await expect(erc721Excubia.check(signerAddress, encodedInvalidOwnerTokenId)).to.be.revertedWithCustomError(
113 | erc721Excubia,
114 | "UnexpectedTokenOwner"
115 | )
116 | })
117 |
118 | it("should check", async () => {
119 | await expect(erc721Excubia.check(signerAddress, encodedValidTokenId)).to.not.be.reverted
120 |
121 | // check does NOT change the state of the contract (see pass()).
122 | expect(await erc721Excubia.passedTokenIds(rawValidTokenId)).to.be.false
123 | })
124 | })
125 |
126 | describe("pass()", function () {
127 | it("should throw when the callee is not the gate", async () => {
128 | await expect(
129 | erc721Excubia.connect(signer).pass(signerAddress, encodedInvalidOwnerTokenId)
130 | ).to.be.revertedWithCustomError(erc721Excubia, "GateOnly")
131 | })
132 |
133 | it("should throw when the token id is not owned by the correct recipient", async () => {
134 | await expect(
135 | erc721Excubia.connect(gate).pass(signerAddress, encodedInvalidOwnerTokenId)
136 | ).to.be.revertedWithCustomError(erc721Excubia, "UnexpectedTokenOwner")
137 | })
138 |
139 | it("should pass", async () => {
140 | const tx = await erc721Excubia.connect(gate).pass(signerAddress, encodedValidTokenId)
141 | const receipt = await tx.wait()
142 | const event = ERC721ExcubiaContract.interface.parseLog(
143 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
144 | ) as unknown as {
145 | args: {
146 | passerby: string
147 | gate: string
148 | }
149 | }
150 |
151 | expect(receipt?.status).to.eq(1)
152 | expect(event.args.passerby).to.eq(signerAddress)
153 | expect(event.args.gate).to.eq(gateAddress)
154 | expect(await erc721Excubia.passedTokenIds(rawValidTokenId)).to.be.true
155 | })
156 |
157 | it("should prevent to pass twice", async () => {
158 | await expect(
159 | erc721Excubia.connect(gate).pass(signerAddress, encodedValidTokenId)
160 | ).to.be.revertedWithCustomError(erc721Excubia, "AlreadyPassed")
161 | })
162 | })
163 | })
164 |
--------------------------------------------------------------------------------
/packages/excubiae/test/FreeForAllExcubia.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai"
2 | import { ethers } from "hardhat"
3 | import { Signer, ZeroAddress, ZeroHash } from "ethers"
4 | import { FreeForAllExcubia, FreeForAllExcubia__factory } from "../typechain-types"
5 |
6 | describe("FreeForAllExcubia", function () {
7 | let FreeForAllExcubiaContract: FreeForAllExcubia__factory
8 | let freeForAllExcubia: FreeForAllExcubia
9 |
10 | let signer: Signer
11 | let signerAddress: string
12 |
13 | let gate: Signer
14 | let gateAddress: string
15 |
16 | before(async function () {
17 | ;[signer, gate] = await ethers.getSigners()
18 | signerAddress = await signer.getAddress()
19 | gateAddress = await gate.getAddress()
20 |
21 | FreeForAllExcubiaContract = await ethers.getContractFactory("FreeForAllExcubia")
22 | freeForAllExcubia = await FreeForAllExcubiaContract.deploy()
23 | })
24 |
25 | describe("constructor()", function () {
26 | it("Should deploy the FreeForAllExcubia contract correctly", async function () {
27 | expect(freeForAllExcubia).to.not.eq(undefined)
28 | })
29 | })
30 |
31 | describe("trait()", function () {
32 | it("should return the trait of the Excubia contract", async () => {
33 | expect(await freeForAllExcubia.trait()).to.be.equal("FreeForAll")
34 | })
35 | })
36 |
37 | describe("setGate()", function () {
38 | it("should fail to set the gate when the caller is not the owner", async () => {
39 | const [, notOwnerSigner] = await ethers.getSigners()
40 |
41 | await expect(freeForAllExcubia.connect(notOwnerSigner).setGate(gateAddress)).to.be.revertedWithCustomError(
42 | freeForAllExcubia,
43 | "OwnableUnauthorizedAccount"
44 | )
45 | })
46 |
47 | it("should fail to set the gate when the gate address is zero", async () => {
48 | await expect(freeForAllExcubia.setGate(ZeroAddress)).to.be.revertedWithCustomError(
49 | freeForAllExcubia,
50 | "ZeroAddress"
51 | )
52 | })
53 |
54 | it("Should set the gate contract address correctly", async function () {
55 | const tx = await freeForAllExcubia.setGate(gateAddress)
56 | const receipt = await tx.wait()
57 | const event = FreeForAllExcubiaContract.interface.parseLog(
58 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
59 | ) as unknown as {
60 | args: {
61 | gate: string
62 | }
63 | }
64 |
65 | expect(receipt?.status).to.eq(1)
66 | expect(event.args.gate).to.eq(gateAddress)
67 | expect(await freeForAllExcubia.gate()).to.eq(gateAddress)
68 | })
69 |
70 | it("Should fail to set the gate if already set", async function () {
71 | await expect(freeForAllExcubia.setGate(gateAddress)).to.be.revertedWithCustomError(
72 | freeForAllExcubia,
73 | "GateAlreadySet"
74 | )
75 | })
76 | })
77 |
78 | describe("check()", function () {
79 | it("should check", async () => {
80 | // `data` parameter value can be whatever (e.g., ZeroHash default).
81 | await expect(freeForAllExcubia.check(signerAddress, ZeroHash)).to.not.be.reverted
82 |
83 | // check does NOT change the state of the contract (see pass()).
84 | expect(await freeForAllExcubia.passedPassersby(signerAddress)).to.be.false
85 | })
86 | })
87 |
88 | describe("pass()", function () {
89 | it("should throw when the callee is not the gate", async () => {
90 | await expect(
91 | // `data` parameter value can be whatever (e.g., ZeroHash default).
92 | freeForAllExcubia.connect(signer).pass(signerAddress, ZeroHash)
93 | ).to.be.revertedWithCustomError(freeForAllExcubia, "GateOnly")
94 | })
95 |
96 | it("should pass", async () => {
97 | // `data` parameter value can be whatever (e.g., ZeroHash default).
98 | const tx = await freeForAllExcubia.connect(gate).pass(signerAddress, ZeroHash)
99 | const receipt = await tx.wait()
100 | const event = FreeForAllExcubiaContract.interface.parseLog(
101 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
102 | ) as unknown as {
103 | args: {
104 | passerby: string
105 | gate: string
106 | }
107 | }
108 |
109 | expect(receipt?.status).to.eq(1)
110 | expect(event.args.passerby).to.eq(signerAddress)
111 | expect(event.args.gate).to.eq(gateAddress)
112 | expect(await freeForAllExcubia.passedPassersby(signerAddress)).to.be.true
113 | })
114 |
115 | it("should prevent to pass twice", async () => {
116 | await expect(
117 | // `data` parameter value can be whatever (e.g., ZeroHash default).
118 | freeForAllExcubia.connect(gate).pass(signerAddress, ZeroHash)
119 | ).to.be.revertedWithCustomError(freeForAllExcubia, "AlreadyPassed")
120 | })
121 | })
122 | })
123 |
--------------------------------------------------------------------------------
/packages/excubiae/test/GitcoinPassportExcubia.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai"
2 | import { ethers } from "hardhat"
3 | import { AbiCoder, Signer, ZeroAddress, toBeHex, zeroPadBytes } from "ethers"
4 | import {
5 | GitcoinPassportExcubia,
6 | GitcoinPassportExcubia__factory,
7 | MockGitcoinPassportDecoder,
8 | MockGitcoinPassportDecoder__factory
9 | } from "../typechain-types"
10 |
11 | describe("GitcoinPassportExcubia", function () {
12 | let MockGitcoinPassportDecoderContract: MockGitcoinPassportDecoder__factory
13 | let GitcoinPassportExcubiaContract: GitcoinPassportExcubia__factory
14 | let gitcoinPassportExcubia: GitcoinPassportExcubia
15 |
16 | let signer: Signer
17 | let signerAddress: string
18 |
19 | let gate: Signer
20 | let gateAddress: string
21 |
22 | let notEnoughScoreSigner: Signer
23 | let notEnoughScoreSignerAddress: string
24 |
25 | let mockGitcoinPassportDecoder: MockGitcoinPassportDecoder
26 | let mockGitcoinPassportDecoderAddress: string
27 |
28 | const thresholdScore = 35
29 |
30 | // Score is 4 digit with 2 decimals (5000 is 50.00).
31 | const validUserScore = 5000
32 | const invalidUserScore = 1000
33 |
34 | // This Excubia do not need any external encoded data for check & pass logic.
35 | const encodedDummyData = AbiCoder.defaultAbiCoder().encode(["uint256"], [0])
36 |
37 | before(async function () {
38 | ;[signer, gate, notEnoughScoreSigner] = await ethers.getSigners()
39 | signerAddress = await signer.getAddress()
40 | gateAddress = await gate.getAddress()
41 | notEnoughScoreSignerAddress = await notEnoughScoreSigner.getAddress()
42 |
43 | MockGitcoinPassportDecoderContract = await ethers.getContractFactory("MockGitcoinPassportDecoder")
44 | mockGitcoinPassportDecoder = await MockGitcoinPassportDecoderContract.deploy(
45 | [signerAddress, notEnoughScoreSignerAddress],
46 | [validUserScore, invalidUserScore]
47 | )
48 | mockGitcoinPassportDecoderAddress = await mockGitcoinPassportDecoder.getAddress()
49 |
50 | GitcoinPassportExcubiaContract = await ethers.getContractFactory("GitcoinPassportExcubia")
51 | gitcoinPassportExcubia = await GitcoinPassportExcubiaContract.deploy(
52 | mockGitcoinPassportDecoderAddress,
53 | thresholdScore
54 | )
55 | })
56 |
57 | describe("constructor()", function () {
58 | it("Should deploy the GitcoinPassportExcubia contract correctly", async function () {
59 | expect(gitcoinPassportExcubia).to.not.eq(undefined)
60 | })
61 |
62 | it("Should deploy the MockGitcoinPassportDecoder contract correctly", async function () {
63 | expect(mockGitcoinPassportDecoder).to.not.eq(undefined)
64 | })
65 |
66 | it("Should fail to deploy GitcoinPassportExcubia when decoder parameter is not valid", async () => {
67 | await expect(
68 | GitcoinPassportExcubiaContract.deploy(ZeroAddress, thresholdScore)
69 | ).to.be.revertedWithCustomError(gitcoinPassportExcubia, "ZeroAddress")
70 | })
71 |
72 | it("Should fail to deploy GitcoinPassportExcubia when thresholdScore parameter is not valid", async () => {
73 | await expect(
74 | GitcoinPassportExcubiaContract.deploy(mockGitcoinPassportDecoderAddress, 0)
75 | ).to.be.revertedWithCustomError(gitcoinPassportExcubia, "NegativeOrZeroThresholdScore")
76 | })
77 | })
78 |
79 | describe("trait()", function () {
80 | it("should return the trait of the Excubia contract", async () => {
81 | expect(await gitcoinPassportExcubia.trait()).to.be.equal("GitcoinPassport")
82 | })
83 | })
84 |
85 | describe("setGate()", function () {
86 | it("should fail to set the gate when the caller is not the owner", async () => {
87 | const [, notOwnerSigner] = await ethers.getSigners()
88 |
89 | await expect(
90 | gitcoinPassportExcubia.connect(notOwnerSigner).setGate(gateAddress)
91 | ).to.be.revertedWithCustomError(gitcoinPassportExcubia, "OwnableUnauthorizedAccount")
92 | })
93 |
94 | it("should fail to set the gate when the gate address is zero", async () => {
95 | await expect(gitcoinPassportExcubia.setGate(ZeroAddress)).to.be.revertedWithCustomError(
96 | gitcoinPassportExcubia,
97 | "ZeroAddress"
98 | )
99 | })
100 |
101 | it("Should set the gate contract address correctly", async function () {
102 | const tx = await gitcoinPassportExcubia.setGate(gateAddress)
103 | const receipt = await tx.wait()
104 | const event = GitcoinPassportExcubiaContract.interface.parseLog(
105 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
106 | ) as unknown as {
107 | args: {
108 | gate: string
109 | }
110 | }
111 |
112 | expect(receipt?.status).to.eq(1)
113 | expect(event.args.gate).to.eq(gateAddress)
114 | expect(await gitcoinPassportExcubia.gate()).to.eq(gateAddress)
115 | })
116 |
117 | it("Should fail to set the gate if already set", async function () {
118 | await expect(gitcoinPassportExcubia.setGate(gateAddress)).to.be.revertedWithCustomError(
119 | gitcoinPassportExcubia,
120 | "GateAlreadySet"
121 | )
122 | })
123 | })
124 |
125 | describe("check()", function () {
126 | it("should throw when the score is not enough", async () => {
127 | await expect(
128 | gitcoinPassportExcubia.check(notEnoughScoreSignerAddress, encodedDummyData)
129 | ).to.be.revertedWithCustomError(gitcoinPassportExcubia, "InsufficientScore")
130 | })
131 |
132 | it("should check", async () => {
133 | await gitcoinPassportExcubia.check(signerAddress, encodedDummyData)
134 | await expect(gitcoinPassportExcubia.check(signerAddress, encodedDummyData)).to.not.be.reverted
135 |
136 | // check does NOT change the state of the contract (see pass()).
137 | expect(await gitcoinPassportExcubia.passedUsers(signerAddress)).to.be.false
138 | })
139 | })
140 |
141 | describe("pass()", function () {
142 | it("should throw when the callee is not the gate", async () => {
143 | await expect(
144 | gitcoinPassportExcubia.connect(signer).pass(signerAddress, encodedDummyData)
145 | ).to.be.revertedWithCustomError(gitcoinPassportExcubia, "GateOnly")
146 | })
147 |
148 | it("should throw when the score is not enough", async () => {
149 | await expect(
150 | gitcoinPassportExcubia.connect(gate).pass(notEnoughScoreSignerAddress, encodedDummyData)
151 | ).to.be.revertedWithCustomError(gitcoinPassportExcubia, "InsufficientScore")
152 | })
153 |
154 | it("should pass", async () => {
155 | const tx = await gitcoinPassportExcubia.connect(gate).pass(signerAddress, encodedDummyData)
156 | const receipt = await tx.wait()
157 | const event = GitcoinPassportExcubiaContract.interface.parseLog(
158 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
159 | ) as unknown as {
160 | args: {
161 | passerby: string
162 | gate: string
163 | }
164 | }
165 |
166 | expect(receipt?.status).to.eq(1)
167 | expect(event.args.passerby).to.eq(signerAddress)
168 | expect(event.args.gate).to.eq(gateAddress)
169 | expect(await gitcoinPassportExcubia.passedUsers(signerAddress)).to.be.true
170 | })
171 |
172 | it("should prevent to pass twice", async () => {
173 | await expect(
174 | gitcoinPassportExcubia.connect(gate).pass(signerAddress, encodedDummyData)
175 | ).to.be.revertedWithCustomError(gitcoinPassportExcubia, "AlreadyPassed")
176 | })
177 | })
178 | })
179 |
--------------------------------------------------------------------------------
/packages/excubiae/test/HatsExcubia.test.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai"
2 | import { ethers } from "hardhat"
3 | import { AbiCoder, Signer, ZeroAddress, toBeHex, zeroPadBytes } from "ethers"
4 | import { HatsExcubia, HatsExcubia__factory, MockHats, MockHats__factory } from "../typechain-types"
5 |
6 | describe("HatsExcubia", function () {
7 | let MockHatsContract: MockHats__factory
8 | let HatsExcubiaContract: HatsExcubia__factory
9 | let hatsExcubia: HatsExcubia
10 |
11 | let signer: Signer
12 | let signerAddress: string
13 |
14 | let gate: Signer
15 | let gateAddress: string
16 |
17 | let notWearerSigner: Signer
18 | let notWearerSignerAddress: string
19 |
20 | let mockHats: MockHats
21 | let mockHatsAddress: string
22 |
23 | const criterionHatsIds = [1]
24 | const invalidCriterionHatsIds = [2, 3]
25 |
26 | const encodedValidCriterionHat = AbiCoder.defaultAbiCoder().encode(["uint256"], [criterionHatsIds[0]])
27 | const encodedInvalidCriterionHat = AbiCoder.defaultAbiCoder().encode(["uint256"], [invalidCriterionHatsIds[0]])
28 |
29 | before(async function () {
30 | ;[signer, gate, notWearerSigner] = await ethers.getSigners()
31 | signerAddress = await signer.getAddress()
32 | gateAddress = await gate.getAddress()
33 | notWearerSignerAddress = await notWearerSigner.getAddress()
34 |
35 | MockHatsContract = await ethers.getContractFactory("MockHats")
36 | mockHats = await MockHatsContract.deploy(criterionHatsIds, [signerAddress])
37 | mockHatsAddress = await mockHats.getAddress()
38 |
39 | HatsExcubiaContract = await ethers.getContractFactory("HatsExcubia")
40 | hatsExcubia = await HatsExcubiaContract.deploy(mockHatsAddress, criterionHatsIds)
41 | })
42 |
43 | describe("constructor()", function () {
44 | it("Should deploy the HatsExcubia contract correctly", async function () {
45 | expect(hatsExcubia).to.not.eq(undefined)
46 | })
47 |
48 | it("Should deploy the MockHats contract correctly", async function () {
49 | expect(mockHats).to.not.eq(undefined)
50 | })
51 |
52 | it("Should fail to deploy HatsExcubia when hats parameter is not valid", async () => {
53 | await expect(HatsExcubiaContract.deploy(ZeroAddress, criterionHatsIds)).to.be.revertedWithCustomError(
54 | hatsExcubia,
55 | "ZeroAddress"
56 | )
57 | })
58 |
59 | it("Should fail to deploy HatsExcubia when hats parameter is not valid", async () => {
60 | await expect(HatsExcubiaContract.deploy(mockHatsAddress, [])).to.be.revertedWithCustomError(
61 | hatsExcubia,
62 | "ZeroCriterionHats"
63 | )
64 | })
65 | })
66 |
67 | describe("trait()", function () {
68 | it("should return the trait of the Excubia contract", async () => {
69 | expect(await hatsExcubia.trait()).to.be.equal("Hats")
70 | })
71 | })
72 |
73 | describe("setGate()", function () {
74 | it("should fail to set the gate when the caller is not the owner", async () => {
75 | const [, notOwnerSigner] = await ethers.getSigners()
76 |
77 | await expect(hatsExcubia.connect(notOwnerSigner).setGate(gateAddress)).to.be.revertedWithCustomError(
78 | hatsExcubia,
79 | "OwnableUnauthorizedAccount"
80 | )
81 | })
82 |
83 | it("should fail to set the gate when the gate address is zero", async () => {
84 | await expect(hatsExcubia.setGate(ZeroAddress)).to.be.revertedWithCustomError(hatsExcubia, "ZeroAddress")
85 | })
86 |
87 | it("Should set the gate contract address correctly", async function () {
88 | const tx = await hatsExcubia.setGate(gateAddress)
89 | const receipt = await tx.wait()
90 | const event = HatsExcubiaContract.interface.parseLog(
91 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
92 | ) as unknown as {
93 | args: {
94 | gate: string
95 | }
96 | }
97 |
98 | expect(receipt?.status).to.eq(1)
99 | expect(event.args.gate).to.eq(gateAddress)
100 | expect(await hatsExcubia.gate()).to.eq(gateAddress)
101 | })
102 |
103 | it("Should fail to set the gate if already set", async function () {
104 | await expect(hatsExcubia.setGate(gateAddress)).to.be.revertedWithCustomError(hatsExcubia, "GateAlreadySet")
105 | })
106 | })
107 |
108 | describe("check()", function () {
109 | it("should throw when the hat is not a criterion one", async () => {
110 | await expect(hatsExcubia.check(signerAddress, encodedInvalidCriterionHat)).to.be.revertedWithCustomError(
111 | hatsExcubia,
112 | "NotCriterionHat"
113 | )
114 |
115 | expect(await hatsExcubia.criterionHat(invalidCriterionHatsIds[0])).to.be.false
116 | })
117 |
118 | it("should throw when the user is not wearing the criterion hat", async () => {
119 | await expect(
120 | hatsExcubia.check(notWearerSignerAddress, encodedValidCriterionHat)
121 | ).to.be.revertedWithCustomError(hatsExcubia, "NotWearingCriterionHat")
122 | })
123 |
124 | it("should check", async () => {
125 | await expect(hatsExcubia.check(signerAddress, encodedValidCriterionHat)).to.not.be.reverted
126 |
127 | // check does NOT change the state of the contract (see pass()).
128 | expect(await hatsExcubia.passedUsers(signerAddress)).to.be.false
129 | })
130 | })
131 |
132 | describe("pass()", function () {
133 | it("should throw when the callee is not the gate", async () => {
134 | await expect(
135 | hatsExcubia.connect(signer).pass(signerAddress, encodedValidCriterionHat)
136 | ).to.be.revertedWithCustomError(hatsExcubia, "GateOnly")
137 | })
138 |
139 | it("should throw when the hat is not a criterion one", async () => {
140 | await expect(
141 | hatsExcubia.connect(gate).pass(signerAddress, encodedInvalidCriterionHat)
142 | ).to.be.revertedWithCustomError(hatsExcubia, "NotCriterionHat")
143 |
144 | expect(await hatsExcubia.criterionHat(invalidCriterionHatsIds[0])).to.be.false
145 | })
146 |
147 | it("should throw when the user is not wearing the criterion hat", async () => {
148 | await expect(
149 | hatsExcubia.connect(gate).pass(notWearerSignerAddress, encodedValidCriterionHat)
150 | ).to.be.revertedWithCustomError(hatsExcubia, "NotWearingCriterionHat")
151 | })
152 |
153 | it("should pass", async () => {
154 | const tx = await hatsExcubia.connect(gate).pass(signerAddress, encodedValidCriterionHat)
155 | const receipt = await tx.wait()
156 | const event = HatsExcubiaContract.interface.parseLog(
157 | receipt?.logs[0] as unknown as { topics: string[]; data: string }
158 | ) as unknown as {
159 | args: {
160 | passerby: string
161 | gate: string
162 | }
163 | }
164 |
165 | expect(receipt?.status).to.eq(1)
166 | expect(event.args.passerby).to.eq(signerAddress)
167 | expect(event.args.gate).to.eq(gateAddress)
168 | expect(await hatsExcubia.passedUsers(signerAddress)).to.be.true
169 | })
170 |
171 | it("should prevent to pass twice", async () => {
172 | await expect(
173 | hatsExcubia.connect(gate).pass(signerAddress, encodedValidCriterionHat)
174 | ).to.be.revertedWithCustomError(hatsExcubia, "AlreadyPassed")
175 | })
176 | })
177 | })
178 |
--------------------------------------------------------------------------------
/packages/excubiae/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true
10 | },
11 | "include": ["scripts/**/*", "tasks/**/*", "test/**/*", "typechain-types/**/*"],
12 | "files": ["hardhat.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/imt/.env.example:
--------------------------------------------------------------------------------
1 | COINMARKETCAP_API_KEY=
2 | REPORT_GAS=false
3 | REPORT_GAS_OUTPUT_JSON=false
4 |
--------------------------------------------------------------------------------
/packages/imt/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "arrowParens": "always",
4 | "trailingComma": "none",
5 | "plugins": ["prettier-plugin-solidity"]
6 | }
7 |
--------------------------------------------------------------------------------
/packages/imt/.solcover.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | istanbulFolder: "../../coverage/imt"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/imt/.solhint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "solhint:recommended",
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "compiler-version": ["error", ">=0.8.0"],
6 | "const-name-snakecase": "off",
7 | "no-empty-blocks": "off",
8 | "constructor-syntax": "error",
9 | "func-visibility": ["error", { "ignoreConstructors": true }],
10 | "max-line-length": ["error", 120],
11 | "not-rely-on-time": "off",
12 | "prettier/prettier": [
13 | "error",
14 | {
15 | "endOfLine": "auto"
16 | }
17 | ],
18 | "reason-string": ["warn", { "maxLength": 80 }]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/imt/LICENSE:
--------------------------------------------------------------------------------
1 | contracts/LICENSE
--------------------------------------------------------------------------------
/packages/imt/README.md:
--------------------------------------------------------------------------------
1 | contracts/README.md
--------------------------------------------------------------------------------
/packages/imt/contracts/BinaryIMT.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.4;
3 |
4 | import {InternalBinaryIMT, BinaryIMTData} from "./InternalBinaryIMT.sol";
5 |
6 | library BinaryIMT {
7 | using InternalBinaryIMT for *;
8 |
9 | function defaultZero(uint256 index) public pure returns (uint256) {
10 | return InternalBinaryIMT._defaultZero(index);
11 | }
12 |
13 | function init(BinaryIMTData storage self, uint256 depth, uint256 zero) public {
14 | InternalBinaryIMT._init(self, depth, zero);
15 | }
16 |
17 | function initWithDefaultZeroes(BinaryIMTData storage self, uint256 depth) public {
18 | InternalBinaryIMT._initWithDefaultZeroes(self, depth);
19 | }
20 |
21 | function insert(BinaryIMTData storage self, uint256 leaf) public returns (uint256) {
22 | return InternalBinaryIMT._insert(self, leaf);
23 | }
24 |
25 | function update(
26 | BinaryIMTData storage self,
27 | uint256 leaf,
28 | uint256 newLeaf,
29 | uint256[] calldata proofSiblings,
30 | uint8[] calldata proofPathIndices
31 | ) public {
32 | InternalBinaryIMT._update(self, leaf, newLeaf, proofSiblings, proofPathIndices);
33 | }
34 |
35 | function remove(
36 | BinaryIMTData storage self,
37 | uint256 leaf,
38 | uint256[] calldata proofSiblings,
39 | uint8[] calldata proofPathIndices
40 | ) public {
41 | InternalBinaryIMT._remove(self, leaf, proofSiblings, proofPathIndices);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/imt/contracts/Constants.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.4;
3 |
4 | uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
5 | uint8 constant MAX_DEPTH = 32;
6 |
--------------------------------------------------------------------------------
/packages/imt/contracts/InternalQuinaryIMT.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.4;
3 |
4 | import {PoseidonT6} from "poseidon-solidity/PoseidonT6.sol";
5 | import {SNARK_SCALAR_FIELD, MAX_DEPTH} from "./Constants.sol";
6 |
7 | // Each incremental tree has certain properties and data that will
8 | // be used to add new leaves.
9 | struct QuinaryIMTData {
10 | uint256 depth; // Depth of the tree (levels - 1).
11 | uint256 root; // Root hash of the tree.
12 | uint256 numberOfLeaves; // Number of leaves of the tree.
13 | mapping(uint256 => uint256) zeroes; // Zero hashes used for empty nodes (level -> zero hash).
14 | // The nodes of the subtrees used in the last addition of a leaf (level -> [nodes]).
15 | mapping(uint256 => uint256[5]) lastSubtrees; // Caching these values is essential to efficient appends.
16 | }
17 |
18 | error ValueGreaterThanSnarkScalarField();
19 | error DepthNotSupported();
20 | error TreeIsFull();
21 | error NewLeafCannotEqualOldLeaf();
22 | error LeafDoesNotExist();
23 | error LeafIndexOutOfRange();
24 | error WrongMerkleProofPath();
25 |
26 | /// @title Incremental quinary Merkle tree.
27 | /// @dev The incremental tree allows to calculate the root hash each time a leaf is added, ensuring
28 | /// the integrity of the tree.
29 | library InternalQuinaryIMT {
30 | /// @dev Initializes a tree.
31 | /// @param self: Tree data.
32 | /// @param depth: Depth of the tree.
33 | /// @param zero: Zero value to be used.
34 | function _init(QuinaryIMTData storage self, uint256 depth, uint256 zero) internal {
35 | if (zero >= SNARK_SCALAR_FIELD) {
36 | revert ValueGreaterThanSnarkScalarField();
37 | } else if (depth <= 0 || depth > MAX_DEPTH) {
38 | revert DepthNotSupported();
39 | }
40 |
41 | self.depth = depth;
42 |
43 | for (uint8 i = 0; i < depth; ) {
44 | self.zeroes[i] = zero;
45 | uint256[5] memory zeroChildren;
46 |
47 | for (uint8 j = 0; j < 5; ) {
48 | zeroChildren[j] = zero;
49 | unchecked {
50 | ++j;
51 | }
52 | }
53 |
54 | zero = PoseidonT6.hash(zeroChildren);
55 |
56 | unchecked {
57 | ++i;
58 | }
59 | }
60 |
61 | self.root = zero;
62 | }
63 |
64 | /// @dev Inserts a leaf in the tree.
65 | /// @param self: Tree data.
66 | /// @param leaf: Leaf to be inserted.
67 | function _insert(QuinaryIMTData storage self, uint256 leaf) internal {
68 | uint256 depth = self.depth;
69 |
70 | if (leaf >= SNARK_SCALAR_FIELD) {
71 | revert ValueGreaterThanSnarkScalarField();
72 | } else if (self.numberOfLeaves >= 5 ** depth) {
73 | revert TreeIsFull();
74 | }
75 |
76 | uint256 index = self.numberOfLeaves;
77 | uint256 hash = leaf;
78 |
79 | for (uint8 i = 0; i < depth; ) {
80 | uint8 position = uint8(index % 5);
81 |
82 | self.lastSubtrees[i][position] = hash;
83 |
84 | if (position == 0) {
85 | for (uint8 j = 1; j < 5; ) {
86 | self.lastSubtrees[i][j] = self.zeroes[i];
87 | unchecked {
88 | ++j;
89 | }
90 | }
91 | }
92 |
93 | hash = PoseidonT6.hash(self.lastSubtrees[i]);
94 | index /= 5;
95 |
96 | unchecked {
97 | ++i;
98 | }
99 | }
100 |
101 | self.root = hash;
102 | self.numberOfLeaves += 1;
103 | }
104 |
105 | /// @dev Updates a leaf in the tree.
106 | /// @param self: Tree data.
107 | /// @param leaf: Leaf to be updated.
108 | /// @param newLeaf: New leaf.
109 | /// @param proofSiblings: Array of the sibling nodes of the proof of membership.
110 | /// @param proofPathIndices: Path of the proof of membership.
111 | function _update(
112 | QuinaryIMTData storage self,
113 | uint256 leaf,
114 | uint256 newLeaf,
115 | uint256[4][] calldata proofSiblings,
116 | uint8[] calldata proofPathIndices
117 | ) internal {
118 | if (newLeaf == leaf) {
119 | revert NewLeafCannotEqualOldLeaf();
120 | } else if (newLeaf >= SNARK_SCALAR_FIELD) {
121 | revert ValueGreaterThanSnarkScalarField();
122 | } else if (!_verify(self, leaf, proofSiblings, proofPathIndices)) {
123 | revert LeafDoesNotExist();
124 | }
125 |
126 | uint256 depth = self.depth;
127 | uint256 hash = newLeaf;
128 | uint256 updateIndex;
129 |
130 | for (uint8 i = 0; i < depth; ) {
131 | uint256[5] memory nodes;
132 | updateIndex += proofPathIndices[i] * 5 ** i;
133 |
134 | for (uint8 j = 0; j < 5; ) {
135 | if (j < proofPathIndices[i]) {
136 | nodes[j] = proofSiblings[i][j];
137 | } else if (j == proofPathIndices[i]) {
138 | nodes[j] = hash;
139 | } else {
140 | nodes[j] = proofSiblings[i][j - 1];
141 | }
142 | unchecked {
143 | ++j;
144 | }
145 | }
146 |
147 | if (nodes[0] == self.lastSubtrees[i][0] || nodes[4] == self.lastSubtrees[i][4]) {
148 | self.lastSubtrees[i][proofPathIndices[i]] = hash;
149 | }
150 |
151 | hash = PoseidonT6.hash(nodes);
152 |
153 | unchecked {
154 | ++i;
155 | }
156 | }
157 |
158 | if (updateIndex >= self.numberOfLeaves) {
159 | revert LeafIndexOutOfRange();
160 | }
161 |
162 | self.root = hash;
163 | }
164 |
165 | /// @dev Removes a leaf from the tree.
166 | /// @param self: Tree data.
167 | /// @param leaf: Leaf to be removed.
168 | /// @param proofSiblings: Array of the sibling nodes of the proof of membership.
169 | /// @param proofPathIndices: Path of the proof of membership.
170 | function _remove(
171 | QuinaryIMTData storage self,
172 | uint256 leaf,
173 | uint256[4][] calldata proofSiblings,
174 | uint8[] calldata proofPathIndices
175 | ) internal {
176 | _update(self, leaf, self.zeroes[0], proofSiblings, proofPathIndices);
177 | }
178 |
179 | /// @dev Verify if the path is correct and the leaf is part of the tree.
180 | /// @param self: Tree data.
181 | /// @param leaf: Leaf to be removed.
182 | /// @param proofSiblings: Array of the sibling nodes of the proof of membership.
183 | /// @param proofPathIndices: Path of the proof of membership.
184 | /// @return True or false.
185 | function _verify(
186 | QuinaryIMTData storage self,
187 | uint256 leaf,
188 | uint256[4][] calldata proofSiblings,
189 | uint8[] calldata proofPathIndices
190 | ) internal view returns (bool) {
191 | uint256 depth = self.depth;
192 |
193 | if (leaf >= SNARK_SCALAR_FIELD) {
194 | revert ValueGreaterThanSnarkScalarField();
195 | } else if (proofPathIndices.length != depth || proofSiblings.length != depth) {
196 | revert WrongMerkleProofPath();
197 | }
198 |
199 | uint256 hash = leaf;
200 |
201 | for (uint8 i = 0; i < depth; ) {
202 | uint256[5] memory nodes;
203 |
204 | if (proofPathIndices[i] < 0 || proofPathIndices[i] >= 5) {
205 | revert WrongMerkleProofPath();
206 | }
207 |
208 | for (uint8 j = 0; j < 5; ) {
209 | if (j < proofPathIndices[i]) {
210 | require(
211 | proofSiblings[i][j] < SNARK_SCALAR_FIELD,
212 | "QuinaryIMT: sibling node must be < SNARK_SCALAR_FIELD"
213 | );
214 |
215 | nodes[j] = proofSiblings[i][j];
216 | } else if (j == proofPathIndices[i]) {
217 | nodes[j] = hash;
218 | } else {
219 | require(
220 | proofSiblings[i][j - 1] < SNARK_SCALAR_FIELD,
221 | "QuinaryIMT: sibling node must be < SNARK_SCALAR_FIELD"
222 | );
223 |
224 | nodes[j] = proofSiblings[i][j - 1];
225 | }
226 |
227 | unchecked {
228 | ++j;
229 | }
230 | }
231 |
232 | hash = PoseidonT6.hash(nodes);
233 |
234 | unchecked {
235 | ++i;
236 | }
237 | }
238 |
239 | return hash == self.root;
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/packages/imt/contracts/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ethereum Foundation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/imt/contracts/QuinaryIMT.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.4;
3 |
4 | import {InternalQuinaryIMT, QuinaryIMTData} from "./InternalQuinaryIMT.sol";
5 |
6 | library QuinaryIMT {
7 | using InternalQuinaryIMT for *;
8 |
9 | function init(QuinaryIMTData storage self, uint256 depth, uint256 zero) public {
10 | InternalQuinaryIMT._init(self, depth, zero);
11 | }
12 |
13 | function insert(QuinaryIMTData storage self, uint256 leaf) public {
14 | InternalQuinaryIMT._insert(self, leaf);
15 | }
16 |
17 | function update(
18 | QuinaryIMTData storage self,
19 | uint256 leaf,
20 | uint256 newLeaf,
21 | uint256[4][] calldata proofSiblings,
22 | uint8[] calldata proofPathIndices
23 | ) public {
24 | InternalQuinaryIMT._update(self, leaf, newLeaf, proofSiblings, proofPathIndices);
25 | }
26 |
27 | function remove(
28 | QuinaryIMTData storage self,
29 | uint256 leaf,
30 | uint256[4][] calldata proofSiblings,
31 | uint8[] calldata proofPathIndices
32 | ) public {
33 | InternalQuinaryIMT._remove(self, leaf, proofSiblings, proofPathIndices);
34 | }
35 |
36 | function verify(
37 | QuinaryIMTData storage self,
38 | uint256 leaf,
39 | uint256[4][] calldata proofSiblings,
40 | uint8[] calldata proofPathIndices
41 | ) private view returns (bool) {
42 | return InternalQuinaryIMT._verify(self, leaf, proofSiblings, proofPathIndices);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/imt/contracts/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Incremental Merkle Tree (Solidity)
4 |
5 | Incremental Merkle tree implementation in Solidity.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 | > [!WARNING]
35 | > These library has **not** been audited.
36 |
37 | > [!WARNING]
38 | > If you are looking for the first version of this package, please visit this [link](https://github.com/privacy-scaling-explorations/zk-kit/tree/imt-v1/packages/incremental-merkle-tree.sol).
39 |
40 | ---
41 |
42 | ## 🛠 Install
43 |
44 | ### npm or yarn
45 |
46 | Install the `@zk-kit/imt.sol` package with npm:
47 |
48 | ```bash
49 | npm i @zk-kit/imt.sol --save
50 | ```
51 |
52 | or yarn:
53 |
54 | ```bash
55 | yarn add @zk-kit/imt.sol
56 | ```
57 |
58 | ## 📜 Usage
59 |
60 | Please, see the [test contracts](./test) for guidance on utilizing the libraries.
61 |
--------------------------------------------------------------------------------
/packages/imt/contracts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@zk-kit/imt.sol",
3 | "version": "2.0.0-beta.12",
4 | "description": "Incremental Merkle tree implementation in Solidity.",
5 | "license": "MIT",
6 | "files": [
7 | "*.sol",
8 | "internal/*",
9 | "!test/*",
10 | "README.md",
11 | "LICENSE"
12 | ],
13 | "keywords": [
14 | "blockchain",
15 | "ethereum",
16 | "hardhat",
17 | "smart-contracts",
18 | "solidity",
19 | "libraries",
20 | "merkle-tree",
21 | "incremental-merkle-tree"
22 | ],
23 | "repository": "git@github.com:privacy-scaling-explorations/zk-kit.solidity.git",
24 | "homepage": "https://github.com/privacy-scaling-explorations/zk-kit.solidity/tree/main/packages/imt.sol",
25 | "publishConfig": {
26 | "access": "public"
27 | },
28 | "dependencies": {
29 | "poseidon-solidity": "0.0.5"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/imt/contracts/test/BinaryIMTTest.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.4;
4 |
5 | import {BinaryIMT, BinaryIMTData} from "../BinaryIMT.sol";
6 |
7 | contract BinaryIMTTest {
8 | BinaryIMTData public data;
9 |
10 | function init(uint256 depth) external {
11 | BinaryIMT.init(data, depth, 0);
12 | }
13 |
14 | function initWithDefaultZeroes(uint256 depth) external {
15 | BinaryIMT.initWithDefaultZeroes(data, depth);
16 | }
17 |
18 | function insert(uint256 leaf) external {
19 | BinaryIMT.insert(data, leaf);
20 | }
21 |
22 | function update(
23 | uint256 leaf,
24 | uint256 newLeaf,
25 | uint256[] calldata proofSiblings,
26 | uint8[] calldata proofPathIndices
27 | ) external {
28 | BinaryIMT.update(data, leaf, newLeaf, proofSiblings, proofPathIndices);
29 | }
30 |
31 | function remove(uint256 leaf, uint256[] calldata proofSiblings, uint8[] calldata proofPathIndices) external {
32 | BinaryIMT.remove(data, leaf, proofSiblings, proofPathIndices);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/imt/contracts/test/QuinaryIMTTest.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.4;
4 |
5 | import {QuinaryIMT, QuinaryIMTData} from "../QuinaryIMT.sol";
6 |
7 | contract QuinaryIMTTest {
8 | QuinaryIMTData public data;
9 |
10 | function init(uint256 depth) external {
11 | QuinaryIMT.init(data, depth, 0);
12 | }
13 |
14 | function insert(uint256 leaf) external {
15 | QuinaryIMT.insert(data, leaf);
16 | }
17 |
18 | function update(
19 | uint256 leaf,
20 | uint256 newLeaf,
21 | uint256[4][] calldata proofSiblings,
22 | uint8[] calldata proofPathIndices
23 | ) external {
24 | QuinaryIMT.update(data, leaf, newLeaf, proofSiblings, proofPathIndices);
25 | }
26 |
27 | function remove(uint256 leaf, uint256[4][] calldata proofSiblings, uint8[] calldata proofPathIndices) external {
28 | QuinaryIMT.remove(data, leaf, proofSiblings, proofPathIndices);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/imt/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import "@nomicfoundation/hardhat-toolbox"
2 | import { HardhatUserConfig } from "hardhat/config"
3 | import "./tasks/deploy-imt-test"
4 | import "dotenv/config"
5 |
6 | const hardhatConfig: HardhatUserConfig = {
7 | solidity: {
8 | version: "0.8.23",
9 | settings: {
10 | optimizer: {
11 | enabled: true
12 | }
13 | }
14 | },
15 | gasReporter: {
16 | currency: "USD",
17 | enabled: process.env.REPORT_GAS === "true",
18 | outputJSONFile: "gas-report-imt.json",
19 | outputJSON: process.env.REPORT_GAS_OUTPUT_JSON === "true"
20 | },
21 | typechain: {
22 | target: "ethers-v6"
23 | }
24 | }
25 |
26 | export default hardhatConfig
27 |
--------------------------------------------------------------------------------
/packages/imt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imt.sol",
3 | "private": true,
4 | "scripts": {
5 | "start": "hardhat node",
6 | "compile": "hardhat compile",
7 | "test": "hardhat test",
8 | "test:report-gas": "REPORT_GAS=true hardhat test",
9 | "test:coverage": "hardhat coverage",
10 | "typechain": "hardhat typechain",
11 | "lint": "solhint 'contracts/**/*.sol'",
12 | "slither": "slither . --include-paths contracts --exclude-dependencies --ignore-compile"
13 | },
14 | "devDependencies": {
15 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.3",
16 | "@nomicfoundation/hardhat-ethers": "^3.0.0",
17 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
18 | "@nomicfoundation/hardhat-toolbox": "^4.0.0",
19 | "@nomicfoundation/hardhat-verify": "^2.0.0",
20 | "@typechain/ethers-v6": "^0.5.0",
21 | "@typechain/hardhat": "^9.0.0",
22 | "@types/chai": "^4.2.0",
23 | "@types/mocha": "^10.0.6",
24 | "@types/node": "^20.10.7",
25 | "@zk-kit/imt": "^2.0.0-beta.5",
26 | "chai": "^4.2.0",
27 | "ethers": "^6.4.0",
28 | "hardhat": "^2.19.4",
29 | "hardhat-gas-reporter": "^2.2.0",
30 | "poseidon-lite": "^0.2.0",
31 | "prettier-plugin-solidity": "^1.3.1",
32 | "solhint": "^3.3.6",
33 | "solhint-plugin-prettier": "^0.1.0",
34 | "solidity-coverage": "^0.8.0",
35 | "ts-node": "^10.9.2",
36 | "typechain": "^8.3.0",
37 | "typescript": "^5.3.3"
38 | },
39 | "dependencies": {
40 | "poseidon-solidity": "0.0.5"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/imt/tasks/deploy-imt-test.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config"
2 |
3 | task("deploy:imt-test", "Deploy an IMT contract for testing a library")
4 | .addParam("library", "The name of the library", undefined, types.string)
5 | .addOptionalParam("logs", "Print the logs", true, types.boolean)
6 | .addOptionalParam("arity", "The arity of the tree", 2, types.int)
7 | .setAction(async ({ logs, library: libraryName, arity }, { ethers }): Promise => {
8 | const PoseidonFactory = await ethers.getContractFactory(`PoseidonT${arity + 1}`)
9 |
10 | const poseidon = await PoseidonFactory.deploy()
11 | const poseidonAddress = await poseidon.getAddress()
12 |
13 | if (logs) {
14 | console.info(`PoseidonT${arity + 1} library has been deployed to: ${poseidonAddress}`)
15 | }
16 |
17 | const LibraryFactory = await ethers.getContractFactory(libraryName, {
18 | libraries: {
19 | [`PoseidonT${arity + 1}`]: poseidonAddress
20 | }
21 | })
22 |
23 | const library = await LibraryFactory.deploy()
24 | const libraryAddress = await library.getAddress()
25 |
26 | if (logs) {
27 | console.info(`${libraryName} library has been deployed to: ${libraryAddress}`)
28 | }
29 |
30 | const ContractFactory = await ethers.getContractFactory(`${libraryName}Test`, {
31 | libraries: {
32 | [libraryName]: libraryAddress
33 | }
34 | })
35 |
36 | const contract = await ContractFactory.deploy()
37 | const contractAddress = await contract.getAddress()
38 |
39 | if (logs) {
40 | console.info(`${libraryName}Test contract has been deployed to: ${contractAddress}`)
41 | }
42 |
43 | return { library, contract }
44 | })
45 |
--------------------------------------------------------------------------------
/packages/imt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true
10 | },
11 | "include": ["scripts/**/*", "tasks/**/*", "test/**/*", "typechain-types/**/*"],
12 | "files": ["hardhat.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/lazy-imt/.env.example:
--------------------------------------------------------------------------------
1 | COINMARKETCAP_API_KEY=
2 | REPORT_GAS=false
3 | REPORT_GAS_OUTPUT_JSON=false
4 |
--------------------------------------------------------------------------------
/packages/lazy-imt/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "arrowParens": "always",
4 | "trailingComma": "none",
5 | "plugins": ["prettier-plugin-solidity"]
6 | }
7 |
--------------------------------------------------------------------------------
/packages/lazy-imt/.solcover.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | istanbulFolder: "../../coverage/lazy-imt"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/lazy-imt/.solhint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "solhint:recommended",
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "compiler-version": ["error", ">=0.8.0"],
6 | "const-name-snakecase": "off",
7 | "no-empty-blocks": "off",
8 | "constructor-syntax": "error",
9 | "func-visibility": ["error", { "ignoreConstructors": true }],
10 | "max-line-length": ["error", 120],
11 | "not-rely-on-time": "off",
12 | "prettier/prettier": [
13 | "error",
14 | {
15 | "endOfLine": "auto"
16 | }
17 | ],
18 | "reason-string": ["warn", { "maxLength": 80 }]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/lazy-imt/LICENSE:
--------------------------------------------------------------------------------
1 | contracts/LICENSE
--------------------------------------------------------------------------------
/packages/lazy-imt/README.md:
--------------------------------------------------------------------------------
1 | contracts/README.md
--------------------------------------------------------------------------------
/packages/lazy-imt/contracts/Constants.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.4;
3 |
4 | uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
5 | uint8 constant MAX_DEPTH = 32;
6 |
--------------------------------------------------------------------------------
/packages/lazy-imt/contracts/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ethereum Foundation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/lazy-imt/contracts/LazyIMT.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.4;
3 |
4 | import {InternalLazyIMT, LazyIMTData} from "./InternalLazyIMT.sol";
5 |
6 | library LazyIMT {
7 | using InternalLazyIMT for *;
8 |
9 | function init(LazyIMTData storage self, uint8 depth) public {
10 | InternalLazyIMT._init(self, depth);
11 | }
12 |
13 | function defaultZero(uint8 index) public pure returns (uint256) {
14 | return InternalLazyIMT._defaultZero(index);
15 | }
16 |
17 | function reset(LazyIMTData storage self) public {
18 | InternalLazyIMT._reset(self);
19 | }
20 |
21 | function indexForElement(uint8 level, uint40 index) public pure returns (uint40) {
22 | return InternalLazyIMT._indexForElement(level, index);
23 | }
24 |
25 | function insert(LazyIMTData storage self, uint256 leaf) public {
26 | InternalLazyIMT._insert(self, leaf);
27 | }
28 |
29 | function update(LazyIMTData storage self, uint256 leaf, uint40 index) public {
30 | InternalLazyIMT._update(self, leaf, index);
31 | }
32 |
33 | function root(LazyIMTData storage self) public view returns (uint256) {
34 | return InternalLazyIMT._root(self);
35 | }
36 |
37 | function root(LazyIMTData storage self, uint8 depth) public view returns (uint256) {
38 | return InternalLazyIMT._root(self, depth);
39 | }
40 |
41 | function merkleProofElements(
42 | LazyIMTData storage self,
43 | uint40 index,
44 | uint8 depth
45 | ) public view returns (uint256[] memory) {
46 | return InternalLazyIMT._merkleProofElements(self, index, depth);
47 | }
48 |
49 | function _root(LazyIMTData storage self, uint40 numberOfLeaves, uint8 depth) internal view returns (uint256) {
50 | return InternalLazyIMT._root(self, numberOfLeaves, depth);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/lazy-imt/contracts/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Lazy Incremental Merkle Tree (Solidity)
4 |
5 | Lazy Incremental Merkle tree implementation in Solidity.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 | > [!WARNING]
35 | > These library has **not** been audited.
36 |
37 | > [!WARNING]
38 | > If you are looking for the first version of this package, please visit this [link](https://github.com/privacy-scaling-explorations/zk-kit/tree/imt-v1/packages/incremental-merkle-tree.sol).
39 |
40 | ---
41 |
42 | ## 🛠 Install
43 |
44 | ### npm or yarn
45 |
46 | Install the `@zk-kit/lazy-imt.sol` package with npm:
47 |
48 | ```bash
49 | npm i @zk-kit/lazy-imt.sol --save
50 | ```
51 |
52 | or yarn:
53 |
54 | ```bash
55 | yarn add @zk-kit/lazy-imt.sol
56 | ```
57 |
58 | ## 📜 Usage
59 |
60 | Please, see the [test contracts](./test) for guidance on utilizing the libraries.
61 |
--------------------------------------------------------------------------------
/packages/lazy-imt/contracts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@zk-kit/lazy-imt.sol",
3 | "version": "2.0.0-beta.12",
4 | "description": "Lazy Incremental Merkle tree implementation in Solidity.",
5 | "license": "MIT",
6 | "files": [
7 | "*.sol",
8 | "!test/*",
9 | "README.md",
10 | "LICENSE"
11 | ],
12 | "keywords": [
13 | "blockchain",
14 | "ethereum",
15 | "hardhat",
16 | "smart-contracts",
17 | "solidity",
18 | "libraries",
19 | "merkle-tree",
20 | "incremental-merkle-tree"
21 | ],
22 | "repository": "git@github.com:privacy-scaling-explorations/zk-kit.solidity.git",
23 | "homepage": "https://github.com/privacy-scaling-explorations/zk-kit.solidity/tree/main/packages/lazy-imt",
24 | "publishConfig": {
25 | "access": "public"
26 | },
27 | "dependencies": {
28 | "poseidon-solidity": "0.0.5"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/lazy-imt/contracts/test/LazyIMTTest.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.4;
4 |
5 | import {LazyIMT, LazyIMTData} from "../LazyIMT.sol";
6 |
7 | contract LazyIMTTest {
8 | LazyIMTData public data;
9 | uint256 _root;
10 |
11 | function init(uint8 depth) public {
12 | LazyIMT.init(data, depth);
13 | }
14 |
15 | function reset() public {
16 | LazyIMT.reset(data);
17 | }
18 |
19 | function insert(uint256 leaf) public {
20 | LazyIMT.insert(data, leaf);
21 | }
22 |
23 | function update(uint256 leaf, uint40 index) public {
24 | LazyIMT.update(data, leaf, index);
25 | }
26 |
27 | // for benchmarking the root cost
28 | function benchmarkRoot() public {
29 | _root = LazyIMT.root(data);
30 | }
31 |
32 | function root() public view returns (uint256) {
33 | return LazyIMT.root(data);
34 | }
35 |
36 | function dynamicRoot(uint8 depth) public view returns (uint256) {
37 | return LazyIMT.root(data, depth);
38 | }
39 |
40 | function staticRoot(uint8 depth) public view returns (uint256) {
41 | return LazyIMT.root(data, depth);
42 | }
43 |
44 | function merkleProofElements(uint40 index, uint8 depth) public view returns (uint256[] memory) {
45 | return LazyIMT.merkleProofElements(data, index, depth);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/lazy-imt/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import "@nomicfoundation/hardhat-toolbox"
2 | import { HardhatUserConfig } from "hardhat/config"
3 | import "./tasks/deploy-imt-test"
4 | import "dotenv/config"
5 |
6 | const hardhatConfig: HardhatUserConfig = {
7 | solidity: {
8 | version: "0.8.23",
9 | settings: {
10 | optimizer: {
11 | enabled: true
12 | }
13 | }
14 | },
15 | gasReporter: {
16 | currency: "USD",
17 | enabled: process.env.REPORT_GAS === "true",
18 | outputJSONFile: "gas-report-lazyimt.json",
19 | outputJSON: process.env.REPORT_GAS_OUTPUT_JSON === "true"
20 | },
21 | typechain: {
22 | target: "ethers-v6"
23 | }
24 | }
25 |
26 | export default hardhatConfig
27 |
--------------------------------------------------------------------------------
/packages/lazy-imt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lazy-imt.sol",
3 | "private": true,
4 | "scripts": {
5 | "start": "hardhat node",
6 | "compile": "hardhat compile",
7 | "test": "hardhat test",
8 | "test:report-gas": "REPORT_GAS=true hardhat test",
9 | "test:coverage": "hardhat coverage",
10 | "typechain": "hardhat typechain",
11 | "lint": "solhint 'contracts/**/*.sol'",
12 | "slither": "slither . --include-paths contracts --exclude-dependencies --ignore-compile"
13 | },
14 | "devDependencies": {
15 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.3",
16 | "@nomicfoundation/hardhat-ethers": "^3.0.0",
17 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
18 | "@nomicfoundation/hardhat-toolbox": "^4.0.0",
19 | "@nomicfoundation/hardhat-verify": "^2.0.0",
20 | "@typechain/ethers-v6": "^0.5.0",
21 | "@typechain/hardhat": "^9.0.0",
22 | "@types/chai": "^4.2.0",
23 | "@types/mocha": "^10.0.6",
24 | "@types/node": "^20.10.7",
25 | "@zk-kit/imt": "^2.0.0-beta.5",
26 | "chai": "^4.2.0",
27 | "ethers": "^6.4.0",
28 | "hardhat": "^2.19.4",
29 | "hardhat-gas-reporter": "^2.2.0",
30 | "poseidon-lite": "^0.2.0",
31 | "prettier-plugin-solidity": "^1.3.1",
32 | "solhint": "^3.3.6",
33 | "solhint-plugin-prettier": "^0.1.0",
34 | "solidity-coverage": "^0.8.0",
35 | "ts-node": "^10.9.2",
36 | "typechain": "^8.3.0",
37 | "typescript": "^5.3.3"
38 | },
39 | "dependencies": {
40 | "poseidon-solidity": "0.0.5"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/lazy-imt/scripts/defaultZeroes.mjs:
--------------------------------------------------------------------------------
1 | import { poseidon } from "circomlibjs"
2 |
3 | const zeroes = [0]
4 |
5 | for (let x = 1; x < 33; x += 1) {
6 | zeroes[x] = poseidon([zeroes[x - 1], zeroes[x - 1]])
7 | }
8 |
9 | console.log(`
10 | ${Array(33)
11 | .fill(0)
12 | .map((_, i) => `uint256 constant public Z_${i} = ${zeroes[i]};`)
13 | .join("\n ")}
14 |
15 | /*
16 | function defaultZeroes() public pure returns (uint256[32] memory) {
17 | return [${Array(32)
18 | .fill()
19 | .map((_, i) => `Z_${i}`)
20 | .join(",")}];
21 | }
22 | */
23 |
24 | function defaultZero(uint256 index) public pure returns (uint256) {
25 | ${Array(33)
26 | .fill()
27 | .map((_, i) => ` if (index == ${i}) return Z_${i};`)
28 | .join("\n")}
29 | revert('badindex');
30 | }
31 | `)
32 |
--------------------------------------------------------------------------------
/packages/lazy-imt/tasks/deploy-imt-test.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config"
2 |
3 | task("deploy:imt-test", "Deploy an IMT contract for testing a library")
4 | .addParam("library", "The name of the library", undefined, types.string)
5 | .addOptionalParam("logs", "Print the logs", true, types.boolean)
6 | .addOptionalParam("arity", "The arity of the tree", 2, types.int)
7 | .setAction(async ({ logs, library: libraryName, arity }, { ethers }): Promise => {
8 | const PoseidonFactory = await ethers.getContractFactory(`PoseidonT${arity + 1}`)
9 |
10 | const poseidon = await PoseidonFactory.deploy()
11 | const poseidonAddress = await poseidon.getAddress()
12 |
13 | if (logs) {
14 | console.info(`PoseidonT${arity + 1} library has been deployed to: ${poseidonAddress}`)
15 | }
16 |
17 | const LibraryFactory = await ethers.getContractFactory(libraryName, {
18 | libraries: {
19 | [`PoseidonT${arity + 1}`]: poseidonAddress
20 | }
21 | })
22 |
23 | const library = await LibraryFactory.deploy()
24 | const libraryAddress = await library.getAddress()
25 |
26 | if (logs) {
27 | console.info(`${libraryName} library has been deployed to: ${libraryAddress}`)
28 | }
29 |
30 | const ContractFactory = await ethers.getContractFactory(`${libraryName}Test`, {
31 | libraries: {
32 | [libraryName]: libraryAddress
33 | }
34 | })
35 |
36 | const contract = await ContractFactory.deploy()
37 | const contractAddress = await contract.getAddress()
38 |
39 | if (logs) {
40 | console.info(`${libraryName}Test contract has been deployed to: ${contractAddress}`)
41 | }
42 |
43 | return { library, contract }
44 | })
45 |
--------------------------------------------------------------------------------
/packages/lazy-imt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true
10 | },
11 | "include": ["scripts/**/*", "tasks/**/*", "test/**/*", "typechain-types/**/*"],
12 | "files": ["hardhat.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/lazytower/.env.example:
--------------------------------------------------------------------------------
1 | COINMARKETCAP_API_KEY=
2 | REPORT_GAS=false
3 | REPORT_GAS_OUTPUT_JSON=false
4 |
--------------------------------------------------------------------------------
/packages/lazytower/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "arrowParens": "always",
4 | "trailingComma": "none",
5 | "plugins": ["prettier-plugin-solidity"]
6 | }
7 |
--------------------------------------------------------------------------------
/packages/lazytower/.solcover.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | istanbulFolder: "../../coverage/lazytower"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/lazytower/.solhint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "solhint:recommended",
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "compiler-version": ["error", ">=0.8.0"],
6 | "const-name-snakecase": "off",
7 | "no-empty-blocks": "off",
8 | "constructor-syntax": "error",
9 | "func-visibility": ["error", { "ignoreConstructors": true }],
10 | "max-line-length": ["error", 120],
11 | "not-rely-on-time": "off",
12 | "prettier/prettier": [
13 | "error",
14 | {
15 | "endOfLine": "auto"
16 | }
17 | ],
18 | "reason-string": ["warn", { "maxLength": 80 }]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/lazytower/.solhintignore:
--------------------------------------------------------------------------------
1 | contracts/Verifier.sol
2 |
--------------------------------------------------------------------------------
/packages/lazytower/LICENSE:
--------------------------------------------------------------------------------
1 | contracts/LICENSE
--------------------------------------------------------------------------------
/packages/lazytower/README.md:
--------------------------------------------------------------------------------
1 | contracts/README.md
--------------------------------------------------------------------------------
/packages/lazytower/contracts/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ethereum Foundation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/lazytower/contracts/LazyTowerHashChain.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.4;
3 |
4 | import {PoseidonT3} from "poseidon-solidity/PoseidonT3.sol";
5 | // CAPACITY = W * (W**0 + W**1 + ... + W**(H - 1)) = W * (W**H - 1) / (W - 1)
6 | // 4 * (4**24 - 1) / (4 - 1) = 375_299_968_947_540;
7 | uint256 constant H = 24;
8 | uint256 constant W = 4;
9 |
10 | uint256 constant bitsPerLevel = 4;
11 | uint256 constant levelBitmask = 15; // (1 << bitsPerLevel) - 1
12 | uint256 constant ones = 0x111111111111111111111111; // H ones
13 |
14 | // Each LazyTower has certain properties and data that will
15 | // be used to add new items.
16 | struct LazyTowerHashChainData {
17 | uint256 levelLengths; // length of each level
18 | uint256[H] digests; // digest of each level
19 | uint256[H] digestOfDigests; // digest of digests
20 | }
21 |
22 | /// @title LazyTower.
23 | /// @dev The LazyTower allows to calculate the digest of digests each time an item is added, ensuring
24 | /// the integrity of the LazyTower.
25 | library LazyTowerHashChain {
26 | uint256 internal constant SNARK_SCALAR_FIELD =
27 | 21888242871839275222246405745257275088548364400416034343698204186575808495617;
28 |
29 | function findLowestNonFullLevelThenInc(
30 | uint256 levelLengths
31 | ) internal pure returns (uint256 level, bool isHead, bool isTop, uint256 newLevelLengths) {
32 | // find the lowest non-full level
33 | uint256 levelLength;
34 | while (true) {
35 | levelLength = levelLengths & levelBitmask;
36 | if (levelLength < W) break;
37 | level++;
38 | levelLengths >>= bitsPerLevel;
39 | }
40 |
41 | isHead = (levelLength == 0);
42 | isTop = ((levelLengths >> bitsPerLevel) == 0);
43 |
44 | // increment the non-full levelLength(s) by one
45 | // all full levels below become ones
46 | uint256 fullLevelBits = level * bitsPerLevel;
47 | uint256 onesMask = (1 << fullLevelBits) - 1;
48 | newLevelLengths = ((levelLengths + 1) << fullLevelBits) + (onesMask & ones);
49 | }
50 |
51 | /// @dev Add an item.
52 | /// @param self: LazyTower data
53 | /// @param item: item to be added
54 | function add(LazyTowerHashChainData storage self, uint256 item) public {
55 | require(item < SNARK_SCALAR_FIELD, "LazyTower: item must be < SNARK_SCALAR_FIELD");
56 |
57 | uint256 level;
58 | bool isHead;
59 | bool isTop;
60 | (level, isHead, isTop, self.levelLengths) = findLowestNonFullLevelThenInc(self.levelLengths);
61 |
62 | uint256 digest;
63 | uint256 digestOfDigests;
64 | uint256 toAdd;
65 |
66 | // append at the first non-full level
67 | toAdd = (level == 0) ? item : self.digests[level - 1];
68 | digest = isHead ? toAdd : PoseidonT3.hash([self.digests[level], toAdd]);
69 | digestOfDigests = isTop ? digest : PoseidonT3.hash([self.digestOfDigests[level + 1], digest]);
70 | self.digests[level] = digest;
71 | self.digestOfDigests[level] = digestOfDigests;
72 |
73 | // the rest of levels are all full
74 | while (level != 0) {
75 | level--;
76 |
77 | toAdd = (level == 0) ? item : self.digests[level - 1];
78 | digest = toAdd;
79 | digestOfDigests = PoseidonT3.hash([digestOfDigests, digest]); // top-down
80 | self.digests[level] = digest;
81 | self.digestOfDigests[level] = digestOfDigests;
82 | }
83 | }
84 |
85 | function getDataForProving(
86 | LazyTowerHashChainData storage self
87 | ) external view returns (uint256, uint256[] memory, uint256) {
88 | uint256 len = self.digests.length;
89 | uint256[] memory digests = new uint256[](len); // for returning a dynamic array
90 | for (uint256 i = 0; i < len; i++) {
91 | digests[i] = self.digests[i];
92 | }
93 | return (self.levelLengths, digests, self.digestOfDigests[0]);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/packages/lazytower/contracts/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | LazyTower (Solidity)
4 |
5 | LazyTower Solidity library.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 | > [!WARNING]
35 | > These library has **not** been audited.
36 |
37 | ---
38 |
39 | ## 🛠 Install
40 |
41 | ### npm or yarn
42 |
43 | Install the `@zk-kit/lazytower.sol` package with npm:
44 |
45 | ```bash
46 | npm i @zk-kit/lazytower.sol --save
47 | ```
48 |
49 | or yarn:
50 |
51 | ```bash
52 | yarn add @zk-kit/lazytower.sol
53 | ```
54 |
55 | ## 📜 Usage
56 |
57 | ### Importing and using the library
58 |
59 | ```solidity
60 | // SPDX-License-Identifier: MIT
61 |
62 | pragma solidity ^0.8.4;
63 |
64 | import "../LazyTowerHashChain.sol";
65 |
66 | contract LazyTowerHashChainTest {
67 | using LazyTowerHashChain for LazyTowerHashChainData;
68 |
69 | event Add(uint256 item);
70 |
71 | // map for multiple test cases
72 | mapping(bytes32 => LazyTowerHashChainData) public towers;
73 |
74 | function add(bytes32 _towerId, uint256 _item) external {
75 | towers[_towerId].add(_item);
76 | emit Add(_item);
77 | }
78 |
79 | function getDataForProving(bytes32 _towerId) external view returns (uint256, uint256[] memory, uint256) {
80 | return towers[_towerId].getDataForProving();
81 | }
82 | }
83 | ```
84 |
85 | ### Creating an Hardhat task to deploy the contract
86 |
87 | ```typescript
88 | import { Contract } from "ethers"
89 | import { task, types } from "hardhat/config"
90 |
91 | task("deploy:lazytower-test", "Deploy a LazyTowerHashChainTest contract")
92 | .addOptionalParam("logs", "Print the logs", true, types.boolean)
93 | .setAction(async ({ logs }, { ethers }): Promise => {
94 | const PoseidonT3Factory = await ethers.getContractFactory("PoseidonT3")
95 | const PoseidonT3 = await PoseidonT3Factory.deploy()
96 |
97 | if (logs) {
98 | console.info(`PoseidonT3 library has been deployed to: ${PoseidonT3.address}`)
99 | }
100 |
101 | const LazyTowerLibFactory = await ethers.getContractFactory("LazyTowerHashChain", {
102 | libraries: {
103 | PoseidonT3: PoseidonT3.address
104 | }
105 | })
106 | const lazyTowerLib = await LazyTowerLibFactory.deploy()
107 |
108 | await lazyTowerLib.deployed()
109 |
110 | if (logs) {
111 | console.info(`LazyTowerHashChain library has been deployed to: ${lazyTowerLib.address}`)
112 | }
113 |
114 | const ContractFactory = await ethers.getContractFactory("LazyTowerHashChainTest", {
115 | libraries: {
116 | LazyTowerHashChain: lazyTowerLib.address
117 | }
118 | })
119 |
120 | const contract = await ContractFactory.deploy()
121 |
122 | await contract.deployed()
123 |
124 | if (logs) {
125 | console.info(`Test contract has been deployed to: ${contract.address}`)
126 | }
127 |
128 | return contract
129 | })
130 | ```
131 |
132 | ## Contacts
133 |
134 | ### Developers
135 |
136 | - e-mail : lcamel@gmail.com
137 | - github : [@LCamel](https://github.com/LCamel)
138 | - website : https://www.facebook.com/LCamel
139 |
--------------------------------------------------------------------------------
/packages/lazytower/contracts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@zk-kit/lazytower.sol",
3 | "version": "0.0.1",
4 | "description": "LazyTower Solidity libraries.",
5 | "license": "MIT",
6 | "files": [
7 | "**/*.sol",
8 | "!test/",
9 | "README.md"
10 | ],
11 | "keywords": [
12 | "blockchain",
13 | "ethereum",
14 | "hardhat",
15 | "smart-contracts",
16 | "solidity",
17 | "libraries",
18 | "merkle-tree",
19 | "incremental-merkle-tree",
20 | "lazytower"
21 | ],
22 | "repository": "git@github.com:privacy-scaling-explorations/zk-kit.solidity.git",
23 | "homepage": "https://github.com/privacy-scaling-explorations/zk-kit.solidity/tree/main/packages/lazytower",
24 | "author": {
25 | "name": "LCamel",
26 | "email": "lcamel@gmail.com",
27 | "url": "https://twitter.com/LCamel"
28 | },
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "dependencies": {
33 | "poseidon-solidity": "0.0.5"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/lazytower/contracts/test/LazyTowerHashChainTest.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.4;
4 |
5 | import "../LazyTowerHashChain.sol";
6 |
7 | contract LazyTowerHashChainTest {
8 | using LazyTowerHashChain for LazyTowerHashChainData;
9 |
10 | event Add(uint256 item);
11 |
12 | // map for multiple test cases
13 | mapping(bytes32 => LazyTowerHashChainData) public towers;
14 |
15 | function add(bytes32 _towerId, uint256 _item) external {
16 | towers[_towerId].add(_item);
17 | emit Add(_item);
18 | }
19 |
20 | function getDataForProving(bytes32 _towerId) external view returns (uint256, uint256[] memory, uint256) {
21 | return towers[_towerId].getDataForProving();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/lazytower/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import "@nomicfoundation/hardhat-toolbox"
2 | import { HardhatUserConfig } from "hardhat/config"
3 | import "./tasks/deploy-lazytower-test"
4 | import "dotenv/config"
5 |
6 | const hardhatConfig: HardhatUserConfig = {
7 | solidity: "0.8.23",
8 | gasReporter: {
9 | currency: "USD",
10 | enabled: process.env.REPORT_GAS === "true",
11 | coinmarketcap: process.env.COINMARKETCAP_API_KEY,
12 | outputJSONFile: "gas-report-lazytower.json",
13 | outputJSON: process.env.REPORT_GAS_OUTPUT_JSON === "true"
14 | },
15 | typechain: {
16 | target: "ethers-v6"
17 | }
18 | }
19 |
20 | export default hardhatConfig
21 |
--------------------------------------------------------------------------------
/packages/lazytower/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lazytower.sol",
3 | "private": true,
4 | "scripts": {
5 | "start": "hardhat node",
6 | "compile": "hardhat compile",
7 | "deploy:test-contracts": "hardhat deploy:tree-contracts",
8 | "test": "hardhat test",
9 | "test:report-gas": "REPORT_GAS=true hardhat test",
10 | "test:coverage": "hardhat coverage",
11 | "typechain": "hardhat typechain",
12 | "lint": "solhint 'contracts/**/*.sol'",
13 | "slither": "slither . --include-paths contracts --exclude-dependencies --ignore-compile"
14 | },
15 | "devDependencies": {
16 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.3",
17 | "@nomicfoundation/hardhat-ethers": "^3.0.0",
18 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
19 | "@nomicfoundation/hardhat-toolbox": "^4.0.0",
20 | "@nomicfoundation/hardhat-verify": "^2.0.0",
21 | "@typechain/ethers-v6": "^0.5.0",
22 | "@typechain/hardhat": "^9.0.0",
23 | "@types/chai": "^4.2.0",
24 | "@types/mocha": "^10.0.6",
25 | "@types/node": "^20.10.7",
26 | "chai": "^4.2.0",
27 | "ethers": "^6.4.0",
28 | "hardhat": "^2.19.4",
29 | "hardhat-gas-reporter": "^2.2.0",
30 | "poseidon-lite": "^0.2.0",
31 | "prettier-plugin-solidity": "^1.3.1",
32 | "solhint": "^3.3.6",
33 | "solhint-plugin-prettier": "^0.1.0",
34 | "solidity-coverage": "^0.8.0",
35 | "ts-node": "^10.9.2",
36 | "typechain": "^8.3.0",
37 | "typescript": "^5.3.3"
38 | },
39 | "dependencies": {
40 | "poseidon-solidity": "0.0.5"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/lazytower/tasks/deploy-lazytower-test.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config"
2 |
3 | task("deploy:lazytower-test", "Deploy a LazyTowerHashChainTest contract")
4 | .addOptionalParam("logs", "Print the logs", true, types.boolean)
5 | .setAction(async ({ logs }, { ethers }): Promise => {
6 | const PoseidonT3Factory = await ethers.getContractFactory("PoseidonT3")
7 |
8 | const poseidonT3 = await PoseidonT3Factory.deploy()
9 | const poseidonT3Address = await poseidonT3.getAddress()
10 |
11 | if (logs) {
12 | console.info(`PoseidonT3 library has been deployed to: ${poseidonT3Address}`)
13 | }
14 |
15 | const LazyTowerLibFactory = await ethers.getContractFactory("LazyTowerHashChain", {
16 | libraries: {
17 | PoseidonT3: poseidonT3Address
18 | }
19 | })
20 |
21 | const lazyTowerLib = await LazyTowerLibFactory.deploy()
22 | const lazyTowerLibAddress = await lazyTowerLib.getAddress()
23 |
24 | if (logs) {
25 | console.info(`LazyTowerHashChain library has been deployed to: ${lazyTowerLibAddress}`)
26 | }
27 |
28 | const ContractFactory = await ethers.getContractFactory("LazyTowerHashChainTest", {
29 | libraries: {
30 | LazyTowerHashChain: lazyTowerLibAddress
31 | }
32 | })
33 |
34 | const contract = await ContractFactory.deploy()
35 | const contractAddress = await lazyTowerLib.getAddress()
36 |
37 | if (logs) {
38 | console.info(`Test contract has been deployed to: ${contractAddress}`)
39 | }
40 |
41 | return contract
42 | })
43 |
--------------------------------------------------------------------------------
/packages/lazytower/test/LazyTowerHashChainTest.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "chai"
2 | import { Contract, encodeBytes32String } from "ethers"
3 | import { run } from "hardhat"
4 | import { poseidon2 } from "poseidon-lite"
5 | import ShiftTower from "./utils"
6 |
7 | describe("LazyTowerHashChainTest", () => {
8 | let contract: Contract
9 |
10 | before(async () => {
11 | contract = await run("deploy:lazytower-test", { logs: false })
12 | })
13 |
14 | it("Should produce correct levelLengths, digests and digest of digests", async () => {
15 | const lazyTowerId = encodeBytes32String("test1")
16 |
17 | const N = 150
18 | for (let i = 0; i < N; i += 1) {
19 | await contract.add(lazyTowerId, i)
20 | }
21 |
22 | const [levelLengths, digests, digestOfDigests] = await contract.getDataForProving(lazyTowerId)
23 |
24 | expect(levelLengths).to.equal(0x2112)
25 |
26 | expect(digests[0]).to.equal(
27 | BigInt("7484852499570635450337779587061833141700590058395918107227385307780465498841")
28 | )
29 | expect(digests[1]).to.equal(
30 | BigInt("18801712394745483811033456933953954791894699812924877968490149877093764724813")
31 | )
32 | expect(digests[2]).to.equal(
33 | BigInt("18495397265763935736123111771752209927150052777598404957994272011704245682779")
34 | )
35 | expect(digests[3]).to.equal(
36 | BigInt("11606235313340788975553986881206148975708550071371494991713397040288897077102")
37 | )
38 | for (let i = 4; i < digests.length; i += 1) {
39 | expect(digests[i]).to.equal(BigInt("0"))
40 | }
41 |
42 | expect(digestOfDigests).to.equal(
43 | BigInt("19260615748091768530426964318883829655407684674262674118201416393073357631548")
44 | )
45 | })
46 |
47 | // TODO: this times out in CI
48 | it.skip("Should have the same output as the Javascript fixture", async () => {
49 | const lazyTowerId = encodeBytes32String("test2")
50 |
51 | const H2 = (a: bigint, b: bigint) => poseidon2([a, b])
52 | const W = 4
53 | const shiftTower = ShiftTower(W, (vs: any[]) => vs.reduce(H2))
54 | for (let i = 0; i < 150; i += 1) {
55 | shiftTower.add(i)
56 |
57 | const tx = contract.add(lazyTowerId, i)
58 |
59 | // event
60 | await expect(tx).to.emit(contract, "Add").withArgs(i)
61 |
62 | // levelLengths and digest
63 | const [levelLengths, digests, digestOfDigests] = await contract.getDataForProving(lazyTowerId)
64 |
65 | expect(levelLengths).to.equal(shiftTower.L.map((l) => l.length).reduce((s, v, lv) => s + (v << (lv * 4))))
66 |
67 | const D = shiftTower.L.map((l: any[]) => l.reduce(H2))
68 | for (let lv = 0; lv < digests.length; lv += 1) {
69 | expect(digests[lv]).to.equal(D[lv] ?? 0)
70 | }
71 |
72 | expect(digestOfDigests).to.equal(D.reverse().reduce(H2))
73 | }
74 | })
75 |
76 | it("Should reject values not in the field", async () => {
77 | const lazyTowerId = encodeBytes32String("test3")
78 |
79 | let item = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495616")
80 |
81 | const tx = contract.add(lazyTowerId, item)
82 | await expect(tx).to.emit(contract, "Add").withArgs(item)
83 |
84 | item += BigInt(1)
85 | const tx2 = contract.add(lazyTowerId, item)
86 | await expect(tx2).to.be.revertedWith("LazyTower: item must be < SNARK_SCALAR_FIELD")
87 | })
88 | })
89 |
--------------------------------------------------------------------------------
/packages/lazytower/test/utils.ts:
--------------------------------------------------------------------------------
1 | export default function ShiftTower(W: number, digest: (values: number[]) => number) {
2 | const S: number[][] = []
3 | const L: number[][] = []
4 |
5 | function _add(lv: number, v: number): number {
6 | if (lv === L.length) {
7 | S[lv] = []
8 | L[lv] = [v]
9 | } else if (L[lv].length < W) {
10 | L[lv].push(v)
11 | } else {
12 | const d = digest(L[lv])
13 | S[lv].push(...L[lv])
14 | L[lv] = [v]
15 | return _add(lv + 1, d)
16 | }
17 | return lv
18 | }
19 | const add = (item: number) => _add(0, item)
20 | return { W, digest, L, S, add }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/lazytower/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true
10 | },
11 | "include": ["scripts/**/*", "tasks/**/*", "test/**/*", "typechain-types/**/*"],
12 | "files": ["hardhat.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/lean-imt/.env.example:
--------------------------------------------------------------------------------
1 | COINMARKETCAP_API_KEY=
2 | REPORT_GAS=false
3 | REPORT_GAS_OUTPUT_JSON=false
4 |
--------------------------------------------------------------------------------
/packages/lean-imt/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "arrowParens": "always",
4 | "trailingComma": "none",
5 | "plugins": ["prettier-plugin-solidity"]
6 | }
7 |
--------------------------------------------------------------------------------
/packages/lean-imt/.solcover.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | istanbulFolder: "../../coverage/lean-imt"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/lean-imt/.solhint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "solhint:recommended",
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "compiler-version": ["error", ">=0.8.0"],
6 | "const-name-snakecase": "off",
7 | "no-empty-blocks": "off",
8 | "constructor-syntax": "error",
9 | "func-visibility": ["error", { "ignoreConstructors": true }],
10 | "max-line-length": ["error", 120],
11 | "not-rely-on-time": "off",
12 | "prettier/prettier": [
13 | "error",
14 | {
15 | "endOfLine": "auto"
16 | }
17 | ],
18 | "reason-string": ["warn", { "maxLength": 80 }]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/lean-imt/LICENSE:
--------------------------------------------------------------------------------
1 | contracts/LICENSE
--------------------------------------------------------------------------------
/packages/lean-imt/README.md:
--------------------------------------------------------------------------------
1 | contracts/README.md
--------------------------------------------------------------------------------
/packages/lean-imt/contracts/Constants.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.4;
3 |
4 | uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
5 |
--------------------------------------------------------------------------------
/packages/lean-imt/contracts/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ethereum Foundation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/lean-imt/contracts/LeanIMT.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.4;
3 |
4 | import {InternalLeanIMT, LeanIMTData} from "./InternalLeanIMT.sol";
5 |
6 | library LeanIMT {
7 | using InternalLeanIMT for *;
8 |
9 | function insert(LeanIMTData storage self, uint256 leaf) public returns (uint256) {
10 | return InternalLeanIMT._insert(self, leaf);
11 | }
12 |
13 | function insertMany(LeanIMTData storage self, uint256[] calldata leaves) public returns (uint256) {
14 | return InternalLeanIMT._insertMany(self, leaves);
15 | }
16 |
17 | function update(
18 | LeanIMTData storage self,
19 | uint256 oldLeaf,
20 | uint256 newLeaf,
21 | uint256[] calldata siblingNodes
22 | ) public returns (uint256) {
23 | return InternalLeanIMT._update(self, oldLeaf, newLeaf, siblingNodes);
24 | }
25 |
26 | function remove(
27 | LeanIMTData storage self,
28 | uint256 oldLeaf,
29 | uint256[] calldata siblingNodes
30 | ) public returns (uint256) {
31 | return InternalLeanIMT._remove(self, oldLeaf, siblingNodes);
32 | }
33 |
34 | function has(LeanIMTData storage self, uint256 leaf) public view returns (bool) {
35 | return InternalLeanIMT._has(self, leaf);
36 | }
37 |
38 | function indexOf(LeanIMTData storage self, uint256 leaf) public view returns (uint256) {
39 | return InternalLeanIMT._indexOf(self, leaf);
40 | }
41 |
42 | function root(LeanIMTData storage self) public view returns (uint256) {
43 | return InternalLeanIMT._root(self);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/lean-imt/contracts/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Lean Incremental Merkle Tree (Solidity)
4 |
5 | Lean Incremental Merkle tree implementation in Solidity.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 | > [!NOTE]
35 | > This library has been audited as part of the Semaphore V4 PSE audit: https://semaphore.pse.dev/Semaphore_4.0.0_Audit.pdf.
36 |
37 | The LeanIMT is an optimized binary version of the [IMT](https://github.com/privacy-scaling-explorations/zk-kit.solidity/tree/main/packages/imt) into binary-focused model, eliminating the need for zero values and allowing dynamic depth adjustment. Unlike the IMT, which uses a zero hash for incomplete nodes, the LeanIMT directly adopts the left child's value when a node lacks a right counterpart. The tree's depth dynamically adjusts to the count of leaves, enhancing efficiency by reducing the number of required hash calculations. To understand more about the LeanIMT, take a look at this [visual explanation](https://hackmd.io/@vplasencia/S1whLBN16).
38 |
39 | ---
40 |
41 | ## 🛠 Install
42 |
43 | ### npm or yarn
44 |
45 | Install the `@zk-kit/lean-imt.sol` package with npm:
46 |
47 | ```bash
48 | npm i @zk-kit/lean-imt.sol --save
49 | ```
50 |
51 | or yarn:
52 |
53 | ```bash
54 | yarn add @zk-kit/lean-imt.sol
55 | ```
56 |
57 | ## 📜 Usage
58 |
59 | Please, see the [test contracts](./test) for guidance on utilizing the libraries.
60 |
--------------------------------------------------------------------------------
/packages/lean-imt/contracts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@zk-kit/lean-imt.sol",
3 | "version": "2.0.1",
4 | "description": "Incremental Merkle tree implementations in Solidity.",
5 | "license": "MIT",
6 | "files": [
7 | "*.sol",
8 | "!test/*",
9 | "README.md",
10 | "LICENSE"
11 | ],
12 | "keywords": [
13 | "blockchain",
14 | "ethereum",
15 | "hardhat",
16 | "smart-contracts",
17 | "solidity",
18 | "libraries",
19 | "merkle-tree",
20 | "incremental-merkle-tree"
21 | ],
22 | "repository": "git@github.com:privacy-scaling-explorations/zk-kit.solidity.git",
23 | "homepage": "https://github.com/privacy-scaling-explorations/zk-kit.solidity/tree/main/packages/imt",
24 | "publishConfig": {
25 | "access": "public"
26 | },
27 | "dependencies": {
28 | "poseidon-solidity": "0.0.5"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/lean-imt/contracts/test/LeanIMTTest.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | pragma solidity ^0.8.4;
4 |
5 | import {LeanIMT, LeanIMTData} from "../LeanIMT.sol";
6 |
7 | contract LeanIMTTest {
8 | LeanIMTData public data;
9 |
10 | function insert(uint256 leaf) external {
11 | LeanIMT.insert(data, leaf);
12 | }
13 |
14 | function insertMany(uint256[] calldata leaves) external {
15 | LeanIMT.insertMany(data, leaves);
16 | }
17 |
18 | function update(uint256 oldLeaf, uint256 newLeaf, uint256[] calldata siblingNodes) external {
19 | LeanIMT.update(data, oldLeaf, newLeaf, siblingNodes);
20 | }
21 |
22 | function remove(uint256 leaf, uint256[] calldata siblingNodes) external {
23 | LeanIMT.remove(data, leaf, siblingNodes);
24 | }
25 |
26 | function has(uint256 leaf) external view returns (bool) {
27 | return LeanIMT.has(data, leaf);
28 | }
29 |
30 | function indexOf(uint256 leaf) external view returns (uint256) {
31 | return LeanIMT.indexOf(data, leaf);
32 | }
33 |
34 | function root() public view returns (uint256) {
35 | return LeanIMT.root(data);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/lean-imt/hardhat.config.ts:
--------------------------------------------------------------------------------
1 | import "@nomicfoundation/hardhat-toolbox"
2 | import { HardhatUserConfig } from "hardhat/config"
3 | import "./tasks/deploy-imt-test"
4 | import "dotenv/config"
5 |
6 | const hardhatConfig: HardhatUserConfig = {
7 | solidity: {
8 | version: "0.8.23",
9 | settings: {
10 | optimizer: {
11 | enabled: true
12 | }
13 | }
14 | },
15 | gasReporter: {
16 | currency: "USD",
17 | enabled: process.env.REPORT_GAS === "true",
18 | outputJSONFile: "gas-report-leanimt.json",
19 | outputJSON: process.env.REPORT_GAS_OUTPUT_JSON === "true"
20 | },
21 | typechain: {
22 | target: "ethers-v6"
23 | }
24 | }
25 |
26 | export default hardhatConfig
27 |
--------------------------------------------------------------------------------
/packages/lean-imt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lean-imt.sol",
3 | "private": true,
4 | "scripts": {
5 | "start": "hardhat node",
6 | "compile": "hardhat compile",
7 | "test": "hardhat test",
8 | "test:report-gas": "REPORT_GAS=true hardhat test",
9 | "test:coverage": "hardhat coverage",
10 | "typechain": "hardhat typechain",
11 | "lint": "solhint 'contracts/**/*.sol'",
12 | "slither": "slither . --include-paths contracts --exclude-dependencies --ignore-compile"
13 | },
14 | "devDependencies": {
15 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.3",
16 | "@nomicfoundation/hardhat-ethers": "^3.0.0",
17 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
18 | "@nomicfoundation/hardhat-toolbox": "^4.0.0",
19 | "@nomicfoundation/hardhat-verify": "^2.0.0",
20 | "@typechain/ethers-v6": "^0.5.0",
21 | "@typechain/hardhat": "^9.0.0",
22 | "@types/chai": "^4.2.0",
23 | "@types/mocha": "^10.0.6",
24 | "@types/node": "^20.10.7",
25 | "@zk-kit/lean-imt": "^2.0.1",
26 | "chai": "^4.2.0",
27 | "ethers": "^6.4.0",
28 | "hardhat": "^2.19.4",
29 | "hardhat-gas-reporter": "^2.2.0",
30 | "poseidon-lite": "^0.2.0",
31 | "prettier-plugin-solidity": "^1.3.1",
32 | "solhint": "^3.3.6",
33 | "solhint-plugin-prettier": "^0.1.0",
34 | "solidity-coverage": "^0.8.0",
35 | "ts-node": "^10.9.2",
36 | "typechain": "^8.3.0",
37 | "typescript": "^5.3.3"
38 | },
39 | "dependencies": {
40 | "poseidon-solidity": "0.0.5"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/lean-imt/tasks/deploy-imt-test.ts:
--------------------------------------------------------------------------------
1 | import { task, types } from "hardhat/config"
2 |
3 | task("deploy:imt-test", "Deploy an IMT contract for testing a library")
4 | .addParam("library", "The name of the library", undefined, types.string)
5 | .addOptionalParam("logs", "Print the logs", true, types.boolean)
6 | .addOptionalParam("arity", "The arity of the tree", 2, types.int)
7 | .setAction(async ({ logs, library: libraryName, arity }, { ethers }): Promise => {
8 | const PoseidonFactory = await ethers.getContractFactory(`PoseidonT${arity + 1}`)
9 |
10 | const poseidon = await PoseidonFactory.deploy()
11 | const poseidonAddress = await poseidon.getAddress()
12 |
13 | if (logs) {
14 | console.info(`PoseidonT${arity + 1} library has been deployed to: ${poseidonAddress}`)
15 | }
16 |
17 | const LibraryFactory = await ethers.getContractFactory(libraryName, {
18 | libraries: {
19 | [`PoseidonT${arity + 1}`]: poseidonAddress
20 | }
21 | })
22 |
23 | const library = await LibraryFactory.deploy()
24 | const libraryAddress = await library.getAddress()
25 |
26 | if (logs) {
27 | console.info(`${libraryName} library has been deployed to: ${libraryAddress}`)
28 | }
29 |
30 | const ContractFactory = await ethers.getContractFactory(`${libraryName}Test`, {
31 | libraries: {
32 | [libraryName]: libraryAddress
33 | }
34 | })
35 |
36 | const contract = await ContractFactory.deploy()
37 | const contractAddress = await contract.getAddress()
38 |
39 | if (logs) {
40 | console.info(`${libraryName}Test contract has been deployed to: ${contractAddress}`)
41 | }
42 |
43 | return { library, contract }
44 | })
45 |
--------------------------------------------------------------------------------
/packages/lean-imt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "resolveJsonModule": true
10 | },
11 | "include": ["scripts/**/*", "tasks/**/*", "test/**/*", "typechain-types/**/*"],
12 | "files": ["hardhat.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/scripts/check-slither.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | CYAN="\033[36m"
5 | RED="\033[31m"
6 | RESET="\033[0m"
7 |
8 | log() {
9 | printf "%b\n" "$1"
10 | }
11 |
12 | main() {
13 | if ! command -v slither >/dev/null; then
14 | log "${RED}error: slither is required but is not installed${RESET}.\nFollow instructions at ${CYAN}https://github.com/crytic/slither#how-to-install${RESET} and try again."
15 | exit 1
16 | fi
17 | }
18 |
19 | main
20 |
--------------------------------------------------------------------------------
/scripts/remove-stable-version-field.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readFileSync, writeFileSync } from "node:fs"
2 |
3 | async function main() {
4 | let dotIndex = process.argv[2].lastIndexOf(".")
5 |
6 | let folderName = dotIndex !== -1 ? process.argv[2].slice(0, dotIndex) : process.argv[2]
7 |
8 | const projectDirectory = `packages/${folderName}`
9 |
10 | let filePath = `${projectDirectory}/package.json`
11 |
12 | if (existsSync(`${projectDirectory}/contracts/package.json`)) {
13 | filePath = `${projectDirectory}/contracts/package.json`
14 | }
15 |
16 | const content = JSON.parse(readFileSync(filePath, "utf8"))
17 |
18 | if (content.stableVersion) {
19 | delete content.stableVersion
20 | }
21 |
22 | writeFileSync(filePath, JSON.stringify(content, null, 4), "utf8")
23 | }
24 |
25 | main()
26 | .then(() => process.exit(0))
27 | .catch((error) => {
28 | console.error(error)
29 | process.exit(1)
30 | })
31 |
--------------------------------------------------------------------------------