Try   HackMD

Introduction

PluginResolver is designed to enhance the Ethereum Attestation Service (EAS) by allowing a single resolver contract 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:

// 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:
// 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:

PluginResolver
// 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;
    }
}

IPluginResolver
// 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);
}

IValidatingResolver
// 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);
}

IExecutingResolver
// 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;
}

Predeploys
// 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 or X for some plugin examples coming soon.

Thanks Megan Dias for editing.