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 18, 2024
1 parent 6a8c72b commit 723a9f3
Show file tree
Hide file tree
Showing 13 changed files with 1,087 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
83 changes: 83 additions & 0 deletions contracts/src/stdlib/ECDSAModuleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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";
import { ECDSA } from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";

/**
* @title ECDSA Module V2
* @author Consensys
* @notice This Module can be used by Portal creators to
* require a signature of the attestation payload
* from an authorized signer before issuing the attestation.
* @dev DISCLAIMER: This module doesn't check that a signature can be used only once!
*/
contract ECDSAModuleV2 is AbstractModuleV2 {
using ECDSA for bytes32;

PortalRegistry public portalRegistry;

mapping(address portal => mapping(address signer => bool authorizedSigners)) public authorizedSigners;

/// @notice Error thrown when an array length mismatch occurs
error ArrayLengthMismatch();
/// @notice Error thrown when a signer is not authorized by the module
error SignerNotAuthorized();

/// @notice Event emitted when the authorized signers are set
event SignersAuthorized(address indexed portal, address[] signers, bool[] authorizationStatus);

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 signers
* @param signers The signers to be set
* @param authorizationStatus The authorization status of signers
* @dev The length of `signers` and `authorizationStatus` must be the same
*/
function setAuthorizedSigners(
address portal,
address[] memory signers,
bool[] memory authorizationStatus
) public onlyPortalOwner(portal) {
if (signers.length != authorizationStatus.length) revert ArrayLengthMismatch();

for (uint256 i = 0; i < signers.length; i++) {
authorizedSigners[portal][signers[i]] = authorizationStatus[i];
}

emit SignersAuthorized(portal, signers, authorizationStatus);
}

/**
* @inheritdoc AbstractModuleV2
* @param attestationPayload The Payload of the attestation
* @param validationPayload The validation payload required for the module, in this case the signature of the payload
* @param portal The Portal issuing the attestation
* @notice If the signer of the transaction payload is not an 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 {
bytes32 messageHash = keccak256(abi.encode(attestationPayload));
address messageSigner = messageHash.toEthSignedMessageHash().recover(validationPayload);
if (!authorizedSigners[portal][messageSigner]) revert SignerNotAuthorized();
}
}
101 changes: 101 additions & 0 deletions contracts/src/stdlib/ERC1271ModuleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

import { AbstractModuleV2 } from "../abstracts/AbstractModuleV2.sol";
import { PortalRegistry } from "../PortalRegistry.sol";
import { AttestationPayload } from "../types/Structs.sol";
import { ECDSA } from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
import { SignatureChecker } from "openzeppelin-contracts/contracts/utils/cryptography/SignatureChecker.sol";
import { IERC1271 } from "openzeppelin-contracts/contracts/interfaces/IERC1271.sol";
import { Address } from "openzeppelin-contracts/contracts/utils/Address.sol";

/**
* @title ERC-1271 Module V2
* @author Consensys
* @notice This Module can be used by Portal creators to
* require an ERC_1271 signature of the attestation payload
* from an authorized signer before issuing the attestation.
* @dev DISCLAIMER: This module doesn't check that a signature can be used only once!
*/
contract ERC1271ModuleV2 is AbstractModuleV2 {
using ECDSA for bytes32;
using Address for address;

PortalRegistry public portalRegistry;

mapping(address portal => mapping(address signer => bool authorizedSigners)) public authorizedSigners;

/// @notice Error thrown when an array length mismatch occurs
error ArrayLengthMismatch();
/// @notice Error thrown when a signer is not authorized by the module
error SignerNotAuthorized();
/// @notice Error thrown when a signature is invalid
error InvalidSignature();
/// @notice Error thrown when a signature validation fails
error FailedValidation();

/// @notice Event emitted when the authorized signers are set
event SignersAuthorized(address indexed portal, address[] signers, bool[] authorizationStatus);

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 accepted status of schemaIds
* @param signers The signers to be set
* @param authorizationStatus The authorization status of signers
* @dev The length of `signers` and `authorizationStatus` must be the same
*/
function setAuthorizedSigners(
address portal,
address[] memory signers,
bool[] memory authorizationStatus
) public onlyPortalOwner(portal) {
if (signers.length != authorizationStatus.length) revert ArrayLengthMismatch();

for (uint256 i = 0; i < signers.length; i++) {
authorizedSigners[portal][signers[i]] = authorizationStatus[i];
}

emit SignersAuthorized(portal, signers, authorizationStatus);
}

/**
* @inheritdoc AbstractModuleV2
* @param attestationPayload The Payload of the attestation
* @param validationPayload The validation payload required for the module, in this case the signature of the payload
* @param portal The Portal issuing the attestation
* @notice If the signer of the transaction payload is not an 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 {
bytes32 messageHash = keccak256(abi.encode(attestationPayload));
address messageSigner = messageHash.toEthSignedMessageHash().recover(validationPayload);
if (!authorizedSigners[portal][messageSigner]) revert SignerNotAuthorized();

if (messageSigner.isContract()) {
try IERC1271(messageSigner).isValidSignature(messageHash.toEthSignedMessageHash(), validationPayload) returns (
bytes4 magicValue
) {
if (magicValue != IERC1271.isValidSignature.selector) revert InvalidSignature();
} catch {
revert FailedValidation();
}
}
}
}
Loading

0 comments on commit 723a9f3

Please sign in to comment.