## 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.