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
.
PluginResolver integrates two types of plugins through defined interfaces:
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.
// 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);
}
These plugins perform actions without restricting attestations. For example, an executing plugin could send an attester an NFT.
// 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;
}
The PluginResolver contract manages arrays of validating and executing plugins, applying their respective logic during the attestation and revocation processes:
// 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;
}
}
// 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);
}
// 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);
}
// 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;
}
// 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;
}
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.