Skip to content

Commit

Permalink
Complete porting for MACI gatekeepers (#31)
Browse files Browse the repository at this point in the history
<!-- 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
  • Loading branch information
0xjei authored Jul 10, 2024
1 parent 1aef325 commit f49d978
Show file tree
Hide file tree
Showing 24 changed files with 1,601 additions and 35 deletions.
3 changes: 3 additions & 0 deletions packages/excubiae/contracts/Excubia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ abstract contract Excubia is IExcubia, Ownable(msg.sender) {
_;
}

/// @inheritdoc IExcubia
function trait() external pure virtual returns (string memory) {}

/// @inheritdoc IExcubia
function setGate(address _gate) public virtual onlyOwner {
if (_gate == address(0)) revert ZeroAddress();
Expand Down
4 changes: 4 additions & 0 deletions packages/excubiae/contracts/IExcubia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ interface IExcubia {
/// @notice Error thrown when the passerby has already passed the gate.
error AlreadyPassed();

/// @notice Gets the trait of the Excubia contract.
/// @return The specific trait of the Excubia contract (e.g., SemaphoreExcubia has trait `Semaphore`).
function trait() external pure returns (string memory);

/// @notice Sets the gate address.
/// @dev Only the owner can set the destination gate address.
/// @param _gate The address of the contract to be set as the gate.
Expand Down
4 changes: 1 addition & 3 deletions packages/excubiae/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,8 @@ contract MyExcubia is Excubia {
// Implement your logic to prevent unwanted access here.
}
function _check(address passerby, bytes calldata data) internal view override returns (bool) {
function _check(address passerby, bytes calldata data) internal view override {
// Implement custom access control logic here.
return true;
}
// ...
Expand Down
19 changes: 12 additions & 7 deletions packages/excubiae/contracts/extensions/EASExcubia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ contract EASExcubia is Excubia {
/// the only ones valid to pass the gate.
address public immutable ATTESTER;

/// @notice Mapping to track which attestations have been registered by the contract to
/// avoid pass the gate twice with the same attestation.
mapping(bytes32 => bool) public registeredAttestations;
/// @notice Mapping to track which attestations have passed the gate to
/// avoid passing it twice using the same attestation.
mapping(bytes32 => bool) public passedAttestations;

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

/// @notice The trait of the Excubia contract.
function trait() external pure override returns (string memory) {
return "EAS";
}

/// @notice Internal function to handle the passing logic with check.
/// @dev Calls the parent `_pass` function and registers the attestation to avoid pass the gate twice.
/// @dev Calls the parent `_pass` function and stores the attestation to avoid pass the gate twice.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Additional data required for the check (e.g., encoded attestation ID).
function _pass(address passerby, bytes calldata data) internal override {
bytes32 attestationId = abi.decode(data, (bytes32));

// Avoiding passing the gate twice using the same attestation.
if (registeredAttestations[attestationId]) revert AlreadyPassed();
if (passedAttestations[attestationId]) revert AlreadyPassed();

super._pass(passerby, data);
passedAttestations[attestationId] = true;

registeredAttestations[attestationId] = true;
super._pass(passerby, data);
}

/// @notice Internal function to handle the gate protection (attestation check) logic.
Expand Down
17 changes: 11 additions & 6 deletions packages/excubiae/contracts/extensions/ERC721Excubia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ contract ERC721Excubia is Excubia {
/// @notice The ERC721 token contract interface.
IERC721 public immutable NFT;

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

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

/// @notice The trait of the Excubia contract.
function trait() external pure override returns (string memory) {
return "ERC721";
}

/// @notice Internal function to handle the passing logic with check.
/// @dev Calls the parent `_pass` function and registers the NFT ID to avoid passing the gate twice.
/// @dev Calls the parent `_pass` function and stores the NFT ID to avoid passing the gate twice.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Additional data required for the check (e.g., encoded token ID).
function _pass(address passerby, bytes calldata data) internal override {
uint256 tokenId = abi.decode(data, (uint256));

// Avoiding passing the gate twice with the same token ID.
if (registeredTokenIds[tokenId]) revert AlreadyPassed();
if (passedTokenIds[tokenId]) revert AlreadyPassed();

super._pass(passerby, data);
passedTokenIds[tokenId] = true;

registeredTokenIds[tokenId] = true;
super._pass(passerby, data);
}

/// @notice Internal function to handle the gate protection (token ownership check) logic.
Expand Down
15 changes: 10 additions & 5 deletions packages/excubiae/contracts/extensions/FreeForAllExcubia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,25 @@ contract FreeForAllExcubia is Excubia {
/// @notice Constructor for the FreeForAllExcubia contract.
constructor() {}

/// @notice Mapping to track already registered passersby.
mapping(address => bool) public registeredPassersby;
/// @notice Mapping to track already passed passersby.
mapping(address => bool) public passedPassersby;

/// @notice The trait of the Excubia contract.
function trait() external pure override returns (string memory) {
return "FreeForAll";
}

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

super._pass(passerby, data);
passedPassersby[passerby] = true;

registeredPassersby[passerby] = true;
super._pass(passerby, data);
}

/// @notice Internal function to handle the gate protection logic.
Expand Down
70 changes: 70 additions & 0 deletions packages/excubiae/contracts/extensions/GitcoinPassportExcubia.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {Excubia} from "../Excubia.sol";
import {IGitcoinPassportDecoder} from "./interfaces/IGitcoinPassportDecoder.sol";

/// @title Gitcoin Passport Excubia Contract.
/// @notice This contract extends the Excubia contract to integrate with the Gitcoin Passport Decoder.
/// This contract checks the Gitcoin Passport user score to permit access through the gate.
/// The Gitcoin Passport smart contract stack is built on top of Ethereum Attestation Service (EAS) contracts.
/// @dev The contract uses a fixed threshold score to admit only passersby with a passport score
/// equal to or greater than the fixed threshold based on their score (see _check() for more).
contract GitcoinPassportExcubia is Excubia {
/// @notice The factor used to scale the score.
/// @dev https://docs.passport.xyz/building-with-passport/smart-contracts/contract-reference#available-methods
uint256 public constant FACTOR = 100;

/// @notice The Gitcoin Passport Decoder contract interface.
IGitcoinPassportDecoder public immutable DECODER;

/// @notice The minimum threshold score required to pass the gate.
uint256 public immutable THRESHOLD_SCORE;

/// @notice Mapping to track which users have already passed through the gate.
mapping(address => bool) public passedUsers;

/// @notice Error thrown when the user's score is insufficient to pass the gate.
error InsufficientScore();

/// @notice Error thrown when the threshold score is negative or zero.
error NegativeOrZeroThresholdScore();

/// @notice Constructor to initialize the contract with the target decoder and threshold score.
/// @param _decoder The address of the Gitcoin Passport Decoder contract.
/// @param _thresholdScore The minimum threshold score required to pass the gate.
constructor(address _decoder, uint256 _thresholdScore) {
if (_decoder == address(0)) revert ZeroAddress();
if (_thresholdScore <= 0) revert NegativeOrZeroThresholdScore();

DECODER = IGitcoinPassportDecoder(_decoder);
THRESHOLD_SCORE = _thresholdScore;
}

/// @notice The trait of the Excubia contract.
function trait() external pure override returns (string memory) {
return "GitcoinPassport";
}

/// @notice Internal function to handle the passing logic with check.
/// @dev Calls the parent `_pass` function and stores the user to avoid passing the gate twice.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Additional data required for the check.
function _pass(address passerby, bytes calldata data) internal override {
if (passedUsers[passerby]) revert AlreadyPassed();

passedUsers[passerby] = true;

super._pass(passerby, data);
}

/// @notice Internal function to handle the gate protection (score check) logic.
/// @dev Checks if the user's Gitcoin Passport score meets the threshold.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Additional data required for the check.
function _check(address passerby, bytes calldata data) internal view override {
super._check(passerby, data);

if ((DECODER.getScore(passerby) / FACTOR) < THRESHOLD_SCORE) revert InsufficientScore();
}
}
76 changes: 76 additions & 0 deletions packages/excubiae/contracts/extensions/HatsExcubia.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {Excubia} from "../Excubia.sol";
import {IHatsMinimal} from "./interfaces/IHatsMinimal.sol";

/// @title Hats Excubia Contract.
/// @notice This contract extends the Excubia contract to integrate with the Hats protocol.
/// This contract checks if a user is wearing a specific hat to permit access through the gate.
/// @dev The contract uses a specific set of hats to admit the passerby wearing any of those hats.
contract HatsExcubia is Excubia {
/// @notice The Hats contract interface.
IHatsMinimal public immutable HATS;

/// @notice Mapping to track which hats are considered valid for passing the gate.
mapping(uint256 => bool) public criterionHat;
/// @notice Mapping to track which users have already passed through the gate.
mapping(address => bool) public passedUsers;

/// @notice Error thrown when the user is not wearing the required hat.
error NotWearingCriterionHat();
/// @notice Error thrown when the specified hat is not a criterion hat.
error NotCriterionHat();
/// @notice Error thrown when the array of criterion hats is empty.
error ZeroCriterionHats();

/// @notice Constructor to initialize the contract with the target Hats contract and criterion hats.
/// @param _hats The address of the Hats contract.
/// @param _criterionHats An array of hat IDs that are considered as criteria for passing the gate.
constructor(address _hats, uint256[] memory _criterionHats) {
if (_hats == address(0)) revert ZeroAddress();
if (_criterionHats.length == 0) revert ZeroCriterionHats();

HATS = IHatsMinimal(_hats);

uint256 numberOfCriterionHats = _criterionHats.length;

for (uint256 i = 0; i < numberOfCriterionHats; ++i) {
criterionHat[_criterionHats[i]] = true;
}
}

/// @notice The trait of the Excubia contract.
function trait() external pure override returns (string memory) {
return "Hats";
}

/// @notice Internal function to handle the passing logic with check.
/// @dev Calls the parent `_pass` function and stores the user to avoid passing the gate twice.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Additional data required for the check.
function _pass(address passerby, bytes calldata data) internal override {
// Avoiding passing the gate twice for the same user.
if (passedUsers[passerby]) revert AlreadyPassed();

passedUsers[passerby] = true;

super._pass(passerby, data);
}

/// @notice Internal function to handle the gate protection (hat check) logic.
/// @dev Checks if the user is wearing one of the criterion hats.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Additional data required for the check.
function _check(address passerby, bytes calldata data) internal view override {
super._check(passerby, data);

uint256 hat = abi.decode(data, (uint256));

// Check if the hat is a criterion hat.
if (!criterionHat[hat]) revert NotCriterionHat();

// Check if the user is wearing the criterion hat.
if (!HATS.isWearerOfHat(passerby, hat)) revert NotWearingCriterionHat();
}
}
13 changes: 9 additions & 4 deletions packages/excubiae/contracts/extensions/SemaphoreExcubia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ contract SemaphoreExcubia is Excubia {
/// @notice Mapping to track which nullifiers have been used to avoid passing the
/// gate twice using the same Semaphore identity.
/// @dev The nullifier is derived from the hash of the secret and group identifier,
/// ensuring that the same identity cannot be registered twice for the same group.
/// ensuring that the same identity cannot pass twice using the same group.
mapping(uint256 => bool) public passedNullifiers;

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

/// @notice The trait of the Excubia contract.
function trait() external pure override returns (string memory) {
return "Semaphore";
}

/// @notice Internal function to handle the passing logic with check.
/// @dev Calls the parent `_pass` function and registers the nullifier to avoid passing the gate twice.
/// @dev Calls the parent `_pass` function and stores the nullifier to avoid passing the gate twice.
/// @param passerby The address of the entity attempting to pass the gate.
/// @param data Additional data required for the check (ie., encoded Semaphore proof).
function _pass(address passerby, bytes calldata data) internal override {
Expand All @@ -54,9 +59,9 @@ contract SemaphoreExcubia is Excubia {
// Avoiding passing the gate twice using the same nullifier.
if (passedNullifiers[proof.nullifier]) revert AlreadyPassed();

super._pass(passerby, data);

passedNullifiers[proof.nullifier] = true;

super._pass(passerby, data);
}

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

0 comments on commit f49d978

Please sign in to comment.