Skip to content

Commit

Permalink
feat: Introduce Modules V2 with more flexibility
Browse files Browse the repository at this point in the history
  • Loading branch information
alainncls committed Mar 16, 2024
1 parent 6a8c72b commit add7f71
Show file tree
Hide file tree
Showing 7 changed files with 439 additions and 4 deletions.
69 changes: 65 additions & 4 deletions contracts/src/ModuleRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.21;

import { AttestationPayload, Module } from "./types/Structs.sol";
import { AbstractModule } from "./abstracts/AbstractModule.sol";
import { AbstractModuleV2 } from "./abstracts/AbstractModuleV2.sol";
import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
// solhint-disable-next-line max-line-length
import { ERC165CheckerUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/utils/introspection/ERC165CheckerUpgradeable.sol";
Expand Down Expand Up @@ -102,8 +103,11 @@ contract ModuleRegistry is OwnableUpgradeable {
if (bytes(name).length == 0) revert ModuleNameMissing();
// Check if moduleAddress is a smart contract address
if (!isContractAddress(moduleAddress)) revert ModuleAddressInvalid();
// Check if module has implemented AbstractModule
if (!ERC165CheckerUpgradeable.supportsInterface(moduleAddress, type(AbstractModule).interfaceId)) {
// Check if module has implemented AbstractModule or AbstractModuleV2
if (
!ERC165CheckerUpgradeable.supportsInterface(moduleAddress, type(AbstractModule).interfaceId) &&
!ERC165CheckerUpgradeable.supportsInterface(moduleAddress, type(AbstractModuleV2).interfaceId)
) {
revert ModuleInvalid();
}
// Module address is used to identify uniqueness of the module
Expand All @@ -127,18 +131,55 @@ contract ModuleRegistry is OwnableUpgradeable {
bytes[] memory validationPayloads,
uint256 value
) public {
// If no modules provided, bypass module validation
// If no module provided, bypass module validation
if (modulesAddresses.length == 0) return;
// Each module involved must have a corresponding item from the validation payload
if (modulesAddresses.length != validationPayloads.length) revert ModuleValidationPayloadMismatch();

// For each module check if it is registered and call run method
// For each module, check if it is registered and call its run method
for (uint32 i = 0; i < modulesAddresses.length; i = uncheckedInc32(i)) {
if (!isRegistered(modulesAddresses[i])) revert ModuleNotRegistered();
AbstractModule(modulesAddresses[i]).run(attestationPayload, validationPayloads[i], tx.origin, value);
}
}

/**
* @notice Executes the V2 run method for all given Modules that are registered
* @param modulesAddresses the addresses of the registered modules
* @param attestationPayload the payload to attest
* @param validationPayloads the payloads to check for each module (one payload per module)
* @param value the value (ETH) optionally passed in the attesting transaction
* @param initialCaller the address of the initial caller (transaction sender)
* @param attester the address defined by the Portal as the attester for this payload
* @dev check if modules are registered and execute the V2 run method for each module
*/
function runModulesV2(
address[] memory modulesAddresses,
AttestationPayload memory attestationPayload,
bytes[] memory validationPayloads,
uint256 value,
address initialCaller,
address attester
) public {
// If no module provided, bypass module validation
if (modulesAddresses.length == 0) return;
// Each module involved must have a corresponding item from the validation payload
if (modulesAddresses.length != validationPayloads.length) revert ModuleValidationPayloadMismatch();

// For each module, check if it is registered and call its run method
for (uint32 i = 0; i < modulesAddresses.length; i = uncheckedInc32(i)) {
if (!isRegistered(modulesAddresses[i])) revert ModuleNotRegistered();
AbstractModuleV2(modulesAddresses[i]).run(
attestationPayload,
validationPayloads[i],
initialCaller,
value,
attester,
msg.sender
);
}
}

/**
* @notice Executes the modules validation for all attestations payloads for all given Modules that are registered
* @param modulesAddresses the addresses of the registered modules
Expand All @@ -157,6 +198,26 @@ contract ModuleRegistry is OwnableUpgradeable {
}
}

/**
* @notice Executes the V2 modules validation for all attestations payloads for all given V2 Modules that are registered
* @param modulesAddresses the addresses of the registered modules
* @param attestationPayloads the payloads to attest
* @param validationPayloads the payloads to check for each module
* @dev NOTE: Currently the bulk run modules does not handle payable modules
* a default value of 0 is used.
*/
function bulkRunModulesV2(
address[] memory modulesAddresses,
AttestationPayload[] memory attestationPayloads,
bytes[][] memory validationPayloads,
address initialCaller,
address attester
) public {
for (uint32 i = 0; i < attestationPayloads.length; i = uncheckedInc32(i)) {
runModulesV2(modulesAddresses, attestationPayloads[i], validationPayloads[i], 0, initialCaller, attester);
}
}

/**
* @notice Get the number of Modules managed by the contract
* @return The number of Modules already registered
Expand Down
42 changes: 42 additions & 0 deletions contracts/src/abstracts/AbstractModuleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import { AttestationPayload } from "../types/Structs.sol";
import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol";

/**
* @title Abstract Module V2
* @author Consensys
* @notice Defines the minimal Module V2 interface
*/
abstract contract AbstractModuleV2 is IERC165 {
/// @notice Error thrown when someone else than the portal's owner is trying to revoke
error OnlyPortalOwner();

/**
* @notice Executes the module's custom logic
* @param attestationPayload The incoming attestation data
* @param validationPayload Additional data required for verification
* @param initialCaller The address of the initial caller (transaction sender)
* @param value The value (ETH) optionally passed in the attesting transaction
* @param attester The address defined by the Portal as the attester for this payload
* @param portal The issuing Portal's address
*/
function run(
AttestationPayload memory attestationPayload,
bytes memory validationPayload,
address initialCaller,
uint256 value,
address attester,
address portal
) public virtual;

/**
* @notice Checks if the contract implements the Module interface.
* @param interfaceID The ID of the interface to check.
* @return A boolean indicating interface support.
*/
function supportsInterface(bytes4 interfaceID) public pure virtual override returns (bool) {
return interfaceID == type(AbstractModuleV2).interfaceId || interfaceID == type(IERC165).interfaceId;
}
}
39 changes: 39 additions & 0 deletions contracts/src/abstracts/AbstractPortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ abstract contract AbstractPortal is IPortal {
attestationRegistry.attest(attestationPayload, getAttester());
}

/**
* @notice Attest the schema with given attestationPayload and validationPayload
* @param attestationPayload the payload to attest
* @param validationPayloads the payloads to validate via the modules to issue the attestations
* @dev Runs all modules for the portal and registers the attestation using AttestationRegistry
*/
function attestV2(AttestationPayload memory attestationPayload, bytes[] memory validationPayloads) public payable {
moduleRegistry.runModulesV2(modules, attestationPayload, validationPayloads, msg.value, msg.sender, getAttester());

_onAttestV2(attestationPayload, validationPayloads, msg.value);

attestationRegistry.attest(attestationPayload, getAttester());
}

/**
* @notice Bulk attest the schema with payloads to attest and validation payloads
* @param attestationsPayloads the payloads to attest
Expand All @@ -74,6 +88,19 @@ abstract contract AbstractPortal is IPortal {
attestationRegistry.bulkAttest(attestationsPayloads, getAttester());
}

/**
* @notice Bulk attest the schema with payloads to attest and validation payloads
* @param attestationPayloads the payloads to attest
* @param validationPayloads the payloads to validate via the modules to issue the attestations
*/
function bulkAttestV2(AttestationPayload[] memory attestationPayloads, bytes[][] memory validationPayloads) public {
moduleRegistry.bulkRunModulesV2(modules, attestationPayloads, validationPayloads, msg.sender, getAttester());

_onBulkAttest(attestationPayloads, validationPayloads);

attestationRegistry.bulkAttest(attestationPayloads, getAttester());
}

/**
* @notice Replaces the attestation for the given identifier and replaces it with a new attestation
* @param attestationId the ID of the attestation to replace
Expand Down Expand Up @@ -169,6 +196,18 @@ abstract contract AbstractPortal is IPortal {
*/
function _onAttest(AttestationPayload memory attestationPayload, address attester, uint256 value) internal virtual {}

/**
* @notice Optional method run before a payload is attested
* @param attestationPayload the attestation payload to attest
* @param validationPayloads the payloads to validate via the modules
* @param value the value sent with the attestation
*/
function _onAttestV2(
AttestationPayload memory attestationPayload,
bytes[] memory validationPayloads,
uint256 value
) internal virtual {}

/**
* @notice Optional method run when an attestation is replaced
* @param attestationId the ID of the attestation being replaced
Expand Down
73 changes: 73 additions & 0 deletions contracts/src/stdlib/FeeModuleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import { AbstractModuleV2 } from "../abstracts/AbstractModuleV2.sol";
import { AttestationPayload } from "../types/Structs.sol";
import { PortalRegistry } from "../PortalRegistry.sol";

/**
* @title Fee Module V2
* @author Consensys
* @notice This module can be used by portal creators to enforce a fee on attestations.
* @dev This Module checks the fee is sent, but doesn't collect the fee
*/
contract FeeModuleV2 is AbstractModuleV2 {
PortalRegistry public portalRegistry;

mapping(address portal => mapping(bytes32 schemaId => uint256 attestationFee)) public attestationFees;

/// @notice Error thrown when an array length mismatch occurs
error ArrayLengthMismatch();
/// @notice Error thrown when an invalid attestation fee is provided
error InvalidAttestationFee();

modifier onlyPortalOwner(address portal) {
if (msg.sender != portalRegistry.getPortalByAddress(portal).ownerAddress) revert OnlyPortalOwner();
_;
}

event FeesSet(address portal, bytes32[] schemaIds, uint256[] attestationFees);

/**
* @notice Contract constructor sets the Portal Registry
*/
constructor(address _portalRegistry) {
portalRegistry = PortalRegistry(_portalRegistry);
}

/**
* @notice Set the fee required to attest for a given Portal and one or multiple Schemas
* @param portal The Portal targeted by the fees
* @param schemaIds The Schema IDs to set the fees for
* @param fees The fees required to attest with the Schemas on the Portal
* @dev The length of `schemaIds` and `attestationFees` must be the same
* Only the Portal owner can call this function to set the fees on his Portal
*/
function setFees(address portal, bytes32[] memory schemaIds, uint256[] memory fees) public onlyPortalOwner(portal) {
if (schemaIds.length != fees.length) revert ArrayLengthMismatch();

for (uint256 i = 0; i < schemaIds.length; i++) {
attestationFees[portal][schemaIds[i]] = fees[i];
}

emit FeesSet(portal, schemaIds, fees);
}

/**
* @inheritdoc AbstractModuleV2
* @param value The value sent for the attestation (= the fee)
* @param portal The Portal issuing the attestation
* @notice If the provided fee is not enough, an error is thrown
*/
function run(
AttestationPayload memory attestationPayload,
bytes memory /*validationPayload*/,
address /*initialCaller*/,
uint256 value,
address /*attester*/,
address portal
) public view override {
uint256 attestationFee = attestationFees[portal][attestationPayload.schemaId];
if (value < attestationFee) revert InvalidAttestationFee();
}
}
42 changes: 42 additions & 0 deletions contracts/src/stdlib/IssuersModuleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import { AbstractModuleV2 } from "../abstracts/AbstractModuleV2.sol";
import { AttestationPayload } from "../types/Structs.sol";
import { PortalRegistry } from "../PortalRegistry.sol";

/**
* @title Issuers Module V2
* @author Consensys
* @notice This Modules checks if the subject of an Attestation is registered as an Issuer
*/
contract IssuersModuleV2 is AbstractModuleV2 {
PortalRegistry public portalRegistry;

/// @notice Error thrown when the subject of an Attestation is not an Issuer
error UnauthorizedSubject();

constructor(address _portalRegistry) {
portalRegistry = PortalRegistry(_portalRegistry);
}

/**
* @inheritdoc AbstractModuleV2
* @notice If the Attestation subject is not an Issuer, an error is thrown
*/
function run(
AttestationPayload memory attestationPayload,
bytes memory /*validationPayload*/,
address /*initialCaller*/,
uint256 /*value*/,
address /*attester*/,
address /*portal*/
) public view override {
address subject = address(0);

if (attestationPayload.subject.length == 32) subject = abi.decode(attestationPayload.subject, (address));
if (attestationPayload.subject.length == 20) subject = address(uint160(bytes20(attestationPayload.subject)));

if (!portalRegistry.isIssuer(subject)) revert UnauthorizedSubject();
}
}
68 changes: 68 additions & 0 deletions contracts/src/stdlib/SenderModuleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import { AbstractModuleV2 } from "../abstracts/AbstractModuleV2.sol";
import { AttestationPayload } from "../types/Structs.sol";
import { PortalRegistry } from "../PortalRegistry.sol";

/**
* @title Sender Module V2
* @author Consensys
* @notice This Module checks if the transaction sender is authorized for a specific Portal
*/
contract SenderModuleV2 is AbstractModuleV2 {
PortalRegistry public portalRegistry;

mapping(address portal => mapping(address sender => bool authorized)) public authorizedSenders;

/// @notice Error thrown when an array length mismatch occurs
error ArrayLengthMismatch();
/// @notice Error thrown when the transaction sender is not authorized
error UnauthorizedSender();

modifier onlyPortalOwner(address portal) {
if (msg.sender != portalRegistry.getPortalByAddress(portal).ownerAddress) revert OnlyPortalOwner();
_;
}

/**
* @notice Contract constructor sets the portal registry
*/
constructor(address _portalRegistry) {
portalRegistry = PortalRegistry(_portalRegistry);
}

/**
* @notice Set the authorized status of senders
* @param senders The senders to be set
* @param authorizedStatus The authorized status of senders
*/
function setAuthorizedSenders(
address portal,
address[] memory senders,
bool[] memory authorizedStatus
) public onlyPortalOwner(portal) {
if (senders.length != authorizedStatus.length) revert ArrayLengthMismatch();

for (uint256 i = 0; i < senders.length; i++) {
authorizedSenders[portal][senders[i]] = authorizedStatus[i];
}
}

/**
* @inheritdoc AbstractModuleV2
* @param initialCaller The initial transaction sender
* @param portal The Portal issuing the attestation
* @notice If the transaction sender is not the expected address, an error is thrown
*/
function run(
AttestationPayload memory /*attestationPayload*/,
bytes memory /*validationPayload*/,
address initialCaller,
uint256 /*value*/,
address /*attester*/,
address portal
) public view override {
if (!authorizedSenders[portal][initialCaller]) revert UnauthorizedSender();
}
}
Loading

0 comments on commit add7f71

Please sign in to comment.