From 6a1b96e8d2e8819d6c86c67da29e8659e9ecef2e Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Wed, 6 Sep 2023 18:26:04 -0300 Subject: [PATCH 1/3] non custodial draft --- contracts/ImmutableVault.sol | 217 --------------------------- contracts/OriumRentalProtocol.sol | 239 ++++++++++++++++++++++++++++++ contracts/RolesRegistry.sol | 197 ++++++++++++++++++++++++ contracts/interfaces/IERC7432.sol | 113 +++++++++++++- 4 files changed, 545 insertions(+), 221 deletions(-) delete mode 100644 contracts/ImmutableVault.sol create mode 100644 contracts/OriumRentalProtocol.sol create mode 100644 contracts/RolesRegistry.sol diff --git a/contracts/ImmutableVault.sol b/contracts/ImmutableVault.sol deleted file mode 100644 index 0980b30..0000000 --- a/contracts/ImmutableVault.sol +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.9; - -import { IRolesRegistry } from "./interfaces/IRolesRegistry.sol"; -import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; - -/// @title ImmutableVault -/// @dev This contract is used by the marketplace to store tokens and role assignment roles to them. -/// @author Orium Network Team - developers@orium.network -contract ImmutableVault is AccessControl { - bytes32 public MARKETPLACE_ROLE = keccak256("MARKETPLACE_ROLE"); - address public rolesRegistry; - - struct RoleAssignment { - bytes32 role; - address grantee; - } - - struct NftInfo { - address owner; - uint64 deadline; - RoleAssignment[] roleAssignments; - uint256 expirationDate; - } - - // tokenAddress => tokenId => nftInfo(owner, deadline, roleAssignments, nonce) - mapping(address => mapping(uint256 => NftInfo)) public nftInfo; - - event Deposit(address indexed tokenAddress, uint256 indexed tokenId, address indexed owner, uint64 deadline); - event Withdraw(address indexed tokenAddress, uint256 indexed tokenId, address indexed owner); - event ExtendDeadline(address indexed tokenAddress, uint256 indexed tokenId, uint64 newDeadline); - - modifier onlyNftOwner(address _tokenAddress, uint256 _tokenId) { - require(msg.sender == nftInfo[_tokenAddress][_tokenId].owner, "ImmutableVault: sender is not the token owner"); - _; - } - - constructor(address _operator, address _rolesRegistry, address _marketplace) { - rolesRegistry = _rolesRegistry; - - _setupRole(DEFAULT_ADMIN_ROLE, _operator); - _setupRole(MARKETPLACE_ROLE, _marketplace); - } - - /// @notice Deposit a token - /// @param _tokenAddress Address of the token to deposit - /// @param _tokenId ID of the token to deposit - /// @param _deadline The maximum date a role assignment can last - function deposit(address _tokenAddress, uint256 _tokenId, uint64 _deadline) external { - _deposit(msg.sender, _tokenAddress, _tokenId, _deadline); - } - - /// @notice Deposit a token on behalf of someone else - /// @dev This function is only callable by some account which has MARKETPLACE_ROLE - /// @param _tokenAddress Address of the token to deposit - /// @param _tokenId ID of the token to deposit - /// @param _deadline The maximum date a role assignment can last - function depositOnBehalfOf( - address _tokenAddress, - uint256 _tokenId, - uint64 _deadline - ) external onlyRole(MARKETPLACE_ROLE) { - _deposit(IERC721(_tokenAddress).ownerOf(_tokenId), _tokenAddress, _tokenId, _deadline); - } - - /// @notice Read documentation above - function _deposit(address _tokenOwner, address _tokenAddress, uint256 _tokenId, uint64 _deadline) internal { - nftInfo[_tokenAddress][_tokenId].owner = _tokenOwner; - nftInfo[_tokenAddress][_tokenId].deadline = _deadline; - - emit Deposit(_tokenAddress, _tokenId, _tokenOwner, _deadline); - - // Check-Effects-Interaction-Effects pattern - IERC721(_tokenAddress).transferFrom(_tokenOwner, address(this), _tokenId); - } - - /// @notice Withdraw a token - /// @param _tokenAddress Address of the token to withdraw - /// @param _tokenId ID of the token to withdraw - function withdraw(address _tokenAddress, uint256 _tokenId) external onlyNftOwner(_tokenAddress, _tokenId) { - _withdraw(msg.sender, _tokenAddress, _tokenId); - } - - /// @notice Withdraw a token on behalf of someone else - /// @dev This function is only callable by some account which has MARKETPLACE_ROLE - /// @param _tokenAddress Address of the token to withdraw - /// @param _tokenId ID of the token to withdraw - function withdrawOnBehalfOf(address _tokenAddress, uint256 _tokenId) external onlyRole(MARKETPLACE_ROLE) { - address _tokenOwner = nftInfo[_tokenAddress][_tokenId].owner; - _withdraw(_tokenOwner, _tokenAddress, _tokenId); - } - - /// @notice Read documentation above - function _withdraw(address _tokenOwner, address _tokenAddress, uint256 _tokenId) internal { - require( - _tokenOwner == nftInfo[_tokenAddress][_tokenId].owner, - "ImmutableVault: _tokenOwner is not the token owner" - ); - - require( - nftInfo[_tokenAddress][_tokenId].expirationDate < block.timestamp, - "ImmutableVault: token has an active role assignment" - ); - - delete nftInfo[_tokenAddress][_tokenId]; - - emit Withdraw(_tokenAddress, _tokenId, _tokenOwner); - - IERC721(_tokenAddress).transferFrom(address(this), _tokenOwner, _tokenId); - } - - /// @notice RoleAssignment a role to a token - /// @dev This function is only callable by accounts with `MARKETPLACE_ROLE` - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _roleAssignments The role assignment struct. - function batchGrantRole( - address _tokenAddress, - uint256 _tokenId, - uint64 _expirationDate, - RoleAssignment[] calldata _roleAssignments, - bytes[] memory _data - ) external onlyRole(MARKETPLACE_ROLE) { - require( - _roleAssignments.length == _data.length, - "ImmutableVault: role assignment roles and data length mismatch" - ); - - require( - nftInfo[_tokenAddress][_tokenId].expirationDate < block.timestamp, - "ImmutableVault: token has an active role assignment" - ); - - require( - nftInfo[_tokenAddress][_tokenId].deadline >= _expirationDate, - "ImmutableVault: token deadline is before the role assignment expiration date" - ); - - for (uint256 i = 0; i < _roleAssignments.length; i++) { - _grantRole(_tokenAddress, _tokenId, _expirationDate, _data[i], _roleAssignments[i]); - nftInfo[_tokenAddress][_tokenId].roleAssignments.push(_roleAssignments[i]); - } - - nftInfo[_tokenAddress][_tokenId].expirationDate = _expirationDate; - } - - /// @notice RoleAssignment internal function - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _expirationDate The expiration date of the role assignment. - /// @param _data The data to pass to the role assignment. - /// @param _roleAssignment The role assignment struct. - function _grantRole( - address _tokenAddress, - uint256 _tokenId, - uint64 _expirationDate, - bytes memory _data, - RoleAssignment memory _roleAssignment - ) internal { - IRolesRegistry(rolesRegistry).grantRole( - _roleAssignment.role, - _tokenAddress, - _tokenId, - _roleAssignment.grantee, - _expirationDate, - _data - ); - } - - /// @notice Revoke all role assignment roles from a token - /// @dev This function is only callable by some account which has MARKETPLACE_ROLE - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - function batchRevokeRole( - address _tokenAddress, - uint256 _tokenId - ) external onlyRole(MARKETPLACE_ROLE) { - RoleAssignment[] memory _roleAssignments = nftInfo[_tokenAddress][_tokenId].roleAssignments; - - for (uint256 i = 0; i < _roleAssignments.length; i++) { - _revokeRole(_roleAssignments[i].role, _tokenAddress, _tokenId, _roleAssignments[i].grantee); - } - - delete nftInfo[_tokenAddress][_tokenId].roleAssignments; // free storage and refund gas - delete nftInfo[_tokenAddress][_tokenId].expirationDate; // free storage and refund gas - } - - /// @notice Revoke a role from a token - /// @dev This function is only callable by some account which has MARKETPLACE_ROLE - /// @param _role The role identifier. - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _grantee The address to revoke the role from. - function _revokeRole(bytes32 _role, address _tokenAddress, uint256 _tokenId, address _grantee) internal { - IRolesRegistry(rolesRegistry).revokeRole(_role, _tokenAddress, _tokenId, _grantee); - } - - /// @notice Extend the deadline for a token - /// @dev This function is only callable by the token owner - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _newDeadline The new deadline. - function extendDeadline( - address _tokenAddress, - uint256 _tokenId, - uint64 _newDeadline - ) external onlyNftOwner(_tokenAddress, _tokenId) { - require( - _newDeadline > nftInfo[_tokenAddress][_tokenId].deadline, - "ImmutableVault: new deadline must be greater than the current one" - ); - nftInfo[_tokenAddress][_tokenId].deadline = _newDeadline; - emit ExtendDeadline(_tokenAddress, _tokenId, _newDeadline); - } -} diff --git a/contracts/OriumRentalProtocol.sol b/contracts/OriumRentalProtocol.sol new file mode 100644 index 0000000..541ee7a --- /dev/null +++ b/contracts/OriumRentalProtocol.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +import { IRolesRegistry } from "./interfaces/IRolesRegistry.sol"; +import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; +import { ECDSAUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +contract OriumRentalProtocol is Initializable, OwnableUpgradeable, EIP712Upgradeable { + string public constant SIGNING_DOMAIN = "Orium-Rental-Marketplace"; + string public constant SIGNATURE_VERSION = "1"; + bytes32 public constant USER_ROLE = keccak256("USER_ROLE"); + bytes public constant EMPTY_BYTES = ""; + + /// @dev nonce => isPresigned + mapping(bytes32 => bool) public preSignedOffer; + + /// @dev maker => nonce => bool + mapping(address => mapping(uint256 => bool)) public invalidNonce; + + /// @dev tokenAddress => registryAddress + mapping(address => address) public tokenAddressToRegistry; + + struct RentalOffer { + address maker; + address taker; + address tokenAddress; + uint256 tokenId; + address feeToken; + uint256 feeAmount; + uint256 nonce; + uint64 expirationDate; + } + enum SignatureType { + PRE_SIGNED, + EIP_712, + EIP_1271 + } + + /** + * @param nonce nonce of the rental offer + * @param maker address of the user renting his NFTs + * @param taker address of the allowed tenant if private rental or `0x0` if public rental + * @param tokenAddress address of the contract of the NFT to rent + * @param tokenId tokenId of the NFT to rent + * @param feeToken address of the ERC20 token for rental fees + * @param feeAmount amount of the upfront rental cost + * @param deadline until when the rental offer is valid + */ + event RentalOfferCreated( + uint256 indexed nonce, + address indexed maker, + address taker, + address tokenAddress, + uint256 tokenId, + address feeToken, + uint256 feeAmount, + uint256 deadline + ); + + /** + * @param nonce nonce of the rental offer + * @param maker address of the user renting his NFTs + */ + event RentalOfferCancelled(uint256 indexed nonce, address indexed maker); + + /** + * @param nonce nonce of the rental offer + * @param lender address of the lender + * @param tenant address of the tenant + * @param token address of the contract of the NFT rented + * @param tokenId tokenId of the rented NFT + * @param expirationDate when the rent ends + */ + event RentalStarted( + uint256 indexed nonce, + address indexed lender, + address indexed tenant, + address token, + uint256 tokenId, + uint256 expirationDate + ); + + /** + * @param lender address of the lender + * @param tenant address of the tenant + * @param token address of the contract of the NFT rented + * @param tokenId tokenId of the rented NFT + */ + event RentalEnded(address indexed lender, address indexed tenant, address token, uint256 tokenId); + + modifier onlyTokenOwner(address _tokenAddress, uint256 _tokenId) { + require( + msg.sender == IERC721(_tokenAddress).ownerOf(_tokenId), + "OriumRentalProtocol: Caller does not have the required permission" + ); + _; + } + + function initialize(address _owner) public initializer { + __EIP712_init(SIGNING_DOMAIN, SIGNATURE_VERSION); + __Ownable_init(); + transferOwnership(_owner); + } + + function preSignRentalOffer(RentalOffer calldata offer) external onlyTokenOwner(offer.tokenAddress, offer.tokenId) { + require(msg.sender == offer.maker, "Signer and Maker mismatch"); + require(msg.sender == IERC721(offer.tokenAddress).ownerOf(offer.tokenId), "OriumRentalProtocol: Sender is not the owner of the NFT"); + + preSignedOffer[hashRentalOffer(offer)] = true; + + emit RentalOfferCreated( + offer.nonce, + offer.maker, + offer.taker, + offer.tokenAddress, + offer.tokenId, + offer.feeToken, + offer.feeAmount, + offer.expirationDate + ); + } + + function cancelRentalOffer(uint256 nonce) external { + require(!invalidNonce[msg.sender][nonce], "OriumRentalProtocol: Nonce already used"); // Avoid multiple cancellations + invalidNonce[msg.sender][nonce] = true; + emit RentalOfferCancelled(nonce, msg.sender); + } + + function rent(RentalOffer calldata offer, SignatureType signatureType, bytes calldata signature) external { + require(offer.expirationDate >= block.timestamp, "OriumRentalProtocol: Offer expired"); + require(!invalidNonce[offer.maker][offer.nonce], "OriumRentalProtocol: Nonce already used"); + require( + msg.sender == offer.taker || offer.taker == address(0), + "OriumRentalProtocol: Caller is not allowed to rent this NFT" + ); + + address _lastGrantee = IRolesRegistry(tokenAddressToRegistry[offer.tokenAddress]).lastGrantee( + USER_ROLE, + offer.maker, + offer.tokenAddress, + offer.tokenId + ); + + require( + !IRolesRegistry(tokenAddressToRegistry[offer.tokenAddress]).hasUniqueRole( + USER_ROLE, + offer.tokenAddress, + offer.tokenId, + offer.maker, + _lastGrantee + ), + "Nft is already rented" + ); + + if (signatureType == SignatureType.PRE_SIGNED) { + require(preSignedOffer[hashRentalOffer(offer)] == true, "Presigned offer not found"); + } else if (signatureType == SignatureType.EIP_712) { + bytes32 _hash = hashRentalOffer(offer); + address signer = ECDSAUpgradeable.recover(_hash, signature); + require(signer == offer.maker, "Signer is not maker"); + } else { + revert("Unsupported signature type"); + } + + address _taker = offer.taker == address(0) ? msg.sender : offer.taker; + + IRolesRegistry(tokenAddressToRegistry[offer.tokenAddress]).grantRoleFrom( + USER_ROLE, + offer.tokenAddress, + offer.tokenId, + offer.maker, + _taker, + offer.expirationDate, + EMPTY_BYTES + ); + + invalidNonce[offer.maker][offer.nonce] = true; + + emit RentalStarted(offer.nonce, offer.maker, _taker, offer.tokenAddress, offer.tokenId, offer.expirationDate); + } + + function endRental(address _tokenAddress, uint256 _tokenId) external { + address _owner = IERC721(_tokenAddress).ownerOf(_tokenId); + address _taker = IRolesRegistry(tokenAddressToRegistry[_tokenAddress]).lastGrantee( + USER_ROLE, + _owner, + _tokenAddress, + _tokenId + ); + + require(msg.sender == _taker, "Only owner or taker can end rental"); + require(_taker != address(0), "OriumRentalProtocol: NFT is not rented"); + + if (msg.sender == _owner) { + uint64 _expirationDate = IRolesRegistry(tokenAddressToRegistry[_tokenAddress]).roleExpirationDate( + USER_ROLE, + _tokenAddress, + _tokenId, + _owner, + _taker + ); + require(block.timestamp > _expirationDate, "OriumRentalProtocol: Rental hasn't ended yet"); + } + + IRolesRegistry(tokenAddressToRegistry[_tokenAddress]).revokeRoleFrom(USER_ROLE, _tokenAddress, _tokenId, _owner, _taker); + + emit RentalEnded(_owner, _taker, _tokenAddress, _tokenId); + } + + function hashRentalOffer(RentalOffer memory offer) public view returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + keccak256( + "RentalOffer(address maker,address taker,address tokenAddress,uint256 tokenId,address feeToken,uint256 feeAmount,uint256 nonce,uint64 expirationDate)" + ), + offer.maker, + offer.taker, + offer.tokenAddress, + offer.tokenId, + offer.feeToken, + offer.feeAmount, + offer.nonce, + offer.expirationDate + ) + ) + ); + } + + function setRolesRegistry(address _tokenAddress, address _registryAddress) external onlyOwner { + //TODO: make this in batch? + tokenAddressToRegistry[_tokenAddress] = _registryAddress; + } +} diff --git a/contracts/RolesRegistry.sol b/contracts/RolesRegistry.sol new file mode 100644 index 0000000..99c8569 --- /dev/null +++ b/contracts/RolesRegistry.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +import { IERC7432 } from "./interfaces/IERC7432.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +contract RolesRegistry is IERC7432 { + + // grantee => tokenAddress => tokenId => role => struct(expirationDate, data) + mapping(address => mapping(address => mapping(uint256 => mapping(bytes32 => RoleData)))) + public roleAssignments; + + // tokenAddress => tokenId => role => grantee + mapping(address => mapping(uint256 => mapping(bytes32 => address))) public lastRoleAssignment; + + // owner => tokenAddress => tokenId => operator => isApproved + mapping(address => mapping(address => mapping(uint256 => mapping(address => bool)))) public tokenApprovals; + + // owner => tokenAddress => operator => operatorApprovals + mapping(address => mapping(address => mapping(address => bool))) public operatorApprovals; + + + modifier validExpirationDate(uint64 _expirationDate) { + require(_expirationDate > block.timestamp, "RolesRegistry: expiration date must be in the future"); + _; + } + + function grantRole( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantee, + uint64 _expirationDate, + bytes calldata _data + ) external validExpirationDate(_expirationDate) { + require(msg.sender == IERC721(_tokenAddress).ownerOf(_tokenId), "RolesRegistry: sender must be owner"); + roleAssignments[_grantee][_tokenAddress][_tokenId][_role] = RoleData(_expirationDate, _data); + lastRoleAssignment[_tokenAddress][_tokenId][_role] = _grantee; + emit RoleGranted(msg.sender, _role, _tokenAddress, _tokenId, _grantee, _expirationDate, _data); + } + + function revokeRole(bytes32 _role, address _tokenAddress, uint256 _tokenId, address _grantee) external { + delete roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; + delete lastRoleAssignment[_tokenAddress][_tokenId][_role]; + emit RoleRevoked(msg.sender, _role, _tokenAddress, _tokenId, _grantee); + } + + function hasRole( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantor, + address _grantee + ) external view returns (bool) { + return roleAssignments[_grantee][_tokenAddress][_tokenId][_role].expirationDate > block.timestamp; + } + + function hasUniqueRole( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantor, + address _grantee + ) external view returns (bool) { + bool isValid = roleAssignments[_grantee][_tokenAddress][_tokenId][_role].expirationDate > + block.timestamp; + + return isValid && lastRoleAssignment[_tokenAddress][_tokenId][_role] == _grantee; + } + + function roleData( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantor, + address _grantee + ) external view returns (bytes memory data_) { + RoleData memory _roleData = roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; + return (_roleData.data); + } + + function roleExpirationDate( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantor, + address _grantee + ) external view returns (uint64 expirationDate_){ + RoleData memory _roleData = roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; + return (_roleData.expirationDate); + } + + function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { + return interfaceId == type(IERC7432).interfaceId; + } + + /// @notice Grants a role to a user from a role assignment. + /// @param _role The role identifier. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _grantor The role creator. + /// @param _grantee The user that receives the role assignment. + /// @param _expirationDate The expiration date of the role assignment. + /// @param _data Any additional data about the role assignment. + function grantRoleFrom( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantor, + address _grantee, + uint64 _expirationDate, + bytes calldata _data + ) external override validExpirationDate(_expirationDate) { + require(_grantor == IERC721(_tokenAddress).ownerOf(_tokenId), "RolesRegistry: sender must be owner"); + require(tokenApprovals[_grantor][_tokenAddress][_tokenId][msg.sender] || operatorApprovals[_grantor][_tokenAddress][msg.sender], "RolesRegistry: sender must be approved"); + + roleAssignments[_grantee][_tokenAddress][_tokenId][_role] = RoleData(_expirationDate, _data); + lastRoleAssignment[_tokenAddress][_tokenId][_role] = _grantee; + emit RoleGranted(_grantor, _role, _tokenAddress, _tokenId, _grantee, _expirationDate, _data); // TODO: We should change event to receive grantor as parameter + } + + /// @notice Revokes a role from a user. + /// @param _role The role identifier. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _grantor The role creator. + /// @param _grantee The user that receives the role revocation. + function revokeRoleFrom( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantor, + address _grantee + ) external override { + require(_grantor == IERC721(_tokenAddress).ownerOf(_tokenId), "RolesRegistry: sender must be owner"); + require(tokenApprovals[_grantor][_tokenAddress][_tokenId][msg.sender] || operatorApprovals[_grantor][_tokenAddress][msg.sender], "RolesRegistry: sender must be approved"); + + delete roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; + delete lastRoleAssignment[_tokenAddress][_tokenId][_role]; + emit RoleRevoked(_grantor, _role, _tokenAddress, _tokenId, _grantee); // TODO: We should change event to receive grantor as parameter + } + + /// @notice Sets the approval for a user to grant a role to another user. + /// @param _operator The user that can grant the role. + /// @param _tokenAddress The token address. + /// @param _isApproved The approval status. + function setRoleApprovalForAll( + address _operator, + address _tokenAddress, + bool _isApproved + ) external override { + operatorApprovals[msg.sender][_tokenAddress][_operator] = _isApproved; + emit RoleApprovalForAll(msg.sender, _tokenAddress, _operator, _isApproved); + } + + /// @notice Sets the approval for a user to grant a role to another user. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _operator The user that can grant the role. + /// @param _approved The approval status. + function approveRole( + address _tokenAddress, + uint256 _tokenId, + address _operator, + bool _approved + ) external override { + tokenApprovals[msg.sender][_tokenAddress][_tokenId][_operator] = _approved; + emit RoleApproval(msg.sender, _tokenAddress, _tokenId, _operator, _approved); + } + + /// @notice Checks if a user is tokenApprovals to grant a role to another user. + /// @param _grantor The user that tokenApprovals the operator. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _operator The user that can grant the role. + function getApprovedRole( + address _grantor, + address _tokenAddress, + uint256 _tokenId, + address _operator + ) external view override returns (bool) { + return tokenApprovals[_grantor][_tokenAddress][_tokenId][_operator]; + } + + /// @notice Checks if a user is tokenApprovals to grant a role to another user. + /// @param _grantor The user that tokenApprovals the operator. + /// @param _operator The user that can grant the role. + /// @param _tokenAddress The token address. + function isRoleApprovedForAll( + address _grantor, + address _operator, + address _tokenAddress + ) external view override returns (bool) { + return operatorApprovals[_grantor][_tokenAddress][_operator]; + } +} diff --git a/contracts/interfaces/IERC7432.sol b/contracts/interfaces/IERC7432.sol index 334b4ca..6128559 100644 --- a/contracts/interfaces/IERC7432.sol +++ b/contracts/interfaces/IERC7432.sol @@ -7,7 +7,7 @@ import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol /// @title ERC-7432 Non-Fungible Token Roles /// @dev See https://eips.ethereum.org/EIPS/eip-7432 -/// Note: the ERC-165 identifier for this interface is 0xeec2fffb. +/// Note: the ERC-165 identifier for this interface is 0x565ccd2b. interface IERC7432 is IERC165 { struct RoleData { uint64 expirationDate; @@ -15,6 +15,7 @@ interface IERC7432 is IERC165 { } /// @notice Emitted when a role is granted. + /// @param _grantor The role creator. /// @param _role The role identifier. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. @@ -22,26 +23,55 @@ interface IERC7432 is IERC165 { /// @param _expirationDate The expiration date of the role assignment. /// @param _data Any additional data about the role assignment. event RoleGranted( + address indexed _grantor, bytes32 indexed _role, address indexed _tokenAddress, - uint256 indexed _tokenId, + uint256 _tokenId, address _grantee, - uint64 _expirationDate, + uint64 _expirationDate, bytes _data ); /// @notice Emitted when a role is revoked. + /// @param _revoker The role revoker. /// @param _role The role identifier. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. /// @param _grantee The user that receives the role revocation. event RoleRevoked( + address indexed _revoker, bytes32 indexed _role, address indexed _tokenAddress, - uint256 indexed _tokenId, + uint256 _tokenId, address _grantee ); + /// @notice Emitted when an operator is approved to grant a role to another user. + /// @param _grantor The role creator. + /// @param _operator The user that can grant the role. + /// @param _tokenAddress The token address. + /// @param _isApproved The approval status. + event RoleApprovalForAll( + address indexed _grantor, + address indexed _operator, + address indexed _tokenAddress, + bool _isApproved + ); + + /// @notice Emitted when an operator is approved to grant a role to another user. + /// @param _grantor The role creator. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _operator The user that can grant the role. + /// @param _isApproved The approval status. + event RoleApproval( + address indexed _grantor, + address indexed _tokenAddress, + uint256 indexed _tokenId, + address _operator, + bool _isApproved + ); + /// @notice Grants a role to a user. /// @param _role The role identifier. /// @param _tokenAddress The token address. @@ -126,4 +156,79 @@ interface IERC7432 is IERC165 { address _grantee ) external view returns (uint64 expirationDate_); + /// @notice Grants a role on behalf of a specified user. + /// @param _role The role identifier. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _grantor The user assigning the role. + /// @param _grantee The user that receives the role assignment. + /// @param _expirationDate The expiration date of the role assignment. + /// @param _data Any additional data about the role assignment. + function grantRoleFrom( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantor, + address _grantee, + uint64 _expirationDate, + bytes calldata _data + ) external; + + /// @notice Revokes a role on behalf of an user. + /// @param _role The role identifier. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _grantor The user assigning the role. + /// @param _grantee The user that receives the role revocation. + function revokeRoleFrom( + bytes32 _role, + address _tokenAddress, + uint256 _tokenId, + address _grantor, + address _grantee + ) external; + + /// @notice Approves user to grant and revoke any roles on behalf of the user. + /// @param _operator The approved user. + /// @param _tokenAddress The token address. + /// @param _approved The approval status. + function setRoleApprovalForAll( + address _operator, + address _tokenAddress, + bool _approved + ) external; + + /// @notice Approves user to grant and revoke any roles on behalf of the user. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _operator The user that can grant the role. + /// @param _approved The approval status. + function approveRole( + address _tokenAddress, + uint256 _tokenId, + address _operator, + bool _approved + ) external; + + /// @notice Checks if a user is approved to grant a role on behalf of another user. + /// @param _grantor The user that approved the operator. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _operator The user that can grant the role. + function getApprovedRole( + address _grantor, + address _tokenAddress, + uint256 _tokenId, + address _operator + ) external view returns (bool); + + /// @notice Checks if a user is approved to grant any role on behalf of another user. + /// @param _grantor The user that approved the operator. + /// @param _operator The user that can grant the role. + /// @param _tokenAddress The token address. + function isRoleApprovedForAll( + address _grantor, + address _operator, + address _tokenAddress + ) external view returns (bool); } \ No newline at end of file From f1457ddf08fea50df3e929ea74e249984f922552 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Wed, 6 Sep 2023 18:27:50 -0300 Subject: [PATCH 2/3] Update comments --- contracts/RolesRegistry.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/RolesRegistry.sol b/contracts/RolesRegistry.sol index 99c8569..e7df3e2 100644 --- a/contracts/RolesRegistry.sol +++ b/contracts/RolesRegistry.sol @@ -41,6 +41,7 @@ contract RolesRegistry is IERC7432 { } function revokeRole(bytes32 _role, address _tokenAddress, uint256 _tokenId, address _grantee) external { + require(msg.sender == IERC721(_tokenAddress).ownerOf(_tokenId), "RolesRegistry: sender must be owner"); delete roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; delete lastRoleAssignment[_tokenAddress][_tokenId][_role]; emit RoleRevoked(msg.sender, _role, _tokenAddress, _tokenId, _grantee); @@ -50,7 +51,7 @@ contract RolesRegistry is IERC7432 { bytes32 _role, address _tokenAddress, uint256 _tokenId, - address _grantor, + address _grantor, // TODO: not being used. Remove? address _grantee ) external view returns (bool) { return roleAssignments[_grantee][_tokenAddress][_tokenId][_role].expirationDate > block.timestamp; @@ -60,7 +61,7 @@ contract RolesRegistry is IERC7432 { bytes32 _role, address _tokenAddress, uint256 _tokenId, - address _grantor, + address _grantor, // TODO: not being used. Remove? address _grantee ) external view returns (bool) { bool isValid = roleAssignments[_grantee][_tokenAddress][_tokenId][_role].expirationDate > @@ -73,7 +74,7 @@ contract RolesRegistry is IERC7432 { bytes32 _role, address _tokenAddress, uint256 _tokenId, - address _grantor, + address _grantor, // TODO: not being used. Remove? address _grantee ) external view returns (bytes memory data_) { RoleData memory _roleData = roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; @@ -84,7 +85,7 @@ contract RolesRegistry is IERC7432 { bytes32 _role, address _tokenAddress, uint256 _tokenId, - address _grantor, + address _grantor,// TODO: not being used. Remove? address _grantee ) external view returns (uint64 expirationDate_){ RoleData memory _roleData = roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; From 15933186da162cada255452163a8525b13235744 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Wed, 6 Sep 2023 18:29:09 -0300 Subject: [PATCH 3/3] remove test --- test/ImmutableVault.test.ts | 125 ------------------------------------ 1 file changed, 125 deletions(-) delete mode 100644 test/ImmutableVault.test.ts diff --git a/test/ImmutableVault.test.ts b/test/ImmutableVault.test.ts deleted file mode 100644 index f298ccf..0000000 --- a/test/ImmutableVault.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import hre, { ethers } from 'hardhat' -import { Contract } from 'ethers' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { EMPTY_BYTES, ONE_DAY, RolesRegistryAddress, USER_ROLE } from '../utils/constants' - -describe('Immutable Vault', () => { - let vault: Contract - let rolesRegistry: Contract - let mockERC721: Contract - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let deployer: SignerWithAddress - let multisig: SignerWithAddress - let nftOwner: SignerWithAddress - let marketplace: SignerWithAddress - let borrower: SignerWithAddress - - const tokenId = 1 - - before(async function () { - // eslint-disable-next-line prettier/prettier - [deployer, multisig, nftOwner, marketplace, borrower] = await ethers.getSigners() - }) - - beforeEach(async () => { - rolesRegistry = await ethers.getContractAt('IRolesRegistry', RolesRegistryAddress) - - const ImmutableVaultFactory = await ethers.getContractFactory('ImmutableVault') - vault = await ImmutableVaultFactory.deploy(multisig.address, rolesRegistry.address, marketplace.address) - await vault.deployed() - - const MockERC721Factory = await ethers.getContractFactory('MockERC721') - mockERC721 = await MockERC721Factory.deploy('Mock ERC721', 'mERC721') - await mockERC721.deployed() - - await mockERC721.mint(nftOwner.address, tokenId) - await mockERC721.connect(nftOwner).approve(vault.address, tokenId) - }) - - describe('Main functions', async () => { - let expirationDate: number - let deadline: number - - beforeEach(async () => { - const blockNumber = await hre.ethers.provider.getBlockNumber() - const block = await hre.ethers.provider.getBlock(blockNumber) - expirationDate = block.timestamp + ONE_DAY - deadline = block.timestamp + ONE_DAY * 30 - }) - - describe('Deposit NFT', async () => { - it('Should deposit NFT', async () => { - await expect(vault.connect(nftOwner).deposit(mockERC721.address, tokenId, expirationDate)) - .to.emit(vault, 'Deposit') - .withArgs(mockERC721.address, tokenId, nftOwner.address, expirationDate) - expect(await mockERC721.ownerOf(tokenId)).to.be.equal(vault.address) - }) - it('Should not deposit NFT if not owner', async () => { - await expect(vault.deposit(mockERC721.address, tokenId, expirationDate)).to.be.revertedWith( - 'ERC721: transfer from incorrect owner', - ) - }) - }) - - describe('Deposit NFT on Behalf of', async () => { - it('Should deposit NFT on behalf of', async () => { - await expect(vault.connect(marketplace).depositOnBehalfOf(mockERC721.address, tokenId, expirationDate)) - .to.emit(vault, 'Deposit') - .withArgs(mockERC721.address, tokenId, nftOwner.address, expirationDate) - }) - it('Should not deposit NFT on behalf of if not marketplace', async () => { - await expect( - vault.connect(nftOwner).depositOnBehalfOf(mockERC721.address, tokenId, expirationDate), - ).to.be.revertedWith( - `AccessControl: account ${nftOwner.address.toLowerCase()} is missing role ${await vault.MARKETPLACE_ROLE()}`, - ) - }) - }) - - describe('Withdraw NFT', async () => { - beforeEach(async () => { - await vault.connect(nftOwner).deposit(mockERC721.address, tokenId, expirationDate) - }) - it('Should withdraw NFT', async () => { - await expect(vault.connect(nftOwner).withdraw(mockERC721.address, tokenId)) - .to.emit(vault, 'Withdraw') - .withArgs(mockERC721.address, tokenId, nftOwner.address) - expect(await mockERC721.ownerOf(tokenId)).to.be.equal(nftOwner.address) - }) - it('Should not withdraw NFT if not owner', async () => { - await expect(vault.withdraw(mockERC721.address, tokenId)).to.be.revertedWith( - 'ImmutableVault: sender is not the token owner', - ) - }) - }) - - describe('Withdraw NFT on Behalf of', async () => { - beforeEach(async () => { - await vault.connect(nftOwner).deposit(mockERC721.address, tokenId, expirationDate) - }) - it('Should withdraw NFT on behalf of', async () => { - await expect(vault.connect(marketplace).withdrawOnBehalfOf(mockERC721.address, tokenId)) - .to.emit(vault, 'Withdraw') - .withArgs(mockERC721.address, tokenId, nftOwner.address) - }) - it('Should not withdraw NFT on behalf of if not marketplace', async () => { - await expect(vault.connect(nftOwner).withdrawOnBehalfOf(mockERC721.address, tokenId)).to.be.revertedWith( - `AccessControl: account ${nftOwner.address.toLowerCase()} is missing role ${await vault.MARKETPLACE_ROLE()}`, - ) - }) - it('Should not withdraw NFT on behalf of if there is token has an active role assignment', async () => { - const roleAssigment = [{ role: USER_ROLE, grantee: borrower.address }] - const data = [EMPTY_BYTES] - - await vault - .connect(marketplace) - .batchGrantRole(mockERC721.address, tokenId, expirationDate, roleAssigment, data) - await expect(vault.connect(marketplace).withdrawOnBehalfOf(mockERC721.address, tokenId)).to.be.revertedWith( - 'ImmutableVault: token has an active role assignment', - ) - }) - }) - }) -})