## Introduction PluginResolver is designed to enhance the [Ethereum Attestation Service](https://attest.org/) (EAS) by allowing a single [resolver contract](https://docs.attest.org/docs/core--concepts/resolver-contracts) to leverage multiple smaller "plugin" resolver contracts. This approach allows developers to mix and match functionalities using `validating plugins` and/or `executing plugins`. ## Key Features * **Stackable Plugins**: Combine multiple validating and executing plugins to create powerful, layered attestation logic. **Example**: Token gating, time-based restrictions, and sending notifications. * **Flexibility and Customization**: The modular nature of PluginResolver enables a high degree of customization, allowing you to tailor attestation flows to specific business needs or use cases. * **Simplified Management**: Easily add, remove, or reuse existing plugins easily across different resolvers without redeploying the entire resolver contract. ## How PluginResolver Works PluginResolver integrates two types of plugins through defined interfaces: 1. Validating plugins (gatekeeping) 2. Executing plugins (non-gatekeeping) #### Validating Plugins These plugins enforce **restrictions on attestations**. For example, a validating plugin might implement token gating, ensuring that only users who hold a specific NFT can make an attestation. ##### Interface: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import {Attestation} from "eas-contracts/IEAS.sol"; interface IValidatingResolver { function onAttest(Attestation calldata attestation, uint256 value) external returns (bool); function onRevoke(Attestation calldata attestation, uint256 value) external returns (bool); } ``` #### Executing Plugins These plugins **perform actions without restricting attestations**. For example, an executing plugin could send an attester an NFT. ##### Interface: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import {Attestation} from "eas-contracts/IEAS.sol"; interface IExecutingResolver { function onAttest(Attestation calldata attestation, uint256 value) external; function onRevoke(Attestation calldata attestation, uint256 value) external; } ``` ## PluginResolver Full Code The PluginResolver contract manages arrays of validating and executing plugins, applying their respective logic during the attestation and revocation processes: :::spoiler PluginResolver ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {SchemaResolver} from "eas-contracts/resolver/SchemaResolver.sol"; import {IEAS, Attestation} from "eas-contracts/IEAS.sol"; import {IPluginResolver} from "./interfaces/IPluginResolver.sol"; import {IValidatingResolver} from "./interfaces/IValidatingResolver.sol"; import {IExecutingResolver} from "./interfaces/IExecutingResolver.sol"; import {Predeploys} from "./libraries/Predeploys.sol"; /** * @title PluginResolver * @author Kyle Kaplan * @dev PluginResolver to add an array of validating and executing resolver contracts onAttest and onRevoke */ contract PluginResolver is Ownable2Step, SchemaResolver, IPluginResolver { ////////////////////////////// State ////////////////////////////// // array of validating resolvers IValidatingResolver[] public s_validatingResolvers; // array of executing resolvers IExecutingResolver[] public s_executingResolvers; ////////////////////////////// Constructor ////////////////////////////// /** * @notice Constructor for PluginResolver * @param _owner The address of the owner of the PluginResolver */ constructor( address _owner ) SchemaResolver(IEAS(Predeploys.EAS)) Ownable(_owner) {} ////////////////////////////// External Functions ////////////////////////////// /// @inheritdoc IPluginResolver function addValidatingResolver( IValidatingResolver resolver ) external onlyOwner { s_validatingResolvers.push(resolver); emit ValidatingResolverAdded(resolver); } /// @inheritdoc IPluginResolver function removeValidatingResolver(uint256 index) external onlyOwner { uint256 length = s_validatingResolvers.length; if (index >= length) { revert PluginResolver__IndexOutOfBounds(index, length); } IValidatingResolver resolver = s_validatingResolvers[index]; for (uint256 i = index; i < length - 1; i++) { s_validatingResolvers[i] = s_validatingResolvers[i + 1]; } s_validatingResolvers.pop(); emit ValidatingResolverRemoved(resolver); } /// @inheritdoc IPluginResolver function addExecutingResolver( IExecutingResolver resolver ) external onlyOwner { s_executingResolvers.push(resolver); emit ExecutingResolverAdded(resolver); } /// @inheritdoc IPluginResolver function removeExecutingResolver(uint256 index) external onlyOwner { uint256 length = s_executingResolvers.length; if (index >= length) { revert PluginResolver__IndexOutOfBounds(index, length); } IExecutingResolver resolver = s_executingResolvers[index]; for (uint256 i = index; i < length - 1; i++) { s_executingResolvers[i] = s_executingResolvers[i + 1]; } s_executingResolvers.pop(); emit ExecutingResolverRemoved(resolver); } /// @inheritdoc IPluginResolver function getValidatingResolversLength() external view returns (uint256) { return s_validatingResolvers.length; } /// @inheritdoc IPluginResolver function getExecutingResolversLength() external view returns (uint256) { return s_executingResolvers.length; } ////////////////////////////// Internal Functions ////////////////////////////// /// @dev First loops through the validatingResolvers and calls onAttest on each, /// if all validatingResolvers return true, it then (and only then) /// loops through the executingResolvers and calls onAttest on each /// @dev This function is called by the EAS contract when an attestation is made and protected by the onlyEAS modifier /// @param attestation The attestation to validate /// @param value The value of the attestation function onAttest( Attestation calldata attestation, uint256 value ) internal override returns (bool) { // iterate over validatingResolvers and call onAttest on each uint256 validatingResolversLength = s_validatingResolvers.length; for (uint256 i = 0; i < validatingResolversLength; i++) { if (!s_validatingResolvers[i].onAttest(attestation, value)) { return false; } } // iterate over executingResolvers and call onAttest on each uint256 executingResolversLength = s_executingResolvers.length; for (uint256 i = 0; i < executingResolversLength; i++) { try s_executingResolvers[i].onAttest(attestation, value) { // Execution successful, continue to the next resolver } catch { // Emit event with the address of the failed resolver emit ExecutingResolverFailed(s_executingResolvers[i], true); } } return true; } /// @dev First loops through the validatingResolvers and calls onRevoke on each, /// if all validatingResolvers return true, it then (and only then) /// loops through the executingResolvers and calls onRevoke on each /// @param attestation The attestation to revoke /// @param value The value of the attestation function onRevoke( Attestation calldata attestation, uint256 value ) internal override returns (bool) { // iterate over validatingResolvers and call onRevoke on each uint256 validatingResolversLength = s_validatingResolvers.length; for (uint256 i = 0; i < validatingResolversLength; i++) { if (!s_validatingResolvers[i].onRevoke(attestation, value)) { return false; } } // iterate over executingResolvers and call onRevoke on each uint256 executingResolversLength = s_executingResolvers.length; for (uint256 i = 0; i < executingResolversLength; i++) { try s_executingResolvers[i].onRevoke(attestation, value) { // Execution successful, continue to the next resolver } catch { // Emit event with the address of the failed resolver emit ExecutingResolverFailed(s_executingResolvers[i], false); } } return true; } } ``` ::: :::spoiler IPluginResolver ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import {ISchemaResolver} from "eas-contracts/resolver/ISchemaResolver.sol"; import {IValidatingResolver} from "./IValidatingResolver.sol"; import {IExecutingResolver} from "./IExecutingResolver.sol"; /** * @title IPluginResolver * @author Kyle Kaplan * @dev Interface for the PluginResolver contract which manages an array of validating and executing resolver contracts */ interface IPluginResolver is ISchemaResolver { ////////////////////////////// Events ////////////////////////////// /// @notice Emitted when an executing resolver fails. event ExecutingResolverFailed( IExecutingResolver indexed resolver, bool isAttestation ); /// @notice Emitted when a validating resolver is added. event ValidatingResolverAdded(IValidatingResolver indexed resolver); /// @notice Emitted when a validating resolver is removed. event ValidatingResolverRemoved(IValidatingResolver indexed resolver); /// @notice Emitted when an executing resolver is added. event ExecutingResolverAdded(IExecutingResolver indexed resolver); /// @notice Emitted when an executing resolver is removed. event ExecutingResolverRemoved(IExecutingResolver indexed resolver); ////////////////////////////// Errors ////////////////////////////// error PluginResolver__IndexOutOfBounds(uint256 index, uint256 length); ////////////////////////////// Functions ////////////////////////////// /// @notice Returns the length of the validatingResolvers array /// @return The number of validating resolvers function getValidatingResolversLength() external view returns (uint256); /// @notice Adds a validating resolver to the array /// @param resolver The resolver to add function addValidatingResolver(IValidatingResolver resolver) external; /// @notice Removes a validating resolver from the array /// @param index The index of the resolver to remove function removeValidatingResolver(uint256 index) external; /// @notice Adds an executing resolver to the array /// @param resolver The resolver to add function addExecutingResolver(IExecutingResolver resolver) external; /// @notice Removes an executing resolver from the array /// @param index The index of the resolver to remove function removeExecutingResolver(uint256 index) external; /// @notice Returns the length of the executingResolvers array /// @return The number of executing resolvers function getExecutingResolversLength() external view returns (uint256); } ``` ::: :::spoiler IValidatingResolver ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import {Attestation} from "eas-contracts/IEAS.sol"; interface IValidatingResolver { /// @notice A resolver callback that should be called by the plugin resolver. /// @param attestation The new attestation. /// @param value An explicit ETH amount that was sent to the resolver. Please note that this value is verified in /// both attest() and multiAttest() callbacks EAS-only callbacks and that in case of multi attestations, it'll /// usually hold that msg.value != value, since msg.value aggregated the sent ETH amounts for all the /// attestations in the batch. /// @return Whether the attestation is valid. function onAttest(Attestation calldata attestation, uint256 value) external returns (bool); /// @notice A resolver callback that should be called by the plugin resolver. /// @param attestation The existing attestation to be revoked. /// @param value An explicit ETH amount that was sent to the resolver. Please note that this value is verified in /// both revoke() and multiRevoke() callbacks EAS-only callbacks and that in case of multi attestations, it'll /// usually hold that msg.value != value, since msg.value aggregated the sent ETH amounts for all the /// attestations in the batch. /// @return Whether the attestation can be revoked. function onRevoke(Attestation calldata attestation, uint256 value) external returns (bool); } ``` ::: :::spoiler IExecutingResolver ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; import {Attestation} from "eas-contracts/IEAS.sol"; interface IExecutingResolver { /// @notice A resolver callback that should be called by the plugin resolver. /// @param attestation The new attestation. /// @param value An explicit ETH amount that was sent to the resolver. Please note that this value is verified in /// both attest() and multiAttest() callbacks EAS-only callbacks and that in case of multi attestations, it'll /// usually hold that msg.value != value, since msg.value aggregated the sent ETH amounts for all the /// attestations in the batch. function onAttest(Attestation calldata attestation, uint256 value) external; /// @notice A resolver callback that should be called by the plugin resolver. /// @param attestation The existing attestation to be revoked. /// @param value An explicit ETH amount that was sent to the resolver. Please note that this value is verified in /// both revoke() and multiRevoke() callbacks EAS-only callbacks and that in case of multi attestations, it'll /// usually hold that msg.value != value, since msg.value aggregated the sent ETH amounts for all the /// attestations in the batch. function onRevoke(Attestation calldata attestation, uint256 value) external; } ``` ::: :::spoiler Predeploys ```solidity! // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; /** * @title Predeploys * @notice Contains constant addresses for contracts that are pre-deployed to the OP Stack L2 system. * @dev Based on https://github.com/ethereum-optimism/optimism/blob/c73850809be1bef888ba7dd1194acdf222e4d819/packages/contracts-bedrock/src/libraries/Predeploys.sol */ library Predeploys { /// @notice Address of the SchemaRegistry predeploy. address internal constant SCHEMA_REGISTRY = 0x4200000000000000000000000000000000000020; /// @notice Address of the EAS predeploy. address internal constant EAS = 0x4200000000000000000000000000000000000021; } ``` ::: ## Conclusion With the PluginResolver, developers can easily integrate and manage various plugins, making it easier to implement tailored attestation logic. Follow me on [Warpcast](https://warpcast.com/kylekaplan) or [X](https://x.com/KylesCrypto1) for some plugin examples coming soon. Thanks [Megan Dias](https://warpcast.com/megandias) for editing.