# excubiae [ToC] ## Introduction ### What is Excubiae? Excubiae is a composable framework for implementing custom, attribute-based access control policies on EVM-compatible networks. At its core, it separates the concerns of **policy** definition (*what rules to enforce*) from policy **checking** (*how to validate those rules*), enabling flexible and reusable access control patterns. The framework's mission is to enable policy enforcement through three key components: **Policies** that define access rules, **Checkers** that validate evidence, and *enforcement* mechanisms that manage the validation flow. Built on values of modularity, reusability, and security, Excubiae provides protocol developers with building blocks to create robust access control systems. The name "[Excubiae](https://www.nihilscio.it/Manuali/Lingua%20latina/Verbi/Coniugazione_latino.aspx?verbo=excubia&lang=IT_#:~:text=1&text=excubia%20%3D%20sentinella...%20guardia,%2C%20excubia%20%2D%20Sostantivo%201%20decl.)" comes from the ancient Roman guards who kept watch and enforced access control - an apt metaphor for a system designed to protect smart contract access through configurable policies. ### Vision In the evolving blockchain ecosystem, protocols continuously generate new forms of verifiable evidence and proofs. While these protocols excel at producing such evidence, integrating them into access control systems outside their standard ways of doing it (e.g., APIs / apps / libs / modules) remains challenging. Excubiae aims to bridge this gap by providing a universal framework for composing and enforcing access policies based and making interoperable forms of on-chain evidence, serving as a foundational layer for access control across the ecosystem. ### Understanding Excubiae The framework serves multiple audiences: protocol developers integrating access control into their systems, as smart contract engineers implementing custom validation logic for access control on-chain. The architecture consists of two main types of contracts working in concert: ``` ┌──────────────┐ enforces ┌──────────────┐ │ Policy │ ───────────────> │ Checker │ └──────────────┘ └──────────────┘ │ │ │ protects │ checks │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ Target │ │ Subject │ └──────────────┘ └──────────────┘ ``` When a **subject** (i.e., EOA or smart contract address) attempts to access a protected **target** (i.e., smart contract protected method / resource), the enforcement flow follows a clear path: 1. Subject provides evidence to a policy. 2. Policy delegates validation to its checker. 3. Checker verifies the evidence. 4. Policy enforces the checker's decision & keeps track of the subject. ### Core Design Philosophy Excubiae embraces modularity through a clear separation between policy and checking logic. Each component maintains a single responsibility, enabling independent auditing and evolution. The framework emphasizes reusability—checkers and policies can be shared and composed across different contexts, allowing developers to build complex access control systems from verified building blocks. This architectural approach not only reduces development time but also strengthens security by enabling thorough component validation, gas optimization, and extension (e.g., `BaseChecker`, `AdvancedPolicy`, and more). Protocols benefit from standardized interfaces that ensure interoperability, while users experience consistent access control with clear validation requirements across different systems. **Minimal Proxy Pattern** Under the hood, Excubiae leverages the [minimal proxy pattern with immutable args](https://github.com/vectorized/solady/blob/main/src/utils/LibClone.sol)minimal proxy pattern with immutable args for `Policy` and `Checker` contracts. This design improves efficiency, reducing gas costs for contract deployment while ensuring immutability for critical parameters. A **Factory** contract is responsible for deploying these minimal proxy clones. Instead of deploying new instances of `Policy` and `Checker` from scratch, the factory replicates an existing implementation while enforcing correct initialization. This results in a modular architecture where: - A `PolicyFactory` contract ensures customizable & cheap policy instance deploy (i.e., Policy contract instance with potentially different and specific sets of parameter values). - A `CheckerFactory` contract ensures customizable & cheap checker instance deploy (i.e., Checker contract instance with potentially different and specific sets of parameter values). - `Policy` and `Checker` clones are initialized post-deployment only once, preventing unauthorized usage before setup. At initialization time, the args can be read from deployment bytecode and accessed / stored for further usage. **Multi-Stage Validation** Excubiae introduces a multi-stage validation system through `AdvancedChecker`, supporting `PRE`, `MAIN`, and `POST` validation phases. This allows protocols to enforce progressively complex access control mechanisms, combining multiple validation steps into a single enforcement flow. For instance, a protocol might require: - **Pre-validation**: Ensuring the subject holds a specific token. - **Main validation**: Checking additional factors such as governance approval or multi-signature confirmation. - **Post-validation**: Logging the access event and updating permission states. By combining minimal proxy pattern, reusability of already deployed Policy & Checker contracts, multi-phase validation, and more; Excubiae provides an efficient and flexible access control solution for smart contracts, ensuring seamless interoperability while maintaining robust security guarantees. ### Roadmap Excubiae began as a prototype within [zk-kit.solidity](https://www.npmjs.com/package/@zk-kit/excubiae), quickly evolving into a standalone framework with its dedicated [monorepo](https://github.com/privacy-scaling-explorations/excubiae). The current [v0.3.1](https://github.com/privacy-scaling-explorations/excubiae/releases/tag/v0.3.1) release provides a robust foundation with a flexible policy framework supporting both basic and advanced verification mechanisms, the minimal proxy pattern with immutable args, the possibility to reuse already deployed instances of Checker & Policy contracts, and a set of extensions ready to use for your specific needs. The framework includes comprehensive test coverage across [Hardhat and Foundry](https://hardhat.org/hardhat-runner/docs/advanced/hardhat-and-foundry) environments, making it ready for initial adoption & integration experiments. Please, note that **we have not conducted any audit yet**. The framework is structured in three distinct yet interconnected areas. At its core lies the smart contract framework (purple big blockk), focusing on composability, reuse, and cost-efficient deployments—this has been completed with [v0.2.0](https://github.com/privacy-scaling-explorations/excubiae/releases/tag/v0.2.0). Complementing this are integration-focused tools, including templates and registries, while user-facing applications provide contract management and exploration capabilities. This architecture allows for natural expansion while maintaining a clear separation of concerns, as represented in the diagram below. The green blocks indicate completed milestones in the latest [release](https://github.com/privacy-scaling-explorations/excubiae/releases). :warning: Please note that the orange and blue big blocks are currently on hold. We consider Excubiae’s core features for adoption complete as of v0.3.1. However, we are still working on extensions set & tasks for deployments (yellow small blocks). ![image](https://hackmd.io/_uploads/HkWq5cDFyx.png) With v0.3.1, Excubiae has reached a fully-fledged MVP. Moving forward, the focus will be on adoption and integration, aiming to extend the set of extensions, along with deployment scripts, guides, and examples. The orange and blue blocks are not an immediate priority but may be revisited based on adoption and team or organization-wide plans (see warning above). Excubiae’s long-term vision is to become a standard component in protocol authentication stacks, enabling sophisticated access control through composable building blocks. The framework’s flexibility ensures it can adapt to emerging verification methods and protocols, fostering an ecosystem of reusable, audited checkers that address evolving access control requirements. The success of this vision depends on close collaboration with early adopters and the careful expansion of the framework’s capabilities. If you are interested in contributing, integrating, or building your own Policy or Checker, or if you have feedback or comments, please reach out on the [PSE Discord](https://discord.com/invite/sF5CT5rzrR) (`🚪-excubiae` channel) or open a new [issue](https://github.com/privacy-scaling-explorations/excubiae/issues/new) / [PR](https://github.com/privacy-scaling-explorations/excubiae/compare) on the [monorepo](https://github.com/privacy-scaling-explorations/excubiae). ## Use Cases ### Basic Access Control for Voting Access control in voting systems presents a common challenge in smart contract development. For example, when implementing restrictions to check voters identity, developers often face the decision of where to place validation logic. Traditional approaches tend to embed these checks directly within voting contracts, leading to tightly coupled systems that prove difficult to maintain or adapt. Excubiae addresses this challenge through its fundamental principle of separation between voting logic and access control. The framework enables a clean architecture where voting contracts remain focused on their core purpose while delegating all validation to specialized components. Consider this implementation using Excubiae's base components to check voters identity through the possession of an NFT (see [BaseVoting](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseVoting.sol) example). ```solidity contract BaseVoting { BasePolicy public immutable POLICY; mapping(address => bool) public hasVoted; mapping(uint8 => uint256) public voteCounts; constructor(BaseERC721Policy _policy) { POLICY = _policy; } function register(uint256 tokenId) external { bytes[] memory _evidence = new bytes[](1); _evidence[0] = abi.encode(tokenId); POLICY.enforce(msg.sender, _evidence); emit Registered(msg.sender); } function vote(uint8 option) external { if (!POLICY.enforced(msg.sender)) revert NotRegistered(); if (hasVoted[msg.sender]) revert AlreadyVoted(); if (option >= 2) revert InvalidOption(); hasVoted[msg.sender] = true; voteCounts[option]++; emit Voted(msg.sender, option); } } ``` The validation logic resides in its own set of specialized contracts (see [BaseERC721Policy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721Policy.sol) and [BaseERC721Checker](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721Checker.sol) examples) along with the respective Factory contracts (see [BaseERC721PolicyFactory](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721PolicyFactory.sol) and [BaseERC721CheckerFactory](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721CheckerFactory.sol) examples) This architecture demonstrates the core strengths of Excubiae's design. The voting contract maintains singular responsibility for vote management, while the policy and checker contracts handle all aspects of access control. This separation enables easy modification of validation rules without affecting the voting logic itself. Indeed, through minimal proxy pattern, anyone is able to deploy, for example, a new Checker with a different NFT address as reference for the checks and a new Policy referencing to another Checker. You can explore complete implementations, including comprehensive test cases ([Solidity](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/Base.t.sol) & [Typescript](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/test/Base.test.ts)) on the monorepo. These examples provide practical insights into integration patterns and usage scenarios. The framework's capabilities extend beyond this basic implementation. For scenarios requiring multi-step validation or combined verification rules, Excubiae provides advanced components that we'll explore next. ### Advanced Multi-Phase Voting System Building upon our [basic voting example](#basic-access-control-for-voting), Excubiae's advanced components enable more sophisticated access control patterns. Consider a voting system that requires registration, allows multiple votes, and includes a reward mechanism - each phase with its own validation requirements. The advanced system leverages Excubiae's multi-phase validation through the [AdvancedChecker](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/advanced/AdvancedERC721Checker.sol) and [AdvancedPolicy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/advanced/AdvancedERC721Policy.sol) contracts and their respective factories. This enables pre-conditions for registration, ongoing validation during voting, and post-conditions for reward distribution (see [AdvancedVoting](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/advanced/AdvancedVoting.sol) example). ```solidity contract AdvancedVoting { AdvancedPolicy public immutable POLICY; mapping(uint8 => uint256) public voteCounts; function register(uint256 tokenId) external { bytes[] memory _evidence = new bytes[](1); _evidence[0] = abi.encode(tokenId); POLICY.enforce(msg.sender, evidence, Check.PRE); emit Registered(msg.sender); } function vote(uint8 option) external { (bool pre, , ) = POLICY.enforced(address(this), msg.sender); if (!pre) revert NotRegistered(); if (option >= 2) revert InvalidOption(); bytes[] memory _evidence = new bytes[](1); _evidence[0] = abi.encode(option); POLICY.enforce(msg.sender, evidence, Check.MAIN); voteCounts[option]++; emit Voted(msg.sender, option); } function reward(uint256 rewardId) external { (bool pre, uint8 main, bool post) = POLICY.enforced(address(this), msg.sender); if (!pre) revert NotRegistered(); if (main == 0) revert NotVoted(); if (post) revert AlreadyClaimed(); POLICY.enforce(msg.sender, new bytes[](1), Check.POST); emit RewardClaimed(msg.sender, rewardId); } } ``` The system introduces distinct validation phases through the [Check](https://github.com/privacy-scaling-explorations/excubiae/blob/07bf4d60353f5b044cfead856d872177f9e48aff/packages/contracts/contracts/src/interfaces/IAdvancedChecker.sol#L8) enumeration: - PRE: Validates initial registration requirements - MAIN: Enables repeated voting with ongoing validation - POST: Controls one-time reward claiming This advanced implementation maintains Excubiae's core principle of separation of concerns while enabling complex state management and multi-phase validation. The complete implementation, including the corresponding checker contracts and test cases, can be found [here](https://github.com/privacy-scaling-explorations/excubiae/tree/main/packages/contracts/contracts/test/examples/advanced). The pattern demonstrated here extends beyond voting systems. Any protocol requiring staged access control, multiple validation steps, or state-dependent permissions can leverage this same architecture. For instance, similar patterns could manage staged token unlocks, tiered protocol access, or multi-signature operations. ## Design ### Policy A [Policy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/src/policy/Policy.sol) in Excubiae acts as a gatekeeper, controlling access to protected resources through well-defined enforcement mechanisms. Think of it as a security checkpoint - it doesn't determine the rules itself, but it ensures they are properly enforced. Each policy maintains one critical pieces of information, the [target](https://github.com/privacy-scaling-explorations/excubiae/blob/07bf4d60353f5b044cfead856d872177f9e48aff/packages/contracts/contracts/src/policy/Policy.sol#L14) address, which represents the contract or resource being protected. Since the Policy is a "clonable" contract (extends the [Clone](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/src/proxy/Clone.sol) contract), it has a specific, base implementation of the `_initialize()` internal method - where the ownership is transferred from the sender to the factory. The framework provides two extendable policy variants: #### [BasePolicy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/src/policy/BasePolicy.sol) Ideal for simple one-time validations, like token-gated access. ```solidity function enforce(address subject, bytes memory evidence) external { if (enforced[subject]) revert AlreadyEnforced(); if (!BASE_CHECKER.check(subject, evidence)) revert UnsuccessfulCheck(); enforced[subject] = true; emit Enforced(subject, target, evidence); } ``` ### [AdvancedPolicy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/src/policy/AdvancedPolicy.sol) Supports multi-phase validation with pre-checks, main validation (potentially repeated), and post-checks. ```solidity function enforce( address subject, bytes memory evidence, Check checkType ) external { CheckStatus storage status = enforced[subject]; if (checkType == Check.PRE) { if (SKIP_PRE) revert CannotPreCheckWhenSkipped(); if (status.pre) revert AlreadyEnforced(); status.pre = true; } else if (checkType == Check.POST) { if (SKIP_POST) revert CannotPostCheckWhenSkipped(); if (status.main == 0) revert MainCheckNotEnforced(); if (status.post) revert AlreadyEnforced(); status.post = true; } else { if (!SKIP_PRE && !status.pre) revert PreCheckNotEnforced(); if (!ALLOW_MULTIPLE_MAIN && status.main > 0) revert MainCheckAlreadyEnforced(); status.main += 1; } if (!ADVANCED_CHECKER.check(subject, evidence, checkType)) revert UnsuccessfulCheck(); emit Enforced(subject, target, evidence, checkType); } ``` Both variants supports their own `_initialize()` internal method override (see [Guides](#guides) for more) for clone initialization after factory deployment. This pattern ensures that: - All access attempts are validated through the checker - State changes only occur after successful validation - Multiple targets can share policies efficiently - Double-pass attempts are prevented - The enforcement flow remains consistent across implementations The state tracking acts as a non-cryptographically signed (dummy) nullifier, preventing subjects from repeatedly passing the same validation. This is particularly important in scenarios like voting or token claiming where double-participation must be prevented. #### Checker A Checker in Excubiae is responsible for validating access conditions. Think of it as the rulebook that defines what constitutes valid access - it receives evidence and determines whether it meets the specified criteria. The checker remains deliberately stateless, focusing solely on validation logic. This design allows checkers to be shared across different policies and enables clear, auditable validation rules. Please note that, also the Checker is a "clonable" contract (extends the [Clone](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/src/proxy/Clone.sol) contract). The framework offers two checker variants: - [BaseChecker](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/src/checker/BaseChecker.sol): perfect for straightforward validations that need a single check, like verifying token ownership, and - [AdvancedChecker](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/src/checker/AdvancedChecker.sol): enables multi-phase validation with distinct checks for different stages of access control. This pattern ensures: - Clean separation between validation logic and state management - Reusable validation components - Gas-efficient operations through view functions - Clear, auditable validation rules - Flexible evidence handling for different validation needs After establishing the core components (Policy and Checker), let's discuss how they work together and the key design decisions that shape the framework. ### Design Decisions #### Enforcement Flow The interaction between policies and checkers follows a deliberate pattern to ensure secure, efficient access control. When a subject attempts to access a protected resource, the flow proceeds through several key stages: ``` 1. Subject provides evidence to Policy Evidence = abi.encode(validationData) 2. Policy delegates to Checker check(subject, evidence) → bool 3. On success, Policy updates state enforced[target][subject] = true 4. Target can verify enforcement if (!policy.enforced(address(this), subject)) revert NotAuthorized ``` This flow remains consistent whether using base or advanced components, though advanced implementations add phase-specific validation steps through the Check enum (PRE, MAIN, POST). #### Evidence-Based Validation Excubiae adopts an evidence-based approach to validation, using encoded data rather than raw parameters: ```solidity bytes[] memory evidence = [abi.encode(data1), abi.encode(data2)]; policy.enforce(subject, evidence); ``` This design decision provides remarkable flexibility in how validation data is structured and processed. Checkers can decode this evidence in various ways, enabling complex validation schemes without changing the core interface. The pattern also future-proofs the framework, allowing for new types of evidence and validation mechanisms while maintaining backward compatibility. ### Extensions The framework's extensibility stems from its carefully designed abstraction layers. Developers can create new implementations by extending either base or advanced contracts, depending on their validation needs: ```solidity contract CustomChecker is BaseChecker { // Some storage / state here. function _initialize() internal override { super._initialize(); bytes memory data = _getAppendedBytes(); // Do something with data. } function _check( address subject, bytes memory evidence ) internal view override returns (bool) { // Custom validation logic } } contract CustomAdvancedChecker is AdvancedChecker { // Some storage / state here. function _initialize() internal override { super._initialize(); bytes memory data = _getAppendedBytes(); // Do something with data. } function _checkPre(...) internal view override returns (bool) { // Pre-validation logic } function _checkMain(...) internal view override returns (bool) { // Main validation logic } function _checkPost(...) internal view override returns (bool) { // Post-validation logic } } ``` This extensible architecture enables developers to implement custom validation logic while maintaining the framework's guarantees. New checkers can incorporate protocol-specific requirements, novel validation mechanisms, or combinations of existing checks. The consistent interface ensures that these custom implementations remain composable with the broader Excubiae ecosystem. In fact, with the release v0.3.1, we provide an extension folder containing ready to use Checker, Policy and their respective, Factory contracts for providing unique features for your use case. #### [Semaphore](https://github.com/privacy-scaling-explorations/excubiae/tree/main/packages/contracts/contracts/extensions) The Semaphore extension in Excubiae enhances access control by incorporating zero-knowledge proof-based identity verification using [Semaphore](https://docs.semaphore.pse.dev). Semaphore allows users to prove membership in a group without revealing their identity, supporting privacy-preserving access control mechanisms. This extension includes two primary components: - **[SemaphoreChecker](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/extensions/SemaphoreChecker.sol)**: Validates zero-knowledge proofs of membership. - **[SemaphorePolicy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/extensions/SemaphorePolicy.sol)**: Enforces access rules based on successful Semaphore proof verifications. ``` ┌────────────────────┐ enforces ┌────────────────────┐ │ SemaphorePolicy │ ───────────────> │ SemaphoreChecker │ └────────────────────┘ └────────────────────┘ │ │ │ protects │ validates │ │ ▼ ▼ ┌─────────────┐ ┌──────────────────────────┐ │ Target │ │ User Membership Proof │ └─────────────┘ └──────────────────────────┘ ``` The **[SemaphoreChecker](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/extensions/SemaphoreChecker.sol)** contract extends the `BaseChecker` and is responsible for verifying zero-knowledge proofs provided by users. It ensures that the submitted proof corresponds to the correct user and group, using the Semaphore protocol. - **Group Verification:** Validates that the proof belongs to the specified Semaphore group. - **Prover Validation:** Ensures the proof matches the user’s address. - **Proof Integrity:** Uses Semaphore’s built-in proof verification logic. ```solidity function _check(address subject, bytes calldata evidence) internal view override returns (bool) { ISemaphore.SemaphoreProof memory proof = abi.decode(evidence, (ISemaphore.SemaphoreProof)); uint256 _scope = proof.scope; address _prover = address(uint160(_scope >> 96)); uint96 _groupId = uint96(_scope & ((1 << 96) - 1)); if (_prover != subject) revert IncorrectProver(); if (_groupId != groupId) revert IncorrectGroupId(); if (!semaphore.verifyProof(_scope, proof)) revert InvalidProof(); return true; } ``` The **[SemaphorePolicy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/extensions/SemaphorePolicy.sol)** contract extends `BasePolicy` to enforce access control rules based on successful Semaphore proof verifications. It ensures one-time usage of proofs through nullifier tracking. - **Nullifier Tracking:** Prevents double usage of the same proof. - **Proof Validation:** Calls `SemaphoreChecker` to verify proofs before granting access. ```solidity function _enforce(address subject, bytes calldata evidence) internal override { ISemaphore.SemaphoreProof memory proof = abi.decode(evidence, (ISemaphore.SemaphoreProof)); uint256 _nullifier = proof.nullifier; if (spentNullifiers[_nullifier]) revert AlreadySpentNullifier(); spentNullifiers[_nullifier] = true; super._enforce(subject, evidence); } ``` The **[SemaphoreCheckerFactory](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/extensions/SemaphoreCheckerFactory.sol)** streamlines the deployment of `SemaphoreChecker` instances, utilizing the minimal proxy pattern for efficient contract cloning. **Deployment Example:** ```solidity function deploy(address _semaphore, uint256 _groupId) public { bytes memory data = abi.encode(_semaphore, _groupId); address clone = super._deploy(data); SemaphoreChecker(clone).initialize(); } ``` The **[SemaphorePolicyFactory](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/extensions/SemaphorePolicyFactory.sol)** streamlines the deployment of `SemaphorePolicy` instances, utilizing the minimal proxy pattern for efficient contract cloning. **Deployment Example:** ```solidity function deploy(address _checker) public { bytes memory data = abi.encode(msg.sender, _checker); address clone = super._deploy(data); SemaphorePolicy(clone).initialize(); } ``` ## Codebase Excubiae is structured as a [TypeScript/Solidity monorepo](https://github.com/privacy-scaling-explorations/excubiae) using [Yarn](https://yarnpkg.com/getting-started) as its package manager. The project is organized into distinct packages and applications: ``` excubiae/ ├── packages/ │ ├── contracts/ # Framework implementation ``` The contracts package uniquely combines [Hardhat](https://hardhat.org/) and [Foundry](https://book.getfoundry.sh/) in a way that they can [coexist together](https://hardhat.org/hardhat-runner/docs/advanced/hardhat-and-foundry), offering developers flexibility in their testing approach. This dual-environment setup enables both JavaScript/TypeScript and Solidity-native testing patterns while maintaining complete coverage. The framework's core implementation resides in `packages/contracts`, structured into distinct layers: - Core contracts implementing base and advanced validation patterns, minimal proxy pattern with immutable args. - Interface definitions ensuring consistent implementation - Test suites demonstrating usage & integration (voting use case for base and advanced scenarios). ## Guides ### Writing a Clonable Checker / Policy When implementing a policy, the first step is defining the criteria for passing validation. These criteria must be verifiable on-chain—such as token ownership, balance thresholds, or protocol-specific credentials. For example, in a voting system where voters must own a specific NFT to participate, the validation logic resides in a **Checker** contract, while a **Policy** enforces the validation result. You can find the complete implementation in our [base test suite](https://github.com/privacy-scaling-explorations/excubiae/tree/main/packages/contracts/contracts/test/examples/base). A checker encapsulates validation logic. The [BaseERC721Checker](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721Checker.sol) is a clonable contract that verifies NFT ownership. To implement a clonable checker: - Override `_initialize()`, which is executed only once at deployment time to store immutable arguments in the contract state. - Implement `_check()`, defining the validation logic. Once the checker is in place, a **Policy** references it to enforce validation. The [BaseERC721Policy](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721Policy.sol) demonstrates how to: - Extend a base policy contract. - Provide a unique trait identifier. To deploy clones dynamically, each Checker and Policy implementation requires a corresponding **Factory** contract. Examples include [BaseERC721CheckerFactory](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721CheckerFactory.sol) and [BaseERC721PolicyFactory](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseERC721PolicyFactory.sol). Each factory must: 1. Specify the implementation contract in the constructor and pass a new instance to the `Factory()` constructor. 2. Implement a `deploy()` method that: - Encodes initialization parameters (**immutable args**). - Calls `_deploy(data)`, deploying a clone. - Initializes the clone via its `initialize()` method. This approach enables efficient deployments and customization at deploy time. For example, different `_nftAddress` values can be set per clone, allowing multiple NFT collections to use the same validation logic while remaining independent. ### Integrating a Policy The [BaseVoting](https://github.com/privacy-scaling-explorations/excubiae/blob/main/packages/contracts/contracts/test/examples/base/BaseVoting.sol) contract demonstrates a complete implementation of policy integration. It shows how to: - Initialize the policy - Enforce checks before actions - Track validation state #### Tracking Mechanisms to Prevent Double Enforcement Each Policy in Excubiae must implement its own tracking mechanism to prevent double enforcement. This ensures that the same proof or validation cannot be reused maliciously. The design of the tracking system may vary depending on the specific requirements of the policy. Common Tracking Mechanisms: Nullifier Tracking (Semaphore): Uses a mapping(uint256 => bool) to track spent nullifiers. This prevents the reuse of zero-knowledge proofs. Address-based Tracking: Maps user addresses to a boolean to ensure a subject cannot re-enforce the same policy multiple times. Custom Identifiers: In more complex scenarios, policies can track composite keys (e.g., mapping(bytes32 => bool)) derived from multiple validation parameters. Example from SemaphorePolicy: ```solidity function _enforce(address subject, bytes calldata evidence) internal override { ISemaphore.SemaphoreProof memory proof = abi.decode(evidence, (ISemaphore.SemaphoreProof)); uint256 _nullifier = proof.nullifier; // track to avoid double spending of the same proof. if (spentNullifiers[_nullifier]) revert AlreadySpentNullifier(); spentNullifiers[_nullifier] = true; // this takes care of unsuccessful checks (check() return false) // and Enforced() event emit. super._enforce(subject, evidence); } ``` This pattern ensures that each proof is only used once, maintaining the integrity of the access control system. ## Wrap up For protocol engineers and smart contract developers, Excubiae offers a powerful new approach to attribute based access control on Ethereum. Whether you're looking to implement token-gated voting, create multi-phase authentication systems, or develop novel verification mechanisms, this framework provides the tools and flexibility to bring your ideas to life. Interested in contributing or exploring the framework further? The project welcomes collaboration through its [GitHub monorepo](https://github.com/privacy-scaling-explorations/excubiae) and [PSE Discord](https://discord.com/invite/sF5CT5rzrR) (`#ask-any-questions` channel) community. Your feedback, contributions, and innovative use cases will be crucial! Thanks for the read-through.