From 43b424e6a43a4f9eb4553b85ff415137552bf8f7 Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 25 Jul 2024 14:47:52 -0700 Subject: [PATCH] chore(interactions): cp over repo --- contracts/interactions/InteractionDataset.sol | 115 +++++++++++ contracts/interactions/InteractionFactory.sol | 125 ++++++++++++ .../interactions/InteractionRegistry.sol | 185 ++++++++++++++++++ 3 files changed, 425 insertions(+) create mode 100644 contracts/interactions/InteractionDataset.sol create mode 100644 contracts/interactions/InteractionFactory.sol create mode 100644 contracts/interactions/InteractionRegistry.sol diff --git a/contracts/interactions/InteractionDataset.sol b/contracts/interactions/InteractionDataset.sol new file mode 100644 index 00000000..a08347aa --- /dev/null +++ b/contracts/interactions/InteractionDataset.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +/// @notice a dataset of interactions +interface IInteractionDataset { + /// @notice an event emitted on merkle tree root update + /// @param relayer address of the relayer caused an update + /// @param merkleRoot new merkle root pushed by the relayer + /// @param proofsHash hash of the merkle tree source file (nullable) + event MerkleRootUpdated( + address indexed relayer, + bytes32 indexed merkleRoot, + bytes32 proofsHash + ); + + /// @notice a role that manage relayer role + /// @return manager role + function MANAGER_ROLE() external pure returns (bytes32 manager); + + /// @notice a role that manage and update merkle roots + /// @return relayer role + function RELAYER_ROLE() external pure returns (bytes32 relayer); + + /// @return current root of the merkle tree of interactions + function merkleRoot() external view returns (bytes32 current); + + /// @return current ipfs hash for the full merkle tree file + function proofsHash() external view returns (bytes32 current); + + /// @return timestamp of the last update of the merkle tree + function updatedAt() external view returns (uint64 timestamp); + + /// @return number of times that merkle tree has been updated + function epoch() external view returns (uint32 number); + + /// @dev checks if the given transaction hash match given interaction id + /// @param txId transaction hash + /// @param interactionId interaction id (see InteractionFactory contract) + /// @return status of inclusion in the interaction dataset for the given entry + function hasEntry( + bytes32 txId, + bytes32 interactionId, + bytes32[] memory hashedPairsProof + ) external view returns (bool status); + + /// @dev updates merkle tree (invoked by "relayer" role) + /// @param nextMerkleRoot root hash of the next merkle tree + /// @param nextProofsHash next proofs hash + function updateRoot(bytes32 nextMerkleRoot, bytes32 nextProofsHash) external; +} + +/// @title a helper contract for the interaction dataset with error definitions +abstract contract InteractionDatasetErrorHelper { + /// @notice raised when initialManager is zero address + error InitialManagerEmptyError(); +} + +contract InteractionDataset is + IInteractionDataset, + InteractionDatasetErrorHelper, + AccessControl +{ + /// @inheritdoc IInteractionDataset + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + /// @inheritdoc IInteractionDataset + bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE"); + + /// @inheritdoc IInteractionDataset + bytes32 public merkleRoot; + /// @inheritdoc IInteractionDataset + bytes32 public proofsHash; + + /// @inheritdoc IInteractionDataset + uint64 public updatedAt; + /// @inheritdoc IInteractionDataset + uint32 public epoch; + + constructor(address initialRelayerManager) { + if (initialRelayerManager == address(0)) { + revert InitialManagerEmptyError(); + } + _setRoleAdmin(RELAYER_ROLE, MANAGER_ROLE); + _grantRole(MANAGER_ROLE, initialRelayerManager); + _grantRole(RELAYER_ROLE, initialRelayerManager); + } + + /// @inheritdoc IInteractionDataset + function hasEntry( + bytes32 txId, + bytes32 interactionId, + bytes32[] calldata hashedPairsProof + ) external view returns (bool) { + return + MerkleProof.verifyCalldata( + hashedPairsProof, + keccak256(abi.encodePacked(txId, ":", interactionId)), + merkleRoot + ); + } + + /// @inheritdoc IInteractionDataset + function updateRoot(bytes32 nextMerkleRoot, bytes32 nextProofsHash) external { + _checkRole(RELAYER_ROLE); + + merkleRoot = nextMerkleRoot; + proofsHash = nextProofsHash; + updatedAt = uint64(block.timestamp); + ++epoch; + + emit MerkleRootUpdated(msg.sender, nextMerkleRoot, nextProofsHash); + } +} \ No newline at end of file diff --git a/contracts/interactions/InteractionFactory.sol b/contracts/interactions/InteractionFactory.sol new file mode 100644 index 00000000..0bbf2b97 --- /dev/null +++ b/contracts/interactions/InteractionFactory.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import { + ERC721Upgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { + ERC721URIStorageUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; +import { + AccessControlUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; + +/// @title a ERC721 upgradeable contract with metadata contract for interactions +contract InteractionFactory is ERC721URIStorageUpgradeable, AccessControlUpgradeable { + /// @notice en event emmited on interactionURI update + /// @param sender sender address of the update + /// @param interactionId id of interaction + /// @param uri the uri itself + event InteractionURIUpdated( + address indexed sender, + uint256 indexed interactionId, + string uri + ); + /// @notice en event emitted on interaction mint + event InteractionMinted(address indexed sender, uint256 interactionId); + /// @notice an event emitted on transferability change + event TransferabilitySet(address indexed sender, bool isTransferable); + + /// @notice an error raised when initialManager is zero address + error InitialManagerEmptyError(); + /// @notice an error raised when attemtep to transfer token + error TransferUnallowedError(); + /// @notice en error raised when metadataURI is empty + error MetadataURIEmptyError(); + + /// @notice a role that manage minter role and trasferability + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + /// @notice a role that manage minting + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @notice a variable that indicates whether tokens are non-transferable + bool public isNonTransferable = false; + /// @notice a variable that stores mint timestamps for each interaction id + mapping(uint256 interactionId => uint64 timestamp) public mintedAt; + + function initialize(address initialManager) public initializer { + if (address(initialManager) == address(0)) { + revert InitialManagerEmptyError(); + } + __ERC721_init("InteractionFactory", "IF"); + _setRoleAdmin(MINTER_ROLE, MANAGER_ROLE); + _grantRole(MANAGER_ROLE, initialManager); + _grantRole(MINTER_ROLE, initialManager); + } + + /// @notice enable or disable transferability of the whole collection + /// @param isTransferable true, if the collection should be transferable, false otherwise + function setTransferability(bool isTransferable) external { + _checkRole(MANAGER_ROLE); + isNonTransferable = !isTransferable; + emit TransferabilitySet(msg.sender, isTransferable); + } + + /// @notice mint interaction + /// @param to address of the recipient of the interaction token + /// @param interactionId id of the interaction + /// @param uri tokenURI + function mintInteraction( + address to, + uint256 interactionId, + string memory uri + ) external { + _checkRole(MINTER_ROLE); + _mint(to, interactionId); + _setInteractionURI(interactionId, uri); + mintedAt[interactionId] = uint64(block.timestamp); + emit InteractionMinted(to, interactionId); + } + + /// @notice update tokenURI for the interaction by the owner of interaction token + /// @param interactionId id of the interaction + /// @param uri new tokenURI + function updateInteractionURI(uint256 interactionId, string memory uri) external { + if (bytes(uri).length == 0) { + revert MetadataURIEmptyError(); + } + address owner = _ownerOf(interactionId); + _checkAuthorized(owner, msg.sender, interactionId); + _setInteractionURI(interactionId, uri); + } + + /// @inheritdoc IERC721 + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override(ERC721Upgradeable, IERC721) { + if (isNonTransferable) { + revert TransferUnallowedError(); + } else { + super.transferFrom(from, to, tokenId); + } + } + + /// @inheritdoc IERC165 + function supportsInterface( + bytes4 interfaceId + ) + public + view + override(ERC721URIStorageUpgradeable, AccessControlUpgradeable) + returns (bool) + { + return (ERC721URIStorageUpgradeable.supportsInterface(interfaceId) || + AccessControlUpgradeable.supportsInterface(interfaceId)); + } + + function _setInteractionURI(uint256 interactionId, string memory uri) internal { + _setTokenURI(interactionId, uri); + emit InteractionURIUpdated(msg.sender, interactionId, uri); + } +} \ No newline at end of file diff --git a/contracts/interactions/InteractionRegistry.sol b/contracts/interactions/InteractionRegistry.sol new file mode 100644 index 00000000..a3ccb8d4 --- /dev/null +++ b/contracts/interactions/InteractionRegistry.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +/// @title a registry contract interface for interactions +interface IInteractionRegistry { + /// @notice an event emmited on interaction type creation + /// @param interactionId interaction type identifier (a keccak256 of chainId, recipient and functionSelector) + /// @param chainId chain id of the interaction + /// @param recipient call address (tx to) of the interaction + /// @param functionSelector 4 bytes of the function selector of the interaction + event InteractionTypeCreated( + bytes32 indexed interactionId, + uint16 chainId, + address recipient, + bytes4 functionSelector + ); + + /// @notice interaction definition data struct + /// @param chainId chain id of the interaction + /// @param recipient call address (tx to) of the interaction + /// @param functionSelector 4 bytes of the function selector of the interaction + struct TInteractionData { + uint16 chainId; + address recipient; + bytes4 functionSelector; + } + + /// @notice a role that manage operator role + /// @return manager role + function MANAGER_ROLE() external pure returns (bytes32 manager); + + /// @notice a role that manage and update interaction types + /// @return operator role + function OPERATOR_ROLE() external pure returns (bytes32 operator); + + /// @notice calculates interaction id by the given interaction data + /// @param chainId chain id of the interaction + /// @param recipient call address (tx to) of the interaction + /// @param functionSelector 4 bytes of the function selector of the interaction + /// @return interactionId identifier of the interaction with the given params + function predictInteractionId( + uint16 chainId, + address recipient, + bytes4 functionSelector + ) external pure returns (bytes32 interactionId); + + /// @notice returns interaction data for the given interaction id + /// @param interactionId identifier of the interaction + /// @return chainId chain id of the interaction + /// @return recipient call address (tx to) of the interaction + /// @return functionSelector 4 bytes of the function selector of the interaction + function interactionDataFor( + bytes32 interactionId + ) external view returns (uint16 chainId, address recipient, bytes4 functionSelector); + + /// @notice registeres interaction type by the given interaction data + /// @param chainId chain id of the interaction + /// @param recipient call address (tx to) of the interaction + /// @param functionSelector 4 bytes of the function selector of the interaction + function registerInteractionId( + uint16 chainId, + address recipient, + bytes4 functionSelector + ) external; +} + +/// @dev a helper contract for the interaction registry with error definitions +contract InteractionRegistryErrorHelper { + /// @notice raised when initialManager is zero address + error InitialManagerEmptyError(); + /// @notice raised when interaction recipient (tx to) address is invalid + error InvalidRecipientError(); + /// @notice raised when interaction chain id is invalid + error InvalidChainIdError(); + /// @notice raised when attempted to create an interaction duplicate + error InteractionAlreadyExistError(); +} + +/// @notice a registry contract for interaction +contract InteractionRegistry is + IInteractionRegistry, + InteractionRegistryErrorHelper, + AccessControl +{ + /// @inheritdoc IInteractionRegistry + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + /// @inheritdoc IInteractionRegistry + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + mapping(bytes32 interactionId => TInteractionData data) internal _interactionDataFor; + bytes32 internal constant INTERACTION_DATA_TYPEHASH = + keccak256( + "TInteractionData(uint16 chainId,address recipient,bytes4 functionSelector)" + ); + + constructor(address initialOperatorManager) { + if (initialOperatorManager == address(0)) { + revert InitialManagerEmptyError(); + } + + _setRoleAdmin(OPERATOR_ROLE, MANAGER_ROLE); + _grantRole(MANAGER_ROLE, initialOperatorManager); + _grantRole(OPERATOR_ROLE, initialOperatorManager); + } + + /// @inheritdoc IInteractionRegistry + function interactionDataFor( + bytes32 key + ) external view returns (uint16, address, bytes4) { + TInteractionData memory interactionData = _interactionDataFor[key]; + return ( + interactionData.chainId, + interactionData.recipient, + interactionData.functionSelector + ); + } + + /// @inheritdoc IInteractionRegistry + function predictInteractionId( + uint16 chainId, + address recipient, + bytes4 functionSelector + ) external pure returns (bytes32) { + TInteractionData memory data = TInteractionData({ + chainId: chainId, + recipient: recipient, + functionSelector: functionSelector + }); + return _calcInteractionIdFor(data); + } + + /// @inheritdoc IInteractionRegistry + function registerInteractionId( + uint16 chainId, + address recipient, + bytes4 functionSelector + ) external { + _checkRole(OPERATOR_ROLE); + if (chainId == 0) { + revert InvalidChainIdError(); + } + if (recipient == address(0)) { + revert InvalidRecipientError(); + } + + TInteractionData memory data = TInteractionData({ + chainId: chainId, + recipient: recipient, + functionSelector: functionSelector + }); + bytes32 interactionId = _calcInteractionIdFor(data); + if ( + // check for non-empty + _interactionDataFor[interactionId].chainId != 0 + ) { + revert InteractionAlreadyExistError(); + } + + _interactionDataFor[interactionId] = data; + + emit InteractionTypeCreated( + interactionId, + chainId, + data.recipient, + data.functionSelector + ); + } + + /// @dev a helper utility for calculating interaction data hash + function _calcInteractionIdFor( + TInteractionData memory data + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + INTERACTION_DATA_TYPEHASH, + data.chainId, + data.recipient, + data.functionSelector + ) + ); + } +} \ No newline at end of file