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 Apr 8, 2024
1 parent f2874e6 commit 86f4ab8
Show file tree
Hide file tree
Showing 25 changed files with 2,050 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

pnpm run prettier:write
pnpm -r run lint
pnpm -r run test
pnpm --filter "{contracts}" run test
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;
}
}
41 changes: 40 additions & 1 deletion contracts/src/abstracts/AbstractPortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IPortal } from "../interfaces/IPortal.sol";
/**
* @title Abstract Portal
* @author Consensys
* @notice This contract is an abstract contract with basic Portal logic
* @notice This contract is an abstracts contract with basic Portal logic
* to be inherited. We strongly encourage all Portals to implement
* this contract.
*/
Expand Down 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
67 changes: 67 additions & 0 deletions contracts/src/examples/modules/ERC712ModuleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

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

/**
* @notice Definition of EIP-712 domain
*/
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}

contract ERC712ModuleV2 is AbstractModuleV2 {
EIP712Domain public domain;
address public sender;
address public receiver;

error InvalidSignature();

constructor(EIP712Domain memory _domain, address _sender, address _receiver) {
domain = _domain;
sender = _sender;
receiver = _receiver;
}

/**
* @inheritdoc AbstractModuleV2
* @notice This method is used to run the module's validation logic
* @param validationPayload - Payload containing the serialized hash. The last one is the 'Root'.
* @param initialCaller - The initial transaction sender
*/
function run(
AttestationPayload memory /*attestationPayload*/,
bytes memory validationPayload,
address initialCaller,
uint256 /*value*/,
address /*attester*/,
address /*portal*/
) public view override {
(uint8 v, bytes32 r, bytes32 s) = abi.decode(validationPayload, (uint8, bytes32, bytes32));

bytes32 DOMAIN_TYPE_HASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 domainHash = keccak256(
abi.encode(
DOMAIN_TYPE_HASH,
keccak256(bytes(domain.name)),
keccak256(bytes(domain.version)),
domain.chainId,
domain.verifyingContract
)
);
bytes32 TXN_TYPE_HASH = keccak256("Transaction(address from,address to,uint256 value)");
bytes32 structHash = keccak256(abi.encode(TXN_TYPE_HASH, sender, receiver, uint256(1234)));
bytes32 hash = keccak256(abi.encodePacked("\x19\x01", domainHash, structHash));

address signer = ecrecover(hash, v, r, s);
if (signer != initialCaller) {
revert InvalidSignature();
}
}
}
50 changes: 50 additions & 0 deletions contracts/src/examples/modules/MerkleProofModuleV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;

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

contract MerkleProofModuleV2 is AbstractModuleV2 {
error MerkelProofVerifyFailed();

/**
* @inheritdoc AbstractModuleV2
* @notice This method is used to run the module's validation logic
* @param attestationPayload - AttestationPayload containing the user address as `subject`
* and nonce as `attestationData`
* @param validationPayload - validationPayload containing the serialized hash.The last one is the 'Root'.
*/
function run(
AttestationPayload memory attestationPayload,
bytes memory validationPayload,
address /*initialCaller*/,
uint256 /*value*/,
address /*attester*/,
address /*portal*/
) public pure override {
bytes32[] memory proof = abi.decode(validationPayload, (bytes32[]));
bytes32 hash = bytes32(attestationPayload.attestationData);
/*
* @notice We send the hardcoded third leaf to verify.
*/
uint256 index = 2;
for (uint256 i = index + 1; i < proof.length; i = uncheckedInc256(i)) {
bytes32 proofElement = proof[i];

if (index % 2 == 0) {
hash = keccak256(abi.encodePacked(hash, proofElement));
} else {
hash = keccak256(abi.encodePacked(proofElement, hash));
}
index = index / 2;
if (index == 0) {
break;
}
}

if (hash != proof[proof.length - 1]) {
revert MerkelProofVerifyFailed();
}
}
}
Loading

0 comments on commit 86f4ab8

Please sign in to comment.