A flexible and modular framework for general-purpose on-chain gatekeepers.
6 |
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/IExcubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | /// @title IExcubia.
5 | /// @notice Excubia contract interface.
6 | interface IExcubia {
7 | /// @notice Event emitted when someone passes the gate check.
8 | /// @param passerby The address of those who have successfully passed the check.
9 | /// @param gate The address of the excubia-protected contract address.
10 | event GatePassed(address indexed passerby, address indexed gate);
11 |
12 | /// @notice Event emitted when the gate address is set.
13 | /// @param gate The address of the contract set as the gate.
14 | event GateSet(address indexed gate);
15 |
16 | /// @notice Error thrown when an address equal to zero is given.
17 | error ZeroAddress();
18 |
19 | /// @notice Error thrown when the gate address is not set.
20 | error GateNotSet();
21 |
22 | /// @notice Error thrown when the callee is not the gate contract.
23 | error GateOnly();
24 |
25 | /// @notice Error thrown when the gate address has been already set.
26 | error GateAlreadySet();
27 |
28 | /// @notice Error thrown when the passerby has already passed the gate.
29 | error AlreadyPassed();
30 |
31 | /// @notice Gets the trait of the Excubia contract.
32 | /// @return The specific trait of the Excubia contract (e.g., SemaphoreExcubia has trait `Semaphore`).
33 | function trait() external pure returns (string memory);
34 |
35 | /// @notice Sets the gate address.
36 | /// @dev Only the owner can set the destination gate address.
37 | /// @param _gate The address of the contract to be set as the gate.
38 | function setGate(address _gate) external;
39 |
40 | /// @notice Enforces the custom gate passing logic.
41 | /// @dev Must call the `check` to handle the logic of checking passerby for specific gate.
42 | /// @param passerby The address of the entity attempting to pass the gate.
43 | /// @param data Additional data required for the check (e.g., encoded token identifier).
44 | function pass(address passerby, bytes calldata data) external;
45 |
46 | /// @dev Defines the custom gate protection logic.
47 | /// @param passerby The address of the entity attempting to pass the gate.
48 | /// @param data Additional data that may be required for the check.
49 | function check(address passerby, bytes calldata data) external view;
50 | }
51 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 pull requests for minor grammatical fixes (e.g., correcting typos, rewording sentences) or for fixing broken links, unless they significantly improve clarity or functionality. These contributions, while appreciated, are not a priority for merging. If you notice any of these issues, please create a [GitHub Issue](https://github.com/privacy-scaling-explorations/zk-kit.solidity/issues/new?template=BLANK_ISSUE) to report them so they can be properly tracked and addressed.
42 |
--------------------------------------------------------------------------------
/packages/lean-imt/contracts/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Lean Incremental Merkle Tree (Solidity)
4 |
5 |
Lean Incremental Merkle tree implementation in Solidity.
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/excubiae/contracts/Excubia.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5 | import {IExcubia} from "./IExcubia.sol";
6 |
7 | /// @title Excubia.
8 | /// @notice Abstract base contract which can be extended to implement a specific excubia.
9 | /// @dev Inherit from this contract and implement the `_pass` & `_check` methods to define
10 | /// your custom gatekeeping logic.
11 | abstract contract Excubia is IExcubia, Ownable(msg.sender) {
12 | /// @notice The excubia-protected contract address.
13 | /// @dev The gate can be any contract address that requires a prior check to enable logic.
14 | /// For example, the gate is a Semaphore group that requires the passerby
15 | /// to meet certain criteria before joining.
16 | address public gate;
17 |
18 | /// @dev Modifier to restrict function calls to only from the gate address.
19 | modifier onlyGate() {
20 | if (msg.sender != gate) revert GateOnly();
21 | _;
22 | }
23 |
24 | /// @inheritdoc IExcubia
25 | function trait() external pure virtual returns (string memory) {}
26 |
27 | /// @inheritdoc IExcubia
28 | function setGate(address _gate) public virtual onlyOwner {
29 | if (_gate == address(0)) revert ZeroAddress();
30 | if (gate != address(0)) revert GateAlreadySet();
31 |
32 | gate = _gate;
33 |
34 | emit GateSet(_gate);
35 | }
36 |
37 | /// @inheritdoc IExcubia
38 | function pass(address passerby, bytes calldata data) external onlyGate {
39 | _pass(passerby, data);
40 | }
41 |
42 | /// @inheritdoc IExcubia
43 | function check(address passerby, bytes calldata data) external view {
44 | _check(passerby, data);
45 | }
46 |
47 | /// @notice Internal function to enforce the custom gate passing logic.
48 | /// @dev Calls the `_check` internal logic and emits the relative event if successful.
49 | /// @param passerby The address of the entity attempting to pass the gate.
50 | /// @param data Additional data required for the check (e.g., encoded token identifier).
51 | function _pass(address passerby, bytes calldata data) internal virtual {
52 | _check(passerby, data);
53 |
54 | emit GatePassed(passerby, gate);
55 | }
56 |
57 | /// @notice Internal function to define the custom gate protection logic.
58 | /// @dev Custom logic to determine if the passerby can pass the gate.
59 | /// @param passerby The address of the entity attempting to pass the gate.
60 | /// @param data Additional data that may be required for the check.
61 | function _check(address passerby, bytes calldata data) internal view virtual {}
62 | }
63 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/lazytower/contracts/README.md:
--------------------------------------------------------------------------------
1 |
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/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/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 |
--------------------------------------------------------------------------------
/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 pull requests for minor grammatical fixes (e.g., correcting typos, rewording sentences) or for fixing broken links, unless they significantly improve clarity or functionality. These contributions, while appreciated, are not a priority for merging. If you notice any of these issues, please create a [GitHub Issue](https://github.com/privacy-scaling-explorations/zk-kit.solidity/issues/new?template=BLANK_ISSUE) to report them so they can be properly tracked and addressed.
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 |