Skip to content

Commit f49d978

Browse files
authored
Complete porting for MACI gatekeepers (#31)
<!-- Please refer to our CONTRIBUTING documentation for any questions on submitting a pull request. --> <!-- Provide a general summary of your changes in the Title above. --> ## Description This PR adds the `GitcoinPassportExcubia`, `ZKEdDSAEventTicketPCDExcubia` (prev Zupass) and, `HatsExcubia`. During the porting process, certain interfaces were extended and controls and methods generalised. The code coverage is 100%. Also, this PR introduces the concept of `trait` aka the specific type of an Excubia contract. For example, `SemaphoreExcubia` has trait `Semaphore` and so on. This will make easy to discriminate and query multiple Excubiae sharing the same characteristics. <!-- Describe your changes in detail. --> <!-- You may want to answer some of the following questions: --> <!-- What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) --> <!-- What is the current behavior?** (You can also link to an open issue here) --> <!-- What is the new behavior (if this is a feature change)? --> <!-- Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?) --> ## Related Issue(s) closes #18 <!-- This project accepts pull requests related to open issues. --> <!-- If suggesting a new feature or change, please discuss it in an issue first. --> <!-- If fixing a bug, there should be an issue describing it with steps to reproduce. --> <!-- Please link to the issue(s) here --> <!-- Closes # --> <!-- Fixes # --> ## Checklist <!-- Please check if the PR fulfills these requirements. --> - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] My changes generate no new warnings - [x] I have run `yarn format` and `yarn compile` without getting any errors - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes
1 parent 1aef325 commit f49d978

24 files changed

+1601
-35
lines changed

packages/excubiae/contracts/Excubia.sol

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ abstract contract Excubia is IExcubia, Ownable(msg.sender) {
2121
_;
2222
}
2323

24+
/// @inheritdoc IExcubia
25+
function trait() external pure virtual returns (string memory) {}
26+
2427
/// @inheritdoc IExcubia
2528
function setGate(address _gate) public virtual onlyOwner {
2629
if (_gate == address(0)) revert ZeroAddress();

packages/excubiae/contracts/IExcubia.sol

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ interface IExcubia {
2828
/// @notice Error thrown when the passerby has already passed the gate.
2929
error AlreadyPassed();
3030

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+
3135
/// @notice Sets the gate address.
3236
/// @dev Only the owner can set the destination gate address.
3337
/// @param _gate The address of the contract to be set as the gate.

packages/excubiae/contracts/README.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,8 @@ contract MyExcubia is Excubia {
7272
// Implement your logic to prevent unwanted access here.
7373
}
7474
75-
function _check(address passerby, bytes calldata data) internal view override returns (bool) {
75+
function _check(address passerby, bytes calldata data) internal view override {
7676
// Implement custom access control logic here.
77-
78-
return true;
7977
}
8078
8179
// ...

packages/excubiae/contracts/extensions/EASExcubia.sol

+12-7
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ contract EASExcubia is Excubia {
1818
/// the only ones valid to pass the gate.
1919
address public immutable ATTESTER;
2020

21-
/// @notice Mapping to track which attestations have been registered by the contract to
22-
/// avoid pass the gate twice with the same attestation.
23-
mapping(bytes32 => bool) public registeredAttestations;
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;
2424

2525
/// @notice Error thrown when the attestation does not match the designed schema.
2626
error UnexpectedSchema();
@@ -46,19 +46,24 @@ contract EASExcubia is Excubia {
4646
SCHEMA = _schema;
4747
}
4848

49+
/// @notice The trait of the Excubia contract.
50+
function trait() external pure override returns (string memory) {
51+
return "EAS";
52+
}
53+
4954
/// @notice Internal function to handle the passing logic with check.
50-
/// @dev Calls the parent `_pass` function and registers the attestation to avoid pass the gate twice.
55+
/// @dev Calls the parent `_pass` function and stores the attestation to avoid pass the gate twice.
5156
/// @param passerby The address of the entity attempting to pass the gate.
5257
/// @param data Additional data required for the check (e.g., encoded attestation ID).
5358
function _pass(address passerby, bytes calldata data) internal override {
5459
bytes32 attestationId = abi.decode(data, (bytes32));
5560

5661
// Avoiding passing the gate twice using the same attestation.
57-
if (registeredAttestations[attestationId]) revert AlreadyPassed();
62+
if (passedAttestations[attestationId]) revert AlreadyPassed();
5863

59-
super._pass(passerby, data);
64+
passedAttestations[attestationId] = true;
6065

61-
registeredAttestations[attestationId] = true;
66+
super._pass(passerby, data);
6267
}
6368

6469
/// @notice Internal function to handle the gate protection (attestation check) logic.

packages/excubiae/contracts/extensions/ERC721Excubia.sol

+11-6
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ contract ERC721Excubia is Excubia {
1212
/// @notice The ERC721 token contract interface.
1313
IERC721 public immutable NFT;
1414

15-
/// @notice Mapping to track which token IDs have been registered by the contract to
15+
/// @notice Mapping to track which token IDs have passed by the gate to
1616
/// avoid passing the gate twice with the same token ID.
17-
mapping(uint256 => bool) public registeredTokenIds;
17+
mapping(uint256 => bool) public passedTokenIds;
1818

1919
/// @notice Error thrown when the passerby is not the owner of the token.
2020
error UnexpectedTokenOwner();
@@ -27,19 +27,24 @@ contract ERC721Excubia is Excubia {
2727
NFT = IERC721(_erc721);
2828
}
2929

30+
/// @notice The trait of the Excubia contract.
31+
function trait() external pure override returns (string memory) {
32+
return "ERC721";
33+
}
34+
3035
/// @notice Internal function to handle the passing logic with check.
31-
/// @dev Calls the parent `_pass` function and registers the NFT ID to avoid passing the gate twice.
36+
/// @dev Calls the parent `_pass` function and stores the NFT ID to avoid passing the gate twice.
3237
/// @param passerby The address of the entity attempting to pass the gate.
3338
/// @param data Additional data required for the check (e.g., encoded token ID).
3439
function _pass(address passerby, bytes calldata data) internal override {
3540
uint256 tokenId = abi.decode(data, (uint256));
3641

3742
// Avoiding passing the gate twice with the same token ID.
38-
if (registeredTokenIds[tokenId]) revert AlreadyPassed();
43+
if (passedTokenIds[tokenId]) revert AlreadyPassed();
3944

40-
super._pass(passerby, data);
45+
passedTokenIds[tokenId] = true;
4146

42-
registeredTokenIds[tokenId] = true;
47+
super._pass(passerby, data);
4348
}
4449

4550
/// @notice Internal function to handle the gate protection (token ownership check) logic.

packages/excubiae/contracts/extensions/FreeForAllExcubia.sol

+10-5
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,25 @@ contract FreeForAllExcubia is Excubia {
1111
/// @notice Constructor for the FreeForAllExcubia contract.
1212
constructor() {}
1313

14-
/// @notice Mapping to track already registered passersby.
15-
mapping(address => bool) public registeredPassersby;
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+
}
1621

1722
/// @notice Internal function to handle the gate passing logic.
1823
/// @dev This function calls the parent `_pass` function and then tracks the passerby.
1924
/// @param passerby The address of the entity passing the gate.
2025
/// @param data Additional data required for the pass (not used in this implementation).
2126
function _pass(address passerby, bytes calldata data) internal override {
2227
// Avoiding passing the gate twice with the same address.
23-
if (registeredPassersby[passerby]) revert AlreadyPassed();
28+
if (passedPassersby[passerby]) revert AlreadyPassed();
2429

25-
super._pass(passerby, data);
30+
passedPassersby[passerby] = true;
2631

27-
registeredPassersby[passerby] = true;
32+
super._pass(passerby, data);
2833
}
2934

3035
/// @notice Internal function to handle the gate protection logic.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
}

packages/excubiae/contracts/extensions/SemaphoreExcubia.sol

+9-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ contract SemaphoreExcubia is Excubia {
1919
/// @notice Mapping to track which nullifiers have been used to avoid passing the
2020
/// gate twice using the same Semaphore identity.
2121
/// @dev The nullifier is derived from the hash of the secret and group identifier,
22-
/// ensuring that the same identity cannot be registered twice for the same group.
22+
/// ensuring that the same identity cannot pass twice using the same group.
2323
mapping(uint256 => bool) public passedNullifiers;
2424

2525
/// @notice Error thrown when the group identifier does not match the expected one.
@@ -44,8 +44,13 @@ contract SemaphoreExcubia is Excubia {
4444
GROUP_ID = _groupId;
4545
}
4646

47+
/// @notice The trait of the Excubia contract.
48+
function trait() external pure override returns (string memory) {
49+
return "Semaphore";
50+
}
51+
4752
/// @notice Internal function to handle the passing logic with check.
48-
/// @dev Calls the parent `_pass` function and registers the nullifier to avoid passing the gate twice.
53+
/// @dev Calls the parent `_pass` function and stores the nullifier to avoid passing the gate twice.
4954
/// @param passerby The address of the entity attempting to pass the gate.
5055
/// @param data Additional data required for the check (ie., encoded Semaphore proof).
5156
function _pass(address passerby, bytes calldata data) internal override {
@@ -54,9 +59,9 @@ contract SemaphoreExcubia is Excubia {
5459
// Avoiding passing the gate twice using the same nullifier.
5560
if (passedNullifiers[proof.nullifier]) revert AlreadyPassed();
5661

57-
super._pass(passerby, data);
58-
5962
passedNullifiers[proof.nullifier] = true;
63+
64+
super._pass(passerby, data);
6065
}
6166

6267
/// @notice Internal function to handle the gate protection (proof check) logic.

0 commit comments

Comments
 (0)